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 }