banger/internal/daemon/nat.go
Thales Maciel fcedacba5c
Make runtime defaults portable
Stop assuming one workstation layout for runtime artifacts, mapdns, and host tooling. The daemon and shell helpers now use portable mapdns configuration, and runtime bundles can carry bundle.json metadata for their default kernel, initrd, modules, rootfs, and helper paths.

Load bundle metadata through config with a legacy layout fallback, thread mapdns_bin/mapdns_data_file through the Go and shell paths, and add command-scoped preflight checks for VM start, NAT, image build, work-disk resize, and SSH so missing tools or artifacts fail with actionable errors.

Update the runtime-bundle manifest, docs, and tests to match the new model. Verified with go test ./..., make build, and bash -n customize.sh interactive.sh dns.sh make-rootfs.sh verify.sh.
2026-03-16 15:30:08 -03:00

156 lines
3.9 KiB
Go

package daemon
import (
"context"
"errors"
"fmt"
"strings"
"banger/internal/model"
"banger/internal/system"
)
type natRule struct {
table string
chain string
args []string
}
func (d *Daemon) ensureNAT(ctx context.Context, vm model.VMRecord, enable bool) error {
uplink, err := d.validateNATPrereqs(ctx)
if err != nil {
return err
}
rules, err := natRulesForVM(vm, uplink)
if err != nil {
return err
}
if enable {
if _, err := d.runner.RunSudo(ctx, "sysctl", "-w", "net.ipv4.ip_forward=1"); err != nil {
return err
}
for _, rule := range rules {
if err := d.addNATRule(ctx, rule); err != nil {
return err
}
}
return nil
}
for _, rule := range rules {
if err := d.removeNATRule(ctx, rule); err != nil {
return err
}
}
return nil
}
func (d *Daemon) validateNATPrereqs(ctx context.Context) (string, error) {
checks := system.NewPreflight()
checks.RequireCommand("ip", toolHint("ip"))
d.addNATPrereqs(ctx, checks)
if err := checks.Err("nat preflight failed"); err != nil {
return "", err
}
return d.defaultUplink(ctx)
}
func (d *Daemon) defaultUplink(ctx context.Context) (string, error) {
out, err := d.runner.Run(ctx, "ip", "route", "show", "default")
if err != nil {
return "", err
}
return parseDefaultUplink(string(out))
}
func parseDefaultUplink(output string) (string, error) {
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) == 0 || fields[0] != "default" {
continue
}
for i := 0; i < len(fields)-1; i++ {
if fields[i] == "dev" && fields[i+1] != "" {
return fields[i+1], nil
}
}
}
return "", errors.New("failed to detect uplink interface")
}
func natRulesForVM(vm model.VMRecord, uplink string) ([]natRule, error) {
guestIP := strings.TrimSpace(vm.Runtime.GuestIP)
if guestIP == "" {
return nil, errors.New("nat requires a guest IP")
}
tap := strings.TrimSpace(vm.Runtime.TapDevice)
if tap == "" {
return nil, errors.New("nat requires a tap device")
}
uplink = strings.TrimSpace(uplink)
if uplink == "" {
return nil, errors.New("nat requires an uplink interface")
}
guestCIDR := guestIP + "/32"
return []natRule{
{
table: "nat",
chain: "POSTROUTING",
args: []string{"-s", guestCIDR, "-o", uplink, "-j", "MASQUERADE"},
},
{
chain: "FORWARD",
args: []string{"-i", tap, "-o", uplink, "-j", "ACCEPT"},
},
{
chain: "FORWARD",
args: []string{"-i", uplink, "-o", tap, "-m", "state", "--state", "RELATED,ESTABLISHED", "-j", "ACCEPT"},
},
}, nil
}
func natRuleArgs(action string, rule natRule) []string {
args := make([]string, 0, len(rule.args)+4)
if rule.table != "" {
args = append(args, "-t", rule.table)
}
args = append(args, action, rule.chain)
args = append(args, rule.args...)
return args
}
func natAddPlan(rules []natRule) [][]string {
plan := make([][]string, 0, len(rules)+1)
plan = append(plan, []string{"sysctl", "-w", "net.ipv4.ip_forward=1"})
for _, rule := range rules {
plan = append(plan, natRuleArgs("-A", rule))
}
return plan
}
func natRemovePlan(rules []natRule) [][]string {
plan := make([][]string, 0, len(rules))
for _, rule := range rules {
plan = append(plan, natRuleArgs("-D", rule))
}
return plan
}
func (d *Daemon) addNATRule(ctx context.Context, rule natRule) error {
if _, err := d.runner.RunSudo(ctx, append([]string{"iptables"}, natRuleArgs("-C", rule)...)...); err == nil {
return nil
}
_, err := d.runner.RunSudo(ctx, append([]string{"iptables"}, natRuleArgs("-A", rule)...)...)
return err
}
func (d *Daemon) removeNATRule(ctx context.Context, rule natRule) error {
if _, err := d.runner.RunSudo(ctx, append([]string{"iptables"}, natRuleArgs("-C", rule)...)...); err != nil {
return nil
}
_, err := d.runner.RunSudo(ctx, append([]string{"iptables"}, natRuleArgs("-D", rule)...)...)
return err
}
func natRuleKey(rule natRule) string {
return fmt.Sprintf("%s:%s:%s", rule.table, rule.chain, strings.Join(rule.args, " "))
}