Zephyr Socket Dispatcher: Bind the Interface Before a Helper Selects the Default Backend
A timeout-setting helper can select a Zephyr socket’s backend before the application chooses its network interface. With the socket dispatcher enabled, the descriptor returned by socket() initially represents a deferred choice. An ordinary option call can resolve that choice using default priorities. A later SO_BINDTODEVICE call then reaches the already selected provider.
This analysis pins Zephyr v4.4.0 with CONFIG_NET_SOCKETS_OFFLOAD_DISPATCHER and eligible native/offloaded or multiple-offloaded implementations. It uses plain UDP as the example. It does not describe every Zephyr build, and native TLS has a separate layered selection sequence outside this discussion.
Find the first operation hidden inside initialization
Consider an application that wants provider B, while default registration order favors eligible provider A. Its connection library creates a UDP socket, sets SO_RCVTIMEO, and returns the descriptor to application code. The application then binds the interface associated with B. No packet needs to have been sent for the intended selection window to have closed.
The tagged dispatcher implementation sends ordinary setsockopt() requests through sock_dispatch_default(). The default search skips the dispatcher, checks family and support predicates, and chooses the first eligible registered implementation. The official socket guide explains that lower numeric registration priorities are processed earlier. This is provider selection, not a search for whichever interface currently has the best route.
A diagnostic helper can have the same consequence: getsockopt() default-dispatches too. So do the dispatcher’s ordinary bind() and connect() wrappers. Do not confuse binding a local address through bind() with the special interface-selection option. Equally, do not claim every operation triggers selection: closing an undispatched descriptor and the internal ZFD_IOCTL_SET_LOCK request are explicit exceptions.

Understand what changes behind the descriptor
Successful dispatch creates a provider socket, obtains its object and operation table, and installs them under the original descriptor. The temporary descriptor slot is freed, and the dispatcher context is released. Application code still holds the same descriptor number, but it now reaches the selected implementation directly.
That replacement explains why a late interface option cannot be treated as a request to rerun dispatcher selection. The provider may accept the option, reject it, or impose provider-specific restrictions. None of those possibilities is a dispatcher guarantee that the socket migrates to another backend.
There is also an important failure boundary. After backend creation succeeds, the dispatcher forwards the original option to that backend. If the forwarded option fails, selection has already occurred. A log containing only “timeout option failed” can therefore conceal a descriptor that is no longer deferred. Record the failing stage rather than assuming a failed option leaves initialization untouched.
Put interface selection inside the constructor
Give the application’s socket constructor responsibility for choosing the interface before handing the descriptor to helpers. A proposed plain-UDP order is:
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
check descriptor result
setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifreq, sizeof(ifreq))
check result and preserve errno on failure
set required ordinary options
check each result
return the initialized descriptor to its owner
This is an ordering sketch, not compiled application code. Resolve and validate ifreq for the actual build first. In v4.4.0, CONFIG_NET_INTERFACE_NAME selects interface-name lookup; without it, the dispatcher uses a device binding followed by interface lookup. A name copied from another board’s example is not a portable identifier.
The special option selects the named interface’s socket-offload constructor when one exists. Otherwise it selects a native implementation. It then forwards the binding option to the chosen provider. Both successful backend creation and successful option application matter; an accepted call still does not demonstrate route reachability or delivered traffic.
Keep a partially initialized descriptor private. If mandatory initialization fails, use the application’s checked cleanup path and create a fresh socket for a new selection attempt. Do not publish the descriptor to a worker and attempt to repair provider choice afterward. Merely moving one option earlier in a function is insufficient if another library or thread already used the descriptor.
Validate the construction sequence directly
A proposed isolated two-provider fixture should compare interface-first and timeout-first sequences using the same socket family, type and protocol. Instrument which provider constructor runs and which operation table receives the later binding request. Capture every return and its associated errno; avoid using a preliminary socket query as a supposedly passive observation of an undispatched socket.
Include an invalid-interface control and a provider that rejects the forwarded ordinary option. These distinguish failure before selection from failure after descriptor replacement. The dispatcher Kconfig also clarifies that its context limit counts still-undispatched sockets, not every live socket, so record that limit separately from descriptor exhaustion.
No fixture, socket traffic or target integration was executed here. The acceptance question is specific: does the intended constructor run before ordinary helpers can select a default provider, and does the resulting provider accept the required initialization? Subsequent network qualification remains necessary.