diff --git a/class_test.go b/class_test.go index d163f73..0a4c7db 100644 --- a/class_test.go +++ b/class_test.go @@ -79,6 +79,37 @@ func TestClassAddDel(t *testing.T) { if htb.Cbuffer != class.Cbuffer { t.Fatal("Cbuffer doesn't match") } + + qattrs := QdiscAttrs{ + LinkIndex: link.Attrs().Index, + Handle: MakeHandle(0x2, 0), + Parent: MakeHandle(0xffff, 2), + } + nattrs := NetemQdiscAttrs{ + Latency: 20000, + Loss: 23.4, + Duplicate: 14.3, + Jitter: 1000, + } + netem := NewNetem(qattrs, nattrs) + if err := QdiscAdd(netem); err != nil { + t.Fatal(err) + } + + qdiscs, err = QdiscList(link) + if err != nil { + t.Fatal(err) + } + if len(qdiscs) != 2 { + t.Fatal("Failed to add qdisc") + } + _, ok = qdiscs[0].(*Htb) + if !ok { + t.Fatal("Qdisc is the wrong type") + } + + // Deletion + if err := ClassDel(class); err != nil { t.Fatal(err) } diff --git a/nl/tc_linux.go b/nl/tc_linux.go index 4a32055..0482e43 100644 --- a/nl/tc_linux.go +++ b/nl/tc_linux.go @@ -60,6 +60,7 @@ const ( SizeofTcActionMsg = 0x04 SizeofTcPrioMap = 0x14 SizeofTcRateSpec = 0x0c + SizeofTcNetemQopt = 0x18 SizeofTcTbfQopt = 2*SizeofTcRateSpec + 0x0c SizeofTcHtbCopt = 2*SizeofTcRateSpec + 0x14 SizeofTcHtbGlob = 0x14 @@ -191,6 +192,53 @@ func (x *TcRateSpec) Serialize() []byte { return (*(*[SizeofTcRateSpec]byte)(unsafe.Pointer(x)))[:] } +/** +* NETEM + */ + +const ( + TCA_NETEM_UNSPEC = iota + TCA_NETEM_CORR + TCA_NETEM_DELAY_DIST + TCA_NETEM_REORDER + TCA_NETEM_CORRUPT + TCA_NETEM_LOSS + TCA_NETEM_RATE + TCA_NETEM_ECN + TCA_NETEM_RATE64 + TCA_NETEM_MAX = TCA_NETEM_RATE64 +) + +// struct tc_netem_qopt { +// __u32 latency; /* added delay (us) */ +// __u32 limit; /* fifo limit (packets) */ +// __u32 loss; /* random packet loss (0=none ~0=100%) */ +// __u32 gap; /* re-ordering gap (0 for none) */ +// __u32 duplicate; /* random packet dup (0=none ~0=100%) */ +// __u32 jitter; /* random jitter in latency (us) */ +// }; + +type TcNetemQopt struct { + Latency uint32 + Limit uint32 + Loss uint32 + Gap uint32 + Duplicate uint32 + Jitter uint32 +} + +func (msg *TcNetemQopt) Len() int { + return SizeofTcNetemQopt +} + +func DeserializeTcNetemQopt(b []byte) *TcNetemQopt { + return (*TcNetemQopt)(unsafe.Pointer(&b[0:SizeofTcNetemQopt][0])) +} + +func (x *TcNetemQopt) Serialize() []byte { + return (*(*[SizeofTcNetemQopt]byte)(unsafe.Pointer(x)))[:] +} + // struct tc_tbf_qopt { // struct tc_ratespec rate; // struct tc_ratespec peakrate; diff --git a/qdisc.go b/qdisc.go index 41a4aa8..29cb958 100644 --- a/qdisc.go +++ b/qdisc.go @@ -2,6 +2,7 @@ package netlink import ( "fmt" + "math" ) const ( @@ -52,6 +53,10 @@ func HandleStr(handle uint32) string { } } +func Percentage(percentage float32) uint32 { + return uint32(math.MaxUint32 * (percentage / 100)) +} + // PfifoFast is the default qdisc created by the kernel if one has not // been defined for the interface type PfifoFast struct { @@ -120,6 +125,73 @@ func (qdisc *Htb) Type() string { return "htb" } +// Netem is a classless qdisc that rate limits based on tokens + +type NetemQdiscAttrs struct { + Latency uint32 // in us + Limit uint32 + Loss float32 // in % + Gap uint32 + Duplicate float32 // in % + Jitter uint32 // in us +} + +func (q NetemQdiscAttrs) String() string { + return fmt.Sprintf( + "{Latency: %d, Limit: %d, Loss: %d, Gap: %d, Duplicate: %d, Jitter: %d}", + q.Latency, q.Limit, q.Loss, q.Gap, q.Duplicate, q.Jitter, + ) +} + +type Netem struct { + QdiscAttrs + Latency uint32 + Limit uint32 + Loss uint32 + Gap uint32 + Duplicate uint32 + Jitter uint32 +} + +func NewNetem(attrs QdiscAttrs, nattrs NetemQdiscAttrs) *Netem { + var limit uint32 = 1000 + + latency := nattrs.Latency + loss := Percentage(nattrs.Loss) + gap := nattrs.Gap + duplicate := Percentage(nattrs.Duplicate) + jitter := nattrs.Jitter + + // FIXME should validate values(like loss/duplicate are percentages...) + latency = time2Tick(latency) + + if nattrs.Limit != 0 { + limit = nattrs.Limit + } + // Jitter is only value if latency is > 0 + if latency > 0 { + jitter = time2Tick(jitter) + } + + return &Netem{ + QdiscAttrs: attrs, + Latency: latency, + Limit: limit, + Loss: loss, + Gap: gap, + Duplicate: duplicate, + Jitter: jitter, + } +} + +func (qdisc *Netem) Attrs() *QdiscAttrs { + return &qdisc.QdiscAttrs +} + +func (qdisc *Netem) Type() string { + return "netem" +} + // Tbf is a classless qdisc that rate limits based on tokens type Tbf struct { QdiscAttrs diff --git a/qdisc_linux.go b/qdisc_linux.go index a16eb99..3acc363 100644 --- a/qdisc_linux.go +++ b/qdisc_linux.go @@ -65,6 +65,15 @@ func QdiscAdd(qdisc Qdisc) error { opt.DirectPkts = htb.DirectPkts nl.NewRtAttrChild(options, nl.TCA_HTB_INIT, opt.Serialize()) // nl.NewRtAttrChild(options, nl.TCA_HTB_DIRECT_QLEN, opt.Serialize()) + } else if netem, ok := qdisc.(*Netem); ok { + opt := nl.TcNetemQopt{} + opt.Latency = netem.Latency + opt.Limit = netem.Limit + opt.Loss = netem.Loss + opt.Gap = netem.Gap + opt.Duplicate = netem.Duplicate + opt.Jitter = netem.Jitter + options = nl.NewRtAttr(nl.TCA_OPTIONS, opt.Serialize()) } else if _, ok := qdisc.(*Ingress); ok { // ingress filters must use the proper handle if msg.Parent != HANDLE_INGRESS { @@ -135,6 +144,8 @@ func QdiscList(link Link) ([]Qdisc, error) { qdisc = &Ingress{} case "htb": qdisc = &Htb{} + case "netem": + qdisc = &Netem{} default: qdisc = &GenericQdisc{QdiscType: qdiscType} } @@ -166,6 +177,10 @@ func QdiscList(link Link) ([]Qdisc, error) { if err := parseHtbData(qdisc, data); err != nil { return nil, err } + case "netem": + if err := parseNetemData(qdisc, attr.Value); err != nil { + return nil, err + } // no options for ingress } @@ -213,6 +228,19 @@ func parseHtbData(qdisc Qdisc, data []syscall.NetlinkRouteAttr) error { } return nil } + +func parseNetemData(qdisc Qdisc, value []byte) error { + netem := qdisc.(*Netem) + opt := nl.DeserializeTcNetemQopt(value) + netem.Latency = opt.Latency + netem.Limit = opt.Limit + netem.Loss = opt.Loss + netem.Gap = opt.Gap + netem.Duplicate = opt.Duplicate + netem.Jitter = opt.Jitter + return nil +} + func parseTbfData(qdisc Qdisc, data []syscall.NetlinkRouteAttr) error { native = nl.NativeEndian() tbf := qdisc.(*Tbf) diff --git a/qdisc_test.go b/qdisc_test.go index 1dc92db..89a2532 100644 --- a/qdisc_test.go +++ b/qdisc_test.go @@ -88,13 +88,6 @@ func TestHtbAddDel(t *testing.T) { t.Fatal(err) } - /* - cmd := exec.Command("tc", "qdisc") - out, err := cmd.CombinedOutput() - if err == nil { - fmt.Printf("%s\n", out) - } - */ qdiscs, err := QdiscList(link) if err != nil { t.Fatal(err) @@ -126,6 +119,7 @@ func TestHtbAddDel(t *testing.T) { t.Fatal("Failed to remove qdisc") } } + func TestPrioAddDel(t *testing.T) { tearDown := setUpNetlinkTest(t) defer tearDown()