integration_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. package aerotun
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "net"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "syscall"
  13. "testing"
  14. "time"
  15. "debtik/internal/bwtest"
  16. )
  17. // Opt in: AEROTUN_INTEGRATION=1 go test -run TestVethBandwidthIntegration -v ./internal/aerotun
  18. // All links/routes and test processes live in disposable network namespaces.
  19. func TestVethBandwidthIntegration(t *testing.T) {
  20. if os.Getenv("AEROTUN_INTEGRATION") != "1" {
  21. t.Skip("requires root, network namespaces, ip, nsenter and iperf3")
  22. }
  23. stage := os.Getenv("AEROTUN_TEST_STAGE")
  24. if stage == "peer" {
  25. runNamespacePeer(t)
  26. return
  27. }
  28. if stage == "" {
  29. ctx, cancel := context.WithTimeout(t.Context(), 100*time.Second)
  30. defer cancel()
  31. cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestVethBandwidthIntegration$", "-test.v")
  32. cmd.Env = append(os.Environ(), "AEROTUN_TEST_STAGE=harness")
  33. cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWNET}
  34. out, err := cmd.CombinedOutput()
  35. if err != nil {
  36. t.Fatalf("isolated harness: %v\n%s", err, out)
  37. }
  38. t.Log(string(out))
  39. return
  40. }
  41. t.Run("default-route", testNamespaceDefaultRoute)
  42. versions := []string{"2", "3"}
  43. if selected := os.Getenv("AEROTUN_TEST_ONLY_VERSION"); selected != "" {
  44. versions = []string{selected}
  45. }
  46. for _, version := range versions {
  47. t.Run("v"+version, func(t *testing.T) {
  48. t.Setenv("AEROTUN_TEST_VERSION", version)
  49. for _, compress := range []bool{false, true} {
  50. t.Run(fmt.Sprintf("compression=%v", compress), func(t *testing.T) { runBandwidthPair(t, compress) })
  51. }
  52. })
  53. }
  54. }
  55. func waitForFile(t *testing.T, path string) {
  56. t.Helper()
  57. deadline := time.Now().Add(12 * time.Second)
  58. for time.Now().Before(deadline) {
  59. if _, err := os.Stat(path); err == nil {
  60. return
  61. }
  62. time.Sleep(25 * time.Millisecond)
  63. }
  64. t.Fatalf("timed out waiting for %s", path)
  65. }
  66. func runNamespacePeer(t *testing.T) {
  67. dir := os.Getenv("AEROTUN_TEST_DIR")
  68. role := os.Getenv("AEROTUN_TEST_ROLE")
  69. waitForFile(t, filepath.Join(dir, role+".start"))
  70. cfg := TunnelInstance{Name: "atest0", Mode: role, ListenPort: 51830, PSK: testPSK, MTU: 1420, LocalAddress: "10.99.0.1/32", PeerAddress: "10.99.0.2/32", Compression: os.Getenv("AEROTUN_TEST_COMPRESS") == "true"}
  71. if role == "client" {
  72. cfg.LocalAddress, cfg.PeerAddress = cfg.PeerAddress, cfg.LocalAddress
  73. cfg.RemoteEndpoint = "192.0.2.1:51830"
  74. }
  75. expectedMTU := 1292
  76. if data, err := os.ReadFile(filepath.Join(dir, role+".config")); err == nil {
  77. if err = json.Unmarshal(data, &cfg); err != nil {
  78. t.Fatal(err)
  79. }
  80. expectedMTU = 1320
  81. }
  82. r, err := NewTunnelRunner(cfg)
  83. if err != nil {
  84. t.Fatal(err)
  85. }
  86. // Exercise the container fallback even on hosts which provide native TUN.
  87. if _, ok := r.dev.(*vethTun); !ok {
  88. r.Stop()
  89. t.Fatal("integration requires veth fallback (/dev/net/tun absent)")
  90. }
  91. if err = r.Start(); err != nil {
  92. r.Stop()
  93. t.Fatal(err)
  94. }
  95. defer r.Stop()
  96. if role == "server" {
  97. benchmark := bwtest.NewServer(bwtest.ServerConfig{Enabled: true, ListenAddr: "10.99.0.1", Port: 15200})
  98. if err := benchmark.Start(); err != nil {
  99. t.Fatal(err)
  100. }
  101. defer benchmark.Stop()
  102. }
  103. _ = os.WriteFile(filepath.Join(dir, role+".ready"), nil, 0600)
  104. deadline := time.Now().Add(45 * time.Second)
  105. checked := false
  106. for time.Now().Before(deadline) {
  107. if r.GetTelemetry().Connected && r.GetTelemetry().MTU == expectedMTU {
  108. _ = os.WriteFile(filepath.Join(dir, role+".connected"), nil, 0600)
  109. if !checked && role == "client" && os.Getenv("AEROTUN_HEALTH_CHECK") == "1" {
  110. checked = true
  111. m := &Manager{instances: map[string]TunnelInstance{"health-test": cfg}, runners: map[string]*TunnelRunner{"health-test": r}}
  112. ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
  113. check, e := m.CheckHealth(ctx, "health-test")
  114. cancel()
  115. if e != nil || check.Reachability != "reachable" || check.MTUProbe != "reachable" || check.RTTMillis == nil {
  116. t.Fatalf("health probe failed: %+v %v", check, e)
  117. }
  118. }
  119. }
  120. if _, err := os.Stat(filepath.Join(dir, "stop")); err == nil {
  121. stats := r.GetTelemetry()
  122. if stats.TxBytes == 0 || stats.RxBytes == 0 {
  123. t.Fatal("no bidirectional data delivered")
  124. }
  125. if cfg.Compression && stats.CompressedPackets == 0 {
  126. t.Fatal("compressed path was not exercised")
  127. }
  128. t.Logf("delivered tx=%d rx=%d compressed=%d replay drops=%d", stats.TxBytes, stats.RxBytes, stats.CompressedPackets, stats.ReplayDrops)
  129. return
  130. }
  131. time.Sleep(50 * time.Millisecond)
  132. }
  133. t.Fatal("peer helper timed out")
  134. }
  135. func runBandwidthPair(t *testing.T, compress bool) {
  136. dir := t.TempDir()
  137. type process struct {
  138. cmd *exec.Cmd
  139. log *os.File
  140. }
  141. if os.Getenv("AEROTUN_TEST_VERSION") == "3" {
  142. client, server := secureConfigs(t)
  143. client.Compression = compress
  144. server.Compression = compress
  145. client.ListenPort = 51830
  146. server.ListenPort = 51830
  147. for role, cfg := range map[string]TunnelInstance{"server": server, "client": client} {
  148. b, _ := json.Marshal(cfg)
  149. if err := os.WriteFile(filepath.Join(dir, role+".config"), b, 0600); err != nil {
  150. t.Fatal(err)
  151. }
  152. }
  153. }
  154. var peers []process
  155. defer func() {
  156. _ = os.WriteFile(filepath.Join(dir, "stop"), nil, 0600)
  157. for _, p := range peers {
  158. err := p.cmd.Wait()
  159. _ = p.log.Close()
  160. b, _ := os.ReadFile(p.log.Name())
  161. if err != nil {
  162. t.Errorf("peer: %v\n%s", err, b)
  163. }
  164. }
  165. }()
  166. for _, role := range []string{"server", "client"} {
  167. logFile, err := os.Create(filepath.Join(dir, role+".log"))
  168. if err != nil {
  169. t.Fatal(err)
  170. }
  171. cmd := exec.Command(os.Args[0], "-test.run=^TestVethBandwidthIntegration$", "-test.v")
  172. cmd.Env = append(os.Environ(), "AEROTUN_TEST_STAGE=peer", "AEROTUN_TEST_DIR="+dir, "AEROTUN_TEST_ROLE="+role, "AEROTUN_TEST_COMPRESS="+strconv.FormatBool(compress))
  173. cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWNET}
  174. cmd.Stdout = logFile
  175. cmd.Stderr = logFile
  176. if err = cmd.Start(); err != nil {
  177. t.Fatal(err)
  178. }
  179. peers = append(peers, process{cmd, logFile})
  180. }
  181. run := func(args ...string) {
  182. t.Helper()
  183. out, err := exec.Command("ip", args...).CombinedOutput()
  184. if err != nil {
  185. t.Fatalf("ip %v: %v %s", args, err, out)
  186. }
  187. }
  188. run("link", "add", "atest-a", "type", "veth", "peer", "name", "atest-b")
  189. for i, p := range peers {
  190. name := []string{"atest-a", "atest-b"}[i]
  191. pid := strconv.Itoa(p.cmd.Process.Pid)
  192. run("link", "set", name, "netns", pid)
  193. for _, args := range [][]string{{"link", "set", "lo", "up"}, {"link", "set", name, "up"}, {"addr", "add", fmt.Sprintf("192.0.2.%d/30", i+1), "dev", name}} {
  194. out, err := exec.Command("nsenter", append([]string{"-t", pid, "-n", "ip"}, args...)...).CombinedOutput()
  195. if err != nil {
  196. t.Fatalf("namespace IP setup: %v %s", err, out)
  197. }
  198. }
  199. }
  200. // Model production: server cached path MTU 1380, client WireGuard link 1420.
  201. for i, mtu := range []string{"1380", "1420"} {
  202. out, err := exec.Command("nsenter", "-t", strconv.Itoa(peers[i].cmd.Process.Pid), "-n", "ip", "link", "set", []string{"atest-a", "atest-b"}[i], "mtu", mtu).CombinedOutput()
  203. if err != nil {
  204. t.Fatalf("set asymmetric MTU: %v %s", err, out)
  205. }
  206. }
  207. for _, role := range []string{"server", "client"} {
  208. _ = os.WriteFile(filepath.Join(dir, role+".start"), nil, 0600)
  209. waitForFile(t, filepath.Join(dir, role+".ready"))
  210. }
  211. for _, role := range []string{"server", "client"} {
  212. waitForFile(t, filepath.Join(dir, role+".connected"))
  213. }
  214. nsCmd := func(ctx context.Context, idx int, args ...string) *exec.Cmd {
  215. return exec.CommandContext(ctx, "nsenter", append([]string{"-t", strconv.Itoa(peers[idx].cmd.Process.Pid), "-n"}, args...)...)
  216. }
  217. for _, mode := range []string{"tcp-forward", "tcp-reverse", "udp"} {
  218. ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
  219. defer cancel()
  220. server := nsCmd(ctx, 0, "iperf3", "-s", "-1", "-B", "10.99.0.1")
  221. if err := server.Start(); err != nil {
  222. t.Fatal(err)
  223. }
  224. time.Sleep(150 * time.Millisecond)
  225. args := []string{"iperf3", "-c", "10.99.0.1", "-t", "2", "-J"}
  226. if compress {
  227. args = append(args, "--repeating-payload")
  228. }
  229. switch mode {
  230. case "tcp-forward":
  231. args = append(args, "-P", "2")
  232. case "tcp-reverse":
  233. args = append(args, "-R")
  234. case "udp":
  235. args = append(args, "-u", "-b", "20M", "-l", "1472")
  236. }
  237. out, err := nsCmd(ctx, 1, args...).Output()
  238. if err != nil {
  239. _ = server.Process.Kill()
  240. _ = server.Wait()
  241. t.Fatalf("%s: %v %s", mode, err, out)
  242. }
  243. if err = server.Wait(); err != nil {
  244. t.Fatal(err)
  245. }
  246. var result struct {
  247. Error string `json:"error"`
  248. End struct {
  249. Received struct {
  250. Bytes uint64 `json:"bytes"`
  251. BPS float64 `json:"bits_per_second"`
  252. } `json:"sum_received"`
  253. } `json:"end"`
  254. }
  255. if err = json.Unmarshal(out, &result); err != nil || result.Error != "" {
  256. t.Fatalf("iperf JSON: %v %s", err, out)
  257. }
  258. if result.End.Received.Bytes < 1<<20 {
  259. t.Fatalf("%s delivered too little data: %s", mode, strings.TrimSpace(string(out)))
  260. }
  261. t.Logf("%s: %.1f Mbit/s, %d received bytes", mode, result.End.Received.BPS/1e6, result.End.Received.Bytes)
  262. }
  263. if !compress {
  264. ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
  265. defer cancel()
  266. for i, name := range []string{"atest-a", "atest-b"} {
  267. out, err := nsCmd(ctx, i, "tc", "qdisc", "add", "dev", name, "root", "netem", "rate", "5mbit", "delay", "15ms", "limit", "64").CombinedOutput()
  268. if err != nil {
  269. t.Fatalf("constrain underlay: %v %s", err, out)
  270. }
  271. }
  272. cmd := nsCmd(ctx, 1, os.Args[0], "-test.run=^TestUDPOverAeroTunConstrainedLink$", "-test.v")
  273. cmd.Env = append(os.Environ(), "AEROTUN_BW_CLIENT=1")
  274. out, err := cmd.CombinedOutput()
  275. if err != nil {
  276. t.Fatalf("UDP feedback over constrained AeroTUN: %v\n%s", err, out)
  277. }
  278. t.Log(string(out))
  279. }
  280. }
  281. func testNamespaceDefaultRoute(t *testing.T) {
  282. run := func(args ...string) []byte {
  283. t.Helper()
  284. out, err := runIP(args...)
  285. if err != nil {
  286. t.Fatal(err)
  287. }
  288. return out
  289. }
  290. run("link", "add", "testwan", "type", "dummy")
  291. defer runIP("link", "del", "testwan")
  292. run("link", "set", "testwan", "up")
  293. run("addr", "add", "198.18.0.2/24", "dev", "testwan")
  294. run("route", "add", "default", "via", "198.18.0.1", "dev", "testwan")
  295. run("link", "add", "testoverlay", "type", "dummy")
  296. defer runIP("link", "del", "testoverlay")
  297. run("link", "set", "testoverlay", "up")
  298. r := routeManager{run: runIP}
  299. if err := r.install("testoverlay", net.ParseIP("203.0.113.9")); err != nil {
  300. t.Fatal(err)
  301. }
  302. defer r.cleanup()
  303. for _, check := range []struct{ target, dev string }{{"203.0.113.9", "testwan"}, {"8.8.8.8", "testoverlay"}, {"200.0.0.1", "testoverlay"}} {
  304. out := run("-j", "route", "get", check.target)
  305. var paths []struct {
  306. Dev string `json:"dev"`
  307. }
  308. if err := json.Unmarshal(out, &paths); err != nil || len(paths) != 1 || paths[0].Dev != check.dev {
  309. t.Fatalf("route to %s: %s %v", check.target, out, err)
  310. }
  311. }
  312. r.cleanup()
  313. out := run("route", "show", "default")
  314. if !strings.Contains(string(out), "via 198.18.0.1 dev testwan") {
  315. t.Fatalf("original default lost: %s", out)
  316. }
  317. if out = run("route", "show", "exact", "203.0.113.9/32"); len(out) != 0 {
  318. t.Fatalf("endpoint route leaked: %s", out)
  319. }
  320. }
  321. func TestUDPOverAeroTunConstrainedLink(t *testing.T) {
  322. if os.Getenv("AEROTUN_BW_CLIENT") != "1" {
  323. t.Skip("run by isolated integration harness")
  324. }
  325. for _, direction := range []string{"rx", "both"} {
  326. t.Run(direction, func(t *testing.T) {
  327. client := bwtest.NewClient()
  328. defer client.Stop()
  329. if err := client.Run(t.Context(), bwtest.SessionRequest{Target: "10.99.0.1", Port: 15200, Protocol: "udp", Direction: direction, Streams: 4, PacketSize: 1472, Duration: 10}); err != nil {
  330. t.Fatal(err)
  331. }
  332. deadline := time.Now().Add(13 * time.Second)
  333. for client.GetStatus().Running && time.Now().Before(deadline) {
  334. time.Sleep(20 * time.Millisecond)
  335. }
  336. st := client.GetStatus()
  337. if st.Running || st.Error != "" || st.Summary == nil {
  338. t.Fatalf("UDP benchmark failed: %+v", st)
  339. }
  340. expectedSize := 1264
  341. if os.Getenv("AEROTUN_TEST_VERSION") == "3" {
  342. expectedSize = 1292
  343. }
  344. if st.PacketSize != expectedSize {
  345. t.Fatalf("datagram not clamped to negotiated overlay MTU: %d", st.PacketSize)
  346. }
  347. if st.Summary.TotalRxBytes < 100000 || (direction == "both" && st.Summary.TotalTxBytes < 100000) {
  348. t.Fatalf("reverse transfer starved: %+v", st.Summary)
  349. }
  350. if st.Summary.AvgLossPct > 40 {
  351. t.Fatalf("automatic UDP flooded constrained link: %+v", st.Summary)
  352. }
  353. t.Logf("%s: confirmed TX %.2f / RX %.2f Mbit/s, loss %.1f%%, payload %d", direction, st.Summary.AvgTxMbps, st.Summary.AvgRxMbps, st.Summary.AvgLossPct, st.PacketSize)
  354. })
  355. }
  356. }