1//go:build linux
2
3package main
4
5import (
6 "context"
7 "net"
8 "os"
9 "path/filepath"
10 "strconv"
11 "testing"
12 "time"
13)
14
15func closeUDPFixture(t *testing.T, c interface{ Close() error }) {
16 t.Helper()
17 t.Cleanup(func() {
18 if err := c.Close(); err != nil {
19 t.Error(err)
20 }
21 })
22}
23
24func TestUDPNetemDropsWhenBoundedSchedulerIsFull(t *testing.T) {
25 r := &udpNetemRelay{
26 done: make(chan struct{}),
27 slots: make(chan struct{}, udpNetemQueueCapacity),
28 queue: make(chan scheduledPacket, udpNetemQueueCapacity),
29 }
30 packet := scheduledPacket{packet: []byte("bounded"), to: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1}}
31 for range udpNetemQueueCapacity {
32 if !r.enqueue(packet) {
33 t.Fatal("enqueue() dropped packet before scheduler capacity was full")
34 }
35 }
36 if r.enqueue(packet) {
37 t.Fatal("enqueue() accepted packet beyond scheduler capacity")
38 }
39 if got := r.queueOverflowDrops.Load(); got != 1 {
40 t.Fatalf("queue overflow drops=%d, want 1", got)
41 }
42}
43
44func TestUDPNetemCloseRejectsQueueOverflow(t *testing.T) {
45 netem, err := newUDPNetem(udpNetemConfig{TargetPath: filepath.Join(t.TempDir(), "target")})
46 if err != nil {
47 t.Fatal(err)
48 }
49 relay, ok := netem.(*udpNetemRelay)
50 if !ok {
51 t.Fatalf("netem type=%T, want *udpNetemRelay", netem)
52 }
53 relay.queueOverflowDrops.Store(3)
54 if err := netem.Close(); err == nil || err.Error() != "udp netem queue overflow drops: 3" {
55 t.Fatalf("Close error=%v, want overflow rejection", err)
56 }
57}
58
59func TestUDPNetemExecutesRTTAndLossFixtures(t *testing.T) {
60 fixtures := []struct {
61 name string
62 rtt time.Duration
63 loss int
64 sent int
65 want int
66 }{
67 {"25ms RTT", 25 * time.Millisecond, 0, 1, 1},
68 {"100ms RTT", 100 * time.Millisecond, 0, 1, 1},
69 {"zero loss", 0, 0, 10, 10},
70 {"one percent loss", 0, 1, 100, 99},
71 }
72 for _, tc := range fixtures {
73 t.Run(tc.name, func(t *testing.T) {
74 var lc net.ListenConfig
75 target, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:0")
76 if err != nil {
77 t.Fatal(err)
78 }
79 closeUDPFixture(t, target)
80 targetAddr, ok := target.LocalAddr().(*net.UDPAddr)
81 if !ok {
82 t.Fatalf("target address type=%T, want *net.UDPAddr", target.LocalAddr())
83 }
84 path := filepath.Join(t.TempDir(), "target")
85 if err := os.WriteFile(path, []byte("VEV-UDP "+strconv.Itoa(targetAddr.Port)+" key\\n"), 0o600); err != nil {
86 t.Fatal(err)
87 }
88 netem, err := newUDPNetem(udpNetemConfig{RTT: tc.rtt, LossPercent: tc.loss, TargetPath: path})
89 if err != nil {
90 t.Fatal(err)
91 }
92 closeUDPFixture(t, netem)
93 client, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:0")
94 if err != nil {
95 t.Fatal(err)
96 }
97 closeUDPFixture(t, client)
98 started := time.Now()
99 for i := 0; i < tc.sent; i++ {
100 if _, err := client.WriteTo([]byte{byte(i)}, &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: netem.Port()}); err != nil {
101 t.Fatal(err)
102 }
103 }
104 if err := target.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
105 t.Fatal(err)
106 }
107 got := 0
108 buf := make([]byte, 32)
109 var relayAddr net.Addr
110 for got < tc.want {
111 if _, relayAddr, err = target.ReadFrom(buf); err != nil {
112 t.Fatalf("received %d packets, want %d: %v", got, tc.want, err)
113 }
114 got++
115 }
116 if tc.rtt > 0 {
117 if elapsed := time.Since(started); elapsed < tc.rtt/2 {
118 t.Fatalf("client-to-target netem delay=%s, want at least %s", elapsed, tc.rtt/2)
119 }
120 }
121 if tc.loss == 0 {
122 // The relay learns the client from the first carriage packet. Replying
123 // through that same relay address verifies the target-to-client half
124 // of the configured RTT as well.
125 started = time.Now()
126 if _, err := target.WriteTo([]byte("reply"), relayAddr); err != nil {
127 t.Fatal(err)
128 }
129 if err := client.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
130 t.Fatal(err)
131 }
132 if _, _, err := client.ReadFrom(buf); err != nil {
133 t.Fatalf("relay reply: %v", err)
134 }
135 if tc.rtt > 0 {
136 if elapsed := time.Since(started); elapsed < tc.rtt/2 {
137 t.Fatalf("target-to-client netem delay=%s, want at least %s", elapsed, tc.rtt/2)
138 }
139 }
140 }
141 // Keep the deadline short: extra carriage would prove the exact 1%%
142 // loss cycle was not enforced.
143 if err := target.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
144 t.Fatal(err)
145 }
146 if _, _, err := target.ReadFrom(buf); tc.want != tc.sent && err == nil {
147 t.Fatalf("received more than %d packets with %d%% loss", tc.want, tc.loss)
148 }
149 })
150 }
151}