This commit is contained in:
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
network-go
|
||||||
|
*.test
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetworksConfig represents the /etc/user/config/networks.json structure
|
||||||
|
type NetworksConfig struct {
|
||||||
|
Networks map[string]NetworkConfig `json:"networks"`
|
||||||
|
IPs map[string]IPConfig `json:"ips"`
|
||||||
|
Policies []PolicyConfig `json:"policies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkConfig represents a single network definition
|
||||||
|
type NetworkConfig struct {
|
||||||
|
NetworkName string `json:"network_name"`
|
||||||
|
Subnet string `json:"subnet"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPConfig represents a single IP assignment
|
||||||
|
type IPConfig struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
ContainerName string `json:"container_name"`
|
||||||
|
Selector string `json:"selector"`
|
||||||
|
ServiceName string `json:"service_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyConfig represents a single policy rule from networks.json
|
||||||
|
type PolicyConfig struct {
|
||||||
|
ServiceName string `json:"service_name,omitempty"`
|
||||||
|
ContainerName string `json:"container_name,omitempty"`
|
||||||
|
Selector string `json:"selector,omitempty"`
|
||||||
|
From string `json:"from,omitempty"`
|
||||||
|
To string `json:"to,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Proto string `json:"proto,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Iface string `json:"iface,omitempty"`
|
||||||
|
Nat string `json:"nat,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirewallRule is a resolved, executable firewall rule derived from a PolicyConfig
|
||||||
|
type FirewallRule struct {
|
||||||
|
// Source info
|
||||||
|
SourceIP string
|
||||||
|
SourcePort int
|
||||||
|
SourceIface string
|
||||||
|
|
||||||
|
// Target info
|
||||||
|
TargetIP string
|
||||||
|
TargetPort int
|
||||||
|
|
||||||
|
// Protocol (tcp/udp)
|
||||||
|
Proto string
|
||||||
|
|
||||||
|
// Chain and table
|
||||||
|
Chain string // PREROUTING, POSTROUTING, FORWARD, DOCKER-USER, etc.
|
||||||
|
Table string // nat, filter
|
||||||
|
|
||||||
|
// Action
|
||||||
|
Action string // DNAT, MASQUERADE, ACCEPT, DROP
|
||||||
|
|
||||||
|
// Comment for iptables
|
||||||
|
Comment string
|
||||||
|
|
||||||
|
// Namespace info (empty = host, non-empty = container PID namespace)
|
||||||
|
ContainerPID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToCIDR converts an IP without mask to a /24 CIDR notation (matching shell script behavior)
|
||||||
|
func ToCIDR(ip string) string {
|
||||||
|
// If it's already a CIDR, return as-is
|
||||||
|
if _, _, err := net.ParseCIDR(ip); err == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
// If last octet is 0, it's already a network address
|
||||||
|
parsed := net.ParseIP(ip)
|
||||||
|
if parsed == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
ipv4 := parsed.To4()
|
||||||
|
if ipv4 == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
if ipv4[3] == 0 {
|
||||||
|
return fmt.Sprintf("%d.%d.%d.0/24", ipv4[0], ipv4[1], ipv4[2])
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkPrefix returns the first three octets as a /24 CIDR
|
||||||
|
func NetworkPrefix(ip string) string {
|
||||||
|
parsed := net.ParseIP(ip)
|
||||||
|
if parsed == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
ipv4 := parsed.To4()
|
||||||
|
if ipv4 == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d.%d.%d.0/24", ipv4[0], ipv4[1], ipv4[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIP checks if a string is an IPv4 address
|
||||||
|
func IsIP(s string) bool {
|
||||||
|
parsed := net.ParseIP(s)
|
||||||
|
return parsed != nil && parsed.To4() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCIDR parses the subnet string into an IPNet
|
||||||
|
func (n NetworkConfig) ParseCIDR() (*net.IPNet, error) {
|
||||||
|
_, ipNet, err := net.ParseCIDR(n.Subnet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse subnet %s: %w", n.Subnet, err)
|
||||||
|
}
|
||||||
|
return ipNet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads and parses the networks.json configuration file
|
||||||
|
func Load(path string) (*NetworksConfig, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config NetworksConfig
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"firewall_containers/network-go/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client wraps the Docker SDK client
|
||||||
|
type Client struct {
|
||||||
|
cli *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Docker client
|
||||||
|
func NewClient() (*Client, error) {
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create Docker client: %w", err)
|
||||||
|
}
|
||||||
|
return &Client{cli: cli}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the Docker client
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.cli.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureNetwork creates a Docker network if it does not already exist
|
||||||
|
func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig) error {
|
||||||
|
// Check if network already exists
|
||||||
|
existingNetworks, err := c.cli.NetworkList(ctx, network.ListOptions{
|
||||||
|
Filters: filters.NewArgs(filters.Arg("name", netCfg.NetworkName)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list networks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range existingNetworks {
|
||||||
|
if n.Name == netCfg.NetworkName {
|
||||||
|
// Network already exists, skip creation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse subnet and gateway
|
||||||
|
_, ipNet, err := net.ParseCIDR(netCfg.Subnet)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse subnet %s: %w", netCfg.Subnet, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayIP := net.ParseIP(netCfg.Gateway)
|
||||||
|
if gatewayIP == nil {
|
||||||
|
return fmt.Errorf("failed to parse gateway IP %s", netCfg.Gateway)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the network
|
||||||
|
createOpts := network.CreateOptions{
|
||||||
|
Driver: "bridge",
|
||||||
|
IPAM: &network.IPAM{
|
||||||
|
Config: []network.IPAMConfig{
|
||||||
|
{
|
||||||
|
Subnet: ipNet.String(),
|
||||||
|
Gateway: gatewayIP.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Labels: map[string]string{
|
||||||
|
"managed-by": "firewall-network-go",
|
||||||
|
},
|
||||||
|
Attachable: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.cli.NetworkCreate(ctx, netCfg.NetworkName, createOpts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create network %s: %w", netCfg.NetworkName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = resp // response contains ID and warnings
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNetwork removes a Docker network
|
||||||
|
func (c *Client) RemoveNetwork(ctx context.Context, networkName string) error {
|
||||||
|
err := c.cli.NetworkRemove(ctx, networkName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove network %s: %w", networkName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectContainer connects a container to a network with a specific IP
|
||||||
|
func (c *Client) ConnectContainer(ctx context.Context, containerName, networkName, ip string) error {
|
||||||
|
endpointSettings := &network.EndpointSettings{
|
||||||
|
IPAMConfig: &network.EndpointIPAMConfig{
|
||||||
|
IPv4Address: ip,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.cli.NetworkConnect(ctx, networkName, containerName, endpointSettings)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect container %s to network %s: %w", containerName, networkName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisconnectContainer disconnects a container from a network
|
||||||
|
func (c *Client) DisconnectContainer(ctx context.Context, containerName, networkName string) error {
|
||||||
|
err := c.cli.NetworkDisconnect(ctx, networkName, containerName, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to disconnect container %s from network %s: %w", containerName, networkName, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectContainer returns the container's details
|
||||||
|
func (c *Client) InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error) {
|
||||||
|
container, err := c.cli.ContainerInspect(ctx, containerName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
|
||||||
|
}
|
||||||
|
return &container, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForContainerRunning waits until a container is in running state, with a timeout
|
||||||
|
func (c *Client) WaitForContainerRunning(ctx context.Context, containerName string, timeout time.Duration) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("timeout waiting for container %s to be running", containerName)
|
||||||
|
case <-ticker.C:
|
||||||
|
container, err := c.cli.ContainerInspect(ctx, containerName)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if container.State != nil && container.State.Running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetContainerPID(ctx context.Context, containerName string) (int, error) {
|
||||||
|
cont, err := c.cli.ContainerInspect(ctx, containerName)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
|
||||||
|
}
|
||||||
|
if cont.State == nil || !cont.State.Running {
|
||||||
|
return 0, fmt.Errorf("container %s is not running", containerName)
|
||||||
|
}
|
||||||
|
return cont.State.Pid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRouteInContainer adds a route inside a container's network namespace using nsenter
|
||||||
|
func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network, gateway string) error {
|
||||||
|
pid, err := c.GetContainerPID(ctx, containerName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get PID for container %s: %w", containerName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-t", fmt.Sprintf("%d", pid),
|
||||||
|
"-n", "--",
|
||||||
|
"ip", "route", "add", network, "via", gateway,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("nsenter", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add route in container %s: %w\noutput: %s", containerName, err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"firewall_containers/network-go/config"
|
||||||
|
"firewall_containers/network-go/docker"
|
||||||
|
"firewall_containers/network-go/iptables"
|
||||||
|
"firewall_containers/network-go/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Orchestrator reconciles the networks.json configuration into Docker networks
|
||||||
|
// and iptables firewall rules
|
||||||
|
type Orchestrator struct {
|
||||||
|
dockerClient *docker.Client
|
||||||
|
iptablesMgr *iptables.Manager
|
||||||
|
resolver *resolver.Resolver
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOrchestrator creates a new firewall orchestrator
|
||||||
|
func NewOrchestrator(dockerClient *docker.Client, iptablesMgr *iptables.Manager, cfg *config.NetworksConfig) *Orchestrator {
|
||||||
|
return &Orchestrator{
|
||||||
|
dockerClient: dockerClient,
|
||||||
|
iptablesMgr: iptablesMgr,
|
||||||
|
resolver: resolver.NewResolver(cfg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReconcileAll runs the full reconciliation: networks, container connections, and firewall rules
|
||||||
|
func (o *Orchestrator) ReconcileAll(ctx context.Context, cfg *config.NetworksConfig) {
|
||||||
|
log.Println("FIREWALL: starting full reconciliation")
|
||||||
|
|
||||||
|
// Update resolver with latest config
|
||||||
|
o.resolver.SetConfig(cfg)
|
||||||
|
|
||||||
|
// Step 0: Enable IP forwarding
|
||||||
|
log.Println("FIREWALL: enabling IP forwarding")
|
||||||
|
if err := o.iptablesMgr.EnsureIPForward(); err != nil {
|
||||||
|
log.Printf("FIREWALL: ERROR enabling ip_forward: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Ensure all defined networks exist
|
||||||
|
o.reconcileNetworks(ctx, cfg)
|
||||||
|
|
||||||
|
// Step 2: Connect containers to networks with assigned IPs
|
||||||
|
o.reconcileIPs(ctx, cfg)
|
||||||
|
|
||||||
|
// Step 3: Reconcile firewall policies
|
||||||
|
o.reconcilePolicies(ctx, cfg)
|
||||||
|
|
||||||
|
log.Println("FIREWALL: full reconciliation completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconcileNetworks creates Docker networks if they don't exist
|
||||||
|
func (o *Orchestrator) reconcileNetworks(ctx context.Context, cfg *config.NetworksConfig) {
|
||||||
|
for name, netCfg := range cfg.Networks {
|
||||||
|
log.Printf("FIREWALL: ensuring network %s (%s, subnet=%s, gateway=%s)", name, netCfg.NetworkName, netCfg.Subnet, netCfg.Gateway)
|
||||||
|
if err := o.dockerClient.EnsureNetwork(ctx, netCfg); err != nil {
|
||||||
|
log.Printf("FIREWALL: ERROR ensuring network %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconcileIPs connects containers to networks with their assigned IPs
|
||||||
|
func (o *Orchestrator) reconcileIPs(ctx context.Context, cfg *config.NetworksConfig) {
|
||||||
|
for ipStr, ipCfg := range cfg.IPs {
|
||||||
|
networkName := findNetworkForIP(cfg, ipStr)
|
||||||
|
if networkName == "" {
|
||||||
|
log.Printf("FIREWALL: WARNING no network found for IP %s (container=%s)", ipStr, ipCfg.ContainerName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("FIREWALL: connecting container %s to network %s with IP %s", ipCfg.ContainerName, networkName, ipStr)
|
||||||
|
|
||||||
|
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
if err := o.dockerClient.WaitForContainerRunning(waitCtx, ipCfg.ContainerName, 10*time.Second); err != nil {
|
||||||
|
log.Printf("FIREWALL: WARNING container %s not running yet: %v, connecting anyway", ipCfg.ContainerName, err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err := o.dockerClient.ConnectContainer(ctx, ipCfg.ContainerName, networkName, ipStr); err != nil {
|
||||||
|
log.Printf("FIREWALL: ERROR connecting container %s to %s: %v", ipCfg.ContainerName, networkName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconcilePolicies translates PolicyConfig entries into iptables rules
|
||||||
|
func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.NetworksConfig) {
|
||||||
|
for i, policy := range cfg.Policies {
|
||||||
|
log.Printf("FIREWALL: processing policy[%d]", i)
|
||||||
|
|
||||||
|
proto := policy.Proto
|
||||||
|
if proto == "" {
|
||||||
|
proto = "tcp"
|
||||||
|
}
|
||||||
|
port := strconv.Itoa(policy.Port)
|
||||||
|
|
||||||
|
// Build comment for iptables (matches shell script's NAME-COMMENT pattern)
|
||||||
|
comment := policy.ServiceName
|
||||||
|
if policy.Name != "" {
|
||||||
|
comment = policy.Name + "-" + policy.ServiceName
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE 1: Rule with "from" field — this is a FORWARD ACCEPT rule
|
||||||
|
if policy.From != "" {
|
||||||
|
o.applyForwardRule(ctx, cfg, policy, proto, port, comment)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE 2: Rule with "nat" field — this is a DNAT/MASQUERADE rule
|
||||||
|
if policy.Nat != "" {
|
||||||
|
o.applyNATRule(ctx, cfg, policy, proto, port, comment)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unhandled pattern
|
||||||
|
log.Printf("FIREWALL: policy[%d] unhandled pattern — service=%s container=%s selector=%s from=%s to=%s port=%d proto=%s nat=%s",
|
||||||
|
i, policy.ServiceName, policy.ContainerName, policy.Selector, policy.From, policy.To, policy.Port, policy.Proto, policy.Nat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Orchestrator) applyForwardRule(ctx context.Context, cfg *config.NetworksConfig, policy config.PolicyConfig, proto, port, comment string) {
|
||||||
|
sourceIP := o.resolveIP(policy.From)
|
||||||
|
targetIP := ""
|
||||||
|
if policy.To != "" {
|
||||||
|
targetIP = o.resolveIP(policy.To)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the chain: use DOCKER-USER (iptables-legacy) or FORWARD
|
||||||
|
chain := "FORWARD"
|
||||||
|
if o.iptablesMgr.Binary() == "/usr/sbin/iptables-legacy" {
|
||||||
|
chain = "DOCKER-USER"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure established/related rule exists at the top
|
||||||
|
if err := o.iptablesMgr.EnsureEstablishedRelated(chain); err != nil {
|
||||||
|
log.Printf("FIREWALL: ERROR ensuring established/related rule in %s: %v", chain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the FORWARD ACCEPT rule
|
||||||
|
if err := o.iptablesMgr.InsertForwardAccept(chain, sourceIP, targetIP, proto, "", port, comment); err != nil {
|
||||||
|
log.Printf("FIREWALL: ERROR inserting FORWARD ACCEPT rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksConfig, policy config.PolicyConfig, proto, port, comment string) {
|
||||||
|
selector := policy.Selector
|
||||||
|
to := policy.To
|
||||||
|
|
||||||
|
// Resolve "to" as target IP
|
||||||
|
targetIP := o.resolveIP(to)
|
||||||
|
|
||||||
|
if targetIP == "" {
|
||||||
|
log.Printf("FIREWALL: WARNING cannot resolve target %s for nat policy", to)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.Nat == "dnat" {
|
||||||
|
// Get the container PID for nsenter
|
||||||
|
pid, err := o.dockerClient.GetContainerPID(ctx, selector)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("FIREWALL: WARNING cannot get PID for container %s: %v, trying host rules", selector, err)
|
||||||
|
// Fall back to host-level PREROUTING
|
||||||
|
if policy.Iface != "" {
|
||||||
|
if err := o.iptablesMgr.InsertPreroutingRuleOnInterface(policy.Iface, proto, port, targetIP, port, comment); err != nil {
|
||||||
|
log.Printf("FIREWALL: ERROR inserting interface PREROUTING rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert DNAT PREROUTING inside container namespace
|
||||||
|
if err := o.iptablesMgr.InsertPreroutingRuleInContainer(pid, "0.0.0.0/0", proto, port, targetIP, port, comment); err != nil {
|
||||||
|
log.Printf("FIREWALL: ERROR inserting container PREROUTING rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveIP resolves a name or IP string to an IP address using networks.json config
|
||||||
|
func (o *Orchestrator) resolveIP(name string) string {
|
||||||
|
// If it's already an IP, return it as CIDR
|
||||||
|
if config.IsIP(name) {
|
||||||
|
return config.ToCIDR(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the resolver which looks up from networks.json
|
||||||
|
ips := o.resolver.Resolve(name)
|
||||||
|
if len(ips) > 0 {
|
||||||
|
return ips[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNetworkForIP finds the network name that contains the given IP in its subnet
|
||||||
|
func findNetworkForIP(cfg *config.NetworksConfig, ip string) string {
|
||||||
|
for _, netCfg := range cfg.Networks {
|
||||||
|
subnet, err := netCfg.ParseCIDR()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
if parsedIP == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if subnet.Contains(parsedIP) {
|
||||||
|
return netCfg.NetworkName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
module firewall_containers/network-go
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require github.com/docker/docker v28.5.2+incompatible
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.7.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
|
github.com/morikuni/aec v1.1.0 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
|
golang.org/x/time v0.15.0 // indirect
|
||||||
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||||
|
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||||
|
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||||
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
|
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# network-go — Docker Network & Firewall Manager
|
||||||
|
|
||||||
|
A Go-based replacement for the `firewall/firewall-add` shell script. It watches
|
||||||
|
`/etc/user/config/networks.json` for changes and reconciles Docker networks,
|
||||||
|
container connections, and iptables firewall rules using the Docker SDK.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
network-go/
|
||||||
|
├── main.go Entry point: watches config, orchestrates reconciliation
|
||||||
|
├── config/
|
||||||
|
│ └── config.go Parses /etc/user/config/networks.json into typed structs
|
||||||
|
├── docker/
|
||||||
|
│ └── docker.go Docker SDK wrapper (network create, container connect,
|
||||||
|
│ container PID for nsenter, route management)
|
||||||
|
├── firewall/
|
||||||
|
│ └── firewall.go Orchestrator: translates policies → iptables rules
|
||||||
|
├── iptables/
|
||||||
|
│ └── iptables.go Manages iptables CLI: PREROUTING DNAT, POSTROUTING MASQUERADE,
|
||||||
|
│ FORWARD ACCEPT, nsenter for container network namespaces
|
||||||
|
├── resolver/
|
||||||
|
│ └── resolver.go Resolves names → IPs using networks.json config
|
||||||
|
├── watcher/
|
||||||
|
│ └── watcher.go Periodic file change detection via MD5 hash polling
|
||||||
|
├── go.mod / go.sum Module definition with Docker SDK dependency
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
### config
|
||||||
|
- `NetworksConfig`, `NetworkConfig`, `IPConfig`, `PolicyConfig` structs matching `networks.json`
|
||||||
|
- `FirewallRule` struct for resolved executable rules
|
||||||
|
- Helpers: `IsIP`, `ToCIDR`, `NetworkPrefix`, `ParseCIDR`, `Load`
|
||||||
|
|
||||||
|
### docker
|
||||||
|
- `Client` wrapping `github.com/docker/docker/client`
|
||||||
|
- `EnsureNetwork` — creates a Docker network if it doesn't exist
|
||||||
|
- `ConnectContainer` / `DisconnectContainer` — manages container ↔ network membership
|
||||||
|
- `GetContainerPID` — returns the container PID for nsenter
|
||||||
|
- `AddRouteInContainer` — adds routes inside a container namespace via nsenter
|
||||||
|
- `WaitForContainerRunning` — polls until a container is running
|
||||||
|
|
||||||
|
### resolver
|
||||||
|
- Resolves names → IPs using the `networks.json` config only
|
||||||
|
- Looks up by `container_name` and `selector` in the `ips` section
|
||||||
|
- Falls back to prefix matching (e.g. `smarthost-backend-1` → prefix `smarthost`)
|
||||||
|
|
||||||
|
### iptables
|
||||||
|
- Auto-detects `iptables-legacy` vs `iptables` (matching shell script logic)
|
||||||
|
- `EnsureIPForward` — enables `/proc/sys/net/ipv4/ip_forward`
|
||||||
|
- `EnsureEstablishedRelated` — inserts ESTABLISHED,RELATED ACCEPT at top of chain
|
||||||
|
- `InsertPreroutingRule` / `InsertPreroutingRuleInContainer` — DNAT rules
|
||||||
|
- `InsertPostroutingMasquerade` — MASQUERADE rules
|
||||||
|
- `InsertForwardAccept` — FORWARD/DOCKER-USER ACCEPT rules
|
||||||
|
- Rule deletion by line-number + grep pattern matching (matching shell script)
|
||||||
|
- nsenter-based execution inside container network namespaces
|
||||||
|
|
||||||
|
### firewall
|
||||||
|
- `Orchestrator` ties all packages together
|
||||||
|
- `ReconcileAll` runs the full reconciliation cycle:
|
||||||
|
1. Enable IP forwarding
|
||||||
|
2. Ensure all Docker networks from config
|
||||||
|
3. Connect containers to networks with assigned IPs
|
||||||
|
4. Apply firewall policies as iptables rules
|
||||||
|
- Policy → rule mapping:
|
||||||
|
- `from` field → FORWARD ACCEPT rule on DOCKER-USER or FORWARD chain
|
||||||
|
- `nat: dnat` field → PREROUTING DNAT inside container namespace via nsenter
|
||||||
|
- Interface-based rules (e.g. `wg0`) → host-level PREROUTING DNAT
|
||||||
|
|
||||||
|
### watcher
|
||||||
|
- `FileWatcher` polls a file at a configurable interval
|
||||||
|
- Computes MD5 hash on each tick
|
||||||
|
- Fires `onChange` callback when hash changes
|
||||||
|
- `Start` / `Stop` for lifecycle management
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The file `/etc/user/config/networks.json` defines:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"networks": {
|
||||||
|
"smarthost-loadbalancer": {
|
||||||
|
"network_name": "smarthost-loadbalancer",
|
||||||
|
"subnet": "172.18.103.0/24",
|
||||||
|
"gateway": "172.18.103.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ips": {
|
||||||
|
"172.18.103.2": {
|
||||||
|
"ip": "172.18.103.2",
|
||||||
|
"container_name": "smarthostloadbalancer",
|
||||||
|
"selector": "smarthostloadbalancer",
|
||||||
|
"service_name": "smarthost-proxy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"service_name": "smarthost-proxy",
|
||||||
|
"container_name": "smarthost_loadbalancer",
|
||||||
|
"selector": "smarthostloadbalancer",
|
||||||
|
"from": "publicbackend",
|
||||||
|
"port": 80,
|
||||||
|
"proto": "tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service_name": "smarthost-proxy",
|
||||||
|
"container_name": "smarthost_loadbalancer",
|
||||||
|
"selector": "smarthostloadbalancer",
|
||||||
|
"name": "wireguardproxy",
|
||||||
|
"iface": "wg0",
|
||||||
|
"nat": "dnat",
|
||||||
|
"to": "smarthostloadbalancer",
|
||||||
|
"port": 80,
|
||||||
|
"proto": "tcp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `NETWORKS_CONFIG_PATH` | `/etc/user/config/networks.json` | Path to the configuration file |
|
||||||
|
| `WATCH_PERIOD_SECONDS` | `30` | Polling interval in seconds for config file changes |
|
||||||
|
| `DEBUG` | `false` | Enable debug output (`1` or `true`) |
|
||||||
|
|
||||||
|
## Key Differences from Shell Script
|
||||||
|
|
||||||
|
| Shell Script (`firewall-add`) | Go Implementation |
|
||||||
|
|---|---|
|
||||||
|
| `docker ps --format`, `docker inspect` | Docker SDK (`github.com/docker/docker`) |
|
||||||
|
| `/etc/dns/hosts.local` lookup | networks.json config lookup |
|
||||||
|
| `$SOURCE` / `$TARGET` env vars | `from` / `to` fields in `networks.json` |
|
||||||
|
| iptables via bash + grep + awk | Go `os/exec` with structured line matching |
|
||||||
|
| `nsenter -t PID -n` for container iptables | `nsenter` via `os/exec` in `iptables.Manager` |
|
||||||
|
| `$ROUTE=true` + `ip route` | `docker.Client.AddRouteInContainer()` |
|
||||||
|
| Manual per-rule invocation | `Orchestrator.ReconcileAll()` batch reconciliation |
|
||||||
|
|
||||||
|
## Build & Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o network-go .
|
||||||
|
./network-go
|
||||||
|
```
|
||||||
|
|
||||||
|
In Docker:
|
||||||
|
```bash
|
||||||
|
go build -o network-go .
|
||||||
|
# Mount Docker socket and config
|
||||||
|
docker run -v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v /etc/user/config:/etc/user/config \
|
||||||
|
network-go
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
package iptables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager manages iptables rules via the iptables/iptables-legacy CLI
|
||||||
|
type Manager struct {
|
||||||
|
binary string // /usr/sbin/iptables or /usr/sbin/iptables-legacy
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new iptables manager, auto-detecting the binary
|
||||||
|
func NewManager(debug bool) *Manager {
|
||||||
|
m := &Manager{debug: debug}
|
||||||
|
m.detectBinary()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectBinary checks if iptables-legacy is available (matching shell script logic)
|
||||||
|
func (m *Manager) detectBinary() {
|
||||||
|
cmd := exec.Command("/usr/sbin/iptables-legacy", "-L")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err == nil && strings.Contains(string(output), "DOCKER-USER") {
|
||||||
|
m.binary = "/usr/sbin/iptables-legacy"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.binary = "/usr/sbin/iptables"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary returns the detected iptables binary path
|
||||||
|
func (m *Manager) Binary() string {
|
||||||
|
return m.binary
|
||||||
|
}
|
||||||
|
|
||||||
|
// run executes an iptables command on the host
|
||||||
|
func (m *Manager) run(args ...string) error {
|
||||||
|
if m.debug {
|
||||||
|
fmt.Printf("[IPTABLES DEBUG] %s %s\n", m.binary, strings.Join(args, " "))
|
||||||
|
}
|
||||||
|
cmd := exec.Command(m.binary, args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables %s failed: %w\noutput: %s", strings.Join(args, " "), err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runInContainer executes an iptables command inside a container's network namespace via nsenter
|
||||||
|
func (m *Manager) runInContainer(pid int, table string, args ...string) error {
|
||||||
|
iptPath := "/sbin/iptables-legacy"
|
||||||
|
if !strings.Contains(m.binary, "legacy") {
|
||||||
|
iptPath = "/sbin/iptables"
|
||||||
|
}
|
||||||
|
|
||||||
|
fullArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath}
|
||||||
|
if table != "" {
|
||||||
|
fullArgs = append(fullArgs, "-t", table)
|
||||||
|
}
|
||||||
|
fullArgs = append(fullArgs, args...)
|
||||||
|
|
||||||
|
if m.debug {
|
||||||
|
fmt.Printf("[IPTABLES DEBUG] nsenter %s\n", strings.Join(fullArgs, " "))
|
||||||
|
}
|
||||||
|
cmd := exec.Command("nsenter", fullArgs...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nsenter iptables failed: %w\noutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureIPForward enables IP forwarding on the host
|
||||||
|
func (m *Manager) EnsureIPForward() error {
|
||||||
|
cmd := exec.Command("sh", "-c", "echo 1 > /proc/sys/net/ipv4/ip_forward")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to enable ip_forward: %w\noutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureEstablishedRelated inserts an ESTABLISHED,RELATED accept rule at the top of a chain
|
||||||
|
// if it doesn't already exist
|
||||||
|
func (m *Manager) EnsureEstablishedRelated(chain string) error {
|
||||||
|
checkArgs := []string{"-w", "-n", "-L", chain}
|
||||||
|
cmd := exec.Command(m.binary, checkArgs...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// Chain may not exist, create it
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only insert if ESTABLISHED,RELATED rule is not present
|
||||||
|
if !strings.Contains(string(output), "ESTABLISHED") || !strings.Contains(string(output), "RELATED") {
|
||||||
|
args := []string{"-w", "-I", chain, "-m", "state", "--state", "established,related", "-j", "ACCEPT"}
|
||||||
|
return m.run(args...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLine deletes a specific line number from a chain
|
||||||
|
func (m *Manager) DeleteLine(chain string, lineNum string) error {
|
||||||
|
args := []string{"-w", "-D", chain, lineNum}
|
||||||
|
return m.run(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLineInContainer deletes a specific line number from a chain inside a container namespace
|
||||||
|
func (m *Manager) DeleteLineInContainer(pid int, table, chain, lineNum string) error {
|
||||||
|
args := []string{"-D", chain, lineNum}
|
||||||
|
return m.runInContainer(pid, table, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLineNumbers returns line numbers matching certain criteria in a chain/table
|
||||||
|
// This implements the grep logic from the shell script: iptables -w --line-number -n -L $CHAIN | grep ...
|
||||||
|
func (m *Manager) getLineNumbers(chain, table string, grepPatterns ...string) []string {
|
||||||
|
args := []string{"-w", "--line-number", "-n", "-L", chain}
|
||||||
|
if table != "" {
|
||||||
|
args = []string{"-w", "-t", table, "--line-number", "-n", "-L", chain}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(m.binary, args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
var matchingLines []string
|
||||||
|
for _, line := range lines {
|
||||||
|
matchesAll := true
|
||||||
|
for _, pattern := range grepPatterns {
|
||||||
|
if !strings.Contains(line, pattern) {
|
||||||
|
matchesAll = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matchesAll {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
matchingLines = append(matchingLines, fields[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchingLines
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteMatchingLines deletes all lines in a chain matching the given patterns
|
||||||
|
func (m *Manager) deleteMatchingLines(chain, table string, grepPatterns ...string) error {
|
||||||
|
lines := m.getLineNumbers(chain, table, grepPatterns...)
|
||||||
|
// Reverse order (highest line first) so deletions don't shift line numbers
|
||||||
|
for i := len(lines) - 1; i >= 0; i-- {
|
||||||
|
if err := m.DeleteLine(chain, lines[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteMatchingLinesInContainer deletes matching lines inside a container namespace
|
||||||
|
func (m *Manager) deleteMatchingLinesInContainer(pid int, table, chain string, grepPatterns ...string) error {
|
||||||
|
// For container namespaces, we use a different approach: list via nsenter + grep
|
||||||
|
iptPath := "/sbin/iptables-legacy"
|
||||||
|
if !strings.Contains(m.binary, "legacy") {
|
||||||
|
iptPath = "/sbin/iptables"
|
||||||
|
}
|
||||||
|
|
||||||
|
nsenterArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath, "-w", "--line-number", "-n", "-t", table, "-L", chain}
|
||||||
|
cmd := exec.Command("nsenter", nsenterArgs...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
var matchingLines []string
|
||||||
|
for _, line := range lines {
|
||||||
|
matchesAll := true
|
||||||
|
for _, pattern := range grepPatterns {
|
||||||
|
if !strings.Contains(line, pattern) {
|
||||||
|
matchesAll = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matchesAll {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
matchingLines = append(matchingLines, fields[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete in reverse order
|
||||||
|
for i := len(matchingLines) - 1; i >= 0; i-- {
|
||||||
|
if err := m.DeleteLineInContainer(pid, table, chain, matchingLines[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPreroutingRule inserts a DNAT PREROUTING rule on the host
|
||||||
|
func (m *Manager) InsertPreroutingRule(sourceIP, proto, sourcePort, targetIP, targetPort, comment string) error {
|
||||||
|
// First, delete existing matching rules
|
||||||
|
patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment}
|
||||||
|
if err := m.deleteMatchingLines("PREROUTING", "nat", patterns...); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old PREROUTING rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the new rule
|
||||||
|
args := []string{
|
||||||
|
"-w", "-t", "nat", "-I", "PREROUTING",
|
||||||
|
"-d", sourceIP,
|
||||||
|
"-p", proto,
|
||||||
|
"--dport", sourcePort,
|
||||||
|
"-m", "comment", "--comment", comment,
|
||||||
|
"-j", "DNAT", "--to", targetIP + ":" + targetPort,
|
||||||
|
}
|
||||||
|
return m.run(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPreroutingRuleOnInterface inserts a DNAT PREROUTING rule on a specific interface
|
||||||
|
func (m *Manager) InsertPreroutingRuleOnInterface(iface, proto, sourcePort, targetIP, targetPort, comment string) error {
|
||||||
|
args := []string{
|
||||||
|
"-w", "-I", "PREROUTING",
|
||||||
|
"-i", iface,
|
||||||
|
"-p", proto,
|
||||||
|
"--dport", sourcePort,
|
||||||
|
"-m", "comment", "--comment", comment,
|
||||||
|
"-j", "DNAT", "--to", targetIP + ":" + targetPort,
|
||||||
|
}
|
||||||
|
return m.run(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPostroutingMasquerade inserts a MASQUERADE POSTROUTING rule on the host
|
||||||
|
func (m *Manager) InsertPostroutingMasquerade(sourceCIDR, proto, sourcePort, comment string) error {
|
||||||
|
// Delete existing matching rules first
|
||||||
|
patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort}
|
||||||
|
if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old POSTROUTING rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-w", "-t", "nat", "-I", "POSTROUTING",
|
||||||
|
"-s", sourceCIDR,
|
||||||
|
"-p", proto,
|
||||||
|
"--sport", sourcePort,
|
||||||
|
"-m", "comment", "--comment", comment,
|
||||||
|
"-j", "MASQUERADE",
|
||||||
|
}
|
||||||
|
return m.run(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPostroutingMasqueradeForTarget inserts a MASQUERADE POSTROUTING rule for a target
|
||||||
|
func (m *Manager) InsertPostroutingMasqueradeForTarget(targetCIDR, proto, targetPort, comment string) error {
|
||||||
|
patterns := []string{"MASQUERADE", comment, targetCIDR, targetPort}
|
||||||
|
if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old POSTROUTING rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-w", "-t", "nat", "-I", "POSTROUTING",
|
||||||
|
"-d", targetCIDR,
|
||||||
|
"-p", proto,
|
||||||
|
"--dport", targetPort,
|
||||||
|
"-m", "comment", "--comment", comment,
|
||||||
|
"-j", "MASQUERADE",
|
||||||
|
}
|
||||||
|
return m.run(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertForwardAccept inserts a FORWARD ACCEPT rule on the host
|
||||||
|
func (m *Manager) InsertForwardAccept(chain, sourceIP, targetIP, proto, sourcePort, targetPort, comment string) error {
|
||||||
|
// Build grep patterns to match existing rules
|
||||||
|
var grepPatterns []string
|
||||||
|
grepPatterns = append(grepPatterns, proto)
|
||||||
|
if sourceIP != "" {
|
||||||
|
grepPatterns = append(grepPatterns, sourceIP)
|
||||||
|
}
|
||||||
|
if targetIP != "" {
|
||||||
|
grepPatterns = append(grepPatterns, targetIP)
|
||||||
|
}
|
||||||
|
if sourcePort != "" {
|
||||||
|
grepPatterns = append(grepPatterns, sourcePort)
|
||||||
|
}
|
||||||
|
if targetPort != "" {
|
||||||
|
grepPatterns = append(grepPatterns, targetPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old matching rules
|
||||||
|
if err := m.deleteMatchingLines(chain, "", grepPatterns...); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old FORWARD rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build iptables args
|
||||||
|
args := []string{"-w", "-I", chain, "-p", proto}
|
||||||
|
if sourceIP != "" {
|
||||||
|
args = append(args, "-s", sourceIP)
|
||||||
|
}
|
||||||
|
if targetIP != "" {
|
||||||
|
args = append(args, "-d", targetIP)
|
||||||
|
}
|
||||||
|
if sourcePort != "" {
|
||||||
|
args = append(args, "--sport", sourcePort)
|
||||||
|
}
|
||||||
|
if targetPort != "" {
|
||||||
|
args = append(args, "--dport", targetPort)
|
||||||
|
}
|
||||||
|
args = append(args, "-m", "comment", "--comment", comment, "-j", "ACCEPT")
|
||||||
|
|
||||||
|
return m.run(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteForwardAccept deletes a FORWARD ACCEPT rule by comment
|
||||||
|
func (m *Manager) DeleteForwardAccept(chain, comment string) error {
|
||||||
|
lines := m.getLineNumbers(chain, "", comment)
|
||||||
|
for i := len(lines) - 1; i >= 0; i-- {
|
||||||
|
if err := m.DeleteLine(chain, lines[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPreroutingRuleInContainer inserts a DNAT PREROUTING rule inside a container namespace
|
||||||
|
func (m *Manager) InsertPreroutingRuleInContainer(pid int, sourceIP, proto, sourcePort, targetIP, targetPort, comment string) error {
|
||||||
|
// Delete existing first
|
||||||
|
patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment}
|
||||||
|
if err := m.deleteMatchingLinesInContainer(pid, "nat", "PREROUTING", patterns...); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old container PREROUTING rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-I", "PREROUTING",
|
||||||
|
"-d", sourceIP,
|
||||||
|
"-p", proto,
|
||||||
|
"--dport", sourcePort,
|
||||||
|
"-m", "comment", "--comment", comment,
|
||||||
|
"-j", "DNAT", "--to", targetIP + ":" + targetPort,
|
||||||
|
}
|
||||||
|
return m.runInContainer(pid, "nat", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPostroutingMasqueradeInContainer inserts a MASQUERADE POSTROUTING rule inside a container namespace
|
||||||
|
func (m *Manager) InsertPostroutingMasqueradeInContainer(pid int, sourceCIDR, proto, sourcePort, comment string) error {
|
||||||
|
patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort}
|
||||||
|
if err := m.deleteMatchingLinesInContainer(pid, "nat", "POSTROUTING", patterns...); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old container POSTROUTING rules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-I", "POSTROUTING",
|
||||||
|
"-s", sourceCIDR,
|
||||||
|
"-p", proto,
|
||||||
|
"--sport", sourcePort,
|
||||||
|
"-m", "comment", "--comment", comment,
|
||||||
|
"-j", "MASQUERADE",
|
||||||
|
}
|
||||||
|
return m.runInContainer(pid, "nat", args...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"firewall_containers/network-go/config"
|
||||||
|
"firewall_containers/network-go/docker"
|
||||||
|
"firewall_containers/network-go/firewall"
|
||||||
|
"firewall_containers/network-go/iptables"
|
||||||
|
"firewall_containers/network-go/watcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config path - can be overridden via environment variable
|
||||||
|
const defaultConfigPath = "/etc/user/config/networks.json"
|
||||||
|
|
||||||
|
// Watch period - can be overridden via environment variable
|
||||||
|
const defaultWatchPeriod = 30 * time.Second
|
||||||
|
|
||||||
|
func getConfigPath() string {
|
||||||
|
path := os.Getenv("NETWORKS_CONFIG_PATH")
|
||||||
|
if path == "" {
|
||||||
|
return defaultConfigPath
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWatchPeriod() time.Duration {
|
||||||
|
periodStr := os.Getenv("WATCH_PERIOD_SECONDS")
|
||||||
|
if periodStr == "" {
|
||||||
|
return defaultWatchPeriod
|
||||||
|
}
|
||||||
|
seconds, err := time.ParseDuration(periodStr + "s")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("MAIN: invalid WATCH_PERIOD_SECONDS=%s, using default %s", periodStr, defaultWatchPeriod)
|
||||||
|
return defaultWatchPeriod
|
||||||
|
}
|
||||||
|
return seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDebug() bool {
|
||||||
|
return os.Getenv("DEBUG") == "1" || os.Getenv("DEBUG") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||||
|
log.Println("MAIN: starting network-go firewall container manager")
|
||||||
|
|
||||||
|
configPath := getConfigPath()
|
||||||
|
watchPeriod := getWatchPeriod()
|
||||||
|
debug := getDebug()
|
||||||
|
|
||||||
|
log.Printf("MAIN: config path = %s", configPath)
|
||||||
|
log.Printf("MAIN: watch period = %s", watchPeriod)
|
||||||
|
log.Printf("MAIN: debug = %v", debug)
|
||||||
|
|
||||||
|
// Create Docker client (uses DOCKER_HOST env var automatically)
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("MAIN: failed to create Docker client: %v", err)
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
|
||||||
|
// Create iptables manager
|
||||||
|
iptablesMgr := iptables.NewManager(debug)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Load initial config
|
||||||
|
cfg, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("MAIN: failed to load initial config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the firewall orchestrator (needs config for resolver)
|
||||||
|
orchestrator := firewall.NewOrchestrator(dockerClient, iptablesMgr, cfg)
|
||||||
|
|
||||||
|
// Run full reconciliation
|
||||||
|
orchestrator.ReconcileAll(ctx, cfg)
|
||||||
|
|
||||||
|
// Set up file watcher to detect changes and re-reconcile
|
||||||
|
onChange := func() {
|
||||||
|
log.Println("MAIN: config file change detected, reloading and reconciling")
|
||||||
|
newCfg, err := config.Load(configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("MAIN: failed to reload config: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
orchestrator.ReconcileAll(ctx, newCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileWatcher := watcher.NewFileWatcher(configPath, watchPeriod, onChange)
|
||||||
|
fileWatcher.Start()
|
||||||
|
|
||||||
|
// Wait for shutdown signal
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
sig := <-sigCh
|
||||||
|
log.Printf("MAIN: received signal %v, shutting down", sig)
|
||||||
|
fileWatcher.Stop()
|
||||||
|
log.Println("MAIN: shutdown complete")
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"firewall_containers/network-go/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolver resolves names to IPs using the networks.json configuration
|
||||||
|
type Resolver struct {
|
||||||
|
cfg *config.NetworksConfig
|
||||||
|
retries int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResolver creates a new name resolver backed by the networks.json config
|
||||||
|
func NewResolver(cfg *config.NetworksConfig) *Resolver {
|
||||||
|
return &Resolver{
|
||||||
|
cfg: cfg,
|
||||||
|
retries: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig updates the config used for name resolution
|
||||||
|
func (r *Resolver) SetConfig(cfg *config.NetworksConfig) {
|
||||||
|
r.cfg = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRetries sets the number of retries for resolution
|
||||||
|
func (r *Resolver) SetRetries(n int) {
|
||||||
|
r.retries = n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve resolves a name to one or more IP addresses
|
||||||
|
// It looks up the name in the networks.json config by container_name and selector fields
|
||||||
|
func (r *Resolver) Resolve(name string) []string {
|
||||||
|
if r.cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ips []string
|
||||||
|
|
||||||
|
// Look up by container_name and selector in the IPs section
|
||||||
|
for _, ipCfg := range r.cfg.IPs {
|
||||||
|
if ipCfg.ContainerName == name || ipCfg.Selector == name {
|
||||||
|
ips = append(ips, ipCfg.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact match, try prefix matching (extract prefix before first dash)
|
||||||
|
if len(ips) == 0 && strings.Contains(name, "-") {
|
||||||
|
prefix := name[:strings.Index(name, "-")]
|
||||||
|
for _, ipCfg := range r.cfg.IPs {
|
||||||
|
if strings.HasPrefix(ipCfg.ContainerName, prefix) || strings.HasPrefix(ipCfg.Selector, prefix) {
|
||||||
|
ips = append(ips, ipCfg.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ips) == 0 {
|
||||||
|
log.Printf("RESOLVER: no IP found for %s in networks.json config", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package watcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileWatcher periodically checks a file for changes using MD5 hash
|
||||||
|
type FileWatcher struct {
|
||||||
|
path string
|
||||||
|
period time.Duration
|
||||||
|
lastHash string
|
||||||
|
onChange func()
|
||||||
|
stopCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileWatcher creates a new file watcher that polls the file at the given period
|
||||||
|
func NewFileWatcher(path string, period time.Duration, onChange func()) *FileWatcher {
|
||||||
|
return &FileWatcher{
|
||||||
|
path: path,
|
||||||
|
period: period,
|
||||||
|
lastHash: "",
|
||||||
|
onChange: onChange,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashFile computes an MD5 hash of the file contents
|
||||||
|
func (fw *FileWatcher) hashFile() (string, error) {
|
||||||
|
data, err := os.ReadFile(fw.path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read file %s: %w", fw.path, err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", md5.Sum(data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins polling the file for changes in a goroutine
|
||||||
|
func (fw *FileWatcher) Start() {
|
||||||
|
// Compute initial hash
|
||||||
|
hash, err := fw.hashFile()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WATCHER: initial hash computation failed for %s: %v", fw.path, err)
|
||||||
|
} else {
|
||||||
|
fw.lastHash = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(fw.period)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
log.Printf("WATCHER: started watching %s every %s", fw.path, fw.period)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-fw.stopCh:
|
||||||
|
log.Printf("WATCHER: stopped watching %s", fw.path)
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
hash, err := fw.hashFile()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WATCHER: failed to hash %s: %v", fw.path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if hash != fw.lastHash {
|
||||||
|
log.Printf("WATCHER: detected change in %s", fw.path)
|
||||||
|
fw.lastHash = hash
|
||||||
|
if fw.onChange != nil {
|
||||||
|
fw.onChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop signals the watcher goroutine to stop
|
||||||
|
func (fw *FileWatcher) Stop() {
|
||||||
|
close(fw.stopCh)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user