1 Commits

Author SHA1 Message Date
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
6 changed files with 104 additions and 4 deletions

View File

@@ -74,11 +74,20 @@ kill -HUP $(pidof gocast)
**What gets reloaded:** **What gets reloaded:**
- BGP configuration (peers, AS numbers, MD5 passwords, communities) - BGP configuration (peers, AS numbers, MD5 passwords, communities)
- Application definitions (add/remove/update apps) - Application definitions (add/remove/update apps)
- Agent settings (Consul, timers, intervals) - Agent settings (Consul, timers, intervals, iptables binary)
**Important:** Reloading BGP configuration causes existing BGP sessions to be restarted, resulting in brief routing interruption. Routes are automatically re-announced after reload. **Important:** Reloading BGP configuration causes existing BGP sessions to be restarted, resulting in brief routing interruption. Routes are automatically re-announced after reload.
Consul-discovered apps are not removed during reload. Consul-discovered apps are not removed during reload.
## Iptables Configuration
On modern Linux systems using nftables, you need to configure gocast to use `iptables-nft` instead of the legacy `iptables` binary (default):
```yaml
agent:
iptables_binary: iptables-nft
```
## Docker support ## Docker support
The docker image at mayuresh82/gocast can be used to run GoCast inside a container. In order for GoCast to manipulate the host network stack correctly, the container needs to run with NET_ADMIN capablity and host mode networking. For example: The docker image at mayuresh82/gocast can be used to run GoCast inside a container. In order for GoCast to manipulate the host network stack correctly, the container needs to run with NET_ADMIN capablity and host mode networking. For example:
``` ```

View File

@@ -11,6 +11,9 @@ agent:
consul_query_interval: 5m consul_query_interval: 5m
# token to authenticate client if consul requires it # token to authenticate client if consul requires it
consul_token: 00000000-0000-0000-0000-000000000000 consul_token: 00000000-0000-0000-0000-000000000000
# iptables binary to use for NAT rules (default: iptables)
# Use "iptables-nft" on modern systems with nftables
# iptables_binary: iptables-nft
bgp: bgp:
local_as: 12345 local_as: 12345

View File

