Add -4/-6 address-family preference - #68
Merged
Merged
Conversation
Until now the IP family was whatever the resolver returned first, with no way to influence it. `etr` and `etrs` both gain -4/--prefer-ipv4 and -6/--prefer-ipv6, plus an `address_family` config key in each section. They are a preference, not a restriction, which is the whole design: if the host has no address of the requested family, or none the kernel can route to, the other family is used and the fallback is reported at -v. That is why the client cannot simply forward the flag to ssh, whose -4/-6 forbid the other family outright -- it resolves the target first and passes ssh the flag only when such an address exists, leaving unresolvable ssh_config aliases alone. The preference covers every place an address is chosen: the SSH bootstrap, the QUIC session, -R targets (resolved on the client) and -L targets (resolved on the server, reached via a new ETRPREFER: bootstrap line that older servers ignore). Forwarded TCP needed connect_tcp_preferred to be included at all -- TcpStream::connect walks every address but in resolver order. AddrPref::Auto means "what this call site did before", not one global default: the QUIC path took the resolver's first answer while resolve_udp_target has preferred IPv6 since v0.4.x, so each caller supplies its own fallback and an unflagged run is unchanged. One deliberate exception: the QUIC peer address now uses the same no-packet routing probe the UDP path always had, so an AAAA-first host on a machine without an IPv6 route falls through to the A record instead of failing the connect. etrs -4 also flips the default bind to 0.0.0.0, since a single wildcard bind cannot express "prefer IPv4"; an explicit -b still wins, and a client's preference deliberately does not touch the server's bind because [::] already serves IPv4. Bootstrap-line handling and the bind default moved into parse_bootstrap_line and effective_bind_ip so the tests exercise the shipped code -- the existing ETRCMD/ETRX11 tests re-implemented the parse loop inside the test. Tests 112 -> 145, new `just e2e-family-local` asserting the flags change the family actually connected over. Version 0.7.9 -> 0.8.0. Assisted-By: Claude Opus 5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
etrandetrsboth gain-4/--prefer-ipv4and-6/--prefer-ipv6, plus anaddress_family = "ipv4" | "ipv6" | "auto"config key in[client]and[server].Until now the IP family was whatever the resolver returned first, and there was no way to
influence it — awkward on a dual-stack host where one family is slow, filtered, or simply
the one you are trying to test.
Why "prefer" rather than "force"
ssh -4/-6forbid the other family. These order it: if the host has no address of therequested family, or none the kernel can route to, the other family is used and the
fallback is reported at
-v. Nothing here can turn a working connection into a failure.That has one non-obvious consequence. The client cannot simply hand
-6tossh, becausessh would hard-fail on an IPv4-only host while
etritself would have fallen back — onesession, two contracts. So the client resolves the target first and passes ssh the flag
only when the host really has such an address; a name that does not resolve locally
(an
ssh_configHostalias) gets no flag, leaving ssh's own resolution untouched.Scope
Every place an address is chosen:
ssh -4/-6, gated on availability-RtargetsAddrPrefthreaded intorun_session-LtargetsETRPREFER:<4|6>bootstrap lineForwarded TCP needed
forward::connect_tcp_preferredto be included at all:TcpStream::connect("host:port")walks every resolved address but in resolver order, sothe flag would have been honoured for QUIC and UDP and silently ignored for TCP.
ETRPREFER:uses the same forward-compatible mechanism asETRCMD:/ETRX11:— an oldserver ignores it, an old client never sends it.
PROTOCOL.md§2 now documents it, andETRX11:which it had never listed, plus the compatibility rule that makes both safe.Behaviour without a flag
AddrPref::Automeans what this call site did before, not one global default — the twocall sites genuinely differed (the QUIC path took the resolver's first answer;
resolve_udp_targethas preferred IPv6 since v0.4.x, with a regression test asserting it).Each caller supplies its own fallback, so an unflagged run behaves as in v0.7.9.
One deliberate exception: the QUIC peer address is now picked with the same no-packet
routing probe the UDP path always used, so a host whose AAAA comes back first, on a machine
with no IPv6 route, falls through to the A record instead of failing the connect. If
nothing is routable the first candidate is returned anyway, so the user still gets a real
connection error rather than "could not resolve".
etrs -4also flips the default bind to0.0.0.0, because a single wildcard bind cannotexpress "prefer IPv4". An explicit
-balways wins, and a client's preferencedeliberately does not touch the server's bind —
[::]already serves IPv4.Tests
112 → 145. Bootstrap-line handling and the bind default were extracted into
parse_bootstrap_lineandeffective_bind_ipso the tests call the shipped code: theexisting
ETRCMD/ETRX11tests re-implemented the parse loop inside the test, which wouldpass just as happily over a broken real loop.
New
just e2e-family-local— a CLI test can only prove the flags parse; this asserts theychange the family actually connected over.
Test plan
Measured on Fedora 44 (all of the above green):
etr -vv localhost[::1]— so-4is the discriminating case hereetr -4/etr -6127.0.0.1/[::1][client] address_family = "ipv4"viaXDG_CONFIG_HOME127.0.0.1etr -4/-6 -L …/udp→ server logUDP forward → 127.0.0.1:…/[::1]:…Reviewer-runnable equivalent of the last row:
Docs
Man pages (both), README (new "IPv4 / IPv6" section),
PROTOCOL.md§2,NOTES.md, andfive wiki pages pushed before this PR per AGENTS.md §4.11 (
854ec79..4693aa6).Found but not fixed
man/etrs.1.md's BUGS section still claims the reconnect window "is not configurable" — ithas been since v0.4.6, and
--reconnect-timeoutis missing from that page's OPTIONS.Recorded in NOTES.md; it needs its own PR rather than being widened into this one.
🤖 Generated with Claude Code