From bcf3a14429634a9cfd94c3e78792b817051bdb22 Mon Sep 17 00:00:00 2001 From: Francesco Altiero Date: Sun, 9 Aug 2026 11:11:09 +0200 Subject: [PATCH 1/2] netem: add Gilbert-Elliot (gemodel) loss support The kernel's netem qdisc supports a Gilbert-Elliot two-state loss model (TCA_NETEM_LOSS / NETEM_LOSS_GE, struct tc_netem_gemodel) as an alternative to the basic Loss/LossCorr correlated-loss model, letting callers model bursty packet loss instead of independent/iid loss. This attribute was previously unimplemented in this package even though the TCA_NETEM_LOSS constant already existed. Adds TcNetemGemodel to the nl package mirroring the kernel ABI directly (net/sched/sch_netem.c, include/uapi/linux/pkt_sched.h), and GELossP/R/H/K1 fields on NetemQdiscAttrs/Netem, wired through NewNetem and the netem qdisc encode/decode path the same way Corrupt/Reorder already are. --- class_test.go | 16 ++++++++++++++++ nl/tc_linux.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ nl/tc_linux_test.go | 30 ++++++++++++++++++++++++++++++ qdisc.go | 15 +++++++++++++++ qdisc_linux.go | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) diff --git a/class_test.go b/class_test.go index ea47036b3..f2730dc55 100644 --- a/class_test.go +++ b/class_test.go @@ -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 { @@ -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 diff --git a/nl/tc_linux.go b/nl/tc_linux.go index bcdd8b34b..b31a67153 100644 --- a/nl/tc_linux.go +++ b/nl/tc_linux.go @@ -116,6 +116,7 @@ const ( SizeofTcNetemCorr = 0x0c SizeofTcNetemReorder = 0x08 SizeofTcNetemCorrupt = 0x08 + SizeofTcNetemGemodel = 0x10 SizeOfTcNetemRate = 0x10 SizeofTcTbfQopt = 2*SizeofTcRateSpec + 0x0c SizeofTcHtbCopt = 2*SizeofTcRateSpec + 0x14 @@ -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) */ @@ -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 diff --git a/nl/tc_linux_test.go b/nl/tc_linux_test.go index b7d0f0595..9b25761a7 100644 --- a/nl/tc_linux_test.go +++ b/nl/tc_linux_test.go @@ -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 diff --git a/qdisc.go b/qdisc.go index 1cde43c94..89389330f 100644 --- a/qdisc.go +++ b/qdisc.go @@ -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 { @@ -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 { diff --git a/qdisc_linux.go b/qdisc_linux.go index 0a2a5891c..b4d566320 100644 --- a/qdisc_linux.go +++ b/qdisc_linux.go @@ -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, @@ -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, } } @@ -241,6 +250,17 @@ 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. + 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()) + } // Rate if qdisc.Rate64 > 0 { rate := nl.TcNetemRate{} @@ -646,6 +666,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 | unix.NLA_F_NESTED: + 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 { From f3ef24bead44f2919bb2ebfd59ac578f46b439aa Mon Sep 17 00:00:00 2001 From: Francesco Altiero Date: Sun, 9 Aug 2026 11:19:15 +0200 Subject: [PATCH 2/2] netem: fix GE loss readback, kernel omits NLA_F_NESTED on dump CI caught this: TestClassAddDel round-tripped GELossP as 0 instead of the value that was set. The parse-side switch case required TCA_NETEM_LOSS | NLA_F_NESTED, matching how iproute2 builds the attribute on the way in (tc/q_netem.c). But net/sched/sch_netem.c's dump_loss_model() builds its reply with nla_nest_start_noflag(), which deliberately omits the flag bit on the way out, so the case never matched and the GE fields stayed zeroed. --- qdisc_linux.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qdisc_linux.go b/qdisc_linux.go index b4d566320..f0759db7c 100644 --- a/qdisc_linux.go +++ b/qdisc_linux.go @@ -252,6 +252,11 @@ func qdiscPayload(req *nl.NetlinkRequest, qdisc Qdisc) error { } // 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 @@ -666,7 +671,7 @@ 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 | unix.NLA_F_NESTED: + case nl.TCA_NETEM_LOSS: lossData, err := nl.ParseRouteAttr(datum.Value) if err != nil { return err