first commit
This commit is contained in:
123
nat/nat.go
Normal file
123
nat/nat.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package nat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const tableName = "alpine-router-nat"
|
||||
|
||||
// Config holds NAT masquerade settings per interface.
|
||||
type Config struct {
|
||||
// Interfaces is the list of LAN interface names for which masquerade is enabled.
|
||||
// Traffic arriving on these interfaces will be NATted to the outgoing WAN interface.
|
||||
Interfaces []string `json:"interfaces"`
|
||||
}
|
||||
|
||||
// configPath returns the path to nat.json next to the running binary.
|
||||
func configPath() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return filepath.Join("configs", "nat.json")
|
||||
}
|
||||
return filepath.Join(filepath.Dir(exe), "configs", "nat.json")
|
||||
}
|
||||
|
||||
// IsInstalled reports whether the nft (nftables) binary is available.
|
||||
func IsInstalled() bool {
|
||||
_, err := exec.LookPath("nft")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Load reads the NAT config from disk.
|
||||
// Returns an empty config if the file does not exist yet.
|
||||
func Load() (*Config, error) {
|
||||
data, err := os.ReadFile(configPath())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &Config{Interfaces: []string{}}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read nat config: %w", err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse nat config: %w", err)
|
||||
}
|
||||
if cfg.Interfaces == nil {
|
||||
cfg.Interfaces = []string{}
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Save writes the NAT config to the configs/ directory next to the binary.
|
||||
func Save(cfg *Config) error {
|
||||
p := configPath()
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
return fmt.Errorf("mkdir configs: %w", err)
|
||||
}
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, data, 0644)
|
||||
}
|
||||
|
||||
// ApplyRules flushes the existing alpine-router NAT table and recreates it
|
||||
// from the provided config. Called on every daemon startup and on config save.
|
||||
//
|
||||
// nftables is used instead of iptables because it applies all rules atomically
|
||||
// in a single kernel call, which is faster and avoids partial-state issues.
|
||||
func ApplyRules(cfg *Config) error {
|
||||
return ApplyRulesWithBlocked(cfg, nil)
|
||||
}
|
||||
|
||||
// ApplyRulesWithBlocked is like ApplyRules but also installs drop rules for
|
||||
// the given list of blocked client IPs.
|
||||
func ApplyRulesWithBlocked(cfg *Config, blockedIPs []string) error {
|
||||
if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644); err != nil {
|
||||
return fmt.Errorf("enable ip_forward: %w", err)
|
||||
}
|
||||
|
||||
exec.Command("nft", "delete", "table", "ip", tableName).Run()
|
||||
|
||||
if len(cfg.Interfaces) == 0 && len(blockedIPs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "table ip %s {\n", tableName)
|
||||
|
||||
sb.WriteString(" chain forward {\n")
|
||||
sb.WriteString(" type filter hook forward priority filter; policy drop;\n")
|
||||
sb.WriteString(" ct state established,related accept\n")
|
||||
|
||||
for _, ip := range blockedIPs {
|
||||
fmt.Fprintf(&sb, " ip saddr %s drop\n", ip)
|
||||
fmt.Fprintf(&sb, " ip daddr %s drop\n", ip)
|
||||
}
|
||||
|
||||
for _, iface := range cfg.Interfaces {
|
||||
fmt.Fprintf(&sb, " iifname \"%s\" accept\n", iface)
|
||||
}
|
||||
sb.WriteString(" }\n")
|
||||
|
||||
sb.WriteString(" chain postrouting {\n")
|
||||
sb.WriteString(" type nat hook postrouting priority srcnat; policy accept;\n")
|
||||
for _, iface := range cfg.Interfaces {
|
||||
fmt.Fprintf(&sb, " iifname \"%s\" masquerade\n", iface)
|
||||
}
|
||||
sb.WriteString(" }\n")
|
||||
sb.WriteString("}\n")
|
||||
|
||||
cmd := exec.Command("nft", "-f", "-")
|
||||
cmd.Stdin = strings.NewReader(sb.String())
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("nft apply: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user