@@ -15,7 +15,8 @@ type AgentConfig struct {
CleanupTimer time.Duration `yaml:"cleanup_timer"` CleanupTimer time.Duration `yaml:"cleanup_timer"`
ConsulAddr string `yaml:"consul_addr"` ConsulAddr string `yaml:"consul_addr"`
ConsulQueryInterval time.Duration `yaml:"consul_query_interval"` ConsulQueryInterval time.Duration `yaml:"consul_query_interval"`
ConsulToken string `yaml:"consul_token"` ConsulToken string `yaml:"consul_token"`
IptablesBinary string `yaml:"iptables_binary"`
} }
type PeerConfig struct { type PeerConfig struct {

View File

@@ -98,6 +98,11 @@ func NewMonitor(config *c.Config) *MonitorMgr {
if config.Agent.CleanupTimer == 0 { if config.Agent.CleanupTimer == 0 {
config.Agent.CleanupTimer = defaultCleanupTimer config.Agent.CleanupTimer = defaultCleanupTimer
} }
// Set iptables binary (defaults to "iptables" if not specified)
if config.Agent.IptablesBinary == "" {
config.Agent.IptablesBinary = "iptables"
}
SetIptablesBinary(config.Agent.IptablesBinary)
mon.config = config mon.config = config
// add apps defined in config // add apps defined in config
for _, a := range config.Apps { for _, a := range config.Apps {
@@ -356,6 +361,15 @@ func (m *MonitorMgr) Reload(configPath string) error {
if newConfig.Agent.CleanupTimer == 0 { if newConfig.Agent.CleanupTimer == 0 {
newConfig.Agent.CleanupTimer = defaultCleanupTimer newConfig.Agent.CleanupTimer = defaultCleanupTimer
} }
if newConfig.Agent.IptablesBinary == "" {
newConfig.Agent.IptablesBinary = "iptables"
}
// Update iptables binary if changed
if m.config.Agent.IptablesBinary != newConfig.Agent.IptablesBinary {
glog.Infof("Iptables binary changed from %s to %s", m.config.Agent.IptablesBinary, newConfig.Agent.IptablesBinary)
SetIptablesBinary(newConfig.Agent.IptablesBinary)
}
// Check if BGP configuration has changed // Check if BGP configuration has changed
bgpChanged := m.bgpConfigChanged(m.config.Bgp, newConfig.Bgp) bgpChanged := m.bgpConfigChanged(m.config.Bgp, newConfig.Bgp)

View File

@@ -8,6 +8,7 @@ import (
) )
var execCmd = "bash" var execCmd = "bash"
var iptablesBinary = "iptables"
func getCmdList(mainCmd string) []string { func getCmdList(mainCmd string) []string {
cmdList := []string{} cmdList := []string{}
@@ -88,8 +89,8 @@ func deleteLoopback(addr *net.IPNet) error {
func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) error { func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) error {
cmd := fmt.Sprintf( cmd := fmt.Sprintf(
"iptables -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s", "%s -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s",
op, protocol, vip.String(), lport, localAddr.String(), dport, iptablesBinary, op, protocol, vip.String(), lport, localAddr.String(), dport,
) )
cmdList := getCmdList(cmd) cmdList := getCmdList(cmd)
_, err := exec.Command(execCmd, cmdList...).Output() _, err := exec.Command(execCmd, cmdList...).Output()
@@ -98,3 +99,10 @@ func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) er
} }
return nil return nil
} }
// SetIptablesBinary sets the iptables binary to use for NAT rules
func SetIptablesBinary(binary string) {
if binary != "" {
iptablesBinary = binary
}
}

View File

@@ -43,6 +43,71 @@ func TestAddLoopback(t *testing.T) {
assert.NotNil(t, err) assert.NotNil(t, err)
} }
func TestSetIptablesBinary(t *testing.T) {
a := assert.New(t)
// Save original value
originalBinary := iptablesBinary
defer func() {
iptablesBinary = originalBinary
}()
// Test setting custom binary
SetIptablesBinary("iptables-nft")
a.Equal("iptables-nft", iptablesBinary)
// Test setting back to default
SetIptablesBinary("iptables")
a.Equal("iptables", iptablesBinary)
// Test that empty string doesn't change the value
SetIptablesBinary("iptables-custom")
a.Equal("iptables-custom", iptablesBinary)
SetIptablesBinary("")
a.Equal("iptables-custom", iptablesBinary, "Empty string should not change the binary")
}
func TestNatRuleCommandFormat(t *testing.T) {
a := assert.New(t)
// Save original values
originalBinary := iptablesBinary
originalExecCmd := execCmd
defer func() {
iptablesBinary = originalBinary
execCmd = originalExecCmd
}()
vip := net.ParseIP("192.0.2.1")
localAddr := net.ParseIP("10.0.0.1")
// Test with default iptables
SetIptablesBinary("iptables")
err := natRule("A", vip, localAddr, "tcp", "80", "8080")
// We expect this to fail in test environment, but we can check the error message
// contains our command
if err != nil {
a.Contains(err.Error(), "iptables -t nat -A PREROUTING")
}
// Test with iptables-nft
SetIptablesBinary("iptables-nft")
err = natRule("A", vip, localAddr, "tcp", "80", "8080")
if err != nil {
a.Contains(err.Error(), "iptables-nft -t nat -A PREROUTING")
}
// Test with custom binary path
SetIptablesBinary("/usr/sbin/iptables")
err = natRule("D", vip, localAddr, "udp", "53", "5353")
if err != nil {
a.Contains(err.Error(), "/usr/sbin/iptables -t nat -D PREROUTING")
a.Contains(err.Error(), "udp")
a.Contains(err.Error(), "53")
a.Contains(err.Error(), "5353")
}
}
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
switch os.Getenv("test_name") { switch os.Getenv("test_name") {
case "test_gateway": case "test_gateway":