Skip to content

feat(netwatch): add a configure-socket hook to BindOptions#182

Open
ifdario wants to merge 8 commits into
n0-computer:mainfrom
rayfish:feat/configure-socket
Open

feat(netwatch): add a configure-socket hook to BindOptions#182
ifdario wants to merge 8 commits into
n0-computer:mainfrom
rayfish:feat/configure-socket

Conversation

@ifdario

@ifdario ifdario commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #179. That change lets a caller set SO_MARK, which covers full-tunnel loop prevention on Linux. This adds the general form to BindOptions: a configurator that runs on every socket right after creation and before bind(), and again on every internal rebind.

What

  • New SocketConfigurator trait and a BindOptions::configure_socket(impl SocketConfigurator) builder option. set_mark is unchanged; the two compose.

    pub trait SocketConfigurator: Send + Sync + 'static {
        fn configure(&self, socket: SockRef<'_>, domain: socket2::Domain) -> io::Result<()>;
    }

    A blanket impl covers Fn(SockRef<'_>, socket2::Domain) -> io::Result<()> + Send + Sync + 'static closures, so simple configurators need no named type.

  • The configurator is stored in both SocketState variants (like the mark), so it reruns on every rebind. The field is #[debug(skip)]ed (derive_more, already a dependency), so BindOptions and SocketState keep derived Debug.

  • An error from the configurator fails the bind (fail closed): for loop-prevention users, a socket that silently misses its configuration would leak traffic into the tunnel it is carrying.

  • It receives the socket2::Domain explicitly because SockRef::domain() rests on SO_DOMAIN, which is Linux-only.

  • BindOptions now holds an Arc<dyn SocketConfigurator>, so it loses Copy/PartialEq/Eq (both unreleased, added in feat(netwatch): allow setting SO_MARK on UdpSocket #179). Tests cover the configurator running on bind and rebind, and the fail-closed path.

Why

Same client as #179 (full tunnel over iroh, WireGuard/Tailscale-style loop prevention), on platforms without fwmark. On macOS the equivalent is binding the socket to the physical default-route interface (IP_BOUND_IF), and that must be re-resolved and re-applied on every rebind because the right interface changes when the default route moves (wifi to ethernet, say). A one-shot option can't express that; a per-(re)bind configurator can, and it also covers whatever other setsockopt a caller needs without growing BindOptions per platform.

ifdario added 3 commits July 15, 2026 16:58
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.
@n0bot n0bot Bot added this to iroh Jul 15, 2026
@github-project-automation github-project-automation Bot moved this to 🚑 Needs Triage in iroh Jul 15, 2026
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.
@flub

flub commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Does this obsolete #179? Should we revert it and work on something more general instead?

ifdario added 2 commits July 16, 2026 15:38
…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.
@ifdario

ifdario commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Does this obsolete #179? Should we revert it and work on something more general instead?

@flub sadly yes. I realized I could do something similar for macos, but I need another type of manipulation for the socket.
In linux I can just set the flag to the socket and it will easily bypass the VPN configuration, but it won't work for MacOS. The trick there will be to bind to another interface that is not the tunnel's interface.

Hope you don't mind the sudden change of mind. I guess it is part of the iterative process.

@ifdario

ifdario commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Also included an exmaple on how to use the mark flag in the comment

@matheus23

Copy link
Copy Markdown
Member

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.
@matheus23

Copy link
Copy Markdown
Member

Well, that PR landed and a new version of cfg_aliases is released now. Just cargo update -p cfg_aliases does the trick now, no need for the #[allow]s anymore.

Kiesen pushed a commit to mira-mobility/net-tools that referenced this pull request Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🚑 Needs Triage

Development

Successfully merging this pull request may close these issues.

3 participants