Skip to content

fix(util/net): make zenoh build on FreeBSD and support bind-to-device there - #2656

Open
apbodrov wants to merge 1 commit into
eclipse-zenoh:mainfrom
bzdOS:feat/freebsd-bind-to-device
Open

fix(util/net): make zenoh build on FreeBSD and support bind-to-device there#2656
apbodrov wants to merge 1 commit into
eclipse-zenoh:mainfrom
bzdOS:feat/freebsd-bind-to-device

Conversation

@apbodrov

@apbodrov apbodrov commented Jun 26, 2026

Copy link
Copy Markdown

Description

zenoh does not build on FreeBSD at all. set_bind_to_device_tcp_socket and set_bind_to_device_udp_socket are defined only for linux | android and for macos | ios | windows, so on FreeBSD zenoh-link-commons fails to compile with cannot find function ... in module 'zenoh_util::net' — regardless of whether iface is ever used.

This PR makes FreeBSD compile, and makes the iface option actually work there.

Note on the original description of this PR: it claimed FreeBSD has IP_BOUND_IF / IPV6_BOUND_IF and that a real implementation was merely out of scope. That was wrong, and the approach has changed accordingly — see below.

Why FreeBSD can't just be added to the existing any() list

There is no SO_BINDTODEVICE / IP_BOUND_IF / IPV6_BOUND_IF on FreeBSD — no such sockopt exists in netinet/in.h, netinet6/in6.h or sys/socket.h. Those are Darwin/Solaris options, not FreeBSD ones.

So set_bind_to_device_* cannot be implemented on FreeBSD in any form: by the time either function is called the socket is already bound, and a socket can only be bound once. Adding freebsd to the any() list would only produce a warning stub, i.e. a function that silently does nothing while the user believes iface took effect.

The one mechanism FreeBSD does offer is binding the socket to one of the interface's own addresses. That must happen before the single bind(2) call each caller already makes — which is why FreeBSD gets an address resolver used at the call sites, rather than another set_bind_to_device_* arm.

What this PR does

  • zenoh-util — adds resolve_bind_addr_for_interface(iface, addr), FreeBSD-only.
  • zenoh-link-commons (tcp.rs, quic/socket.rs) and zenoh-link-udp (unicast.rs) — on FreeBSD, resolve the bind address before bind(). On every other platform the existing set_bind_to_device_* call is untouched.

Resolution semantics:

  • A concrete address is validated against the interface, never rewritten. A mismatched configuration is reported instead of silently binding somewhere the caller never asked for.
  • An unspecified address (0.0.0.0 / ::) selects an address of the same family from the interface, preferring a routable address over a link-local one, skipping loopback.
  • A link-local IPv6 address carries the interface index as its scope id. Without it FreeBSD rejects the bind with EADDRNOTAVAIL, which makes iface + IPv6 unusable.

One caveat worth stating: the interface list is a process-lifetime snapshot, so an address assigned to the interface after startup is not visible. This is inherent to resolving an address instead of naming a device.

Behaviour on other platforms

Unchanged. Every new code path is behind #[cfg(target_os = "freebsd")].

Testing

  • cargo check --target x86_64-unknown-freebsd --all-features for zenoh-util and zenoh-link-commons — clean. This is the check that fails on main.
  • cargo check --all-features on Linux for those two crates plus zenoh-link-udp — clean, no regression.
  • Runtime, on FreeBSD 15.1, across two interfaces:
    • a listener with iface set binds that interface's address and is reachable on it;
    • an IPv6 listener with iface set binds successfully — it fails with EADDRNOTAVAIL without the scope-id handling;
    • an explicit address that is not on the interface is rejected with a clear error rather than being silently moved;
    • an explicit address that is on the interface is honoured verbatim;
    • with no iface, behaviour is identical to before.

There is no automated coverage for this, because there is no FreeBSD runner in CI — the verification above was done by hand. Happy to follow whatever the project prefers here.

Known gap, not addressed here

zenoh-link-udp still does not compile on FreeBSD, for a reason unrelated to this change: src/pktinfo/pktinfo_unix.rs uses the Linux-only IP_PKTINFO and in_pktinfo (FreeBSD has IP_RECVDSTADDR / IP_RECVIF instead). This was previously masked, since zenoh-link-commons failed to compile first. I've left it alone to keep this PR to one concern — glad to open a separate issue or PR for it.

