1// SPDX-License-Identifier: MIT
2
3package netsetup
4
5import (
6 "context"
7 "errors"
8 "net/netip"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "bnema.dev/ermite-runner/internal/cmdrun"
15 "bnema.dev/ermite-runner/internal/netpolicy"
16)
17
18func guest() netpolicy.Guest {
19 return netpolicy.Guest{Iface: "ertap0", Gateway: netip.MustParseAddr("127.0.0.1"), Addr: netip.MustParseAddr("10.200.0.2")}
20}
21
22func TestAttachDetach(t *testing.T) {
23 f := &cmdrun.Fake{}
24 n := New(f, 0, "")
25 t.Cleanup(n.Close)
26 ctx := context.Background()
27 if err := n.Attach(ctx, guest()); err != nil {
28 t.Skip("cannot bind loopback:", err)
29 }
30 if _, ok := n.DNS.Addr(guest().Gateway); !ok {
31 t.Fatal("DNS forwarder not listening on the gateway")
32 }
33 if err := n.Detach(ctx, "ertap0", guest().Gateway); err != nil {
34 t.Fatal(err)
35 }
36 if _, ok := n.DNS.Addr(guest().Gateway); ok {
37 t.Fatal("DNS forwarder still listening after Detach")
38 }
39 if len(f.Calls) != 2 || f.Calls[1].Stdin != netpolicy.Ruleset(nil) {
40 t.Fatalf("calls: %v", f.Lines())
41 }
42 if err := n.Detach(ctx, "ertap0", guest().Gateway); err != nil {
43 t.Fatalf("Detach must be idempotent: %v", err)
44 }
45}
46
47// The host network surface of a guest is DNS and the firewall policy, nothing
48// else: Attach opens no cache port, neither in the policy nor as a listener.
49func TestAttachOpensNoCachePort(t *testing.T) {
50 f := &cmdrun.Fake{}
51 n := New(f, 0, "")
52 t.Cleanup(n.Close)
53 if err := n.Attach(context.Background(), guest()); err != nil {
54 t.Skip("cannot bind loopback:", err)
55 }
56 rs := f.Calls[0].Stdin
57 for _, port := range []string{"8089", "9089"} {
58 if strings.Contains(rs, port) {
59 t.Fatalf("the policy mentions the cache port %s:\n%s", port, rs)
60 }
61 }
62 if !strings.Contains(rs, "dport 53 accept") {
63 t.Fatalf("the policy lost DNS:\n%s", rs)
64 }
65}
66
67func TestAttachRollsBackPolicyWhenDNSFails(t *testing.T) {
68 f := &cmdrun.Fake{}
69 n := New(f, 0, "")
70 t.Cleanup(n.Close)
71 ctx := context.Background()
72 if err := n.Attach(ctx, guest()); err != nil {
73 t.Skip(err)
74 }
75 before := len(f.Calls)
76 if err := n.Attach(ctx, guest()); err == nil { // already listening
77 t.Fatal("second Attach on the same gateway must fail")
78 }
79 if len(f.Calls) != before+2 {
80 t.Fatalf("expected add then rollback remove, got %v", f.Lines())
81 }
82}
83
84func TestAttachPolicyError(t *testing.T) {
85 boom := errors.New("nft failed")
86 n := New(&cmdrun.Fake{Handle: func(cmdrun.Call) ([]byte, error) { return nil, boom }}, 0, "")
87 if err := n.Attach(context.Background(), guest()); !errors.Is(err, boom) {
88 t.Fatalf("got %v", err)
89 }
90 if _, ok := n.DNS.Addr(guest().Gateway); ok {
91 t.Fatal("DNS must not start when the policy failed")
92 }
93}
94
95func TestEnableForwarding(t *testing.T) {
96 n := New(&cmdrun.Fake{}, 0, "")
97 n.ForwardFile = filepath.Join(t.TempDir(), "ip_forward")
98 _ = os.WriteFile(n.ForwardFile, []byte("0\n"), 0o644)
99 if err := n.EnableForwarding(); err != nil {
100 t.Fatal(err)
101 }
102 if b, _ := os.ReadFile(n.ForwardFile); strings.TrimSpace(string(b)) != "1" {
103 t.Fatalf("not enabled: %q", b)
104 }
105 n.ForwardFile = filepath.Join(t.TempDir(), "missing", "ip_forward")
106 if err := n.EnableForwarding(); err == nil || !strings.Contains(err.Error(), "ip_forward") {
107 t.Fatalf("want a clear error, got %v", err)
108 }
109}