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
25 changes: 20 additions & 5 deletions patchbay/src/iface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,15 +375,20 @@ impl Iface {

/// Brings this interface administratively up.
///
/// If this is the device's default route interface (and it is routed),
/// the default route is re-added (Linux removes routes when a link
/// goes down).
/// Linux removes routes and the IPv6 link-local address when a link
/// goes down, so this re-adds the link-local and, if this is the
/// device's routed default route interface, the default routes. Routes
/// added outside patchbay are not restored.
///
/// With [`Ipv6DadMode::Enabled`](crate::Ipv6DadMode::Enabled), IPv6
/// addresses rerun duplicate address detection after this returns and
/// stay unusable until it completes, which takes about two seconds.
pub async fn link_up(&self) -> Result<()> {
use crate::{
device::select_default_v6_gateway, netlink::Netlink, wiring, Ipv6ProvisioningMode,
};

let (ns, uplink, is_default_via, dummy, op) = {
let (ns, uplink, is_default_via, dummy, ll_v6, op) = {
let inner = self.lab.core.lock().expect("poisoned");
let dev = inner
.device(self.device)
Expand All @@ -396,6 +401,7 @@ impl Iface {
iface.uplink(),
*dev.default_via == *self.ifname,
iface.is_dummy(),
iface.ll_v6,
Arc::clone(&dev.op),
)
};
Expand All @@ -404,7 +410,16 @@ impl Iface {
let ifname = self.ifname.to_string();
wiring::nl_run(&self.lab.netns, &ns, {
let ifname = ifname.clone();
move |nl: Netlink| async move { nl.set_link_up(&ifname).await }
move |nl: Netlink| async move {
nl.set_link_up(&ifname).await?;
// Global v6 addresses survive link down via `keep_addr_on_down`
// (see `wiring::create_named_netns`), but the kernel always
// drops link-locals, so the seeded one is re-added here.
if let Some(ll6) = ll_v6 {
nl.add_addr6(&ifname, ll6, 64).await?;
}
Ok(())
}
})
.await?;

Expand Down
21 changes: 21 additions & 0 deletions patchbay/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ pub(crate) async fn run_reflector(
Ok(())
}

/// Lists the IPv6 addresses assigned to `ifname` in the current namespace.
///
/// Reads `/proc/net/if_inet6`, whose rows are the address as 32 hex digits
/// followed by index, prefix length, scope, flags, and the interface name.
pub(crate) fn iface_v6_addrs(ifname: &str) -> Result<Vec<Ipv6Addr>> {
let table = std::fs::read_to_string("/proc/net/if_inet6").context("read if_inet6")?;
let mut addrs = Vec::new();
for line in table.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
let [hex, .., name] = fields.as_slice() else {
continue;
};
if *name != ifname {
continue;
}
let raw = u128::from_str_radix(hex, 16).with_context(|| format!("parse {hex}"))?;
addrs.push(Ipv6Addr::from(raw));
}
Ok(addrs)
}

/// Sends a UDP probe to `reflector` and returns the observed external address.
///
/// Assumes the calling thread is already in the target namespace.
Expand Down
45 changes: 45 additions & 0 deletions patchbay/src/tests/link_condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,51 @@ async fn link_down_up() -> Result<()> {
Ok(())
}

/// Link down then up on a dual-stack device keeps its IPv6 addresses and
/// restores v6 connectivity.
///
/// Linux flushes global IPv6 addresses when a link goes down, so without
/// `keep_addr_on_down` the v6 default route cannot be re-added on link up.
#[tokio::test(flavor = "current_thread")]
#[traced_test]
async fn link_down_up_dual_stack() -> Result<()> {
check_caps()?;
let lab = Lab::new().await?;
let dc = lab
.add_router("dc")
.ip_support(IpSupport::DualStack)
.build()
.await?;
let dev = lab.add_device("dev").uplink(dc.id()).build().await?;
let eth0 = dev.iface("eth0").context("eth0")?;
let ip6 = eth0.ip6().context("eth0 has no v6 address")?;
let ll6 = eth0.ll6().context("eth0 has no v6 link-local")?;

let dc_ip_v6 = dc.uplink_ip_v6().context("no dc v6 uplink ip")?;
let r = SocketAddr::new(IpAddr::V6(dc_ip_v6), 16_700);
let _r = dc.spawn_reflector(r).await?;
dev.run_sync(move || test_utils::udp_roundtrip(r))
.context("before link_down")?;

eth0.link_down().await?;
eth0.link_up().await?;
tokio::time::sleep(Duration::from_millis(100)).await;

let addrs = dev.run_sync(|| test_utils::iface_v6_addrs("eth0"))?;
assert!(
addrs.contains(&ip6),
"global v6 {ip6} lost after link_up: {addrs:?}"
);
assert!(
addrs.contains(&ll6),
"link-local {ll6} lost after link_up: {addrs:?}"
);

dev.run_sync(move || test_utils::udp_roundtrip(r))
.context("after link_up")?;
Ok(())
}

// ── Rate limiting ────────────────────────────────────────────────────

/// 2 Mbit/s upload cap via tc on device interface.
Expand Down
23 changes: 13 additions & 10 deletions patchbay/src/wiring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -876,10 +876,13 @@ pub(crate) fn link_local_from_seed(seed: u64) -> Ipv6Addr {
// Netns + process helpers
// ─────────────────────────────────────────────

/// Creates a namespace with optional DNS overlay and applies IPv6 DAD mode.
/// Creates a namespace with optional DNS overlay and applies IPv6 sysctls.
///
/// When `dad_mode` is disabled, this sets `accept_dad=0` and
/// `dad_transmits=0` before interfaces are moved in.
/// Global IPv6 addresses are kept across link down/up (`keep_addr_on_down=1`)
/// so that `Iface::link_up` can re-add the v6 default route. Linux flushes
/// them by default, which leaves the gateway off-link. When `dad_mode` is
/// disabled, this also sets `accept_dad=0` and `dad_transmits=0`. All sysctls
/// are set before interfaces are moved in.
pub(crate) fn create_named_netns(
netns: &netns::NetnsManager,
name: &str,
Expand All @@ -888,17 +891,17 @@ pub(crate) fn create_named_netns(
dad_mode: Ipv6DadMode,
) -> Result<()> {
netns.create_netns(name, dns_overlay, log_prefix)?;
if dad_mode == Ipv6DadMode::Disabled {
// Disable DAD before any interfaces are created or moved in.
netns.run_closure_in(name, || {
netns.run_closure_in(name, move || {
set_sysctl_root("net/ipv6/conf/all/keep_addr_on_down", "1").ok();
set_sysctl_root("net/ipv6/conf/default/keep_addr_on_down", "1").ok();
if dad_mode == Ipv6DadMode::Disabled {
set_sysctl_root("net/ipv6/conf/all/accept_dad", "0").ok();
set_sysctl_root("net/ipv6/conf/default/accept_dad", "0").ok();
set_sysctl_root("net/ipv6/conf/all/dad_transmits", "0").ok();
set_sysctl_root("net/ipv6/conf/default/dad_transmits", "0").ok();
Ok(())
})?;
}
Ok(())
}
Ok(())
})
}

/// Sets a sysctl value in the current namespace (caller must already be in the ns).
Expand Down
Loading