mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Implement inbound mesh port forwarding
Mesh peers can now reach a configured host:port on the gateway's LAN
via static port-forward rules on fips-gateway. Mirror of the outbound
LAN gateway (IDEA-0079 / TASK-2026-0056).
Config: new gateway.port_forwards list of { listen_port, proto,
target } entries. Targets are SocketAddrV6 — IPv4 is rejected at
parse time. Validation rejects zero listen ports and duplicate
(listen_port, proto) pairs.
NAT: NatManager gains a port_forwards field and set_port_forwards()
setter, rebuilt in the same atomic rustables batch as the address
mappings. Each forward emits a prerouting DNAT rule keyed on
(iifname fips0, nfproto ipv6, l4proto, tcp/udp dport) that rewrites
destination address and port via Nat::with_ip_register +
with_port_register. When any forwards are configured, a single
LAN-side masquerade is installed on (iifname fips0, oifname
lan_interface, nfproto ipv6) so the LAN host sees the gateway as
source and replies flow back through conntrack. This rule is
distinct from the existing oifname fips0 masquerade that serves the
outbound pool.
Binary: fips-gateway validates port_forwards at startup and calls
set_port_forwards after NatManager construction; startup failure
cleans up the nftables table before exiting.
Test: extend testing/static/scripts/gateway-test.sh with Phase 7
that runs a marker HTTP server on the LAN-side client (fd02::20:8080)
and, from the mesh peer, curls the gateway's fips0 address on port
18080 to exercise the full DNAT + LAN masquerade path. The
LAN-side HTTP server is started with 'docker exec -d' plus a
bind-ready poll on ss; 'docker exec bash -c "cmd &"' does not
keep the child alive past the exec session even with nohup.
Test-infra: ci-local.sh now builds/clippies/tests with
--features gateway, matching GitHub CI. Without this the release
fips-gateway binary silently stays stale across runs, since
cargo build --release alone does not compile the gateway bin.
Verified locally: cargo test --features gateway --lib (991 pass),
clippy + fmt clean, full testing/ci-local.sh green (21/21 suites
in 8m36s, including the new gateway Phase 7).
This commit is contained in:
@@ -100,7 +100,17 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
info!(pool = %gw_config.pool, lan_interface = %gw_config.lan_interface, "Gateway config loaded");
|
||||
if let Err(e) = gw_config.validate_port_forwards() {
|
||||
error!("Invalid gateway.port_forwards: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
info!(
|
||||
pool = %gw_config.pool,
|
||||
lan_interface = %gw_config.lan_interface,
|
||||
port_forwards = gw_config.port_forwards.len(),
|
||||
"Gateway config loaded"
|
||||
);
|
||||
|
||||
// --- Prerequisites ---
|
||||
|
||||
@@ -221,7 +231,7 @@ async fn main() {
|
||||
};
|
||||
|
||||
// NAT manager
|
||||
let mut nat_mgr = match nat::NatManager::new() {
|
||||
let mut nat_mgr = match nat::NatManager::new(gw_config.lan_interface.clone()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
error!(error = %e, "Failed to create nftables table — do you have CAP_NET_ADMIN?");
|
||||
@@ -229,6 +239,13 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
// Install inbound port-forward rules (TASK-2026-0061).
|
||||
if let Err(e) = nat_mgr.set_port_forwards(&gw_config.port_forwards) {
|
||||
error!(error = %e, "Failed to install port-forward rules");
|
||||
let _ = nat_mgr.cleanup();
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Network setup
|
||||
let mut net_setup = net::NetSetup::new(gw_config.lan_interface.clone(), gw_config.pool.clone());
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
//!
|
||||
//! Configuration for the outbound LAN gateway (`gateway.*`).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::SocketAddrV6;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Default gateway DNS listen address.
|
||||
@@ -52,6 +55,10 @@ pub struct GatewayConfig {
|
||||
/// Conntrack timeout overrides.
|
||||
#[serde(default)]
|
||||
pub conntrack: ConntrackConfig,
|
||||
|
||||
/// Inbound mesh port forwarding rules. See TASK-2026-0061.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub port_forwards: Vec<PortForward>,
|
||||
}
|
||||
|
||||
impl GatewayConfig {
|
||||
@@ -59,6 +66,46 @@ impl GatewayConfig {
|
||||
pub fn grace_period(&self) -> u64 {
|
||||
self.pool_grace_period.unwrap_or(DEFAULT_GRACE_PERIOD)
|
||||
}
|
||||
|
||||
/// Validate inbound port-forward rules: non-zero listen ports and
|
||||
/// uniqueness of `(listen_port, proto)` pairs across the list.
|
||||
/// IPv6-only targets are enforced by `SocketAddrV6` at deserialize
|
||||
/// time.
|
||||
pub fn validate_port_forwards(&self) -> Result<(), String> {
|
||||
let mut seen = HashSet::new();
|
||||
for pf in &self.port_forwards {
|
||||
if pf.listen_port == 0 {
|
||||
return Err("port_forward listen_port must be non-zero".to_string());
|
||||
}
|
||||
if !seen.insert((pf.listen_port, pf.proto)) {
|
||||
return Err(format!(
|
||||
"duplicate port_forward ({:?} {}) — each (listen_port, proto) must be unique",
|
||||
pf.proto, pf.listen_port
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Transport protocol for an inbound port forward.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Proto {
|
||||
Tcp,
|
||||
Udp,
|
||||
}
|
||||
|
||||
/// An inbound port-forward rule: `fips0:listen_port/proto` → `target`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PortForward {
|
||||
/// Port on `fips0` that mesh peers connect to.
|
||||
pub listen_port: u16,
|
||||
/// Transport protocol to match.
|
||||
pub proto: Proto,
|
||||
/// IPv6 LAN destination (`[addr]:port`). IPv4 targets are rejected
|
||||
/// at parse time by `SocketAddrV6`.
|
||||
pub target: SocketAddrV6,
|
||||
}
|
||||
|
||||
/// Gateway DNS resolver configuration (`gateway.dns.*`).
|
||||
@@ -206,4 +253,109 @@ gateway:
|
||||
let config: crate::Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.gateway.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_forwards_default_empty() {
|
||||
let yaml = r#"
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "eth0"
|
||||
"#;
|
||||
let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.port_forwards.is_empty());
|
||||
config.validate_port_forwards().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_forwards_parse() {
|
||||
let yaml = r#"
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "eth0"
|
||||
port_forwards:
|
||||
- listen_port: 8080
|
||||
proto: tcp
|
||||
target: "[fd12:3456::10]:80"
|
||||
- listen_port: 2222
|
||||
proto: tcp
|
||||
target: "[fd12:3456::20]:22"
|
||||
- listen_port: 5353
|
||||
proto: udp
|
||||
target: "[fd12:3456::10]:53"
|
||||
"#;
|
||||
let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(config.port_forwards.len(), 3);
|
||||
assert_eq!(config.port_forwards[0].listen_port, 8080);
|
||||
assert_eq!(config.port_forwards[0].proto, Proto::Tcp);
|
||||
assert_eq!(
|
||||
config.port_forwards[0].target,
|
||||
"[fd12:3456::10]:80".parse::<SocketAddrV6>().unwrap()
|
||||
);
|
||||
assert_eq!(config.port_forwards[2].proto, Proto::Udp);
|
||||
config.validate_port_forwards().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_forwards_reject_ipv4_target() {
|
||||
let yaml = r#"
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "eth0"
|
||||
port_forwards:
|
||||
- listen_port: 8080
|
||||
proto: tcp
|
||||
target: "192.168.1.10:80"
|
||||
"#;
|
||||
let result: Result<GatewayConfig, _> = serde_yaml::from_str(yaml);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"IPv4 target must fail to deserialize as SocketAddrV6"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_forwards_reject_zero_listen_port() {
|
||||
let yaml = r#"
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "eth0"
|
||||
port_forwards:
|
||||
- listen_port: 0
|
||||
proto: tcp
|
||||
target: "[fd12:3456::10]:80"
|
||||
"#;
|
||||
let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.validate_port_forwards().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_forwards_reject_duplicate() {
|
||||
let yaml = r#"
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "eth0"
|
||||
port_forwards:
|
||||
- listen_port: 8080
|
||||
proto: tcp
|
||||
target: "[fd12:3456::10]:80"
|
||||
- listen_port: 8080
|
||||
proto: tcp
|
||||
target: "[fd12:3456::20]:80"
|
||||
"#;
|
||||
let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
let err = config.validate_port_forwards().unwrap_err();
|
||||
assert!(err.contains("duplicate"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_forwards_same_port_different_proto_ok() {
|
||||
let yaml = r#"
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "eth0"
|
||||
port_forwards:
|
||||
- listen_port: 53
|
||||
proto: tcp
|
||||
target: "[fd12:3456::10]:53"
|
||||
- listen_port: 53
|
||||
proto: udp
|
||||
target: "[fd12:3456::10]:53"
|
||||
"#;
|
||||
let config: GatewayConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
config.validate_port_forwards().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "gateway")]
|
||||
pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig};
|
||||
pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig, PortForward, Proto};
|
||||
pub use node::{
|
||||
BloomConfig, BuffersConfig, CacheConfig, ControlConfig, DiscoveryConfig, LimitsConfig,
|
||||
NodeConfig, RateLimitConfig, RekeyConfig, RetryConfig, SessionConfig, SessionMmpConfig,
|
||||
|
||||
@@ -9,10 +9,12 @@ use tracing::{debug, info};
|
||||
|
||||
use rustables::expr::{
|
||||
Cmp, CmpOp, HighLevelPayload, IPv6HeaderField, Immediate, Masquerade, Meta, MetaType, Nat,
|
||||
NatType, NetworkHeaderField, Register,
|
||||
NatType, NetworkHeaderField, Register, TCPHeaderField, TransportHeaderField, UDPHeaderField,
|
||||
};
|
||||
use rustables::{Batch, Chain, ChainType, Hook, HookClass, MsgType, ProtocolFamily, Rule, Table};
|
||||
|
||||
use crate::config::{PortForward, Proto};
|
||||
|
||||
const TABLE_NAME: &str = "fips_gateway";
|
||||
const PREROUTING_CHAIN: &str = "prerouting";
|
||||
const POSTROUTING_CHAIN: &str = "postrouting";
|
||||
@@ -59,8 +61,13 @@ pub struct NatManager {
|
||||
table: Table,
|
||||
pre_chain: Chain,
|
||||
post_chain: Chain,
|
||||
/// LAN interface name, used to gate the port-forward LAN-side
|
||||
/// masquerade rule (distinct from the fips0 egress masquerade).
|
||||
lan_interface: String,
|
||||
/// Active mappings keyed by virtual IP.
|
||||
mappings: HashMap<Ipv6Addr, NatMapping>,
|
||||
/// Inbound port-forward rules (TASK-2026-0061).
|
||||
port_forwards: Vec<PortForward>,
|
||||
}
|
||||
|
||||
impl NatManager {
|
||||
@@ -69,7 +76,10 @@ impl NatManager {
|
||||
/// Installs a masquerade rule for traffic exiting via `fips0` so that
|
||||
/// LAN client source addresses are rewritten to the gateway's mesh
|
||||
/// address, allowing return traffic to route back through the mesh.
|
||||
pub fn new() -> Result<Self, NatError> {
|
||||
///
|
||||
/// `lan_interface` is the gateway's LAN-facing interface name,
|
||||
/// needed by the port-forward LAN-side masquerade rule.
|
||||
pub fn new(lan_interface: String) -> Result<Self, NatError> {
|
||||
let table = Table::new(ProtocolFamily::Inet).with_name(TABLE_NAME);
|
||||
let pre_chain = Chain::new(&table)
|
||||
.with_name(PREROUTING_CHAIN)
|
||||
@@ -84,7 +94,9 @@ impl NatManager {
|
||||
table,
|
||||
pre_chain,
|
||||
post_chain,
|
||||
lan_interface,
|
||||
mappings: HashMap::new(),
|
||||
port_forwards: Vec::new(),
|
||||
};
|
||||
mgr.rebuild()?;
|
||||
|
||||
@@ -92,6 +104,18 @@ impl NatManager {
|
||||
Ok(mgr)
|
||||
}
|
||||
|
||||
/// Replace the current inbound port-forward rule set and rebuild
|
||||
/// the nftables table atomically. Pass an empty slice to clear.
|
||||
pub fn set_port_forwards(&mut self, forwards: &[PortForward]) -> Result<(), NatError> {
|
||||
self.port_forwards = forwards.to_vec();
|
||||
self.rebuild()?;
|
||||
info!(
|
||||
count = self.port_forwards.len(),
|
||||
"Applied inbound port forwards"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add DNAT and SNAT rules for a virtual IP ↔ mesh address mapping.
|
||||
pub fn add_mapping(
|
||||
&mut self,
|
||||
@@ -211,6 +235,62 @@ impl NatManager {
|
||||
batch.add(&snat_rule, MsgType::Add);
|
||||
}
|
||||
|
||||
// Inbound port-forward rules (TASK-2026-0061). Each forward is
|
||||
// one DNAT rule in prerouting keyed on (iif fips0, nfproto ipv6,
|
||||
// l4proto, th dport). When any forwards are configured, emit a
|
||||
// single LAN-side masquerade in postrouting so the LAN target
|
||||
// host sees the gateway's LAN address as source and replies
|
||||
// flow back through conntrack.
|
||||
for pf in &self.port_forwards {
|
||||
let l4proto: u8 = match pf.proto {
|
||||
Proto::Tcp => libc::IPPROTO_TCP as u8,
|
||||
Proto::Udp => libc::IPPROTO_UDP as u8,
|
||||
};
|
||||
let dport_field = match pf.proto {
|
||||
Proto::Tcp => TransportHeaderField::Tcp(TCPHeaderField::Dport),
|
||||
Proto::Udp => TransportHeaderField::Udp(UDPHeaderField::Dport),
|
||||
};
|
||||
let target_ip = *pf.target.ip();
|
||||
let target_port_be = pf.target.port().to_be_bytes();
|
||||
|
||||
let dnat_rule = Rule::new(&self.pre_chain)?
|
||||
.with_expr(Meta::new(MetaType::IifName))
|
||||
.with_expr(Cmp::new(CmpOp::Eq, b"fips0\0".to_vec()))
|
||||
.with_expr(Meta::new(MetaType::NfProto))
|
||||
.with_expr(Cmp::new(CmpOp::Eq, [libc::NFPROTO_IPV6 as u8]))
|
||||
.with_expr(Meta::new(MetaType::L4Proto))
|
||||
.with_expr(Cmp::new(CmpOp::Eq, [l4proto]))
|
||||
.with_expr(HighLevelPayload::Transport(dport_field).build())
|
||||
.with_expr(Cmp::new(CmpOp::Eq, pf.listen_port.to_be_bytes().to_vec()))
|
||||
.with_expr(Immediate::new_data(
|
||||
target_ip.octets().to_vec(),
|
||||
Register::Reg1,
|
||||
))
|
||||
.with_expr(Immediate::new_data(target_port_be.to_vec(), Register::Reg2))
|
||||
.with_expr(
|
||||
Nat::default()
|
||||
.with_nat_type(NatType::DNat)
|
||||
.with_family(ProtocolFamily::Ipv6)
|
||||
.with_ip_register(Register::Reg1)
|
||||
.with_port_register(Register::Reg2),
|
||||
);
|
||||
batch.add(&dnat_rule, MsgType::Add);
|
||||
}
|
||||
|
||||
if !self.port_forwards.is_empty() {
|
||||
let mut lan_iface = self.lan_interface.clone().into_bytes();
|
||||
lan_iface.push(0);
|
||||
let lan_masq = Rule::new(&self.post_chain)?
|
||||
.with_expr(Meta::new(MetaType::IifName))
|
||||
.with_expr(Cmp::new(CmpOp::Eq, b"fips0\0".to_vec()))
|
||||
.with_expr(Meta::new(MetaType::OifName))
|
||||
.with_expr(Cmp::new(CmpOp::Eq, lan_iface))
|
||||
.with_expr(Meta::new(MetaType::NfProto))
|
||||
.with_expr(Cmp::new(CmpOp::Eq, [libc::NFPROTO_IPV6 as u8]))
|
||||
.with_expr(Masquerade::default());
|
||||
batch.add(&lan_masq, MsgType::Add);
|
||||
}
|
||||
|
||||
batch
|
||||
.send()
|
||||
.map_err(|e| NatError::Nftables(e.to_string()))?;
|
||||
|
||||
@@ -149,8 +149,8 @@ record() {
|
||||
run_build() {
|
||||
stage "Stage 1: Build"
|
||||
|
||||
info "cargo build --release"
|
||||
if cargo build --release 2>&1; then
|
||||
info "cargo build --release --features gateway"
|
||||
if cargo build --release --features gateway 2>&1; then
|
||||
record "build" 0
|
||||
else
|
||||
record "build" 1
|
||||
@@ -165,8 +165,8 @@ run_build() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "cargo clippy --all -- -D warnings"
|
||||
if cargo clippy --all -- -D warnings 2>&1; then
|
||||
info "cargo clippy --all --features gateway -- -D warnings"
|
||||
if cargo clippy --all --features gateway -- -D warnings 2>&1; then
|
||||
record "clippy" 0
|
||||
else
|
||||
record "clippy" 1
|
||||
@@ -181,7 +181,7 @@ run_tests() {
|
||||
|
||||
local cmd
|
||||
if command -v cargo-nextest &>/dev/null; then
|
||||
cmd="cargo nextest run --all"
|
||||
cmd="cargo nextest run --all --features gateway"
|
||||
info "$cmd"
|
||||
if $cmd 2>&1; then
|
||||
record "unit-tests" 0
|
||||
@@ -189,7 +189,7 @@ run_tests() {
|
||||
record "unit-tests" 1
|
||||
fi
|
||||
else
|
||||
cmd="cargo test --all"
|
||||
cmd="cargo test --all --features gateway"
|
||||
info "$cmd (nextest not found, using cargo test)"
|
||||
if $cmd 2>&1; then
|
||||
record "unit-tests" 0
|
||||
|
||||
@@ -44,12 +44,21 @@ with open('$config_file') as f:
|
||||
cfg['gateway'] = {
|
||||
'enabled': True,
|
||||
'pool': 'fd01::/112',
|
||||
'lan_interface': 'eth0',
|
||||
# Docker assigns gateway-lan to eth1 (fips-net is eth0). The
|
||||
# LAN-side masquerade for inbound port forwards gates on this.
|
||||
'lan_interface': 'eth1',
|
||||
'dns': {
|
||||
'listen': '[::]:53',
|
||||
'ttl': 5,
|
||||
},
|
||||
'pool_grace_period': 5,
|
||||
'port_forwards': [
|
||||
{
|
||||
'listen_port': 18080,
|
||||
'proto': 'tcp',
|
||||
'target': '[fd02::20]:8080',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
with open('$config_file', 'w') as f:
|
||||
@@ -163,9 +172,62 @@ else
|
||||
check "nftables DNAT rules" 1
|
||||
fi
|
||||
|
||||
# Phase 7: TTL expiration and pool reclamation
|
||||
# Phase 7: Inbound port forwarding (TASK-2026-0061)
|
||||
#
|
||||
# Mesh peer (gw-server) → gw-gateway fips0:18080 → DNAT → [fd02::20]:8080
|
||||
# (gw-client LAN HTTP server). Exercises the DNAT rule + LAN-side
|
||||
# masquerade installed by set_port_forwards().
|
||||
echo ""
|
||||
echo "Phase 7: TTL expiration and pool reclamation"
|
||||
echo "Phase 7: Inbound port forward"
|
||||
|
||||
# Confirm the port-forward DNAT rule is present on the gateway. The
|
||||
# distinctive listen port (18080) identifies our rule regardless of how
|
||||
# nft renders the l4proto/dport predicates.
|
||||
if echo "$NFT_RULES" | grep -q "18080"; then
|
||||
check "nftables port-forward DNAT rule (tcp 18080)" 0
|
||||
else
|
||||
check "nftables port-forward DNAT rule (tcp 18080)" 1
|
||||
fi
|
||||
|
||||
# Start a marker HTTP server on the LAN-side client (fd02::20:8080).
|
||||
# `docker exec -d` is required; `docker exec bash -c 'cmd &'` doesn't
|
||||
# keep the child alive past the exec session, even with nohup.
|
||||
docker exec "$CLIENT" sh -c \
|
||||
'mkdir -p /tmp/inbound && echo "inbound-forward-ok" > /tmp/inbound/index.html && pkill -f "http.server 8080" 2>/dev/null || true' \
|
||||
>/dev/null 2>&1 || true
|
||||
docker exec -d "$CLIENT" python3 -m http.server 8080 --bind :: --directory /tmp/inbound \
|
||||
>/dev/null 2>&1 || true
|
||||
# Give the server a moment to bind.
|
||||
for _ in 1 2 3 4 5; do
|
||||
if docker exec "$CLIENT" ss -6lnt 2>/dev/null | grep -q ':8080'; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Derive the gateway's mesh IPv6 (fd00::/8 address assigned to fips0).
|
||||
GW_MESH_IP=$(docker exec "$GATEWAY" bash -c \
|
||||
"ip -6 -o addr show fips0 | awk '/inet6 fd/ {print \$4}' | cut -d/ -f1 | head -1" \
|
||||
2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$GW_MESH_IP" ]; then
|
||||
check "Gateway fips0 IPv6 address" 1
|
||||
else
|
||||
echo " Gateway mesh IPv6: $GW_MESH_IP"
|
||||
|
||||
# From the mesh side (gw-server), fetch through the forward rule.
|
||||
FWD_RESPONSE=$(docker exec "$SERVER" curl -6 -s --max-time 10 \
|
||||
"http://[${GW_MESH_IP}]:18080/" 2>&1) || true
|
||||
if echo "$FWD_RESPONSE" | grep -q "inbound-forward-ok"; then
|
||||
check "Inbound HTTP via port forward 18080 → [fd02::20]:8080" 0
|
||||
else
|
||||
check "Inbound HTTP via port forward (response: '${FWD_RESPONSE:0:80}')" 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Phase 8: TTL expiration and pool reclamation
|
||||
echo ""
|
||||
echo "Phase 8: TTL expiration and pool reclamation"
|
||||
# Flush conntrack so stale sessions from Phase 5 don't keep the mapping alive.
|
||||
docker exec "$GATEWAY" conntrack -F 2>/dev/null || true
|
||||
# Config uses ttl=5, pool_grace_period=5. Pool tick interval is 10s, so:
|
||||
@@ -185,9 +247,9 @@ else
|
||||
check "Mapping reclaimed (count: $MAPPING_COUNT)" 1
|
||||
fi
|
||||
|
||||
# Phase 8: SERVFAIL when daemon DNS is down
|
||||
# Phase 9: SERVFAIL when daemon DNS is down
|
||||
echo ""
|
||||
echo "Phase 8: SERVFAIL when daemon DNS is down"
|
||||
echo "Phase 9: SERVFAIL when daemon DNS is down"
|
||||
# Kill the fips daemon inside the gateway container (gateway stays running)
|
||||
docker exec "$GATEWAY" pkill -f "^fips --config" 2>/dev/null || true
|
||||
sleep 2
|
||||
@@ -201,9 +263,9 @@ else
|
||||
check "SERVFAIL when daemon DNS down (got: '${SERVFAIL_RESULT:0:80}')" 1
|
||||
fi
|
||||
|
||||
# Phase 9: Cleanup verification (nftables removed on shutdown)
|
||||
# Phase 10: Cleanup verification (nftables removed on shutdown)
|
||||
echo ""
|
||||
echo "Phase 9: Cleanup on shutdown"
|
||||
echo "Phase 10: Cleanup on shutdown"
|
||||
# fips-gateway is PID 1 (exec in entrypoint), so SIGTERM stops the container.
|
||||
# Verify cleanup by checking container logs for the shutdown sequence.
|
||||
docker stop --time=10 "$GATEWAY" >/dev/null 2>&1 || true
|
||||
|
||||
Reference in New Issue
Block a user