package firewall import ( "context" "testing" "firewall_containers/network-go/config" "firewall_containers/network-go/mock" ) func testConfig() *config.NetworksConfig { return &config.NetworksConfig{ Networks: map[string]config.NetworkConfig{ "smarthost-loadbalancer": { NetworkName: "smarthost-loadbalancer", Subnet: "172.18.103.0/24", Gateway: "172.18.103.1", }, "smarthost_backend-1": { NetworkName: "smarthost_backend-1", Subnet: "172.18.104.0/24", Gateway: "172.18.104.1", }, }, IPs: map[string]config.IPConfig{ "172.18.103.2": { IP: "172.18.103.2", ContainerName: "smarthostloadbalancer", Selector: "smarthostloadbalancer", ServiceName: "smarthost-proxy", }, "172.18.104.2": { IP: "172.18.104.2", ContainerName: "smarthostbackend-1", Selector: "smarthostbackend-1", ServiceName: "smarthost-proxy", }, }, Policies: []config.PolicyConfig{}, } } func TestReconcileAllCreatesNetworks(t *testing.T) { cfg := testConfig() docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.ReconcileAll(ctx, cfg) if !docker.EnsureNetworkCalled { t.Error("EnsureNetwork was not called") } } func TestReconcileAllConnectsContainers(t *testing.T) { cfg := testConfig() docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.ReconcileAll(ctx, cfg) if !docker.ConnectContainerCalled { t.Error("ConnectContainer was not called") } } func TestReconcileAllEnablesIPForward(t *testing.T) { cfg := testConfig() docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.ReconcileAll(ctx, cfg) if !iptables.EnsureIPForwardCalled { t.Error("EnsureIPForward was not called") } } func TestReconcilePoliciesForwardRule(t *testing.T) { cfg := testConfig() cfg.Policies = []config.PolicyConfig{ { ServiceName: "smarthost-proxy", ContainerName: "smarthostloadbalancer", Selector: "smarthostloadbalancer", From: "publicbackend", Port: 80, Proto: "tcp", }, } docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{BinaryResult: "/usr/sbin/iptables"} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.reconcilePolicies(ctx, cfg) // "publicbackend" should be resolved to an IP from the config (if it matches) // Since "publicbackend" is not in the IPs map, sourceIP will be empty if !iptables.InsertForwardAcceptCalled { t.Error("InsertForwardAccept was not called") } // Should use FORWARD chain (not iptables-legacy) if iptables.InsertForwardAcceptChain != "FORWARD" { t.Errorf("expected FORWARD chain, got %s", iptables.InsertForwardAcceptChain) } } func TestReconcilePoliciesForwardRuleWithLegacy(t *testing.T) { cfg := testConfig() cfg.Policies = []config.PolicyConfig{ { ServiceName: "smarthost-proxy", From: "publicbackend", Port: 80, Proto: "tcp", }, } docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{BinaryResult: "/usr/sbin/iptables-legacy"} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.reconcilePolicies(ctx, cfg) if !iptables.InsertForwardAcceptCalled { t.Error("InsertForwardAccept was not called") } // Should use DOCKER-USER chain when iptables-legacy if iptables.InsertForwardAcceptChain != "DOCKER-USER" { t.Errorf("expected DOCKER-USER chain, got %s", iptables.InsertForwardAcceptChain) } } func TestReconcilePoliciesDNATWithInterface(t *testing.T) { cfg := testConfig() cfg.Policies = []config.PolicyConfig{ { ServiceName: "smarthost-proxy", Name: "wireguardproxy", Selector: "smarthostloadbalancer", Iface: "wg0", Nat: "dnat", To: "smarthostloadbalancer", Port: 80, Proto: "tcp", }, } docker := &mock.MockDockerClient{ GetContainerPIDResult: 0, // simulate no PID available GetContainerPIDErr: assertError("container not running"), } iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.reconcilePolicies(ctx, cfg) // When GetContainerPID fails, should fall back to interface-based rule if !iptables.InsertPreroutingRuleOnInterfaceCalled { t.Error("InsertPreroutingRuleOnInterface was not called (should fall back from nsenter)") } if len(iptables.InsertPreroutingRuleOnInterfaceArgs) > 0 { iface := iptables.InsertPreroutingRuleOnInterfaceArgs[0] if iface != "wg0" { t.Errorf("expected interface wg0, got %s", iface) } } } func TestReconcilePoliciesDNATWithContainerPID(t *testing.T) { cfg := testConfig() cfg.Policies = []config.PolicyConfig{ { ServiceName: "smarthost-proxy", Name: "wireguardproxy", Selector: "smarthostloadbalancer", Nat: "dnat", To: "smarthostloadbalancer", Port: 80, Proto: "tcp", }, } docker := &mock.MockDockerClient{ GetContainerPIDResult: 1234, GetContainerPIDErr: nil, } iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.reconcilePolicies(ctx, cfg) if !docker.GetContainerPIDCalled { t.Error("GetContainerPID was not called") } if !iptables.InsertPreroutingRuleInContainerCalled { t.Error("InsertPreroutingRuleInContainer was not called") } if iptables.InsertPreroutingRuleInContainerPID != 1234 { t.Errorf("expected PID 1234, got %d", iptables.InsertPreroutingRuleInContainerPID) } } func TestReconcilePoliciesUnresolvedTarget(t *testing.T) { cfg := testConfig() cfg.Policies = []config.PolicyConfig{ { ServiceName: "test", Nat: "dnat", Selector: "container1", To: "nonexistent-target", Port: 80, }, } docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.reconcilePolicies(ctx, cfg) // Should not call GetContainerPID when target can't be resolved if docker.GetContainerPIDCalled { t.Error("GetContainerPID should not be called when target is unresolvable") } } func TestResolveIPDirectIP(t *testing.T) { cfg := testConfig() docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) // Direct IP should be returned as CIDR result := orch.resolveIP("172.18.103.2") if result != "172.18.103.2" { t.Errorf("expected 172.18.103.2, got %s", result) } // .0 ending should be converted to /24 result = orch.resolveIP("172.18.103.0") if result != "172.18.103.0/24" { t.Errorf("expected 172.18.103.0/24, got %s", result) } } func TestResolveIPFromConfig(t *testing.T) { cfg := testConfig() docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) // Should resolve container name from config result := orch.resolveIP("smarthostloadbalancer") if result != "172.18.103.2" { t.Errorf("expected 172.18.103.2, got %s", result) } } func TestFindNetworkForIP(t *testing.T) { cfg := testConfig() tests := []struct { ip string want string }{ {"172.18.103.5", "smarthost-loadbalancer"}, {"172.18.104.5", "smarthost_backend-1"}, {"10.0.0.1", ""}, {"", ""}, } for _, tt := range tests { got := findNetworkForIP(cfg, tt.ip) if got != tt.want { t.Errorf("findNetworkForIP(%q) = %q, want %q", tt.ip, got, tt.want) } } } func TestReconcileAllReproducible(t *testing.T) { cfg := testConfig() cfg.Policies = []config.PolicyConfig{ { ServiceName: "test-svc", From: "publicbackend", Port: 80, Proto: "tcp", }, } // Run reconciliation twice with separate mocks for i := 0; i < 2; i++ { docker := &mock.MockDockerClient{} iptables := &mock.MockIPTablesManager{} orch := NewOrchestrator(docker, iptables, cfg) ctx := context.Background() orch.ReconcileAll(ctx, cfg) if !docker.EnsureNetworkCalled { t.Errorf("run %d: EnsureNetwork not called", i) } if !iptables.InsertForwardAcceptCalled { t.Errorf("run %d: InsertForwardAccept not called", i) } } } // assertError is a helper to create a simple error for tests type simpleErr struct{ msg string } func (e simpleErr) Error() string { return e.msg } func assertError(msg string) error { return simpleErr{msg: msg} }