Files
gocast/controller/system.go
Ben Roberts 256afcbd97 Add configuration option to select iptables implementation
When running gocast in a container, the default iptables implementation
may not match that used on the underlying host kernel. The current
container uses the legacy iptables implementation and calls the
`iptables` binary. This fails with exit code 3 when running on a host
using the newer nftables implementation. The container already has
`iptables-nft` binary included, so just needs a way to call this instead
of the default `iptables` binary.

This change implements a new `iptables_binary` config option, defaulting
to `iptables`, and calls this when adding or removing NAT rules.

Fixes #32

This change was written using AI LLM.

Authored-By: Claude Code (Sonnet 4.5)
2026-06-17 17:18:59 +01:00

109 lines
2.8 KiB
Go

package controller
import (
"fmt"
"net"
"os/exec"
"strings"
)
var execCmd = "bash"
var iptablesBinary = "iptables"
func getCmdList(mainCmd string) []string {
cmdList := []string{}
if execCmd == "bash" {
cmdList = append(cmdList, "-c")
}
cmdList = append(cmdList, mainCmd)
return cmdList
}
func gateway() (net.IP, error) {
cmd := `ip route | grep "^default" | cut -d" " -f3`
cmdList := getCmdList(cmd)
out, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return nil, fmt.Errorf("Failed to execute command: %s: %v", cmd, err)
}
return net.ParseIP(strings.TrimSpace(string(out))), nil
}
func via(dest net.IP) (net.IP, error) {
cmd := fmt.Sprintf(`ip route get %s | grep via | cut -d" " -f3`, dest.String())
cmdList := getCmdList(cmd)
out, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return nil, fmt.Errorf("Failed to execute command: %s: %v", cmd, err)
}
if string(out) == "" {
// assume the provided dest is the next hop
return dest, nil
}
return net.ParseIP(strings.TrimSpace(string(out))), nil
}
func localAddress(gw net.IP) (net.IP, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
switch v := addr.(type) {
case *net.IPNet:
if v.Contains(gw) {
return v.IP, nil
}
}
}
return nil, fmt.Errorf("Unable to find local address")
}
func addLoopback(name string, addr *net.IPNet) error {
deleteLoopback(addr)
prefixLen, _ := addr.Mask.Size()
label := fmt.Sprintf("lo:%s", name)
// linux kernel limits labels to 15 chars
if len(label) > 15 {
label = label[:15]
}
cmd := fmt.Sprintf("ip address add %s/%d dev lo label %s", addr.IP.String(), prefixLen, label)
cmdList := getCmdList(cmd)
_, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return fmt.Errorf("Failed to Add loopback command: %s: %v", cmd, err)
}
return nil
}
func deleteLoopback(addr *net.IPNet) error {
prefixLen, _ := addr.Mask.Size()
cmd := fmt.Sprintf("ip address delete %s/%d dev lo", addr.IP.String(), prefixLen)
cmdList := getCmdList(cmd)
_, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return fmt.Errorf("Failed to delete loopback command: %s: %v", cmd, err)
}
return nil
}
func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) error {
cmd := fmt.Sprintf(
"%s -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s",
iptablesBinary, op, protocol, vip.String(), lport, localAddr.String(), dport,
)
cmdList := getCmdList(cmd)
_, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return fmt.Errorf("Failed to %s nat rule: %s: %v", op, cmd, err)
}
return nil
}
// SetIptablesBinary sets the iptables binary to use for NAT rules
func SetIptablesBinary(binary string) {
if binary != "" {
iptablesBinary = binary
}
}