Skip to content

Fix relayed keepalive and direct-upgrade handoff#266

Merged
lejeunerenard merged 4 commits into
mainfrom
fix/relay-server-connection-keepalive
Jun 30, 2026
Merged

Fix relayed keepalive and direct-upgrade handoff#266
lejeunerenard merged 4 commits into
mainfrom
fix/relay-server-connection-keepalive

Conversation

@marcus-pousette-hp

@marcus-pousette-hp marcus-pousette-hp commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

This fixes server-side relayed connections so they inherit connectionKeepAlive the same way direct server sockets and client sockets already do.

Enabling that exposed a relay-to-direct upgrade race: HyperDHT was closing the relay transport after local remote-changed, but that only proves this side switched to direct. The peer may not have observed the direct path yet. To avoid stranding the peer, the upgrade path now sends one zero-length UDX message after changeRemote() so the other side can observe the new direct path before relay cleanup.

One subtle behavior change here is that the old server-side relayed path had keepAlive: 0, so empty secret-stream keepalive frames from the client could reach app code. In the upgrade test, the server echoes all data, so those empty frames could accidentally create traffic during the relay-to-direct handoff. Once server-side keepalive is enabled, secret-stream correctly consumes those empty frames instead, so the upgrade path needs its own explicit UDX-level handoff packet.

Co-authored with AI

@marcus-pousette-hp marcus-pousette-hp requested a review from a team June 15, 2026 09:49
@marcus-pousette-hp marcus-pousette-hp changed the title Keep relayed server sockets alive Fix relayed keepalive and direct-upgrade handoff Jun 15, 2026

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

Addressed/will address the upgrading of the relay connection in the other stacked PRs. Looking at the connectionKeepAlive change and found the new assertion isn't testing the missing configuration path.

Comment thread test/relaying.js Outdated

const bServer = b.createServer({ relayThrough: aServer.publicKey }, function (socket) {
lc.pass('server socket opened')
lc.is(socket.keepAlive, 12345, 'server relayed socket inherits connectionKeepAlive')

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.

This assertion passes without the keepAlive change in server.js so isn't covering that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed! 9dc7833

@marcus-pousette-hp marcus-pousette-hp force-pushed the fix/relay-server-connection-keepalive branch 2 times, most recently from b3ef84b to 9dc7833 Compare June 23, 2026 19:29

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

Just one nit: Otherwise looks good to merge #268 in.

Comment thread test/relaying.js Outdated
relayThrough: aServer.publicKey
relayThrough: aServer.publicKey,
createSecretStream(isInitiator, rawStream, opts) {
if (!isInitiator && !assertedRelayKeepAlive) {

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.

Is the assertedRelayKeepAlive necessary? I've tried removing it and the assert is only ever called once.

@marcus-pousette-hp marcus-pousette-hp Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good point, missed it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed f19226f

@marcus-pousette-hp marcus-pousette-hp force-pushed the fix/relay-server-connection-keepalive branch from 9dc7833 to f19226f Compare June 23, 2026 19:51
#268)

* Validate upgrading to direct when changing remote from relay to direct

Changing the remote and immediately closing the relay connection leads
to situations where one side is left trying to ack the packets sent via
the relay connection but unable to since the peer closed their side
since the remote changed successfully.

Instead this approach uses the fact that changing the remote (whether
immediately or deferred) changes the stream's socket which means that
the `firewall` callback will be called for each packet received that
comes from a different socket, in the case of changing remote from the
relay -> direct means `firewall` only triggers from the relay.

This means the direct connection can be validated by waiting for a
packet to come through that doesn't trigger the firewall. This is done
by setting a flag on the connection / hs that assumes the next `data`
event will be direct. The flag is set to false when the firewall is
called and the socket its called with is shown to be the relay's. Once
direct the relay connection is cleaned up via the normal method.

* Test/relay upgrade passive confirmation (#269)

* test: cover passive relay upgrade confirmation

* test: fold relay upgrade cases into one test

* fix: nudge idle relay upgrades (#270)

* fix: nudge idle relay upgrades

* fix: simplify idle relay upgrade confirmation

* refactor: centralize relay upgrade confirmation

---------

Co-authored-by: marcus-pousette-hp <marcus-pousette-hp@users.noreply.github.com>

---------

Co-authored-by: marcus-pousette-hp <marcus-pousette-hp@users.noreply.github.com>

* Remove unnecessary setting `validUpgrade` to `true`

---------

Co-authored-by: Marcus Pousette <marcus.pousette@tether.io>
Co-authored-by: marcus-pousette-hp <marcus-pousette-hp@users.noreply.github.com>
@marcus-pousette-hp

marcus-pousette-hp commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

So I did an investigation about validUpgrade flag flipflop and we seem to have bug

#271 (stacked PR with just the test)

The bug is that HyperDHT is trying to decide whether relay-to-direct upgrade is confirmed from raw data/message events, but those events do not say whether the packet came from the old relay socket or the new direct socket.

In theory in-flight messages could screw things up with this approach but is fixable if we patch libudx/udx-native fix we can fix this cleanly but require more work.

I amend my previous comment (above) as invalid conclusions and I reminded myself of the implied order assumptions we do around the firewall callback.

I would argue there is missing test coverage for the order of the callbacks: holepunchto/libudx#293 this would prevent a downstream refactor to potentially screw with assumption we are making with this PR.

Otherwise this PR is good to go now!

@lejeunerenard lejeunerenard merged commit 120364a into main Jun 30, 2026
4 checks passed
@lejeunerenard lejeunerenard deleted the fix/relay-server-connection-keepalive branch June 30, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants