feat(netwatch): add a configure-socket hook to BindOptions - #182
feat(netwatch): add a configure-socket hook to BindOptions#182ifdario wants to merge 11 commits into
Conversation
SO_MARK covers Linux, but it has no equivalent on other platforms, and the option that replaces it there (IP_BOUND_IF / IPV6_BOUND_IF on Apple) pins the socket to an interface, so it needs the current default-route interface rather than a constant, and has to be re-resolved whenever the socket rebinds. Rather than grow a per-platform option for each of these, hand the caller the socket. The hook runs before bind and again on every rebind, so a hook that reads current network state re-reads it on each network change. An error from the hook fails the bind: a socket that was meant to be kept out of a tunnel and silently wasn't is worse than one that failed to bind. The socket's Domain is passed in because it cannot be read back off the socket portably (SO_DOMAIN is Linux-only) and the option to set is family-specific.
A named trait documents the contract (reruns per rebind, an error fails the bind, the Domain parameter) better than a bare Arc<dyn Fn> alias, and lets a caller implement it on a stateful type. Closures still work via a blanket impl. The stored form is a private newtype with an opaque Debug, so BindOptions and SocketState go back to plain derives.
Drops the newtype and its hand-written Debug impl: the field is stored as a plain Arc<dyn SocketConfigurator> and BindOptions/SocketState derive Debug through derive_more with #[debug(skip)], which the crate already uses elsewhere.
|
Does this obsolete #179? Should we revert it and work on something more general instead? |
…Options::set_mark A mark is a one-line configurator, so the dedicated option is redundant. Reverts the SO_MARK plumbing from n0-computer#179 (unreleased) in favor of the general hook.
@flub sadly yes. I realized I could do something similar for macos, but I need another type of manipulation for the socket. Hope you don't mind the sudden change of mind. I guess it is part of the iterative process. |
|
Also included an exmaple on how to use the mark flag in the comment |
|
CI needs similar treatment to n0-computer/noq@fcd420c due to cargo nightly changes. |
New nightly cargo lints fire inside the cfg_aliases macro; allow it until katharostech/cfg_aliases#15 lands, same as n0-computer/noq#748.
|
Well, that PR landed and a new version of |
…ter#185) Reverts n0-computer#179 See n0-computer#182: this needs more design.
# Conflicts: # netwatch/src/lib.rs # netwatch/src/udp.rs
cfg_aliases 0.2.2 no longer trips the lint, so the allow in build.rs is dead weight.
main reverted n0-computer#179, so this re-adds BindOptions carrying only the general hook: a closure run on every socket right after creation and before bind(), and again on every internal rebind. bind_with replaces the private bind_raw, which all the other bind_* constructors now go through. The hook takes a SocketRef, a borrowed handle that implements AsFd on unix and AsSocket on Windows, and netwatch's own IpFamily. Neither socket2 nor any other external type reaches the public API, so callers can reach for whatever socket crate they like (socket2::SockRef::from(&socket), plain setsockopt) without having to match netwatch's version of it.
|
@flub this is ready and has been green for a month. It replaces the reverted #179 with the general hook: SO_MARK on Linux, IP_BOUND_IF on Apple, one closure on BindOptions that re-runs on every rebind so it picks up a changed default route. The iroh side is n0-computer/iroh#4406, which is still sitting on the old socket_mark API. I am rewriting it onto this hook and pointing its patch section at this branch, as you suggested in July, so there will be a consumer PR to review alongside it. If you would rather have typed options than a closure, say so and I will reshape it. |
SO_MARK only answers for Linux. Apple platforms have no fwmark, and the way to keep a socket off a tunnel route there is to pin it to an interface with IP_BOUND_IF, which is a different call on a different value. Modelling each of those as its own builder option means a new option every time a platform needs one, so hand out the socket instead. The hook runs on every socket the endpoint opens, the UDP transport sockets and the relay connection alike, before bind or connect and again on every rebind, so a hook that reads the current default route re-reads it when the route changes. An error fails the bind rather than leaving a socket that silently missed its configuration. The UDP half needs the BindOptions hook from n0-computer/net-tools#182, which is not released yet, hence the patch section on the workspace.
Follow-up to #179, which was reverted in #185 pending a more general design. Merged with current
main.What
BindOptionscomes back carrying only the general form: a hook that runs on every socket right after creation and beforebind(), and again on every internal rebind.UdpSocket::bind_with(addr, BindOptions)replaces the privatebind_raw; the otherbind_*constructors all go through it.SocketStatevariants, so it reruns on every rebind. That is the point of it: on macOS the equivalent ofSO_MARKis binding to the physical default-route interface (IP_BOUND_IF), and the right interface changes when the default route moves (wifi to ethernet, say), so it has to be re-resolved per bind. A one-shot option cannot express that.socket2in the public API. The hook gets aSocketRef, a borrowed handle that implementsAsFdon unix andAsSocketon Windows, plus netwatch's ownIpFamily(the family cannot be read back off the socket portably,SO_DOMAINis Linux-only). Callers reach for whatever socket crate they like,socket2::SockRef::from(&socket)or plainlibc::setsockopton the raw fd, without having to match netwatch's socket2 version. This is also what keepscargo check-external-typesgreen; the earlierSockRef/Domainsignature failed that job.Arc<dyn Fn(SocketRef<'_>, IpFamily) -> io::Result<()> + Send + Sync>. So the added API surface isBindOptions,BindOptions::{new, configure_socket},SocketRefandUdpSocket::bind_with, and nothing else to keep compatible later.BindOptionskeepsDebug(the field is#[debug(skip)]ed via derive_more, already a dependency),DefaultandClone. It is notCopy/PartialEq/Eq.main, so no semver break.Tests cover the hook running on bind and on rebind, and the fail-closed path.
Also drops the
semicolon_in_expressions_from_macrosallow inbuild.rs, per @matheus23:cfg_aliases0.2.2 no longer trips the lint.Why
Same client as #179: full tunnel over iroh, WireGuard/Tailscale-style loop prevention.
SO_MARKcovers Linux, macOS needsIP_BOUND_IFre-applied on every bind, and other platforms need yet other setsockopts. One hook covers all of them, and whatever else a caller needs, without growingBindOptionsper platform.