detached a706491

fix(netpolicy): check guest traffic before conntrack accepts, close gateway DNS to other interfaces, block more special-use ranges

commit
a70649190fd58a0d001450ba056b2ccf0c33cc74
author
brice · · 2026-10-12 15:53 UTC
parent
b5ce612
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +337 -24
A internal/netpolicy/netns_test.go +202 -0
@@ -0,0 +1,202 @@
1+// SPDX-License-Identifier: MIT
2+
3+package netpolicy
4+
5+import (
6+ "context"
7+ "fmt"
8+ "os"
9+ "os/exec"
10+ "strings"
11+ "testing"
12+ "time"
13+
14+ "bnema.dev/ermite-runner/internal/cmdrun"
15+)
16+
17+// TestRulesetBetweenTwoGuestsAndAForgedSource loads the real ruleset into the
18+// kernel and sends real packets: two guests (ertap0, ertap1) and an outside
19+// host hang off veth pairs, each in its own network namespace. It needs
20+// CAP_NET_ADMIN, which scripts/it-vm-userns.sh provides without root:
21+//
22+// ERMITE_RUNNER_IT_NET=1 unshare --user --map-root-user --net go test -run Netns ./internal/netpolicy
23+func TestRulesetBetweenTwoGuestsAndAForgedSource(t *testing.T) {
24+ if os.Getenv("ERMITE_RUNNER_IT_NET") != "1" {
25+ t.Skip("set ERMITE_RUNNER_IT_NET=1 inside a user+network namespace (scripts/it-vm-userns.sh)")
26+ }
27+ for _, c := range []string{"ip", "nft", "nsenter", "unshare", "python3"} {
28+ if _, err := exec.LookPath(c); err != nil {
29+ t.Skipf("%s not found", c)
30+ }
31+ }
32+ sh := func(args ...string) string {
33+ t.Helper()
34+ out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
35+ if err != nil {
36+ t.Fatalf("%s: %v: %s", strings.Join(args, " "), err, out)
37+ }
38+ return strings.TrimSpace(string(out))
39+ }
40+ if out, err := exec.Command("ip", "link", "add", "ertapprobe", "type", "dummy").CombinedOutput(); err != nil {
41+ t.Skipf("no CAP_NET_ADMIN: %v: %s", err, out)
42+ }
43+ sh("ip", "link", "del", "ertapprobe")
44+ sh("ip", "link", "set", "lo", "up")
45+
46+ // Three machines: guest A, guest B, an outside host.
47+ pid := map[string]int{}
48+ for _, n := range []string{"a", "b", "ext"} {
49+ c := exec.Command("unshare", "--net", "sleep", "120")
50+ if err := c.Start(); err != nil {
51+ t.Fatal(err)
52+ }
53+ pid[n] = c.Process.Pid
54+ t.Cleanup(func() { _ = c.Process.Kill(); _ = c.Wait() })
55+ }
56+ time.Sleep(300 * time.Millisecond)
57+ in := func(n string, args ...string) string {
58+ t.Helper()
59+ return sh(append([]string{"nsenter", fmt.Sprintf("--net=/proc/%d/ns/net", pid[n])}, args...)...)
60+ }
61+ link := func(host, n string) {
62+ sh("ip", "link", "add", host, "type", "veth", "peer", "name", "v0", "netns", fmt.Sprint(pid[n]))
63+ in(n, "ip", "link", "set", "lo", "up")
64+ in(n, "ip", "link", "set", "v0", "up")
65+ sh("sysctl", "-qw", "net.ipv4.conf."+host+".rp_filter=0")
66+ }
67+ link("ertap0", "a")
68+ link("ertap1", "b")
69+ link("extif", "ext")
70+ sh("sysctl", "-qw", "net.ipv4.conf.all.rp_filter=0")
71+ sh("sysctl", "-qw", "net.ipv4.ip_forward=1")
72+
73+ gA := guest(t, "ertap0", 0) // 10.200.0.1 / .2
74+ gB := guest(t, "ertap1", 1) // 10.200.0.5 / .6
75+ for _, g := range []Guest{gA, gB} {
76+ sh("ip", "addr", "add", g.Gateway.String()+"/30", "dev", g.Iface)
77+ sh("ip", "link", "set", g.Iface, "up")
78+ }
79+ sh("ip", "addr", "add", "203.0.114.1/24", "dev", "extif")
80+ sh("ip", "link", "set", "extif", "up")
81+ in("a", "ip", "addr", "add", gA.Addr.String()+"/30", "dev", "v0")
82+ in("a", "ip", "route", "add", "default", "via", gA.Gateway.String())
83+ in("b", "ip", "addr", "add", gB.Addr.String()+"/30", "dev", "v0")
84+ in("b", "ip", "route", "add", "default", "via", gB.Gateway.String())
85+ in("ext", "ip", "addr", "add", "203.0.114.2/24", "dev", "v0")
86+ in("ext", "ip", "route", "add", "default", "via", "203.0.114.1")
87+ // Guest A holds guest B's address too, to be able to forge it.
88+ in("a", "ip", "addr", "add", gB.Addr.String()+"/32", "dev", "lo")
89+
90+ m := NewManager(cmdrun.Exec{}, "nft")
91+ ctx := context.Background()
92+ t.Cleanup(func() { _ = m.Reset(ctx) })
93+ for _, g := range []Guest{gA, gB} {
94+ if err := m.Add(ctx, g); err != nil {
95+ t.Fatal(err)
96+ }
97+ }
98+
99+ // An outside UDP server: logs each payload with the source it saw, and
100+ // answers "pong".
101+ logf := t.TempDir() + "/ext.log"
102+ const server = `
103+import socket,sys
104+s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(("0.0.0.0",9999))
105+f=open(sys.argv[1],"a",buffering=1)
106+while True:
107+ d,a=s.recvfrom(100); f.write(d.decode()+" from "+a[0]+"\n"); s.sendto(b"pong",a)
108+`
109+ srv := exec.Command("nsenter", fmt.Sprintf("--net=/proc/%d/ns/net", pid["ext"]), "python3", "-c", server, logf)
110+ if err := srv.Start(); err != nil {
111+ t.Fatal(err)
112+ }
113+ t.Cleanup(func() { _ = srv.Process.Kill(); _ = srv.Wait() })
114+ time.Sleep(500 * time.Millisecond)
115+
116+ // udp sends one datagram from machine n (optionally bound to src:sport) and
117+ // reports whether an answer came back.
118+ const client = `
119+import socket,sys
120+dst,payload,src,sport=sys.argv[1],sys.argv[2],sys.argv[3],int(sys.argv[4])
121+s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(2)
122+if src: s.bind((src,sport))
123+s.sendto(payload.encode(),(dst,int(sys.argv[5]) if len(sys.argv)>5 else 9999))
124+try: s.recv(10); print("answer")
125+except Exception: print("silence")
126+`
127+ udp := func(n, dst, payload, src string, sport int, dport int) string {
128+ return in(n, "python3", "-c", client, dst, payload, src, fmt.Sprint(sport), fmt.Sprint(dport))
129+ }
130+ seen := func() string { b, _ := os.ReadFile(logf); return string(b) }
131+
132+ // Control: a guest reaches the outside host and gets the reply (NAT and the
133+ // established path work, so the drops below are the policy, not a broken rig).
134+ if got := udp("a", "203.0.114.2", "ctl-a", "", 0, 9999); got != "answer" {
135+ t.Fatalf("control: guest A cannot reach the outside host: %s", got)
136+ }
137+
138+ // Guests cannot reach each other, directly or through the other's gateway.
139+ if got := udp("a", gB.Addr.String(), "a-to-b", "", 0, 9999); got != "silence" {
140+ t.Errorf("guest A reached guest B: %s", got)
141+ }
142+ // Forged reply (S1): B opens a flow to the outside host from port 5555 and
143+ // keeps listening. Guest A then forges a reply of that flow: source = the
144+ // outside host's address and port, destination = the host's masquerade
145+ // address and B's NAT port. Conntrack would map it onto B's flow and deliver
146+ // it to B as "established" traffic if an established-accept came before the
147+ // source check.
148+ in("a", "ip", "addr", "add", "203.0.114.2/32", "dev", "lo")
149+ bLog := t.TempDir() + "/b.log"
150+ const listener = `
151+import socket,sys
152+s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(6)
153+s.bind(("10.200.0.6",5555)); s.sendto(b"open-flow",("203.0.114.2",9999))
154+f=open(sys.argv[1],"a",buffering=1)
155+try:
156+ while True:
157+ d,a=s.recvfrom(100); f.write(d.decode()+" from "+a[0]+"\n")
158+except Exception: pass
159+`
160+ bProc := exec.Command("nsenter", fmt.Sprintf("--net=/proc/%d/ns/net", pid["b"]), "python3", "-c", listener, bLog)
161+ if err := bProc.Start(); err != nil {
162+ t.Fatal(err)
163+ }
164+ time.Sleep(1500 * time.Millisecond)
165+ if got := udp("a", "203.0.114.1", "forged-reply-to-b", "203.0.114.2", 9999, 5555); got != "silence" {
166+ t.Errorf("unexpected answer to the forgery: %s", got)
167+ }
168+ _ = bProc.Wait()
169+ if b, _ := os.ReadFile(bLog); strings.Contains(string(b), "forged-reply-to-b") {
170+ t.Errorf("guest B received a reply forged by guest A through conntrack:\n%s", b)
171+ }
172+ // A forged source address on a plain forwarded packet is dropped as well.
173+ if got := udp("a", "203.0.114.2", "forged-src", gB.Addr.String(), 7777, 9999); got != "silence" {
174+ t.Errorf("a packet with another guest's source address got through: %s", got)
175+ }
176+ // S6: guest A cannot use guest B's DNS gateway, nor the host's other ports.
177+ if got := udp("a", gB.Gateway.String(), "dns-a-to-b-gw", "", 0, 53); got != "silence" {
178+ t.Errorf("guest A reached guest B's gateway on 53: %s", got)
179+ }
180+ // The outside host cannot open connections to a guest.
181+ if got := in("ext", "python3", "-c", client, gA.Addr.String(), "ext-to-a", "", "0", "9999"); got != "silence" {
182+ t.Errorf("the outside host reached guest A: %s", got)
183+ }
184+ // Special-use ranges (S10) are dropped even though the rig could route them.
185+ in("ext", "ip", "addr", "add", "198.51.100.7/32", "dev", "lo")
186+ sh("ip", "route", "add", "198.51.100.7/32", "via", "203.0.114.2")
187+ if got := udp("a", "198.51.100.7", "doc-range", "", 0, 9999); got != "silence" {
188+ t.Errorf("TEST-NET-2 reached: %s", got)
189+ }
190+
191+ out := seen()
192+ for _, bad := range []string{"forged-src", "a-to-b", "ext-to-a", "doc-range"} {
193+ if strings.Contains(out, bad) {
194+ t.Errorf("outside host saw %q:\n%s", bad, out)
195+ }
196+ }
197+ for _, good := range []string{"ctl-a", "open-flow"} {
198+ if !strings.Contains(out, good) {
199+ t.Errorf("outside host did not see %q:\n%s", good, out)
200+ }
201+ }
202+}
@@ -0,0 +1,202 @@
1+// SPDX-License-Identifier: MIT
2+
3+package netpolicy
4+
5+import (
6+ "context"
7+ "fmt"
8+ "os"
9+ "os/exec"
10+ "strings"
11+ "testing"
12+ "time"
13+
14+ "bnema.dev/ermite-runner/internal/cmdrun"
15+)
16+
17+// TestRulesetBetweenTwoGuestsAndAForgedSource loads the real ruleset into the
18+// kernel and sends real packets: two guests (ertap0, ertap1) and an outside
19+// host hang off veth pairs, each in its own network namespace. It needs
20+// CAP_NET_ADMIN, which scripts/it-vm-userns.sh provides without root:
21+//
22+// ERMITE_RUNNER_IT_NET=1 unshare --user --map-root-user --net go test -run Netns ./internal/netpolicy
23+func TestRulesetBetweenTwoGuestsAndAForgedSource(t *testing.T) {
24+ if os.Getenv("ERMITE_RUNNER_IT_NET") != "1" {
25+ t.Skip("set ERMITE_RUNNER_IT_NET=1 inside a user+network namespace (scripts/it-vm-userns.sh)")
26+ }
27+ for _, c := range []string{"ip", "nft", "nsenter", "unshare", "python3"} {
28+ if _, err := exec.LookPath(c); err != nil {
29+ t.Skipf("%s not found", c)
30+ }
31+ }
32+ sh := func(args ...string) string {
33+ t.Helper()
34+ out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
35+ if err != nil {
36+ t.Fatalf("%s: %v: %s", strings.Join(args, " "), err, out)
37+ }
38+ return strings.TrimSpace(string(out))
39+ }
40+ if out, err := exec.Command("ip", "link", "add", "ertapprobe", "type", "dummy").CombinedOutput(); err != nil {
41+ t.Skipf("no CAP_NET_ADMIN: %v: %s", err, out)
42+ }
43+ sh("ip", "link", "del", "ertapprobe")
44+ sh("ip", "link", "set", "lo", "up")
45+
46+ // Three machines: guest A, guest B, an outside host.
47+ pid := map[string]int{}
48+ for _, n := range []string{"a", "b", "ext"} {
49+ c := exec.Command("unshare", "--net", "sleep", "120")
50+ if err := c.Start(); err != nil {
51+ t.Fatal(err)
52+ }
53+ pid[n] = c.Process.Pid
54+ t.Cleanup(func() { _ = c.Process.Kill(); _ = c.Wait() })
55+ }
56+ time.Sleep(300 * time.Millisecond)
57+ in := func(n string, args ...string) string {
58+ t.Helper()
59+ return sh(append([]string{"nsenter", fmt.Sprintf("--net=/proc/%d/ns/net", pid[n])}, args...)...)
60+ }
61+ link := func(host, n string) {
62+ sh("ip", "link", "add", host, "type", "veth", "peer", "name", "v0", "netns", fmt.Sprint(pid[n]))
63+ in(n, "ip", "link", "set", "lo", "up")
64+ in(n, "ip", "link", "set", "v0", "up")
65+ sh("sysctl", "-qw", "net.ipv4.conf."+host+".rp_filter=0")
66+ }
67+ link("ertap0", "a")
68+ link("ertap1", "b")
69+ link("extif", "ext")
70+ sh("sysctl", "-qw", "net.ipv4.conf.all.rp_filter=0")
71+ sh("sysctl", "-qw", "net.ipv4.ip_forward=1")
72+
73+ gA := guest(t, "ertap0", 0) // 10.200.0.1 / .2
74+ gB := guest(t, "ertap1", 1) // 10.200.0.5 / .6
75+ for _, g := range []Guest{gA, gB} {
76+ sh("ip", "addr", "add", g.Gateway.String()+"/30", "dev", g.Iface)
77+ sh("ip", "link", "set", g.Iface, "up")
78+ }
79+ sh("ip", "addr", "add", "203.0.114.1/24", "dev", "extif")
80+ sh("ip", "link", "set", "extif", "up")
81+ in("a", "ip", "addr", "add", gA.Addr.String()+"/30", "dev", "v0")
82+ in("a", "ip", "route", "add", "default", "via", gA.Gateway.String())
83+ in("b", "ip", "addr", "add", gB.Addr.String()+"/30", "dev", "v0")
84+ in("b", "ip", "route", "add", "default", "via", gB.Gateway.String())
85+ in("ext", "ip", "addr", "add", "203.0.114.2/24", "dev", "v0")
86+ in("ext", "ip", "route", "add", "default", "via", "203.0.114.1")
87+ // Guest A holds guest B's address too, to be able to forge it.
88+ in("a", "ip", "addr", "add", gB.Addr.String()+"/32", "dev", "lo")
89+
90+ m := NewManager(cmdrun.Exec{}, "nft")
91+ ctx := context.Background()
92+ t.Cleanup(func() { _ = m.Reset(ctx) })
93+ for _, g := range []Guest{gA, gB} {
94+ if err := m.Add(ctx, g); err != nil {
95+ t.Fatal(err)
96+ }
97+ }
98+
99+ // An outside UDP server: logs each payload with the source it saw, and
100+ // answers "pong".
101+ logf := t.TempDir() + "/ext.log"
102+ const server = `
103+import socket,sys
104+s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(("0.0.0.0",9999))
105+f=open(sys.argv[1],"a",buffering=1)
106+while True:
107+ d,a=s.recvfrom(100); f.write(d.decode()+" from "+a[0]+"\n"); s.sendto(b"pong",a)
108+`
109+ srv := exec.Command("nsenter", fmt.Sprintf("--net=/proc/%d/ns/net", pid["ext"]), "python3", "-c", server, logf)
110+ if err := srv.Start(); err != nil {
111+ t.Fatal(err)
112+ }
113+ t.Cleanup(func() { _ = srv.Process.Kill(); _ = srv.Wait() })
114+ time.Sleep(500 * time.Millisecond)
115+
116+ // udp sends one datagram from machine n (optionally bound to src:sport) and
117+ // reports whether an answer came back.
118+ const client = `
119+import socket,sys
120+dst,payload,src,sport=sys.argv[1],sys.argv[2],sys.argv[3],int(sys.argv[4])
121+s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(2)
122+if src: s.bind((src,sport))
123+s.sendto(payload.encode(),(dst,int(sys.argv[5]) if len(sys.argv)>5 else 9999))
124+try: s.recv(10); print("answer")
125+except Exception: print("silence")
126+`
127+ udp := func(n, dst, payload, src string, sport int, dport int) string {
128+ return in(n, "python3", "-c", client, dst, payload, src, fmt.Sprint(sport), fmt.Sprint(dport))
129+ }
130+ seen := func() string { b, _ := os.ReadFile(logf); return string(b) }
131+
132+ // Control: a guest reaches the outside host and gets the reply (NAT and the
133+ // established path work, so the drops below are the policy, not a broken rig).
134+ if got := udp("a", "203.0.114.2", "ctl-a", "", 0, 9999); got != "answer" {
135+ t.Fatalf("control: guest A cannot reach the outside host: %s", got)
136+ }
137+
138+ // Guests cannot reach each other, directly or through the other's gateway.
139+ if got := udp("a", gB.Addr.String(), "a-to-b", "", 0, 9999); got != "silence" {
140+ t.Errorf("guest A reached guest B: %s", got)
141+ }
142+ // Forged reply (S1): B opens a flow to the outside host from port 5555 and
143+ // keeps listening. Guest A then forges a reply of that flow: source = the
144+ // outside host's address and port, destination = the host's masquerade
145+ // address and B's NAT port. Conntrack would map it onto B's flow and deliver
146+ // it to B as "established" traffic if an established-accept came before the
147+ // source check.
148+ in("a", "ip", "addr", "add", "203.0.114.2/32", "dev", "lo")
149+ bLog := t.TempDir() + "/b.log"
150+ const listener = `
151+import socket,sys
152+s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(6)
153+s.bind(("10.200.0.6",5555)); s.sendto(b"open-flow",("203.0.114.2",9999))
154+f=open(sys.argv[1],"a",buffering=1)
155+try:
156+ while True:
157+ d,a=s.recvfrom(100); f.write(d.decode()+" from "+a[0]+"\n")
158+except Exception: pass
159+`
160+ bProc := exec.Command("nsenter", fmt.Sprintf("--net=/proc/%d/ns/net", pid["b"]), "python3", "-c", listener, bLog)
161+ if err := bProc.Start(); err != nil {
162+ t.Fatal(err)
163+ }
164+ time.Sleep(1500 * time.Millisecond)
165+ if got := udp("a", "203.0.114.1", "forged-reply-to-b", "203.0.114.2", 9999, 5555); got != "silence" {
166+ t.Errorf("unexpected answer to the forgery: %s", got)
167+ }
168+ _ = bProc.Wait()
169+ if b, _ := os.ReadFile(bLog); strings.Contains(string(b), "forged-reply-to-b") {
170+ t.Errorf("guest B received a reply forged by guest A through conntrack:\n%s", b)
171+ }
172+ // A forged source address on a plain forwarded packet is dropped as well.
173+ if got := udp("a", "203.0.114.2", "forged-src", gB.Addr.String(), 7777, 9999); got != "silence" {
174+ t.Errorf("a packet with another guest's source address got through: %s", got)
175+ }
176+ // S6: guest A cannot use guest B's DNS gateway, nor the host's other ports.
177+ if got := udp("a", gB.Gateway.String(), "dns-a-to-b-gw", "", 0, 53); got != "silence" {
178+ t.Errorf("guest A reached guest B's gateway on 53: %s", got)
179+ }
180+ // The outside host cannot open connections to a guest.
181+ if got := in("ext", "python3", "-c", client, gA.Addr.String(), "ext-to-a", "", "0", "9999"); got != "silence" {
182+ t.Errorf("the outside host reached guest A: %s", got)
183+ }
184+ // Special-use ranges (S10) are dropped even though the rig could route them.
185+ in("ext", "ip", "addr", "add", "198.51.100.7/32", "dev", "lo")
186+ sh("ip", "route", "add", "198.51.100.7/32", "via", "203.0.114.2")
187+ if got := udp("a", "198.51.100.7", "doc-range", "", 0, 9999); got != "silence" {
188+ t.Errorf("TEST-NET-2 reached: %s", got)
189+ }
190+
191+ out := seen()
192+ for _, bad := range []string{"forged-src", "a-to-b", "ext-to-a", "doc-range"} {
193+ if strings.Contains(out, bad) {
194+ t.Errorf("outside host saw %q:\n%s", bad, out)
195+ }
196+ }
197+ for _, good := range []string{"ctl-a", "open-flow"} {
198+ if !strings.Contains(out, good) {
199+ t.Errorf("outside host did not see %q:\n%s", good, out)
200+ }
201+ }
202+}
M internal/netpolicy/netpolicy.go +52 -14
@@ -10,7 +10,8 @@
1010 // container):
1111 // - to the host itself (input hook): only DNS (udp/tcp 53) to the gateway
1212 // address of that guest, where the runner's DNS forwarder listens; every
13-// other packet is dropped;
13+// other packet is dropped, and port 53 of a gateway is closed to every
14+// other interface;
1415 // - IPv6 from the guest is dropped (the guest also disables IPv6);
1516 // - forwarded traffic must carry the guest's own source address;
1617 // - forwarded traffic to RFC 1918, CGNAT 100.64.0.0/10 (VPN overlays),
@@ -42,15 +43,29 @@ var IfacePrefixes = []string{"ertap", "erbr"}
4243
4344 // blockedV4 are the destinations a guest may never reach.
4445 var blockedV4 = []string{
45- "0.0.0.0/8", // "this network"
46- "10.0.0.0/8", // RFC 1918
47- "100.64.0.0/10", // RFC 6598 shared address space (CGNAT, VPN overlays)
48- "127.0.0.0/8", // loopback
49- "169.254.0.0/16", // link-local, incl. the 169.254.169.254 metadata service
50- "172.16.0.0/12", // RFC 1918
51- "192.168.0.0/16", // RFC 1918
52- "224.0.0.0/4", // multicast
53- "240.0.0.0/4", // reserved, incl. limited broadcast
46+ "0.0.0.0/8", // "this network"
47+ "10.0.0.0/8", // RFC 1918
48+ "100.64.0.0/10", // RFC 6598 shared address space (CGNAT, VPN overlays)
49+ "127.0.0.0/8", // loopback
50+ "169.254.0.0/16", // link-local, incl. the 169.254.169.254 metadata service
51+ "172.16.0.0/12", // RFC 1918
52+ "192.0.0.0/24", // IETF protocol assignments
53+ "192.0.2.0/24", // TEST-NET-1
54+ "192.168.0.0/16", // RFC 1918
55+ "198.18.0.0/15", // benchmarking
56+ "198.51.100.0/24", // TEST-NET-2
57+ "203.0.113.0/24", // TEST-NET-3
58+ "224.0.0.0/4", // multicast
59+ "240.0.0.0/4", // reserved, incl. limited broadcast
60+}
61+
62+// prefixSet renders IfacePrefixes as an nft anonymous set of interface wildcards.
63+func prefixSet() string {
64+ q := make([]string, len(IfacePrefixes))
65+ for i, p := range IfacePrefixes {
66+ q[i] = `"` + p + `*"`
67+ }
68+ return "{ " + strings.Join(q, ", ") + " }"
5469 }
5570
5671 // Guest is one sandbox network attachment.
@@ -129,19 +144,31 @@ func Ruleset(guests []Guest) string {
129144 for _, p := range IfacePrefixes {
130145 fmt.Fprintf(&b, "\t\tiifname \"%s*\" meta nfproto ipv6 drop\n", p)
131146 }
147+ // The DNS forwarder of a guest answers that guest only: port 53 of a gateway
148+ // address is dropped from every other interface (other guests, the LAN).
149+ for _, g := range gs {
150+ for _, proto := range []string{"udp", "tcp"} {
151+ fmt.Fprintf(&b, "\t\tip daddr %s %s dport 53 iifname != \"%s\" drop\n", g.Gateway, proto, g.Iface)
152+ }
153+ }
132154 for _, g := range gs {
133155 for _, proto := range []string{"udp", "tcp"} {
134156 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip saddr %s ip daddr %s %s dport 53 accept\n", g.Iface, g.Addr, g.Gateway, proto)
135157 }
136158 fmt.Fprintf(&b, "\t\tiifname \"%s\" drop\n", g.Iface)
137159 }
160+ // An interface with our prefix that no live guest owns (a leftover) is
161+ // dropped as well.
162+ for _, p := range IfacePrefixes {
163+ fmt.Fprintf(&b, "\t\tiifname \"%s*\" drop\n", p)
164+ }
138165 b.WriteString("\t}\n")
139166
167+ // Forward chain. Order matters: everything a guest sends is checked (family,
168+ // source address, destination ranges) BEFORE any "established" accept, or a
169+ // guest could forge the source of another guest's flow and have it accepted
170+ // as a reply.
140171 b.WriteString("\tchain forward {\n\t\ttype filter hook forward priority filter; policy accept;\n")
141- for _, p := range IfacePrefixes {
142- fmt.Fprintf(&b, "\t\toifname \"%s*\" ct state established,related accept\n", p)
143- fmt.Fprintf(&b, "\t\toifname \"%s*\" drop\n", p)
144- }
145172 for _, p := range IfacePrefixes {
146173 fmt.Fprintf(&b, "\t\tiifname \"%s*\" meta nfproto ipv6 drop\n", p)
147174 }
@@ -149,6 +176,17 @@ func Ruleset(guests []Guest) string {
149176 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip saddr != %s drop\n", g.Iface, g.Addr)
150177 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip daddr @blocked_v4 drop\n", g.Iface)
151178 }
179+ // Replies from outside to a guest; never for packets that come from a guest.
180+ b.WriteString("\t\tiifname != " + prefixSet() + " oifname " + prefixSet() + " ct state established,related accept\n")
181+ for _, p := range IfacePrefixes {
182+ fmt.Fprintf(&b, "\t\toifname \"%s*\" drop\n", p)
183+ }
184+ for _, g := range gs {
185+ fmt.Fprintf(&b, "\t\tiifname \"%s\" accept\n", g.Iface)
186+ }
187+ for _, p := range IfacePrefixes {
188+ fmt.Fprintf(&b, "\t\tiifname \"%s*\" drop\n", p)
189+ }
152190 b.WriteString("\t}\n")
153191
154192 b.WriteString("\tchain postrouting {\n\t\ttype nat hook postrouting priority srcnat; policy accept;\n")
@@ -10,7 +10,8 @@
10 // container):10 // container):
11 // - to the host itself (input hook): only DNS (udp/tcp 53) to the gateway11 // - to the host itself (input hook): only DNS (udp/tcp 53) to the gateway
12 // address of that guest, where the runner's DNS forwarder listens; every12 // address of that guest, where the runner's DNS forwarder listens; every
13-// other packet is dropped;13+// other packet is dropped, and port 53 of a gateway is closed to every
14+// other interface;
14 // - IPv6 from the guest is dropped (the guest also disables IPv6);15 // - IPv6 from the guest is dropped (the guest also disables IPv6);
15 // - forwarded traffic must carry the guest's own source address;16 // - forwarded traffic must carry the guest's own source address;
16 // - forwarded traffic to RFC 1918, CGNAT 100.64.0.0/10 (VPN overlays),17 // - forwarded traffic to RFC 1918, CGNAT 100.64.0.0/10 (VPN overlays),
@@ -42,15 +43,29 @@ var IfacePrefixes = []string{"ertap", "erbr"}
42 43
43 // blockedV4 are the destinations a guest may never reach.44 // blockedV4 are the destinations a guest may never reach.
44 var blockedV4 = []string{45 var blockedV4 = []string{
45- "0.0.0.0/8", // "this network"46+ "0.0.0.0/8", // "this network"
46- "10.0.0.0/8", // RFC 191847+ "10.0.0.0/8", // RFC 1918
47- "100.64.0.0/10", // RFC 6598 shared address space (CGNAT, VPN overlays)48+ "100.64.0.0/10", // RFC 6598 shared address space (CGNAT, VPN overlays)
48- "127.0.0.0/8", // loopback49+ "127.0.0.0/8", // loopback
49- "169.254.0.0/16", // link-local, incl. the 169.254.169.254 metadata service50+ "169.254.0.0/16", // link-local, incl. the 169.254.169.254 metadata service
50- "172.16.0.0/12", // RFC 191851+ "172.16.0.0/12", // RFC 1918
51- "192.168.0.0/16", // RFC 191852+ "192.0.0.0/24", // IETF protocol assignments
52- "224.0.0.0/4", // multicast53+ "192.0.2.0/24", // TEST-NET-1
53- "240.0.0.0/4", // reserved, incl. limited broadcast54+ "192.168.0.0/16", // RFC 1918
55+ "198.18.0.0/15", // benchmarking
56+ "198.51.100.0/24", // TEST-NET-2
57+ "203.0.113.0/24", // TEST-NET-3
58+ "224.0.0.0/4", // multicast
59+ "240.0.0.0/4", // reserved, incl. limited broadcast
60+}
61+
62+// prefixSet renders IfacePrefixes as an nft anonymous set of interface wildcards.
63+func prefixSet() string {
64+ q := make([]string, len(IfacePrefixes))
65+ for i, p := range IfacePrefixes {
66+ q[i] = `"` + p + `*"`
67+ }
68+ return "{ " + strings.Join(q, ", ") + " }"
54 }69 }
55 70
56 // Guest is one sandbox network attachment.71 // Guest is one sandbox network attachment.
@@ -129,19 +144,31 @@ func Ruleset(guests []Guest) string {
129 for _, p := range IfacePrefixes {144 for _, p := range IfacePrefixes {
130 fmt.Fprintf(&b, "\t\tiifname \"%s*\" meta nfproto ipv6 drop\n", p)145 fmt.Fprintf(&b, "\t\tiifname \"%s*\" meta nfproto ipv6 drop\n", p)
131 }146 }
147+ // The DNS forwarder of a guest answers that guest only: port 53 of a gateway
148+ // address is dropped from every other interface (other guests, the LAN).
149+ for _, g := range gs {
150+ for _, proto := range []string{"udp", "tcp"} {
151+ fmt.Fprintf(&b, "\t\tip daddr %s %s dport 53 iifname != \"%s\" drop\n", g.Gateway, proto, g.Iface)
152+ }
153+ }
132 for _, g := range gs {154 for _, g := range gs {
133 for _, proto := range []string{"udp", "tcp"} {155 for _, proto := range []string{"udp", "tcp"} {
134 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip saddr %s ip daddr %s %s dport 53 accept\n", g.Iface, g.Addr, g.Gateway, proto)156 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip saddr %s ip daddr %s %s dport 53 accept\n", g.Iface, g.Addr, g.Gateway, proto)
135 }157 }
136 fmt.Fprintf(&b, "\t\tiifname \"%s\" drop\n", g.Iface)158 fmt.Fprintf(&b, "\t\tiifname \"%s\" drop\n", g.Iface)
137 }159 }
160+ // An interface with our prefix that no live guest owns (a leftover) is
161+ // dropped as well.
162+ for _, p := range IfacePrefixes {
163+ fmt.Fprintf(&b, "\t\tiifname \"%s*\" drop\n", p)
164+ }
138 b.WriteString("\t}\n")165 b.WriteString("\t}\n")
139 166
167+ // Forward chain. Order matters: everything a guest sends is checked (family,
168+ // source address, destination ranges) BEFORE any "established" accept, or a
169+ // guest could forge the source of another guest's flow and have it accepted
170+ // as a reply.
140 b.WriteString("\tchain forward {\n\t\ttype filter hook forward priority filter; policy accept;\n")171 b.WriteString("\tchain forward {\n\t\ttype filter hook forward priority filter; policy accept;\n")
141- for _, p := range IfacePrefixes {
142- fmt.Fprintf(&b, "\t\toifname \"%s*\" ct state established,related accept\n", p)
143- fmt.Fprintf(&b, "\t\toifname \"%s*\" drop\n", p)
144- }
145 for _, p := range IfacePrefixes {172 for _, p := range IfacePrefixes {
146 fmt.Fprintf(&b, "\t\tiifname \"%s*\" meta nfproto ipv6 drop\n", p)173 fmt.Fprintf(&b, "\t\tiifname \"%s*\" meta nfproto ipv6 drop\n", p)
147 }174 }
@@ -149,6 +176,17 @@ func Ruleset(guests []Guest) string {
149 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip saddr != %s drop\n", g.Iface, g.Addr)176 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip saddr != %s drop\n", g.Iface, g.Addr)
150 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip daddr @blocked_v4 drop\n", g.Iface)177 fmt.Fprintf(&b, "\t\tiifname \"%s\" ip daddr @blocked_v4 drop\n", g.Iface)
151 }178 }
179+ // Replies from outside to a guest; never for packets that come from a guest.
180+ b.WriteString("\t\tiifname != " + prefixSet() + " oifname " + prefixSet() + " ct state established,related accept\n")
181+ for _, p := range IfacePrefixes {
182+ fmt.Fprintf(&b, "\t\toifname \"%s*\" drop\n", p)
183+ }
184+ for _, g := range gs {
185+ fmt.Fprintf(&b, "\t\tiifname \"%s\" accept\n", g.Iface)
186+ }
187+ for _, p := range IfacePrefixes {
188+ fmt.Fprintf(&b, "\t\tiifname \"%s*\" drop\n", p)
189+ }
152 b.WriteString("\t}\n")190 b.WriteString("\t}\n")
153 191
154 b.WriteString("\tchain postrouting {\n\t\ttype nat hook postrouting priority srcnat; policy accept;\n")192 b.WriteString("\tchain postrouting {\n\t\ttype nat hook postrouting priority srcnat; policy accept;\n")
M internal/netpolicy/netpolicy_test.go +58 -0
@@ -78,6 +78,64 @@ func TestRulesetPolicyInvariants(t *testing.T) {
7878 }
7979 }
8080
81+// S1: nothing a guest sends may be accepted by a conntrack rule before the
82+// per-guest checks: a forged "reply" from guest A to guest B's flow would
83+// otherwise be accepted as established traffic.
84+func TestForwardChainChecksGuestTrafficBeforeEstablished(t *testing.T) {
85+ rs := Ruleset([]Guest{guest(t, "ertap0", 0), guest(t, "erbr1", 1)})
86+ fwd := rs[strings.Index(rs, "chain forward"):strings.Index(rs, "chain postrouting")]
87+ lines := strings.Split(fwd, "\n")
88+ idx := func(sub string) int {
89+ for i, l := range lines {
90+ if strings.Contains(l, sub) {
91+ return i
92+ }
93+ }
94+ t.Fatalf("forward chain lacks %q:\n%s", sub, fwd)
95+ return -1
96+ }
97+ est := idx("ct state established,related accept")
98+ for _, must := range []string{
99+ `iifname "ertap*" meta nfproto ipv6 drop`, `iifname "erbr*" meta nfproto ipv6 drop`,
100+ `iifname "ertap0" ip saddr != 10.200.0.2 drop`, `iifname "ertap0" ip daddr @blocked_v4 drop`,
101+ `iifname "erbr1" ip saddr != 10.200.0.6 drop`, `iifname "erbr1" ip daddr @blocked_v4 drop`,
102+ } {
103+ if idx(must) > est {
104+ t.Errorf("%q must come before the established accept", must)
105+ }
106+ }
107+ // And the established accept itself never applies to packets that enter
108+ // through a sandbox interface.
109+ if !strings.Contains(lines[est], `iifname != { "ertap*", "erbr*" }`) {
110+ t.Errorf("established accept must exclude sandbox ingress: %s", lines[est])
111+ }
112+}
113+
114+// S6: port 53 of a gateway address is closed to every interface but its guest's.
115+func TestInputChainClosesGatewayDNSToOtherInterfaces(t *testing.T) {
116+ rs := Ruleset([]Guest{guest(t, "ertap0", 0), guest(t, "ertap1", 1)})
117+ for _, want := range []string{
118+ `ip daddr 10.200.0.1 udp dport 53 iifname != "ertap0" drop`,
119+ `ip daddr 10.200.0.1 tcp dport 53 iifname != "ertap0" drop`,
120+ `ip daddr 10.200.0.5 udp dport 53 iifname != "ertap1" drop`,
121+ `ip daddr 10.200.0.5 tcp dport 53 iifname != "ertap1" drop`,
122+ } {
123+ if !strings.Contains(rs, want) {
124+ t.Errorf("lacks %q", want)
125+ }
126+ }
127+}
128+
129+// S10: documentation, test-net and benchmarking ranges are blocked too.
130+func TestBlockedSetCoversSpecialUseRanges(t *testing.T) {
131+ rs := Ruleset([]Guest{guest(t, "ertap0", 0)})
132+ for _, r := range []string{"192.0.0.0/24", "192.0.2.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4"} {
133+ if !strings.Contains(rs, r) {
134+ t.Errorf("blocked set lacks %s", r)
135+ }
136+ }
137+}
138+
81139 func TestSubnet(t *testing.T) {
82140 base := netip.MustParsePrefix("10.200.0.0/16")
83141 n, err := Subnet(base, 3)
@@ -78,6 +78,64 @@ func TestRulesetPolicyInvariants(t *testing.T) {
78 }78 }
79 }79 }
80 80
81+// S1: nothing a guest sends may be accepted by a conntrack rule before the
82+// per-guest checks: a forged "reply" from guest A to guest B's flow would
83+// otherwise be accepted as established traffic.
84+func TestForwardChainChecksGuestTrafficBeforeEstablished(t *testing.T) {
85+ rs := Ruleset([]Guest{guest(t, "ertap0", 0), guest(t, "erbr1", 1)})
86+ fwd := rs[strings.Index(rs, "chain forward"):strings.Index(rs, "chain postrouting")]
87+ lines := strings.Split(fwd, "\n")
88+ idx := func(sub string) int {
89+ for i, l := range lines {
90+ if strings.Contains(l, sub) {
91+ return i
92+ }
93+ }
94+ t.Fatalf("forward chain lacks %q:\n%s", sub, fwd)
95+ return -1
96+ }
97+ est := idx("ct state established,related accept")
98+ for _, must := range []string{
99+ `iifname "ertap*" meta nfproto ipv6 drop`, `iifname "erbr*" meta nfproto ipv6 drop`,
100+ `iifname "ertap0" ip saddr != 10.200.0.2 drop`, `iifname "ertap0" ip daddr @blocked_v4 drop`,
101+ `iifname "erbr1" ip saddr != 10.200.0.6 drop`, `iifname "erbr1" ip daddr @blocked_v4 drop`,
102+ } {
103+ if idx(must) > est {
104+ t.Errorf("%q must come before the established accept", must)
105+ }
106+ }
107+ // And the established accept itself never applies to packets that enter
108+ // through a sandbox interface.
109+ if !strings.Contains(lines[est], `iifname != { "ertap*", "erbr*" }`) {
110+ t.Errorf("established accept must exclude sandbox ingress: %s", lines[est])
111+ }
112+}
113+
114+// S6: port 53 of a gateway address is closed to every interface but its guest's.
115+func TestInputChainClosesGatewayDNSToOtherInterfaces(t *testing.T) {
116+ rs := Ruleset([]Guest{guest(t, "ertap0", 0), guest(t, "ertap1", 1)})
117+ for _, want := range []string{
118+ `ip daddr 10.200.0.1 udp dport 53 iifname != "ertap0" drop`,
119+ `ip daddr 10.200.0.1 tcp dport 53 iifname != "ertap0" drop`,
120+ `ip daddr 10.200.0.5 udp dport 53 iifname != "ertap1" drop`,
121+ `ip daddr 10.200.0.5 tcp dport 53 iifname != "ertap1" drop`,
122+ } {
123+ if !strings.Contains(rs, want) {
124+ t.Errorf("lacks %q", want)
125+ }
126+ }
127+}
128+
129+// S10: documentation, test-net and benchmarking ranges are blocked too.
130+func TestBlockedSetCoversSpecialUseRanges(t *testing.T) {
131+ rs := Ruleset([]Guest{guest(t, "ertap0", 0)})
132+ for _, r := range []string{"192.0.0.0/24", "192.0.2.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4"} {
133+ if !strings.Contains(rs, r) {
134+ t.Errorf("blocked set lacks %s", r)
135+ }
136+ }
137+}
138+
81 func TestSubnet(t *testing.T) {139 func TestSubnet(t *testing.T) {
82 base := netip.MustParsePrefix("10.200.0.0/16")140 base := netip.MustParsePrefix("10.200.0.0/16")
83 n, err := Subnet(base, 3)141 n, err := Subnet(base, 3)
M internal/netpolicy/testdata/one.nft +11 -5
@@ -4,26 +4,32 @@ table inet ermite_runner {
44 set blocked_v4 {
55 type ipv4_addr
66 flags interval
7- elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }
7+ elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
88 }
99 chain input {
1010 type filter hook input priority filter; policy accept;
1111 iifname "ertap*" meta nfproto ipv6 drop
1212 iifname "erbr*" meta nfproto ipv6 drop
13+ ip daddr 10.200.0.1 udp dport 53 iifname != "ertap0" drop
14+ ip daddr 10.200.0.1 tcp dport 53 iifname != "ertap0" drop
1315 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 udp dport 53 accept
1416 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 tcp dport 53 accept
1517 iifname "ertap0" drop
18+ iifname "ertap*" drop
19+ iifname "erbr*" drop
1620 }
1721 chain forward {
1822 type filter hook forward priority filter; policy accept;
19- oifname "ertap*" ct state established,related accept
20- oifname "ertap*" drop
21- oifname "erbr*" ct state established,related accept
22- oifname "erbr*" drop
2323 iifname "ertap*" meta nfproto ipv6 drop
2424 iifname "erbr*" meta nfproto ipv6 drop
2525 iifname "ertap0" ip saddr != 10.200.0.2 drop
2626 iifname "ertap0" ip daddr @blocked_v4 drop
27+ iifname != { "ertap*", "erbr*" } oifname { "ertap*", "erbr*" } ct state established,related accept
28+ oifname "ertap*" drop
29+ oifname "erbr*" drop
30+ iifname "ertap0" accept
31+ iifname "ertap*" drop
32+ iifname "erbr*" drop
2733 }
2834 chain postrouting {
2935 type nat hook postrouting priority srcnat; policy accept;
@@ -4,26 +4,32 @@ table inet ermite_runner {
4 set blocked_v4 {4 set blocked_v4 {
5 type ipv4_addr5 type ipv4_addr
6 flags interval6 flags interval
7- elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }7+ elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
8 }8 }
9 chain input {9 chain input {
10 type filter hook input priority filter; policy accept;10 type filter hook input priority filter; policy accept;
11 iifname "ertap*" meta nfproto ipv6 drop11 iifname "ertap*" meta nfproto ipv6 drop
12 iifname "erbr*" meta nfproto ipv6 drop12 iifname "erbr*" meta nfproto ipv6 drop
13+ ip daddr 10.200.0.1 udp dport 53 iifname != "ertap0" drop
14+ ip daddr 10.200.0.1 tcp dport 53 iifname != "ertap0" drop
13 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 udp dport 53 accept15 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 udp dport 53 accept
14 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 tcp dport 53 accept16 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 tcp dport 53 accept
15 iifname "ertap0" drop17 iifname "ertap0" drop
18+ iifname "ertap*" drop
19+ iifname "erbr*" drop
16 }20 }
17 chain forward {21 chain forward {
18 type filter hook forward priority filter; policy accept;22 type filter hook forward priority filter; policy accept;
19- oifname "ertap*" ct state established,related accept
20- oifname "ertap*" drop
21- oifname "erbr*" ct state established,related accept
22- oifname "erbr*" drop
23 iifname "ertap*" meta nfproto ipv6 drop23 iifname "ertap*" meta nfproto ipv6 drop
24 iifname "erbr*" meta nfproto ipv6 drop24 iifname "erbr*" meta nfproto ipv6 drop
25 iifname "ertap0" ip saddr != 10.200.0.2 drop25 iifname "ertap0" ip saddr != 10.200.0.2 drop
26 iifname "ertap0" ip daddr @blocked_v4 drop26 iifname "ertap0" ip daddr @blocked_v4 drop
27+ iifname != { "ertap*", "erbr*" } oifname { "ertap*", "erbr*" } ct state established,related accept
28+ oifname "ertap*" drop
29+ oifname "erbr*" drop
30+ iifname "ertap0" accept
31+ iifname "ertap*" drop
32+ iifname "erbr*" drop
27 }33 }
28 chain postrouting {34 chain postrouting {
29 type nat hook postrouting priority srcnat; policy accept;35 type nat hook postrouting priority srcnat; policy accept;
M internal/netpolicy/testdata/two.nft +14 -5
@@ -4,31 +4,40 @@ table inet ermite_runner {
44 set blocked_v4 {
55 type ipv4_addr
66 flags interval
7- elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }
7+ elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
88 }
99 chain input {
1010 type filter hook input priority filter; policy accept;
1111 iifname "ertap*" meta nfproto ipv6 drop
1212 iifname "erbr*" meta nfproto ipv6 drop
13+ ip daddr 10.200.0.5 udp dport 53 iifname != "erbr1" drop
14+ ip daddr 10.200.0.5 tcp dport 53 iifname != "erbr1" drop
15+ ip daddr 10.200.0.1 udp dport 53 iifname != "ertap0" drop
16+ ip daddr 10.200.0.1 tcp dport 53 iifname != "ertap0" drop
1317 iifname "erbr1" ip saddr 10.200.0.6 ip daddr 10.200.0.5 udp dport 53 accept
1418 iifname "erbr1" ip saddr 10.200.0.6 ip daddr 10.200.0.5 tcp dport 53 accept
1519 iifname "erbr1" drop
1620 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 udp dport 53 accept
1721 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 tcp dport 53 accept
1822 iifname "ertap0" drop
23+ iifname "ertap*" drop
24+ iifname "erbr*" drop
1925 }
2026 chain forward {
2127 type filter hook forward priority filter; policy accept;
22- oifname "ertap*" ct state established,related accept
23- oifname "ertap*" drop
24- oifname "erbr*" ct state established,related accept
25- oifname "erbr*" drop
2628 iifname "ertap*" meta nfproto ipv6 drop
2729 iifname "erbr*" meta nfproto ipv6 drop
2830 iifname "erbr1" ip saddr != 10.200.0.6 drop
2931 iifname "erbr1" ip daddr @blocked_v4 drop
3032 iifname "ertap0" ip saddr != 10.200.0.2 drop
3133 iifname "ertap0" ip daddr @blocked_v4 drop
34+ iifname != { "ertap*", "erbr*" } oifname { "ertap*", "erbr*" } ct state established,related accept
35+ oifname "ertap*" drop
36+ oifname "erbr*" drop
37+ iifname "erbr1" accept
38+ iifname "ertap0" accept
39+ iifname "ertap*" drop
40+ iifname "erbr*" drop
3241 }
3342 chain postrouting {
3443 type nat hook postrouting priority srcnat; policy accept;
@@ -4,31 +4,40 @@ table inet ermite_runner {
4 set blocked_v4 {4 set blocked_v4 {
5 type ipv4_addr5 type ipv4_addr
6 flags interval6 flags interval
7- elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }7+ elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
8 }8 }
9 chain input {9 chain input {
10 type filter hook input priority filter; policy accept;10 type filter hook input priority filter; policy accept;
11 iifname "ertap*" meta nfproto ipv6 drop11 iifname "ertap*" meta nfproto ipv6 drop
12 iifname "erbr*" meta nfproto ipv6 drop12 iifname "erbr*" meta nfproto ipv6 drop
13+ ip daddr 10.200.0.5 udp dport 53 iifname != "erbr1" drop
14+ ip daddr 10.200.0.5 tcp dport 53 iifname != "erbr1" drop
15+ ip daddr 10.200.0.1 udp dport 53 iifname != "ertap0" drop
16+ ip daddr 10.200.0.1 tcp dport 53 iifname != "ertap0" drop
13 iifname "erbr1" ip saddr 10.200.0.6 ip daddr 10.200.0.5 udp dport 53 accept17 iifname "erbr1" ip saddr 10.200.0.6 ip daddr 10.200.0.5 udp dport 53 accept
14 iifname "erbr1" ip saddr 10.200.0.6 ip daddr 10.200.0.5 tcp dport 53 accept18 iifname "erbr1" ip saddr 10.200.0.6 ip daddr 10.200.0.5 tcp dport 53 accept
15 iifname "erbr1" drop19 iifname "erbr1" drop
16 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 udp dport 53 accept20 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 udp dport 53 accept
17 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 tcp dport 53 accept21 iifname "ertap0" ip saddr 10.200.0.2 ip daddr 10.200.0.1 tcp dport 53 accept
18 iifname "ertap0" drop22 iifname "ertap0" drop
23+ iifname "ertap*" drop
24+ iifname "erbr*" drop
19 }25 }
20 chain forward {26 chain forward {
21 type filter hook forward priority filter; policy accept;27 type filter hook forward priority filter; policy accept;
22- oifname "ertap*" ct state established,related accept
23- oifname "ertap*" drop
24- oifname "erbr*" ct state established,related accept
25- oifname "erbr*" drop
26 iifname "ertap*" meta nfproto ipv6 drop28 iifname "ertap*" meta nfproto ipv6 drop
27 iifname "erbr*" meta nfproto ipv6 drop29 iifname "erbr*" meta nfproto ipv6 drop
28 iifname "erbr1" ip saddr != 10.200.0.6 drop30 iifname "erbr1" ip saddr != 10.200.0.6 drop
29 iifname "erbr1" ip daddr @blocked_v4 drop31 iifname "erbr1" ip daddr @blocked_v4 drop
30 iifname "ertap0" ip saddr != 10.200.0.2 drop32 iifname "ertap0" ip saddr != 10.200.0.2 drop
31 iifname "ertap0" ip daddr @blocked_v4 drop33 iifname "ertap0" ip daddr @blocked_v4 drop
34+ iifname != { "ertap*", "erbr*" } oifname { "ertap*", "erbr*" } ct state established,related accept
35+ oifname "ertap*" drop
36+ oifname "erbr*" drop
37+ iifname "erbr1" accept
38+ iifname "ertap0" accept
39+ iifname "ertap*" drop
40+ iifname "erbr*" drop
32 }41 }
33 chain postrouting {42 chain postrouting {
34 type nat hook postrouting priority srcnat; policy accept;43 type nat hook postrouting priority srcnat; policy accept;

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