Play with friends: the host attaches from any port when its direct port times out

The host attaches every relayed session from its direct port, so each
attach to a relay reuses one address pair. The relay closes a finished
session first and keeps that pair in TIME_WAIT; a relay on Windows then
ignores the host's next attach from the same port (Linux takes it), the
connect times out instead of reporting the address in use, and
connect_from's fallback never runs. The second guest within a couple of
minutes got HostUnreachable.

connect_relay now gives the relay's addresses that timed out from the
direct port one more try from any port, after every address had its try
from the direct port (a dead IPv6 route doesn't eat the relay's attach
window twice). An attach from another port no longer offers what the relay
saw of it as a direct candidate: nothing listens there. The direct race's
dials keep their port.

Fixes #6342

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Evrim CicekandClaude Opus 5.5 committed 2026-11-01 22:37:49 +03:00
1 parent 62bdd29b23
commit f3de190aaf
1 file changed
+27 -4
+27 -4
View File
@@ -383,9 +383,29 @@ fn carry(shared: &Arc<Shared>, session: u64, (r, w): session::Secure, direct: bo
// ----- The relay (optional) ---------------------------------------------------------------------
fn connect_relay(relay: &str, port: Option<u16>) -> io::Result<(TcpStream, SocketAddr)> {
let wait = Duration::from_secs(5);
let mut last = io::Error::other("no address");
let mut timed_out = Vec::new();
for address in resolve_relay(relay)? {
match direct::connect_from(port, address, Duration::from_secs(5)) {
match direct::connect_from(port, address, wait) {
Ok(s) => {
let _ = s.set_nodelay(true);
return Ok((s, address));
}
Err(e) => {
if port.is_some() && e.kind() == io::ErrorKind::TimedOut {
timed_out.push(address);
}
last = e;
}
}
}
// A relay on Windows ignores a new connection from the address of one it
// closed less than its TIME_WAIT ago (the last guest's attach from the
// direct port), where Linux takes it: those get one more try from any
// port, once every address had its try from the direct port.
for address in timed_out {
match direct::connect_from(None, address, wait) {
Ok(s) => {
let _ = s.set_nodelay(true);
return Ok((s, address));
@@ -513,6 +533,9 @@ fn serve_relayed(
let Ok((mut stream, _)) = connect_relay(relay_url, port) else {
return;
};
// What the relay sees of an attach from another port is no address of
// this host's: nothing listens there.
let from_port = stream.local_addr().ok().map(|a| a.port()) == port;
let _ = stream.set_read_timeout(Some(Duration::from_secs(15)));
let attach = ToRelay::Attach {
v: wire::VERSION,
@@ -523,9 +546,9 @@ fn serve_relayed(
return;
}
let observed = match wire::recv::<_, FromRelay>(&mut stream) {
Ok(FromRelay::Bridged { observed, .. }) => {
observed.and_then(|o| o.parse::<SocketAddr>().ok())
}
Ok(FromRelay::Bridged { observed, .. }) => observed
.filter(|_| from_port)
.and_then(|o| o.parse::<SocketAddr>().ok()),
_ => return,
};
let Ok(key) = crypto::exchange(&mut stream, &code.secret, &code.nameplate, session) else {