v0.5.1 25d29f5

netsetup_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package netsetup
4
5import (
6	"context"
7	"errors"
8	"net"
9	"net/netip"
10	"os"
11	"path/filepath"
12	"strings"
13	"testing"
14
15	"bnema.dev/ermite-runner/internal/cmdrun"
16	"bnema.dev/ermite-runner/internal/netpolicy"
17	"bnema.dev/ermite-runner/internal/tcprelay"
18)
19
20// freePort returns a TCP port of 127.0.0.1 that was free when probed.
21func freePort(t *testing.T) uint16 {
22	t.Helper()
23	ln, err := net.Listen("tcp", "127.0.0.1:0")
24	if err != nil {
25		t.Skip("cannot bind loopback:", err)
26	}
27	defer func() { _ = ln.Close() }()
28	return uint16(ln.Addr().(*net.TCPAddr).Port)
29}
30
31func guest() netpolicy.Guest {
32	return netpolicy.Guest{Iface: "ertap0", Gateway: netip.MustParseAddr("127.0.0.1"), Addr: netip.MustParseAddr("10.200.0.2")}
33}
34
35func TestAttachDetach(t *testing.T) {
36	f := &cmdrun.Fake{}
37	n := New(f, 0, "")
38	t.Cleanup(n.Close)
39	ctx := context.Background()
40	if err := n.Attach(ctx, guest()); err != nil {
41		t.Skip("cannot bind loopback:", err)
42	}
43	if _, ok := n.DNS.Addr(guest().Gateway); !ok {
44		t.Fatal("DNS forwarder not listening on the gateway")
45	}
46	if err := n.Detach(ctx, "ertap0", guest().Gateway); err != nil {
47		t.Fatal(err)
48	}
49	if _, ok := n.DNS.Addr(guest().Gateway); ok {
50		t.Fatal("DNS forwarder still listening after Detach")
51	}
52	if len(f.Calls) != 2 || f.Calls[1].Stdin != netpolicy.Ruleset(nil) {
53		t.Fatalf("calls: %v", f.Lines())
54	}
55	if err := n.Detach(ctx, "ertap0", guest().Gateway); err != nil {
56		t.Fatalf("Detach must be idempotent: %v", err)
57	}
58}
59
60// The host network surface of a guest is DNS and the firewall policy, nothing
61// else: Attach opens no cache port, neither in the policy nor as a listener.
62func TestAttachOpensNoCachePort(t *testing.T) {
63	f := &cmdrun.Fake{}
64	n := New(f, 0, "")
65	t.Cleanup(n.Close)
66	if err := n.Attach(context.Background(), guest()); err != nil {
67		t.Skip("cannot bind loopback:", err)
68	}
69	rs := f.Calls[0].Stdin
70	for _, port := range []string{"8089", "9089"} {
71		if strings.Contains(rs, port) {
72			t.Fatalf("the policy mentions the cache port %s:\n%s", port, rs)
73		}
74	}
75	if !strings.Contains(rs, "dport 53 accept") {
76		t.Fatalf("the policy lost DNS:\n%s", rs)
77	}
78}
79
80func TestAttachRollsBackPolicyWhenDNSFails(t *testing.T) {
81	f := &cmdrun.Fake{}
82	n := New(f, 0, "")
83	t.Cleanup(n.Close)
84	ctx := context.Background()
85	if err := n.Attach(ctx, guest()); err != nil {
86		t.Skip(err)
87	}
88	before := len(f.Calls)
89	if err := n.Attach(ctx, guest()); err == nil { // already listening
90		t.Fatal("second Attach on the same gateway must fail")
91	}
92	if len(f.Calls) != before+2 {
93		t.Fatalf("expected add then rollback remove, got %v", f.Lines())
94	}
95}
96
97// With a relay, Attach relays the guest forge port of the gateway, Detach stops
98// it, and a guest without a forge port gets no relay listener.
99func TestAttachStartsAndStopsTheForgeRelay(t *testing.T) {
100	f := &cmdrun.Fake{}
101	n := New(f, 0, "")
102	n.Relay = tcprelay.New("127.0.0.1:1")
103	t.Cleanup(n.Close)
104	ctx := context.Background()
105	g := guest()
106	if err := n.Attach(ctx, g); err != nil {
107		t.Skip("cannot bind loopback:", err)
108	}
109	if _, ok := n.Relay.Addr(g.Gateway); ok {
110		t.Fatal("a relay was started for a guest without a forge port")
111	}
112	if err := n.Detach(ctx, g.Iface, g.Gateway); err != nil {
113		t.Fatal(err)
114	}
115	// A port that was free a moment ago: Attach itself starts the relay.
116	g.ForgePort = freePort(t)
117	if err := n.Attach(ctx, g); err != nil {
118		t.Fatalf("Attach with a forge port: %v", err)
119	}
120	if a, ok := n.Relay.Addr(g.Gateway); !ok || a.(*net.TCPAddr).Port != int(g.ForgePort) {
121		t.Fatalf("relay not listening on the forge port: %v %v", a, ok)
122	}
123	if err := n.Detach(ctx, g.Iface, g.Gateway); err != nil {
124		t.Fatal(err)
125	}
126	if _, ok := n.Relay.Addr(g.Gateway); ok {
127		t.Fatal("relay still listening after Detach")
128	}
129}
130
131// A relay that cannot bind rolls back the DNS listener and the policy.
132func TestAttachRollsBackWhenTheRelayFails(t *testing.T) {
133	f := &cmdrun.Fake{}
134	n := New(f, 0, "")
135	n.Relay = tcprelay.New("127.0.0.1:1")
136	t.Cleanup(n.Close)
137	ctx := context.Background()
138	g := guest()
139	g.ForgePort = 1 // privileged port: binding fails without CAP_NET_BIND_SERVICE
140	if err := n.Attach(ctx, g); err == nil {
141		t.Skip("port 1 could be bound here")
142	}
143	if _, ok := n.DNS.Addr(g.Gateway); ok {
144		t.Fatal("DNS still listening after a failed Attach")
145	}
146	if len(f.Calls) != 2 {
147		t.Fatalf("expected add then rollback remove, got %v", f.Lines())
148	}
149}
150
151func TestAttachPolicyError(t *testing.T) {
152	boom := errors.New("nft failed")
153	n := New(&cmdrun.Fake{Handle: func(cmdrun.Call) ([]byte, error) { return nil, boom }}, 0, "")
154	if err := n.Attach(context.Background(), guest()); !errors.Is(err, boom) {
155		t.Fatalf("got %v", err)
156	}
157	if _, ok := n.DNS.Addr(guest().Gateway); ok {
158		t.Fatal("DNS must not start when the policy failed")
159	}
160}
161
162func TestEnableForwarding(t *testing.T) {
163	n := New(&cmdrun.Fake{}, 0, "")
164	n.ForwardFile = filepath.Join(t.TempDir(), "ip_forward")
165	_ = os.WriteFile(n.ForwardFile, []byte("0\n"), 0o644)
166	if err := n.EnableForwarding(); err != nil {
167		t.Fatal(err)
168	}
169	if b, _ := os.ReadFile(n.ForwardFile); strings.TrimSpace(string(b)) != "1" {
170		t.Fatalf("not enabled: %q", b)
171	}
172	n.ForwardFile = filepath.Join(t.TempDir(), "missing", "ip_forward")
173	if err := n.EnableForwarding(); err == nil || !strings.Contains(err.Error(), "ip_forward") {
174		t.Fatalf("want a clear error, got %v", err)
175	}
176}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close