-
Notifications
You must be signed in to change notification settings - Fork 71
Keep LAN shortcut from blocking holepunch fallback #259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
601f026
d7db4f1
0c06e40
df72545
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -181,6 +181,76 @@ test('createServer + connect - force holepunch', async function (t) { | |
| await b.destroy() | ||
| }) | ||
|
|
||
| test('createServer + connect - failed LAN ping falls back to holepunch', async function (t) { | ||
| const [boot] = await swarm(t) | ||
|
|
||
| // Use loopback to deterministically enter the same-LAN shortcut in CI. | ||
| // This does not model NAT; the injected ping failure below verifies that | ||
| // a blocked LAN shortcut still falls through to normal holepunch. | ||
| const bootstrap = [{ host: '127.0.0.1', port: boot.address().port }] | ||
|
marcus-pousette-hp marked this conversation as resolved.
Outdated
|
||
| const a = createDHT({ bootstrap, quickFirewall: false, ephemeral: true }) | ||
| const b = createDHT({ bootstrap, quickFirewall: false, ephemeral: true }) | ||
|
|
||
| await a.fullyBootstrapped() | ||
| await b.fullyBootstrapped() | ||
|
|
||
| const lc = t.test('socket lifecycle') | ||
| lc.plan(5) | ||
|
|
||
| const server = a.createServer(function (socket) { | ||
| lc.pass('server side opened') | ||
|
|
||
| socket.once('data', function (data) { | ||
| lc.alike(data, Buffer.from('hello')) | ||
| socket.end('world') | ||
| }) | ||
| }) | ||
|
|
||
| await server.listen() | ||
|
|
||
| const serverPort = a.io.serverSocket.address().port | ||
| const ping = b.ping.bind(b) | ||
| let blockedLanPing = false | ||
|
|
||
| b.ping = function (addr, opts) { | ||
| if (addr.host === '127.0.0.1' && addr.port === serverPort) { | ||
| blockedLanPing = true | ||
| return Promise.reject(new Error('LAN ping blocked')) | ||
| } | ||
| return ping(addr, opts) | ||
| } | ||
|
|
||
| const socket = b.connect(server.publicKey, { | ||
| fastOpen: false, | ||
| holepunch() { | ||
| lc.pass('client used normal holepunch') | ||
| return true | ||
| } | ||
| }) | ||
|
|
||
| socket.once('open', function () { | ||
| lc.pass('client side opened') | ||
| socket.write('hello') | ||
| }) | ||
|
|
||
| socket.once('data', function (data) { | ||
| lc.alike(data, Buffer.from('world')) | ||
| socket.end() | ||
| }) | ||
|
|
||
| socket.once('error', function (err) { | ||
| lc.fail('client should not error: ' + err.code) | ||
| }) | ||
|
|
||
| await lc | ||
|
|
||
| t.ok(blockedLanPing, 'exercised the LAN ping failure path') | ||
|
|
||
| await server.close() | ||
| await a.destroy() | ||
| await b.destroy() | ||
| }) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the test is correct, but I do get a little nervous whenever I see this sort of thing tested against loopback IP. I think it's just out of domain, in the sense that the fix was designed to solve the opposite case -- the case where a NATed peer's public IP is not the same as its LAN candidate. (I think here Not suggesting any changes, it was just a bit challenging to reason about whether this test was actually exercising the right paths. Compromises for testing...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see your concern. I added a clarifying comment in 0c06e40 about the test purpose
Yeps! In this test |
||
| test('server choosing to abort holepunch', async function (t) { | ||
| const [boot] = await swarm(t) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewing at @marcus-pousette-hp's request...
For context, I'm familiar with this NAT traversal IP candidate optimization problem, but not yet familiar with hyperdht's implementation. First time poking around this code.
I think this change is a clever race which AFAICT solves the problem correctly.
Only thing I wasn't sure about is the condition when holepunching is mid-flight when the LAN ping succeeds, and so
isDone(c) = false. But I guessc.onsocketis the guard that resolves the race there?Zooming out: the race paths are now pretty subtle and challenging to reason about, probably a bit too subtle for my taste. But I suppose a different structure would trigger a much larger refactor, so this seems like a reasonable compromise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
d7db4f1
I added a comment here, both isDone and c.onsocket correctly drop the other. The only real "issue" I would say here worth thinking about is that if the fallback (normal) Holepunch path wins, the LAN shortcut path will be ignored. In theory this would mean that we would take a little bit longer path than what the LAN shortcut path would offer
Context:
LAN shortcut
Uses the peer’s advertised local address, like 192.168.x.x / 10.x.x.x / loopback in the test. If both peers are truly reachable on that LAN address, this is usually the best path: fewer NAT hops, lower latency.
Normal holepunch path
Uses the externally observed/NAT address learned through relays/bootstrap nodes. If both peers share a public IP and the NAT supports hairpinning, this can still connect directly, but traffic may go through the router’s NAT hairpin path rather than straight peer-to-peer LAN addressing.
This PR makes the race and the faster wins, and there is an assumption here that if LAN shortcut is the fastest path it would win anyway here, but the shortcoming of this PR is perhaps it does not prove for example a bad scenario where the normal holepunch path would win the race, even though it is slower later when data is transmitting. My own judgement finds this unlikely, which is the reason why I went with this solution