Related Issues

None. This is a build fix for FreeBSD support.


🏷️ Label-Based Checklist

Based on the labels applied to this PR, please complete these additional requirements:

Labels: enhancement

✨ Enhancement Requirements

Since this PR enhances existing functionality:

  • Enhancement scope documented - Clear description of what is being improved
  • Minimum necessary code - Implementation is as simple as possible, doesn't overcomplicate the system
  • Backwards compatible - Existing code/APIs still work unchanged
  • No new APIs added - Only improving existing functionality
  • Tests updated - Existing tests pass, new test cases added if needed
  • Performance improvement measured - If applicable, before/after metrics provided
  • Documentation updated - Existing docs updated to reflect improvements
  • User impact documented - How users benefit from this enhancement

Remember: Enhancements should not introduce new APIs or breaking changes.

Instructions:

  1. Check off items as you complete them (change - [ ] to - [x])
  2. The PR checklist CI will verify these are completed

This checklist updates automatically when labels change, but preserves your checked boxes.


On the one unchecked box: this PR does add one new public item, zenoh_util::net::resolve_bind_addr_for_interface, so "No new APIs added" cannot honestly be ticked. It has to be pub because zenoh-link-commons and zenoh-link-udp call it across crate boundaries, and it is #[cfg(target_os = "freebsd")], so it does not widen the API surface on any platform that builds today.

This is really a build fix rather than an enhancement, so the enhancement checklist is a slightly awkward fit. I don't have permission to relabel — if a maintainer retags this as bug, the checklist should regenerate accordingly.

@apbodrov
apbodrov force-pushed the feat/freebsd-bind-to-device branch from 0db485e to 06b090d Compare June 26, 2026 08:43
@diogomatsubara diogomatsubara added the enhancement Existing things could work better label Jul 23, 2026

@diogomatsubara diogomatsubara left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apbodrov Thanks for your contribution! You'd need to sign the Eclipse contributor agreement so we're able to merge this change.

Comment thread commons/zenoh-util/src/net/mod.rs
@apbodrov
apbodrov force-pushed the feat/freebsd-bind-to-device branch from 06b090d to 5a86785 Compare July 26, 2026 21:02
zenoh does not currently build on FreeBSD: set_bind_to_device_tcp_socket and
set_bind_to_device_udp_socket are defined only for linux/android and for
macos/ios/windows, so zenoh-link-commons fails to compile there.

FreeBSD cannot implement those functions as written. It has no
SO_BINDTODEVICE / IP_BOUND_IF equivalent — there is no such sockopt in
netinet/in.h, netinet6/in6.h or sys/socket.h — so the interface cannot be
selected once the socket exists. Restrict the socket to the interface by
binding one of that interface's own addresses instead, resolved before the
single bind(2) call each caller already makes. FreeBSD therefore gets an
address resolver rather than a warning stub: a function that can only be
called too late to have any effect is worse than no function at all.

A concrete address is validated against the interface rather than rewritten,
so a mismatched configuration is reported instead of binding somewhere the
caller never asked for. For an unspecified address a routable address of the
matching family is preferred over a link-local, and a link-local carries the
interface index as its scope id, without which FreeBSD rejects the bind with
EADDRNOTAVAIL.

Behaviour on other platforms is unchanged.

Note that zenoh-link-udp still does not build on FreeBSD: its pktinfo module
uses the Linux-only IP_PKTINFO and in_pktinfo. That is a separate pre-existing
gap, previously masked by zenoh-link-commons failing to compile first.

Signed-off-by: Andrey Bodrov <ap.bodrov@gmail.com>
@apbodrov
apbodrov force-pushed the feat/freebsd-bind-to-device branch from 5a86785 to 02900ff Compare July 26, 2026 21:08
@apbodrov apbodrov changed the title feat(util/net): add FreeBSD stubs for set_bind_to_device_{tcp,udp}_socket fix(util/net): make zenoh build on FreeBSD and support bind-to-device there Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Existing things could work better

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants