feat(P3): nft SNAT+DNAT + orca init ingress bootstrap (REQ-173)
nft emitter (internal/emitter/nft.go): - Add DNATTarget field (C-51: validated via net.ParseIP; injection guard). Default 127.0.0.1; proxmox native uses LXC bridge IP. - Add EnableSNAT field (default true for zero-value config). - Add postrouting masquerade chain (research Topic 1): ip saddr 127.0.0.0/8 oifname != lo masquerade - Shift input/forward priority from filter (=0) to -10 (research Topic 2: pve-firewall coexistence — avoids same-priority undefined evaluation order). internal/ingress/bootstrap.go (new): - BootstrapLocalIngress: mkdir dirs, push step-ca root CA (C-60: certpaths.CACertPath not CAPath), render+write traefik static config (C-58: preserves traefik-on-public-ip opt-out), render+ write+apply nft ruleset, pre-create table (C-55: avoids first- apply flush-table error), ensure podman container. All non-fatal. init.go: Step 4d now calls ingress.BootstrapLocalIngress (R-024). doctor_nft.go: assert postrouting masquerade + priority -10. Tests: nft_test.go — DNATTarget substitution, invalid DNATTarget rejection (C-51), EnableSNAT=false omits postrouting, priority -10. ---ci--- project: orca phase: 3 milestone: v0.14 status: execute ---/ci---
This commit is contained in:
@@ -113,6 +113,20 @@ func runNftChecks(ctx context.Context) []nftCheckResult {
|
||||
results = append(results, nftCheckResult{Name: "nft:dnat-80", Result: "FAIL", Message: "DNAT :80->127.0.0.1:8080 missing"})
|
||||
}
|
||||
|
||||
// Research Topic 1: postrouting masquerade for DNAT return path.
|
||||
if strings.Contains(tableStr, "masquerade") {
|
||||
results = append(results, nftCheckResult{Name: "nft:snat-masquerade", Result: "PASS", Message: "postrouting masquerade (SNAT) present"})
|
||||
} else {
|
||||
results = append(results, nftCheckResult{Name: "nft:snat-masquerade", Result: "FAIL", Message: "postrouting masquerade missing (R-024)"})
|
||||
}
|
||||
|
||||
// Research Topic 2: priority -10 on input/forward (pve-firewall coexistence).
|
||||
if strings.Contains(tableStr, "priority -10") {
|
||||
results = append(results, nftCheckResult{Name: "nft:priority", Result: "PASS", Message: "input/forward chains at priority -10 (pve-firewall coexistence)"})
|
||||
} else {
|
||||
results = append(results, nftCheckResult{Name: "nft:priority", Result: "WARN", Message: "priority -10 not found (may be pre-v0.14 ruleset)"})
|
||||
}
|
||||
|
||||
if strings.Contains(tableStr, "ora_rl") {
|
||||
results = append(results, nftCheckResult{Name: "nft:rate-limit", Result: "PASS", Message: "rate-limit meter ora_rl present"})
|
||||
} else {
|
||||
|
||||
+13
-10
@@ -9,6 +9,7 @@ import (
|
||||
"git.cloudinit.dev/coreci/orca/internal/acl"
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
"git.cloudinit.dev/coreci/orca/internal/identity"
|
||||
"git.cloudinit.dev/coreci/orca/internal/ingress"
|
||||
"git.cloudinit.dev/coreci/orca/internal/model"
|
||||
"git.cloudinit.dev/coreci/orca/internal/paths"
|
||||
"git.cloudinit.dev/coreci/orca/internal/secrets"
|
||||
@@ -251,20 +252,22 @@ func runInit(out interface{ Write([]byte) (int, error) }) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4d: Ensure orca-traefik podman container on the lead node
|
||||
// (REQ-172, R-024). Replaces v0.13 binary+systemd install.
|
||||
// The container runs traefik from the orca-traefik image with
|
||||
// --network host, --restart=unless-stopped, and volume mounts for
|
||||
// dynamic config + step-ca root CA. Idempotent.
|
||||
if err := ensureTraefikContainerLocal(); err != nil {
|
||||
// Step 4d: Ensure complete ingress stack on the lead node (R-024).
|
||||
// This replaces the v0.13 binary+systemd traefik install with:
|
||||
// 1. Render + write traefik static config (traefik.yml)
|
||||
// 2. Render + write + apply nft DNAT/SNAT ruleset (orca.nft)
|
||||
// 3. Push step-ca root CA to /etc/orca/step-ca-root.crt
|
||||
// 4. Ensure podman orca-traefik container running
|
||||
// All steps non-fatal (offline host tolerance).
|
||||
if err := ingress.BootstrapLocalIngress(context.Background(), version); err != nil {
|
||||
if !jsonOutput {
|
||||
fmt.Fprintf(out, "Traefik container skipped: %v\n", err)
|
||||
fmt.Fprintf(out, "Ingress bootstrap skipped: %v\n", err)
|
||||
}
|
||||
summary.Steps = append(summary.Steps, stepResult{Label: "traefik", Status: "skipped", Detail: err.Error()})
|
||||
summary.Steps = append(summary.Steps, stepResult{Label: "ingress", Status: "skipped", Detail: err.Error()})
|
||||
} else {
|
||||
summary.Steps = append(summary.Steps, stepResult{Label: "traefik", Status: "ok", Detail: "podman container running"})
|
||||
summary.Steps = append(summary.Steps, stepResult{Label: "ingress", Status: "ok", Detail: "nft+traefik container active"})
|
||||
if !jsonOutput {
|
||||
fmt.Fprintf(out, "Traefik container: running (podman orca-traefik)\n")
|
||||
fmt.Fprintf(out, "Ingress: nft DNAT+SNAT applied, traefik container running\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user