Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions members/nullnet-client/src/commands/dnat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,38 @@ pub(crate) fn init() {

/// Install a DNAT for `port → overlay_ip:port`. When `container_ip` is a
/// real address, the rule is scoped to that source via `-s` so co-located
/// replicas hit independent chains. `Ipv4Addr::UNSPECIFIED` (0.0.0.0) means
/// "no source filter" — used by legacy callers that don't know the source.
/// replicas hit independent chains. When `dest_ip` is a real address, the
/// rule is additionally scoped to that destination via `-d` — so two
/// backend-trigger dependencies sharing a port from the same initiator (each
/// with its own placeholder address, see `placeholder.rs`) get independent
/// rules instead of one clobbering the other. `Ipv4Addr::UNSPECIFIED`
/// (0.0.0.0) means "no filter" for either — used by legacy callers that
/// don't know the source/destination.
/// Returns `false` if any of the per-proto `iptables` rules failed to apply.
pub(crate) fn install(port: u16, overlay_ip: Ipv4Addr, container_ip: Ipv4Addr) -> bool {
pub(crate) fn install(
port: u16,
overlay_ip: Ipv4Addr,
container_ip: Ipv4Addr,
dest_ip: Ipv4Addr,
) -> bool {
let mut ok = true;
for proto in PROTOS {
ok &= run_iptables("-A", proto, port, overlay_ip, container_ip);
ok &= run_iptables("-A", proto, port, overlay_ip, container_ip, dest_ip);
}
flush_conntrack(port);
ok
}

/// Returns `false` if any of the per-proto `iptables` rules failed to delete.
pub(crate) fn remove(port: u16, overlay_ip: Ipv4Addr, container_ip: Ipv4Addr) -> bool {
pub(crate) fn remove(
port: u16,
overlay_ip: Ipv4Addr,
container_ip: Ipv4Addr,
dest_ip: Ipv4Addr,
) -> bool {
let mut ok = true;
for proto in PROTOS {
ok &= run_iptables("-D", proto, port, overlay_ip, container_ip);
ok &= run_iptables("-D", proto, port, overlay_ip, container_ip, dest_ip);
}
flush_conntrack(port);
ok
Expand All @@ -57,14 +72,19 @@ fn run_iptables(
port: u16,
overlay_ip: Ipv4Addr,
container_ip: Ipv4Addr,
dest_ip: Ipv4Addr,
) -> bool {
let port_s = port.to_string();
let target = format!("{overlay_ip}:{port}");
let container_ip_s = container_ip.to_string();
let dest_ip_s = dest_ip.to_string();
let mut args: Vec<&str> = vec!["iptables", "-t", "nat", action, CHAIN, "-p", proto];
if !container_ip.is_unspecified() {
args.extend_from_slice(&["-s", &container_ip_s]);
}
if !dest_ip.is_unspecified() {
args.extend_from_slice(&["-d", &dest_ip_s]);
}
args.extend_from_slice(&[
"--dport",
&port_s,
Expand All @@ -79,19 +99,28 @@ fn run_iptables(
} else {
container_ip_s.clone()
};
let dst = if dest_ip.is_unspecified() {
"any".to_string()
} else {
dest_ip_s.clone()
};
match status {
Ok(s) if s.success() => {
println!("[dnat] iptables {action} {CHAIN} {proto}/{port} -s {src} -> {target}");
println!(
"[dnat] iptables {action} {CHAIN} {proto}/{port} -s {src} -d {dst} -> {target}"
);
true
}
Ok(s) => {
eprintln!(
"[dnat] iptables {action} {CHAIN} {proto}/{port} -s {src} -> {target} exited {s}"
"[dnat] iptables {action} {CHAIN} {proto}/{port} -s {src} -d {dst} -> {target} exited {s}"
);
false
}
Err(e) => {
eprintln!("[dnat] iptables {action} {CHAIN} {proto}/{port} -s {src} -> {target}: {e}");
eprintln!(
"[dnat] iptables {action} {CHAIN} {proto}/{port} -s {src} -d {dst} -> {target}: {e}"
);
false
}
}
Expand Down
34 changes: 34 additions & 0 deletions members/nullnet-client/src/commands/egress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ pub(crate) fn init() {
&["ipset", "add", "-exist", INTERNAL_SET, range],
);
}
// The backend-trigger placeholder block (see `placeholder::placeholder_cidr`)
// is synthetic — never real internet traffic — but isn't one of the
// static private/special ranges above. Without this, a trigger dial to
// a port `nullnet_watched_ports` hasn't caught up on yet (a startup race
// against the gRPC round trip that populates it, see `nfqueue::apply_ports_diff`)
// falls through to this rule instead and gets misclassified as an egress
// candidate — held for the egress-trigger timeout, then dropped, well
// past most callers' own request timeout.
let placeholder_cidr = crate::placeholder::placeholder_cidr();
sudo_ok(
"ipset add internal placeholder",
&["ipset", "add", "-exist", INTERNAL_SET, &placeholder_cidr],
);

// NFQUEUE trigger rule: NEW flows to non-internal destinations → queue 1.
// The listener filters by container (registered services only). --queue-bypass
Expand Down Expand Up @@ -213,6 +226,27 @@ pub(crate) fn install_steer(
],
);
}
// Same bypass for the backend-trigger placeholder block — see the
// matching comment in `init()`. Lands at base+9, still clear of the
// catch-all at base+15.
let placeholder_cidr = crate::placeholder::placeholder_cidr();
let placeholder_prio = (base + INTERNAL_RANGES.len() as u32).to_string();
ok &= sudo_ok(
"ip rule add placeholder bypass",
&[
"ip",
"rule",
"add",
"from",
&cip,
"to",
&placeholder_cidr,
"lookup",
"main",
"priority",
&placeholder_prio,
],
);
// Catch-all: everything else from this container → the egress table.
let catch_all = (base + 15).to_string();
ok &= sudo_ok(
Expand Down
Loading