1// SPDX-License-Identifier: MIT
2
3// Package netsetup is the host-side network plumbing shared by both sandbox
4// drivers: the nftables policy (package netpolicy), the DNS forwarder (package
5// dnsfwd) and IPv4 forwarding. Each driver creates its own interface (a TAP or
6// a Docker bridge) and then Attaches it here.
7package netsetup
8
9import (
10 "context"
11 "errors"
12 "fmt"
13 "net/netip"
14 "os"
15 "strings"
16
17 "bnema.dev/ermite-runner/internal/cmdrun"
18 "bnema.dev/ermite-runner/internal/dnsfwd"
19 "bnema.dev/ermite-runner/internal/netpolicy"
20)
21
22// Plumbing is the host network surface a driver needs. Net implements it; tests
23// of the drivers use a fake because binding gateway addresses and loading nft
24// rules needs privileges.
25type Plumbing interface {
26 EnableForwarding() error
27 Attach(ctx context.Context, g netpolicy.Guest) error
28 Detach(ctx context.Context, iface string, gateway netip.Addr) error
29 Reset(ctx context.Context) error
30 Close()
31}
32
33var _ Plumbing = (*Net)(nil)
34
35// Net is the host network state of one driver.
36type Net struct {
37 Policy *netpolicy.Manager
38 DNS *dnsfwd.Forwarder
39 // ForwardFile is the ip_forward sysctl; overridden in tests.
40 ForwardFile string
41}
42
43// New builds a Net: nft runs through run, DNS listens on dnsPort and forwards
44// to the resolvers of resolvConf ("" = /etc/resolv.conf).
45func New(run cmdrun.Runner, dnsPort int, resolvConf string) *Net {
46 r := dnsfwd.NewResolvers(resolvConf)
47 return &Net{
48 Policy: netpolicy.NewManager(run, "nft"),
49 DNS: dnsfwd.New(r.Addrs, dnsPort),
50 ForwardFile: "/proc/sys/net/ipv4/ip_forward",
51 }
52}
53
54// EnableForwarding turns on IPv4 forwarding (NAT needs it). It is a no-op when
55// already on, and an error that names the fix when it cannot be turned on.
56func (n *Net) EnableForwarding() error {
57 b, err := os.ReadFile(n.ForwardFile)
58 if err == nil && strings.TrimSpace(string(b)) == "1" {
59 return nil
60 }
61 if err := os.WriteFile(n.ForwardFile, []byte("1\n"), 0o644); err != nil {
62 return fmt.Errorf("enable IPv4 forwarding (%s): %w; set net.ipv4.ip_forward=1", n.ForwardFile, err)
63 }
64 return nil
65}
66
67// Attach installs the firewall policy of g and starts the DNS forwarder on its
68// gateway address, which must already be configured on the host interface.
69// On error nothing stays installed.
70func (n *Net) Attach(ctx context.Context, g netpolicy.Guest) error {
71 if err := n.Policy.Add(ctx, g); err != nil {
72 return err
73 }
74 if err := n.DNS.Listen(ctx, g.Gateway); err != nil {
75 return errors.Join(err, n.Policy.Remove(context.WithoutCancel(ctx), g.Iface))
76 }
77 return nil
78}
79
80// Detach undoes Attach. It is idempotent and keeps going after an error.
81func (n *Net) Detach(ctx context.Context, iface string, gateway netip.Addr) error {
82 n.DNS.Close(gateway)
83 return n.Policy.Remove(ctx, iface)
84}
85
86// Reset removes the runner's nftables table: used at startup to reap a previous
87// run. Only the table is touched, never other tables.
88func (n *Net) Reset(ctx context.Context) error { return n.Policy.Reset(ctx) }
89
90// Close stops every DNS listener.
91func (n *Net) Close() { n.DNS.CloseAll() }