From d5cbbfb3e5b6acde20a606b3d93982bee5679e3b Mon Sep 17 00:00:00 2001 From: Teodor Calin Date: Mon, 15 Jun 2026 14:35:53 +0300 Subject: [PATCH] feat(driver): PreferDirect IPC command + driver method Adds the cmd_prefer_direct IPC opcode and Driver.PreferDirect(nodeID), letting pilotctl ask the daemon to drop a peer's tunnel + cached resolution so the next dial re-runs the full resolve + NAT hole-punch flow and prefers the direct path. Used by `pilotctl prefer-direct` and `send-file --prefer-direct`. Backward compatible: old daemons reply "unknown command", which the client treats as a best-effort hint. Co-Authored-By: Claude Opus 4.8 (1M context) --- driver/driver.go | 23 +++++++++++++++++++++++ driver/ipc.go | 10 +++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/driver/driver.go b/driver/driver.go index e83385f..8eca2d4 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -282,6 +282,29 @@ func (d *Driver) RevokeTrust(nodeID uint32) (map[string]interface{}, error) { return d.jsonRPC(msg, cmdHandshakeOK, "revoke") } +// PreferDirect asks the daemon to drop the existing tunnel to the peer +// and any sticky routing state (cached endpoint, cached resolve, unpinned +// relay flag), then re-resolve from the registry and prefer a direct UDP +// path on the next dial. Returns the new routing state the daemon arrived +// at — typically {"node_id": N, "relay_active": false, "pinned": false, +// "real_addr": "..."} when a direct path was found, or relay_active=true +// when the registry's relay_only flag is authoritative or the punch +// failed. +// +// Useful when stream traffic (pilotctl send-file) is failing on a relay +// path while small UDP (pilotctl ping) still works — typical symptom of +// a beacon-mediated tunnel that established once and then stuck. +// +// Backward compatibility: an old daemon (no CmdPreferDirect) returns an +// "unknown command" error — callers should treat that as "best-effort +// hint" and proceed with the normal dial, not abort the operation. +func (d *Driver) PreferDirect(nodeID uint32) (map[string]interface{}, error) { + msg := make([]byte, 5) + msg[0] = cmdPreferDirect + binary.BigEndian.PutUint32(msg[1:5], nodeID) + return d.jsonRPC(msg, cmdPreferDirectOK, "prefer_direct") +} + // ResolveHostname resolves a hostname to node info via the daemon. func (d *Driver) ResolveHostname(hostname string) (map[string]interface{}, error) { msg := make([]byte, 1+len(hostname)) diff --git a/driver/ipc.go b/driver/ipc.go index 457a458..38cd06d 100644 --- a/driver/ipc.go +++ b/driver/ipc.go @@ -53,6 +53,13 @@ const ( cmdRotateKeyOK byte = 0x26 cmdBroadcast byte = 0x29 cmdBroadcastOK byte = 0x2A + // cmdPreferDirect asks the daemon to drop the existing tunnel to a + // peer (and any cached endpoint / sticky relay flag) and re-resolve + // + redial fresh — preferring a direct UDP path. Useful when a peer + // got stuck on the beacon relay after an unlucky punch and stream + // traffic (send-file) is failing while small messages (ping) work. + cmdPreferDirect byte = 0x2D + cmdPreferDirectOK byte = 0x2E ) // Network sub-commands (must match daemon SubNetwork* constants) @@ -184,7 +191,8 @@ func (c *ipcClient) readLoop() { case cmdBindOK, cmdDialOK, cmdError, cmdInfoOK, cmdHandshakeOK, cmdResolveHostnameOK, cmdSetHostnameOK, cmdSetVisibilityOK, cmdDeregisterOK, cmdSetTagsOK, cmdSetWebhookOK, cmdNetworkOK, - cmdHealthOK, cmdManagedOK, cmdRotateKeyOK, cmdBroadcastOK: + cmdHealthOK, cmdManagedOK, cmdRotateKeyOK, cmdBroadcastOK, + cmdPreferDirectOK: // Known response cmds: route to pending for the in-flight sendAndWait. select { case c.pending <- &pendingResponse{cmd: cmd, payload: append([]byte(nil), payload...)}: