Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
10 changes: 9 additions & 1 deletion driver/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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...)}:
Expand Down
50 changes: 50 additions & 0 deletions driver/zz_driver_simple_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package driver

import (
"encoding/binary"
"testing"
)

Expand Down Expand Up @@ -105,6 +106,55 @@ func TestDriverWaitForTrust(t *testing.T) {
}
}

// TestDriverPreferDirect covers PreferDirect's JSON-RPC roundtrip:
// - the request frame is exactly [cmdPreferDirect(0x2D)][big-endian uint32 nodeID] (5 bytes),
// - the cmdPreferDirectOK (0x2E) reply is routed/accepted by readLoop (not dropped) —
// proven by the happy path returning a non-nil result and nil error, which only
// happens if the OK frame reaches the in-flight sendAndWait via c.pending,
// - the daemon's returned routing state is unmarshalled and surfaced.
func TestDriverPreferDirect(t *testing.T) {
t.Parallel()
d := newFakeDaemon(t)
defer d.close()

const nodeID uint32 = 0xDEADBEEF

d.onCmd(cmdPreferDirect, func(frame []byte) [][]byte {
// Frame is [cmd][payload]; assert the exact 5-byte wire shape.
if len(frame) != 5 {
t.Errorf("PreferDirect frame len = %d, want 5", len(frame))
return [][]byte{{cmdError, 0, 0, 'l', 'e', 'n'}}
}
if frame[0] != cmdPreferDirect {
t.Errorf("PreferDirect opcode = 0x%02X, want 0x%02X", frame[0], cmdPreferDirect)
}
if got := binary.BigEndian.Uint32(frame[1:5]); got != nodeID {
t.Errorf("PreferDirect nodeID = 0x%08X, want 0x%08X", got, nodeID)
}
body := []byte(`{"node_id":3735928559,"relay_active":false,"pinned":false,"real_addr":"1.2.3.4:9000"}`)
return [][]byte{append([]byte{cmdPreferDirectOK}, body...)}
})

drv, err := Connect(d.path)
if err != nil {
t.Fatalf("Connect: %v", err)
}
defer drv.Close()

result, err := drv.PreferDirect(nodeID)
if err != nil {
t.Fatalf("PreferDirect: %v", err)
}
if result == nil {
t.Fatalf("PreferDirect result is nil")
}
// The cmdPreferDirectOK payload must have been routed (not dropped) and
// unmarshalled — check a field round-tripped.
if ra, ok := result["relay_active"].(bool); !ok || ra {
t.Errorf("relay_active = %v (ok=%v), want false", result["relay_active"], ok)
}
}

// TestDriverRotateKey covers RotateKey's JSON-RPC roundtrip.
func TestDriverRotateKey(t *testing.T) {
t.Parallel()
Expand Down
Loading