Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions class_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ func TestClassAddDel(t *testing.T) {
CorruptProb: 10.0,
CorruptCorr: 10,
Rate64: 10 * 1024 * 1024,
GELossP: 5.0,
GELossR: 95.0,
GELossH: 20.0,
GELossK1: 2.0,
}
qdiscnetem := NewNetem(qattrs, nattrs)
if err := QdiscAdd(qdiscnetem); err != nil {
Expand Down Expand Up @@ -198,6 +202,18 @@ func TestClassAddDel(t *testing.T) {
if netem.Rate64 != qdiscnetem.Rate64 {
t.Fatalf("Rate64 does not match. Expected %d, got %d", netem.Rate64, qdiscnetem.Rate64)
}
if netem.GELossP != qdiscnetem.GELossP {
t.Fatalf("GELossP does not match. Expected %d, got %d", qdiscnetem.GELossP, netem.GELossP)
}
if netem.GELossR != qdiscnetem.GELossR {
t.Fatalf("GELossR does not match. Expected %d, got %d", qdiscnetem.GELossR, netem.GELossR)
}
if netem.GELossH != qdiscnetem.GELossH {
t.Fatalf("GELossH does not match. Expected %d, got %d", qdiscnetem.GELossH, netem.GELossH)
}
if netem.GELossK1 != qdiscnetem.GELossK1 {
t.Fatalf("GELossK1 does not match. Expected %d, got %d", qdiscnetem.GELossK1, netem.GELossK1)
}

// Deletion
// automatically removes netem qdisc
Expand Down
45 changes: 45 additions & 0 deletions nl/tc_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const (
SizeofTcNetemCorr = 0x0c
SizeofTcNetemReorder = 0x08
SizeofTcNetemCorrupt = 0x08
SizeofTcNetemGemodel = 0x10
SizeOfTcNetemRate = 0x10
SizeofTcTbfQopt = 2*SizeofTcRateSpec + 0x0c
SizeofTcHtbCopt = 2*SizeofTcRateSpec + 0x14
Expand Down Expand Up @@ -287,6 +288,16 @@ const (
TCA_NETEM_MAX = TCA_NETEM_RATE64
)

// Sub-attributes nested inside TCA_NETEM_LOSS, selecting the correlated
// packet loss model applied by netem. NETEM_LOSS_GI (4-state) is not
// implemented by this package; only NETEM_LOSS_GE (Gilbert-Elliot) is.
const (
NETEM_LOSS_UNSPEC = iota
NETEM_LOSS_GI
NETEM_LOSS_GE
NETEM_LOSS_MAX = NETEM_LOSS_GE
)

// struct tc_netem_qopt {
// __u32 latency; /* added delay (us) */
// __u32 limit; /* fifo limit (packets) */
Expand Down Expand Up @@ -385,6 +396,40 @@ func (x *TcNetemCorrupt) Serialize() []byte {
return (*(*[SizeofTcNetemCorrupt]byte)(unsafe.Pointer(x)))[:]
}

// struct tc_netem_gemodel {
// __u32 p;
// __u32 r;
// __u32 h;
// __u32 k1;
// };
//
// Fields hold the raw kernel-scaled percentages (0 to ~MaxUint32) of the
// Gilbert-Elliot two-state loss model, as documented in
// net/sched/sch_netem.c: P is the Good -> Bad transition probability, R is
// the Bad -> Good transition probability, H is the loss probability while
// in the Bad state, and K1 is the loss probability while in the Good
// state. Note this differs from the `tc` command line, which asks for
// "1-H" and "1-K" and performs the complement internally before handing
// the values to the kernel; this struct mirrors the kernel ABI directly.
type TcNetemGemodel struct {
P uint32
R uint32
H uint32
K1 uint32
}

func (msg *TcNetemGemodel) Len() int {
return SizeofTcNetemGemodel
}

func DeserializeTcNetemGemodel(b []byte) *TcNetemGemodel {
return (*TcNetemGemodel)(unsafe.Pointer(&b[0:SizeofTcNetemGemodel][0]))
}

func (x *TcNetemGemodel) Serialize() []byte {
return (*(*[SizeofTcNetemGemodel]byte)(unsafe.Pointer(x)))[:]
}

// TcNetemRate is a struct that represents the rate of a netem qdisc
type TcNetemRate struct {
Rate uint32
Expand Down
30 changes: 30 additions & 0 deletions nl/tc_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,36 @@ func TestTcHtbCoptDeserializeSerialize(t *testing.T) {
testDeserializeSerialize(t, orig, safemsg, msg)
}

/* TcNetemGemodel */
func (msg *TcNetemGemodel) write(b []byte) {
native := NativeEndian()
native.PutUint32(b[0:4], msg.P)
native.PutUint32(b[4:8], msg.R)
native.PutUint32(b[8:12], msg.H)
native.PutUint32(b[12:16], msg.K1)
}

func (msg *TcNetemGemodel) serializeSafe() []byte {
length := SizeofTcNetemGemodel
b := make([]byte, length)
msg.write(b)
return b
}

func deserializeTcNetemGemodelSafe(b []byte) *TcNetemGemodel {
var msg = TcNetemGemodel{}
binary.Read(bytes.NewReader(b[0:SizeofTcNetemGemodel]), NativeEndian(), &msg)
return &msg
}

func TestTcNetemGemodelDeserializeSerialize(t *testing.T) {
var orig = make([]byte, SizeofTcNetemGemodel)
rand.Read(orig)
safemsg := deserializeTcNetemGemodelSafe(orig)
msg := DeserializeTcNetemGemodel(orig)
testDeserializeSerialize(t, orig, safemsg, msg)
}

func TestParsePeditEthKeys(t *testing.T) {
tests := []struct {
name string
Expand Down
15 changes: 15 additions & 0 deletions qdisc.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,17 @@ type NetemQdiscAttrs struct {
CorruptProb float32 // in %
CorruptCorr float32 // in %
Rate64 uint64
// GELossP, GELossR, GELossH and GELossK1 configure the Gilbert-Elliot
// two-state loss model (percentages in [0, 100]), an alternative to
// Loss/LossCorr for modeling bursty, correlated packet loss. They are
// mutually exclusive with Loss/LossCorr: the kernel applies only one
// loss model, selected by whether GELossP is set. Unlike the `tc`
// command line, GELossH and GELossK1 are not pre-complemented: they
// map directly onto the kernel's tc_netem_gemodel h/k1 fields.
GELossP float32 // in %
GELossR float32 // in %
GELossH float32 // in %
GELossK1 float32 // in %
}

func (q NetemQdiscAttrs) String() string {
Expand All @@ -188,6 +199,10 @@ type Netem struct {
CorruptProb uint32
CorruptCorr uint32
Rate64 uint64
GELossP uint32
GELossR uint32
GELossH uint32
GELossK1 uint32
}

func (netem *Netem) String() string {
Expand Down
39 changes: 39 additions & 0 deletions qdisc_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ func NewNetem(attrs QdiscAttrs, nattrs NetemQdiscAttrs) *Netem {
corruptCorr = Percentage2u32(nattrs.CorruptCorr)
rate64 = nattrs.Rate64

geLossP := Percentage2u32(nattrs.GELossP)
geLossR := Percentage2u32(nattrs.GELossR)
geLossH := Percentage2u32(nattrs.GELossH)
geLossK1 := Percentage2u32(nattrs.GELossK1)

return &Netem{
QdiscAttrs: attrs,
Latency: latency,
Expand All @@ -78,6 +83,10 @@ func NewNetem(attrs QdiscAttrs, nattrs NetemQdiscAttrs) *Netem {
CorruptProb: corruptProb,
CorruptCorr: corruptCorr,
Rate64: rate64,
GELossP: geLossP,
GELossR: geLossR,
GELossH: geLossH,
GELossK1: geLossK1,
}
}

Expand Down Expand Up @@ -241,6 +250,22 @@ func qdiscPayload(req *nl.NetlinkRequest, qdisc Qdisc) error {
if reorder.Probability > 0 {
options.AddRtAttr(nl.TCA_NETEM_REORDER, reorder.Serialize())
}
// Gilbert-Elliot loss model. Mutually exclusive with the basic
// Loss/LossCorr model: the kernel selects whichever was supplied.
// NLA_F_NESTED is set here on the way in, matching iproute2's
// tc/q_netem.c; on the way out, however, the kernel's own
// dump_loss_model() builds this attribute with
// nla_nest_start_noflag(), so the flag bit is absent when this
// same attribute is read back below in parseNetemData.
if qdisc.GELossP > 0 {
gemodel := nl.TcNetemGemodel{}
gemodel.P = qdisc.GELossP
gemodel.R = qdisc.GELossR
gemodel.H = qdisc.GELossH
gemodel.K1 = qdisc.GELossK1
loss := options.AddRtAttr(nl.TCA_NETEM_LOSS|unix.NLA_F_NESTED, nil)
loss.AddRtAttr(nl.NETEM_LOSS_GE, gemodel.Serialize())
}
Comment on lines +253 to +268

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate qdisc_linux.go =="
fd -a 'qdisc_linux\.go$' . || true

echo "== relevant symbol excerpts =="
file="$(fd 'qdisc_linux\.go$' . | head -n1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '55,95p' "$file" | cat -n
  sed -n '235,270p' "$file" | cat -n
  sed -n '655,690p' "$file" | cat -n
fi

echo "== git diff stat/name =="
git diff --stat || true
git diff --name-only || true

Repository: vishvananda/netlink

Length of output: 4410


🌐 Web query:

Linux kernel netem NETEM_LOSS_GE nla nested p = 0 accepted

💡 Result:

In the Linux kernel's netem (Network Emulator) implementation, NETEM_LOSS_GE refers to the Gilbert-Elliot packet loss model [1]. This model uses a nested Netlink attribute structure to pass parameters between user space (typically the tc tool) and the kernel [2][3]. The p = 0 value in your query refers to the transition probability parameter within the Gilbert-Elliot model configuration [4][3]. When configuring netem loss gemodel via tc, the parameters correspond to the structure struct tc_netem_gemodel [3][1]: - p: Probability of transitioning to the bad (lossy) state [4][5]. - r: Probability of exiting the bad state [4][5]. - h: Loss probability in the bad state (often represented as 1-h in documentation) [4][5]. - k1: Loss probability in the good state (often represented as 1-k in documentation) [4][3]. The kernel's get_loss_clg function handles the parsing of these nested attributes [2][3]. It expects the data size to match sizeof(struct tc_netem_gemodel) [3][6]. If the parameters are provided as p = 0, the kernel accepts this value as a valid probability (specifically, 0% probability of transitioning to the bad state in this context) [4][3]. The input is mapped directly to the clg.a1 field in the kernel's internal netem_sched_data structure [3][7]. There is no specific mechanism that rejects p=0 as an invalid configuration; rather, it is processed as a standard integer value for the Gilbert-Elliot model [3]. Documentation confirms that the Bernoulli model is a special case of the Gilbert-Elliot model, where setting specific parameters (like p) allows the model to function as a simpler random loss generator [4][8].

Citations:


🌐 Web query:

site:codebrowser.dev sch_netem.c get_loss_clg TCA_NETEM_LOSS NLA_F_NESTED NETEM_LOSS_GE

💡 Result:

In the Linux kernel, specifically within the network traffic control (tc) subsystem, the terms identified relate to the configuration and handling of netem (Network Emulator) attributes via Netlink messages. 1. TCA_NETEM_LOSS: This is an enumeration value in the Linux kernel (defined in include/uapi/linux/pkt_sched.h) used as a Netlink attribute type to configure packet loss for the netem qdisc [1]. 2. NETEM_LOSS_GE: This represents the "Gilbert-Elliot" packet loss model, also defined in include/uapi/linux/pkt_sched.h within the NETEM_LOSS enumeration [1]. It is used to specify advanced loss patterns beyond simple random loss [1]. 3. NLA_F_NESTED: This is a Netlink flag (defined in include/uapi/linux/netlink.h) used to indicate that an attribute contains a nested list of further attributes [2]. When configuring complex netem attributes (like specific loss models), the kernel often expects them to be nested within a parent Netlink attribute, requiring this flag to be set to ensure proper parsing and validation by the netlink attribute validation logic [3][4]. 4. sch_netem.c and get_loss_clg: While get_loss_clg is not a standard, globally exposed symbol, it refers to the pattern of internal helper functions found in sch_netem.c (and other qdisc implementations) that parse and "get" configuration data—such as loss model parameters (clg likely standing for "Gilbert-Elliot" or a similar configuration model)—from Netlink attributes [1][4][5]. In the context of traffic control, code paths like these utilize the NLA_F_NESTED flag to safely unpack nested structures describing complex loss behaviors, such as the parameters for the NETEM_LOSS_GE model, into the kernel's internal qdisc structures [4][5].

Citations:


🌐 Web query:

sch_netem.c nla_attr_size TCA_NETEM_LOSS NETEM_LOSS_GE nla_f_nested

💡 Result:

In the Linux kernel's Network Emulator (netem) implementation (sch_netem.c), these constants and identifiers are used to manage loss models via Netlink attributes [1][2]. TCA_NETEM_LOSS is a Netlink attribute type used to group nested loss model parameters [1][3]. It is defined with the NLA_NESTED policy [3][4], allowing multiple specific loss model parameters to be included within the same nested structure [1][5]. NETEM_LOSS_GE refers to the Gilbert-Elliot loss model, which is one of the supported loss models in netem [1][2]. When userspace configures this model, it passes a struct tc_netem_gemodel containing parameters (p, r, h, k1) [1][3]. NLA_F_NESTED is a flag used by userspace tools (such as iproute2's tc command) when creating nested Netlink attributes [6][7]. When the tc command adds the TCA_NETEM_LOSS attribute, it performs bitwise OR with NLA_F_NESTED (TCA_NETEM_LOSS | NLA_F_NESTED) to indicate to the kernel that the attribute contains further nested attributes [6][8]. The kernel's netem implementation in sch_netem.c includes a get_loss_clg function that uses nla_for_each_nested to iterate through the attributes inside the TCA_NETEM_LOSS nest, identifies them by type (e.g., NETEM_LOSS_GE), and parses the data accordingly [1][5]. The use of nla_attr_size is typically used in the kernel to validate the size of incoming Netlink attributes, including nested ones [3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('/home/jailuser/git/qdisc_linux.go')
text = p.read_text()

checks = {
    "serialize_condition": "if qdisc.GELossP > 0" in text,
    "gemodel_assignments_present": all(s in text for s in [
        "gemodel := nl.TcNetemGemodel{}",
        "gemodel.P = qdisc.GELossP",
        "gemodel.R = qdisc.GELossR",
        "gemodel.H = qdisc.GELossH",
        "gemodel.K1 = qdisc.GELossK1",
    ]),
    "nested_loss_attribute_present": "nl.TCA_NETEM_LOSS|unix.NLA_F_NESTED" in text,
    "ge_add_attribute_present": "loss.AddRtAttr(nl.NETEM_LOSS_GE" in text,
    "ge_fields_retained_from_nattrs": all(s in text for s in [
        "geLossP := Percentage2u32(nattrs.GELossP)",
        "geLossR := Percentage2u32(nattrs.GELossR)",
        "geLossH := Percentage2u32(nattrs.GELossH)",
        "geLossK1 := Percentage2u32(nattrs.GELossK1)",
    ]),
    "dump_case_exact": "case nl.TCA_NETEM_LOSS | unix.NLA_F_NESTED:" in text,
}
for name, ok in checks.items():
    print(f"{name}: {ok}")

# Simulate the relevant branch invariants for representative configured inputs.
class Qdisc:
    GELossP = 0
    GELossR = 0
    GELossH = 0
    GELossK1 = 0

inputs = [
    {"labels": ["GELossP=0,GELossK1=1"], "q": (0, 0, 0, 1)},
    {"labels": ["GELossR=1"], "q": (0, 1, 0, 0)},
    {"labels": ["GELossH=1"], "q": (0, 0, 1, 0)},
    {"labels": ["GELossP=1"], "q": (1, 0, 0, 0)},
]
for case in inputs:
    qdisc = Qdisc()
    qdisc.GELossP = case["q"][0]
    qdisc.GELossR = case["q"][1]
    qdisc.GELossH = case["q"][2]
    qdisc.GELossK1 = case["q"][3]
    legacy_branch = not (qdisc.GELossP > 0 or qdisc.GELossR > 0 or qdisc.GELossH > 0 or qdisc.GELossK1 > 0)
    print(f"configured input {case['labels']}: GELoss config branch selected = {not legacy_branch}")
PY

Repository: vishvananda/netlink

Length of output: 630


Serialize the GE model when any GE parameter is configured.

p = 0 is valid for the GE ABI, so GELossP = 0 with any nonzero GE parameter still requires TCA_NETEM_LOSS to select the GE loss model. The current GELossP > 0 check can fall back to the legacy loss model.

Proposed fix
-		if qdisc_GELossP > 0 {
+		if qdisc.GELossP > 0 || qdisc.GELossR > 0 ||
+			qdisc.GELossH > 0 || qdisc.GELossK1 > 0 {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qdisc_linux.go` around lines 253 - 263, Update the GE model condition in the
qdisc serialization path to emit the nested NETEM_LOSS_GE attributes whenever
any GE parameter is configured, including when GELossP is zero. Use the existing
GELossP, GELossR, GELossH, and GELossK1 fields to detect configuration, while
preserving the current gemodel serialization and legacy-model exclusivity.

// Rate
if qdisc.Rate64 > 0 {
rate := nl.TcNetemRate{}
Expand Down Expand Up @@ -646,6 +671,20 @@ func parseNetemData(qdisc Qdisc, value []byte) error {
rate = nl.DeserializeTcNetemRate(datum.Value)
case nl.TCA_NETEM_RATE64:
rate64 = native.Uint64(datum.Value)
case nl.TCA_NETEM_LOSS:
lossData, err := nl.ParseRouteAttr(datum.Value)
if err != nil {
return err
}
for _, lossDatum := range lossData {
if lossDatum.Attr.Type == nl.NETEM_LOSS_GE {
opt := nl.DeserializeTcNetemGemodel(lossDatum.Value)
netem.GELossP = opt.P
netem.GELossR = opt.R
netem.GELossH = opt.H
netem.GELossK1 = opt.K1
}
}
}
}
if rate != nil {
Expand Down
Loading