Files
tomswall/internal/config/snat.go
T
unkinben 8d9a76c751 Add comprehensive nftables compiler with shorewall feature parity
Rewrites the compiler from ~440 to ~1700 lines covering all major shorewall
firewall features: loopback, conntrack fast-path, anti-spoof, DHCP, intra-zone,
blacklist/whitelist, conntrack notrack, tunnels (13 types), rules with sections,
DNAT/redirect, SNAT/masquerade, static NAT, policies with zone exclusions,
MSS clamping, rate limiting, connection limiting, negated addresses, ICMP type
matching, TCP RST reject, user/UID matching, mark match/set, NFQUEUE, NONAT,
and policy-level rate/conn limiting.

Adds full config types for all shorewall subsystems (mangle, accounting, maclist,
netmap, providers, tunnels, conntrack, blrules, proxyarp/ndp, routes, tc, secmarks),
shorewall migration tooling, expanded CLI commands, expression-level diff engine,
and 49 unit tests.
2026-07-01 23:56:44 +10:00

98 lines
3.0 KiB
Go

package config
import "fmt"
type SNATAction string
const (
SNATMasquerade SNATAction = "masquerade"
SNATAddress SNATAction = "snat"
SNATContinue SNATAction = "continue"
SNATLog SNATAction = "log"
)
// SNATRule defines a source NAT or masquerade rule.
// Rules are evaluated in order — first match wins.
type SNATRule struct {
Action SNATAction `yaml:"action"`
// For SNAT: the source address (or address range first-last) to rewrite to.
// Supports: single IP, IP range (1.2.3.4-1.2.3.7), or "detect" (use interface addresses).
Address string `yaml:"address,omitempty"`
// Port remapping for SNAT/MASQUERADE. Format: lowport-highport or single port.
// Requires proto to be tcp, udp, dccp, or sctp.
PortRange string `yaml:"port_range,omitempty"`
// Randomize port mapping.
Random bool `yaml:"random,omitempty"`
// Give a client the same source/destination IP pair (only with address ranges).
Persistent bool `yaml:"persistent,omitempty"`
// Source addresses/networks to match for masquerading.
// Supports: CIDR, host address, comma-separated list, ipset (+name).
Source string `yaml:"source,omitempty"`
// Outgoing interface(s) and optional destination address qualification.
// Format: interface, interface:dest-address, or comma-separated interfaces.
// Use "$FW" for SNAT in the INPUT chain.
Dest string `yaml:"dest"`
// Protocol restriction. Comma-separated list allowed.
Proto string `yaml:"proto,omitempty"`
// Destination port(s).
DPort PortSpec `yaml:"dport,omitempty"`
// Source port(s).
SPort PortSpec `yaml:"sport,omitempty"`
// Packet/connection mark test. Format: [!]value[/mask][:C]
Mark string `yaml:"mark,omitempty"`
// Original destination address filter — match only connections that were
// previously DNAT'd to these addresses.
OrigDest string `yaml:"origdest,omitempty"`
// Random matching probability (0 < p <= 1) for load-balancing across
// multiple SNAT addresses.
Probability float64 `yaml:"probability,omitempty"`
// Log level (for log action, or appended to other actions).
Log string `yaml:"log,omitempty"`
Comment string `yaml:"comment,omitempty"`
}
func (c *Config) validateSNAT() error {
for i, s := range c.SNAT {
switch s.Action {
case SNATMasquerade, SNATAddress, SNATContinue, SNATLog:
default:
return fmt.Errorf("snat[%d]: unknown action %q", i, s.Action)
}
if s.Action == SNATAddress && s.Address == "" {
return fmt.Errorf("snat[%d]: address required for snat action", i)
}
if s.Dest == "" {
return fmt.Errorf("snat[%d]: dest required", i)
}
if s.PortRange != "" && s.Proto == "" {
return fmt.Errorf("snat[%d]: port_range requires proto (tcp, udp, dccp, or sctp)", i)
}
if s.Persistent && s.Address == "" {
return fmt.Errorf("snat[%d]: persistent requires an address or address range", i)
}
if s.Probability != 0 && (s.Probability <= 0 || s.Probability > 1) {
return fmt.Errorf("snat[%d]: probability must be between 0 (exclusive) and 1 (inclusive)", i)
}
}
return nil
}