|
| 1 | +// Design: plan/spec-cp-survival-3-egress-cs6-sched.md -- CS6 classification integration test |
| 2 | + |
| 3 | +//go:build integration && linux |
| 4 | + |
| 5 | +package trafficnetlink |
| 6 | + |
| 7 | +import ( |
| 8 | + "context" |
| 9 | + "net" |
| 10 | + "path/filepath" |
| 11 | + "syscall" |
| 12 | + "testing" |
| 13 | + |
| 14 | + "github.com/vishvananda/netlink" |
| 15 | + |
| 16 | + "codeberg.org/thomas-mangin/ze/internal/component/traffic" |
| 17 | +) |
| 18 | + |
| 19 | +func setTOS(fd uintptr, tos int) error { |
| 20 | + return syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, tos) |
| 21 | +} |
| 22 | + |
| 23 | +// VALIDATES: spec-cp-survival-3 AC-2 -- CS6-marked packets hit the control |
| 24 | +// class counter (classification works after the translateFilter fix). |
| 25 | +// PREVENTS: regression to the broken state where U32 had no Sel and matched nothing. |
| 26 | +func TestCS6ClassifyNetns(t *testing.T) { |
| 27 | + withTrafficNetNS(t, func() { |
| 28 | + const ifaceName = "ze_cs0" |
| 29 | + link := addTrafficVeth(t, ifaceName, "ze_cs1") |
| 30 | + |
| 31 | + path := filepath.Join(t.TempDir(), "state", "traffic-tc-snapshots.json") |
| 32 | + b := newBackendWithOps(netlinkOps{}, path, nil, "boot-1", nil) |
| 33 | + |
| 34 | + desired := map[string]traffic.InterfaceQoS{ |
| 35 | + ifaceName: { |
| 36 | + Interface: ifaceName, |
| 37 | + Qdisc: traffic.Qdisc{ |
| 38 | + Type: traffic.QdiscHTB, |
| 39 | + DefaultClass: "default", |
| 40 | + Classes: []traffic.TrafficClass{ |
| 41 | + { |
| 42 | + Name: "control", |
| 43 | + Rate: 1_000_000, |
| 44 | + Ceil: 10_000_000, |
| 45 | + Priority: 0, |
| 46 | + Filters: []traffic.TrafficFilter{ |
| 47 | + {Type: traffic.FilterDSCP, Value: 48}, // CS6 |
| 48 | + }, |
| 49 | + }, |
| 50 | + { |
| 51 | + Name: "default", |
| 52 | + Rate: 1_000_000, |
| 53 | + Ceil: 10_000_000, |
| 54 | + Priority: 1, |
| 55 | + }, |
| 56 | + }, |
| 57 | + }, |
| 58 | + }, |
| 59 | + } |
| 60 | + if err := b.Apply(context.Background(), desired); err != nil { |
| 61 | + t.Fatalf("Apply: %v", err) |
| 62 | + } |
| 63 | + |
| 64 | + if got := rootQdiscTypeInKernel(t, ifaceName); got != "htb" { |
| 65 | + t.Fatalf("root qdisc = %q, want htb", got) |
| 66 | + } |
| 67 | + |
| 68 | + filters, err := netlink.FilterList(link, netlink.HANDLE_ROOT) |
| 69 | + if err != nil { |
| 70 | + t.Fatalf("FilterList: %v", err) |
| 71 | + } |
| 72 | + |
| 73 | + var u32Count int |
| 74 | + for _, f := range filters { |
| 75 | + if _, ok := f.(*netlink.U32); ok { |
| 76 | + u32Count++ |
| 77 | + } |
| 78 | + } |
| 79 | + if u32Count == 0 { |
| 80 | + t.Fatal("no u32 filters installed (the bug: translateFilter produced U32 with no Sel)") |
| 81 | + } |
| 82 | + if u32Count < 2 { |
| 83 | + t.Errorf("u32 filter count = %d, want >= 2 (IPv4 + IPv6)", u32Count) |
| 84 | + } |
| 85 | + |
| 86 | + peerLink, err := netlink.LinkByName("ze_cs1") |
| 87 | + if err != nil { |
| 88 | + t.Fatalf("link ze_cs1: %v", err) |
| 89 | + } |
| 90 | + |
| 91 | + addr := &netlink.Addr{IPNet: &net.IPNet{ |
| 92 | + IP: net.IPv4(10, 99, 0, 1), |
| 93 | + Mask: net.CIDRMask(24, 32), |
| 94 | + }} |
| 95 | + if err := netlink.AddrAdd(link, addr); err != nil { |
| 96 | + t.Fatalf("addr add ze_cs0: %v", err) |
| 97 | + } |
| 98 | + peerAddr := &netlink.Addr{IPNet: &net.IPNet{ |
| 99 | + IP: net.IPv4(10, 99, 0, 2), |
| 100 | + Mask: net.CIDRMask(24, 32), |
| 101 | + }} |
| 102 | + if err := netlink.AddrAdd(peerLink, peerAddr); err != nil { |
| 103 | + t.Fatalf("addr add ze_cs1: %v", err) |
| 104 | + } |
| 105 | + |
| 106 | + conn, err := net.DialUDP("udp4", &net.UDPAddr{IP: net.IPv4(10, 99, 0, 1)}, &net.UDPAddr{IP: net.IPv4(10, 99, 0, 2), Port: 9999}) |
| 107 | + if err != nil { |
| 108 | + t.Fatalf("dial: %v", err) |
| 109 | + } |
| 110 | + defer conn.Close() |
| 111 | + |
| 112 | + rawConn, err := conn.SyscallConn() |
| 113 | + if err != nil { |
| 114 | + t.Fatalf("SyscallConn: %v", err) |
| 115 | + } |
| 116 | + var setErr error |
| 117 | + rawConn.Control(func(fd uintptr) { |
| 118 | + setErr = setTOS(fd, 0xC0) // CS6 = DSCP 48, TOS = 0xC0 |
| 119 | + }) |
| 120 | + if setErr != nil { |
| 121 | + t.Fatalf("setsockopt IP_TOS: %v", setErr) |
| 122 | + } |
| 123 | + |
| 124 | + payload := []byte("cs6test") |
| 125 | + for i := 0; i < 50; i++ { |
| 126 | + conn.Write(payload) //nolint:errcheck // best-effort: some may fail (no listener) |
| 127 | + } |
| 128 | + |
| 129 | + classes, err := netlink.ClassList(link, netlink.HANDLE_ROOT) |
| 130 | + if err != nil { |
| 131 | + t.Fatalf("ClassList: %v", err) |
| 132 | + } |
| 133 | + |
| 134 | + var controlStats *netlink.ClassStatistics |
| 135 | + for _, cls := range classes { |
| 136 | + htb, ok := cls.(*netlink.HtbClass) |
| 137 | + if !ok { |
| 138 | + continue |
| 139 | + } |
| 140 | + if htb.Handle == makeHandle(1, 1) { |
| 141 | + controlStats = htb.Statistics |
| 142 | + } |
| 143 | + } |
| 144 | + if controlStats == nil { |
| 145 | + t.Fatal("control class (1:1) not found in kernel") |
| 146 | + } |
| 147 | + if controlStats.Basic.Packets == 0 { |
| 148 | + t.Error("control class packet count = 0; CS6-marked packets were not classified (AC-2 fail)") |
| 149 | + } |
| 150 | + }) |
| 151 | +} |
0 commit comments