continuous-integration/drone/push Build is passing
Move chain detection logic from firewall to iptables manager for better encapsulation. The manager now auto-detects both the iptables binary and chain (DOCKER-USER or FORWARD) based on the presence of the Docker-managed chain, but always defaults to DOCKER-USER for consistency. This simplifies firewall code and ensures proper Docker integration regardless of iptables version.
333 lines
8.6 KiB
Go
333 lines
8.6 KiB
Go
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 DOCKER-USER chain (default, even with non-legacy iptables)
|
|
if iptables.InsertForwardAcceptChain != "DOCKER-USER" {
|
|
t.Errorf("expected DOCKER-USER 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}
|
|
} |