diff --git a/README.md b/README.md index 6b7ed95..9cb0d54 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,20 @@ kill -HUP $(pidof gocast) **What gets reloaded:** - BGP configuration (peers, AS numbers, MD5 passwords, communities) - 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. 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 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: ``` diff --git a/config.yaml b/config.yaml index c65d2bf..c4deec1 100644 --- a/config.yaml +++ b/config.yaml @@ -11,6 +11,9 @@ agent: consul_query_interval: 5m # token to authenticate client if consul requires it 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: local_as: 12345 diff --git a/config/config.go b/config/config.go index 2ce2d2e..5fec71e 100644 --- a/config/config.go +++ b/config/config.go @@ -15,7 +15,8 @@ type AgentConfig struct { CleanupTimer time.Duration `yaml:"cleanup_timer"` ConsulAddr string `yaml:"consul_addr"` 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 { diff --git a/controller/monitor.go b/controller/monitor.go index f418058..dc50c25 100644 --- a/controller/monitor.go +++ b/controller/monitor.go @@ -98,6 +98,11 @@ func NewMonitor(config *c.Config) *MonitorMgr { if config.Agent.CleanupTimer == 0 { 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 // add apps defined in config for _, a := range config.Apps { @@ -356,6 +361,15 @@ func (m *MonitorMgr) Reload(configPath string) error { if newConfig.Agent.CleanupTimer == 0 { 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 bgpChanged := m.bgpConfigChanged(m.config.Bgp, newConfig.Bgp) diff --git a/controller/system.go b/controller/system.go index 96146cc..558dd4a 100644 --- a/controller/system.go +++ b/controller/system.go @@ -8,6 +8,7 @@ import ( ) var execCmd = "bash" +var iptablesBinary = "iptables" func getCmdList(mainCmd string) []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 { cmd := fmt.Sprintf( - "iptables -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s", - op, protocol, vip.String(), lport, localAddr.String(), dport, + "%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() @@ -98,3 +99,10 @@ func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) er } return nil } + +// SetIptablesBinary sets the iptables binary to use for NAT rules +func SetIptablesBinary(binary string) { + if binary != "" { + iptablesBinary = binary + } +} diff --git a/controller/system_test.go b/controller/system_test.go index 44b2219..8da48da 100644 --- a/controller/system_test.go +++ b/controller/system_test.go @@ -43,6 +43,71 @@ func TestAddLoopback(t *testing.T) { 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) { switch os.Getenv("test_name") { case "test_gateway":