pax_global_header00006660000000000000000000000064144676360330014526gustar00rootroot0000000000000052 comment=3d964b92d99acea1eda5ff41772ab671cdbe6636 ema-qdisc-3d964b9/000077500000000000000000000000001446763603300137375ustar00rootroot00000000000000ema-qdisc-3d964b9/.github/000077500000000000000000000000001446763603300152775ustar00rootroot00000000000000ema-qdisc-3d964b9/.github/workflows/000077500000000000000000000000001446763603300173345ustar00rootroot00000000000000ema-qdisc-3d964b9/.github/workflows/makefile.yml000066400000000000000000000003571446763603300216410ustar00rootroot00000000000000name: Makefile CI on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build and test run: make build ema-qdisc-3d964b9/.travis.yml000066400000000000000000000005611446763603300160520ustar00rootroot00000000000000language: go go: - 1.x env: - GO111MODULE=on os: - linux sudo: required before_install: - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1 - go get -d ./... script: - go build -tags=gofuzz ./... - go vet ./... - golangci-lint run ./... - go test -v -race -tags=integration ./... ema-qdisc-3d964b9/LICENSE.md000066400000000000000000000020731446763603300153450ustar00rootroot00000000000000MIT License =========== Copyright (C) 2017 Emanuele Rocca Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ema-qdisc-3d964b9/Makefile000066400000000000000000000002731446763603300154010ustar00rootroot00000000000000build: go fmt go build go vet #staticcheck #golint -set_exit_status go test -v -race -tags=integration cover: go test -coverprofile=coverage.out go tool cover -html=coverage.out ema-qdisc-3d964b9/README.md000066400000000000000000000010571446763603300152210ustar00rootroot00000000000000qdisc [![Build Status](https://github.com/ema/qdisc/actions/workflows/makefile.yml/badge.svg)](https://github.com/ema/qdisc/actions/) ===== Package `qdisc` allows getting queuing discipline information via netlink, similarly to what `tc -s qdisc show` does. Example usage ------------- package main import ( "fmt" "github.com/ema/qdisc" ) func main() { info, err := qdisc.Get() if err == nil { for _, msg := range info { fmt.Printf("%+v\n", msg) } } } ema-qdisc-3d964b9/get.go000066400000000000000000000152031446763603300150460ustar00rootroot00000000000000package qdisc import ( "errors" "fmt" "math" "net" "syscall" "github.com/mdlayher/netlink" "github.com/mdlayher/netlink/nlenc" ) const ( TCA_UNSPEC = iota TCA_KIND TCA_OPTIONS TCA_STATS TCA_XSTATS TCA_RATE TCA_FCNT TCA_STATS2 TCA_STAB // __TCA_MAX ) const ( TCA_STATS_UNSPEC = iota TCA_STATS_BASIC TCA_STATS_RATE_EST TCA_STATS_QUEUE TCA_STATS_APP TCA_STATS_RATE_EST64 // __TCA_STATS_MAX ) // See struct tc_stats in /usr/include/linux/pkt_sched.h type TC_Stats struct { Bytes uint64 Packets uint32 Drops uint32 Overlimits uint32 Bps uint32 Pps uint32 Qlen uint32 Backlog uint32 } // See /usr/include/linux/gen_stats.h type TC_Stats2 struct { // struct gnet_stats_basic Bytes uint64 Packets uint32 // struct gnet_stats_queue Qlen uint32 Backlog uint32 Drops uint32 Requeues uint32 Overlimits uint32 } // See struct tc_fq_qd_stats /usr/include/linux/pkt_sched.h type TC_Fq_Qd_Stats struct { GcFlows uint64 HighprioPackets uint64 TcpRetrans uint64 Throttled uint64 FlowsPlimit uint64 PktsTooLong uint64 AllocationErrors uint64 TimeNextDelayedFlow int64 Flows uint32 InactiveFlows uint32 ThrottledFlows uint32 UnthrottleLatencyNs uint32 } type QdiscInfo struct { IfaceName string Parent uint32 Handle uint32 Kind string Bytes uint64 Packets uint32 Drops uint32 Requeues uint32 Overlimits uint32 GcFlows uint64 Throttled uint64 FlowsPlimit uint64 Qlen uint32 Backlog uint32 } func parseTCAStats(attr netlink.Attribute) TC_Stats { var stats TC_Stats stats.Bytes = nlenc.Uint64(attr.Data[0:8]) stats.Packets = nlenc.Uint32(attr.Data[8:12]) stats.Drops = nlenc.Uint32(attr.Data[12:16]) stats.Overlimits = nlenc.Uint32(attr.Data[16:20]) stats.Bps = nlenc.Uint32(attr.Data[20:24]) stats.Pps = nlenc.Uint32(attr.Data[24:28]) stats.Qlen = nlenc.Uint32(attr.Data[28:32]) stats.Backlog = nlenc.Uint32(attr.Data[32:36]) return stats } func parseTCAStats2(attr netlink.Attribute) TC_Stats2 { var stats TC_Stats2 nested, _ := netlink.UnmarshalAttributes(attr.Data) for _, a := range nested { switch a.Type { case TCA_STATS_BASIC: stats.Bytes = nlenc.Uint64(a.Data[0:8]) stats.Packets = nlenc.Uint32(a.Data[8:12]) case TCA_STATS_QUEUE: stats.Qlen = nlenc.Uint32(a.Data[0:4]) stats.Backlog = nlenc.Uint32(a.Data[4:8]) stats.Drops = nlenc.Uint32(a.Data[8:12]) stats.Requeues = nlenc.Uint32(a.Data[12:16]) stats.Overlimits = nlenc.Uint32(a.Data[16:20]) default: } } return stats } func parseTC_Fq_Qd_Stats(attr netlink.Attribute) (TC_Fq_Qd_Stats, error) { var stats TC_Fq_Qd_Stats nested, err := netlink.UnmarshalAttributes(attr.Data) if err != nil { return stats, err } pts := []*uint64{ &stats.GcFlows, &stats.HighprioPackets, &stats.TcpRetrans, &stats.Throttled, &stats.FlowsPlimit, &stats.PktsTooLong, &stats.AllocationErrors, } for _, a := range nested { switch a.Type { case TCA_STATS_APP: for i := 0; i < len(pts) && (i+1)*8 <= len(a.Data); i++ { *pts[i] = nlenc.Uint64(a.Data[i*8 : (i+1)*8]) } default: } } return stats, nil } func getQdiscMsgs(c *netlink.Conn) ([]netlink.Message, error) { req := netlink.Message{ Header: netlink.Header{ Flags: netlink.Request | netlink.Dump, Type: 38, // RTM_GETQDISC }, Data: make([]byte, 20), } // Perform a request, receive replies, and validate the replies msgs, err := c.Execute(req) if err != nil { return nil, fmt.Errorf("failed to execute request: %v", err) } return msgs, nil } // See https://tools.ietf.org/html/rfc3549#section-3.1.3 func parseMessage(msg netlink.Message, ifaceNamesByID map[int]string) (QdiscInfo, error) { var m QdiscInfo var s TC_Stats var s2 TC_Stats2 var s_fq TC_Fq_Qd_Stats /* struct tcmsg { unsigned char tcm_family; unsigned char tcm__pad1; unsigned short tcm__pad2; int tcm_ifindex; __u32 tcm_handle; __u32 tcm_parent; __u32 tcm_info; }; */ if len(msg.Data) < 20 { return m, fmt.Errorf("short message, len=%d", len(msg.Data)) } ifaceIdx := nlenc.Uint32(msg.Data[4:8]) m.Handle = nlenc.Uint32(msg.Data[8:12]) m.Parent = nlenc.Uint32(msg.Data[12:16]) if m.Parent == math.MaxUint32 { m.Parent = 0 } // The first 20 bytes are taken by tcmsg attrs, err := netlink.UnmarshalAttributes(msg.Data[20:]) if err != nil { return m, fmt.Errorf("failed to unmarshal attributes: %v", err) } for _, attr := range attrs { switch attr.Type { case TCA_KIND: m.Kind = nlenc.String(attr.Data) case TCA_STATS2: s_fq, err = parseTC_Fq_Qd_Stats(attr) if err != nil { return m, err } if s_fq.GcFlows > 0 { m.GcFlows = s_fq.GcFlows } if s_fq.Throttled > 0 { m.Throttled = s_fq.Throttled } if s_fq.FlowsPlimit > 0 { m.FlowsPlimit = s_fq.FlowsPlimit } s2 = parseTCAStats2(attr) m.Bytes = s2.Bytes m.Packets = s2.Packets m.Drops = s2.Drops // requeues only available in TCA_STATS2, not in TCA_STATS m.Requeues = s2.Requeues m.Overlimits = s2.Overlimits m.Qlen = s2.Qlen m.Backlog = s2.Backlog case TCA_STATS: // Legacy s = parseTCAStats(attr) m.Bytes = s.Bytes m.Packets = s.Packets m.Drops = s.Drops m.Overlimits = s.Overlimits m.Qlen = s.Qlen m.Backlog = s.Backlog default: // TODO: TCA_OPTIONS and TCA_XSTATS } } m.IfaceName = ifaceNamesByID[int(ifaceIdx)] return m, err } func getInterfaceNames() (map[int]string, error) { ifas, err := net.Interfaces() if err != nil { return nil, err } ifNamesByID := make(map[int]string) for _, ifa := range ifas { ifNamesByID[ifa.Index] = ifa.Name } return ifNamesByID, nil } func getAndParse(c *netlink.Conn) ([]QdiscInfo, error) { var res []QdiscInfo msgs, err := getQdiscMsgs(c) if err != nil { return nil, err } ifNamesByID, err := getInterfaceNames() if err != nil { return nil, err } for _, msg := range msgs { m, err := parseMessage(msg, ifNamesByID) if err != nil { return nil, err } res = append(res, m) } return res, nil } func Get() ([]QdiscInfo, error) { const familyRoute = 0 c, err := netlink.Dial(familyRoute, nil) if err != nil { return nil, fmt.Errorf("failed to dial netlink: %v", err) } if err := c.SetOption(netlink.GetStrictCheck, true); err != nil { // silently accept ENOPROTOOPT errors when kernel is not > 4.20 if !errors.Is(err, syscall.ENOPROTOOPT) { return nil, fmt.Errorf("unexpected error trying to set option NETLINK_GET_STRICT_CHK: %v", err) } } defer c.Close() return getAndParse(c) } ema-qdisc-3d964b9/get_test.go000066400000000000000000000072501446763603300161100ustar00rootroot00000000000000package qdisc import ( "syscall" "testing" "github.com/mdlayher/netlink" "github.com/mdlayher/netlink/nltest" ) func TestGetAndParseFail(t *testing.T) { c := nltest.Dial(func(req []netlink.Message) ([]netlink.Message, error) { return nltest.Error(int(syscall.ENOENT), req) }) msgs, err := getAndParse(c) if msgs != nil { t.Fatalf("msgs should be nil, got '%v' instead", msgs) } if err == nil { t.Fatalf("err should not be nil") } } func TestGetAndParseShort(t *testing.T) { msg := netlink.Message{Data: []byte{0xff, 0xff, 0xff, 0xff}} c := nltest.Dial(func(req []netlink.Message) ([]netlink.Message, error) { msg.Header.Sequence = req[0].Header.Sequence return []netlink.Message{msg}, nil }) msgs, err := getAndParse(c) if msgs != nil { t.Fatalf("msgs should be nil, got '%v' instead", msgs) } if err.Error() != "short message, len=4" { t.Fatalf("expected short message error, got '%v' instead", err) } } func TestGetAndParseUnmarshal(t *testing.T) { msg := netlink.Message{ Data: []byte{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }, } c := nltest.Dial(func(req []netlink.Message) ([]netlink.Message, error) { msg.Header.Sequence = req[0].Header.Sequence return []netlink.Message{msg}, nil }) msgs, err := getAndParse(c) if msgs != nil { t.Fatalf("msgs should be nil, got '%v' instead", msgs) } if err.Error() != "failed to unmarshal attributes: invalid attribute; length too short or too large" { t.Fatalf("expected unmarshal failure error, got '%v' instead", err) } } func TestGetAndParseOK(t *testing.T) { d := []byte{ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 128, 255, 255, 255, 255, 2, 0, 0, 0, 7, 0, 1, 0, 102, 113, 0, 0, 84, 0, 2, 0, 8, 0, 1, 0, 16, 39, 0, 0, 8, 0, 2, 0, 100, 0, 0, 0, 8, 0, 3, 0, 212, 11, 0, 0, 8, 0, 4, 0, 36, 59, 0, 0, 8, 0, 5, 0, 1, 0, 0, 0, 8, 0, 7, 0, 255, 255, 255, 255, 8, 0, 9, 0, 64, 156, 0, 0, 8, 0, 10, 0, 255, 3, 0, 0, 8, 0, 11, 0, 142, 12, 1, 0, 8, 0, 8, 0, 10, 0, 0, 0, 132, 0, 7, 0, 84, 0, 4, 0, 125, 97, 0, 0, 0, 0, 0, 0, 86, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 203, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 61, 37, 209, 53, 184, 251, 255, 255, 7, 0, 0, 255, 7, 0, 0, 0, 0, 0, 0, 227, 183, 1, 0, 20, 0, 1, 0, 139, 42, 111, 15, 0, 0, 0, 0, 159, 76, 30, 0, 0, 0, 0, 0, 24, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 44, 0, 3, 0, 139, 42, 111, 15, 0, 0, 0, 0, 159, 76, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 0, 4, 0, 125, 97, 0, 0, 0, 0, 0, 0, 86, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 203, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 61, 37, 209, 53, 184, 251, 255, 255, 7, 0, 0, 255, 7, 0, 0, 0, 0, 0, 0, 227, 183, 1, 0, } msg := netlink.Message{Data: d} c := nltest.Dial(func(req []netlink.Message) ([]netlink.Message, error) { msg.Header.Sequence = req[0].Header.Sequence return []netlink.Message{msg}, nil }) msgs, err := getAndParse(c) if err != nil { t.Fatalf("err should be nil, got %v instead", err) } expect := []QdiscInfo{ { IfaceName: "lo", Parent: 0, Handle: 2147549184, Kind: "fq", Bytes: 258943627, Packets: 1985695, Drops: 0, Requeues: 22, Overlimits: 0, GcFlows: 24957, Throttled: 13515, FlowsPlimit: 0, }, } if len(msgs) != 1 { t.Fatalf("expected 1 message, got %v instead", len(msgs)) } if msgs[0] != expect[0] { t.Fatalf("messages not as expected: %v", msgs) } } ema-qdisc-3d964b9/go.mod000066400000000000000000000005321446763603300150450ustar00rootroot00000000000000module github.com/ema/qdisc go 1.18 require github.com/mdlayher/netlink v1.7.0 require ( github.com/google/go-cmp v0.5.9 // indirect github.com/josharian/native v1.0.0 // indirect github.com/mdlayher/socket v0.4.0 // indirect golang.org/x/net v0.2.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.2.0 // indirect ) ema-qdisc-3d964b9/go.sum000066400000000000000000000021631446763603300150740ustar00rootroot00000000000000github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mdlayher/netlink v1.7.0 h1:ZNGI4V7i1fJ94DPYtWhI/R85i/Q7ZxnuhUJQcJMoodI= github.com/mdlayher/netlink v1.7.0/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=