Skip to content

Commit 862ff31

Browse files
TeoSlayerteovl
andauthored
Harden driver IPC and registry client robustness (#33)
driver/ipc.go: drop late IPC replies after timeout instead of mis-correlating them with the next request. Each sendAndWait now registers a private, single-use waiter slot; readLoop delivers only to the active waiter and only when the reply cmd matches (cmdError always allowed). On timeout/disconnect the slot is abandoned, so a reply for an already-timed-out request finds no matching waiter and is dropped rather than handed to an unrelated caller. Full cross-process correctness still needs daemon-side request IDs (coordinated change + version bump), documented as a TODO. driver/driver.go: DialAddr now applies a 30s default timeout via DialAddrTimeout so a non-responsive daemon can't block forever; Listen and Broadcast get the same bound. jsonRPC paths (incl. WaitForTrust, which blocks in the daemon by design) keep the unbounded path. Document SendTo as fire-and-forget (nil means local IPC enqueue, not delivery). driver/conn.go: serialize Read with a dedicated mutex so recvBuf can't be corrupted by concurrent readers; implement SetWriteDeadline (was a silent no-op) backed by a writeDeadline checked in Write; SetDeadline now sets both read and write deadlines; document Write success as local IPC enqueue, not remote delivery. registry/client/client.go: bound every initial TCP/TLS dial with a 5s timeout (matching the reconnect paths) so registry ops can't hang on an unreachable host. Tests: add TestLateReplyAfterTimeoutNotMisdelivered (verified to fail without the waiter cmd-match guard) and a real SetWriteDeadline behavior test; update fixtures for the removed shared pending buffer. Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
1 parent 542658f commit 862ff31

8 files changed

Lines changed: 365 additions & 82 deletions

File tree

driver/conn.go

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,37 @@ import (
2020
const maxSendChunk = ipcutil.MaxMessageSize - 64
2121

2222
// Conn implements net.Conn over a Pilot Protocol stream.
23+
//
24+
// Concurrency: like *net.TCPConn, a Conn may be used by at most one reader
25+
// and one writer goroutine at a time. Read serialises concurrent callers
26+
// with readMu (so recvBuf is never corrupted), but interleaving two
27+
// readers still yields each a non-deterministic slice of the stream; do
28+
// not do that. Write is safe for one writer; concurrent writers may
29+
// interleave chunks on the wire. SetDeadline/SetReadDeadline/
30+
// SetWriteDeadline are safe to call from any goroutine.
2331
type Conn struct {
2432
id uint32
2533
localAddr protocol.SocketAddr
2634
remoteAddr protocol.SocketAddr
2735
ipc *ipcClient
2836
recvCh chan []byte
29-
recvBuf []byte // leftover from previous read
30-
closed bool
3137

32-
mu sync.Mutex
33-
readDeadline time.Time
34-
deadlineCh chan struct{} // closed when deadline is set/changed
38+
// readMu serialises Read so recvBuf (leftover from a previous read)
39+
// cannot be observed or mutated by two readers at once.
40+
readMu sync.Mutex
41+
recvBuf []byte // leftover from previous read; guarded by readMu
42+
43+
mu sync.Mutex
44+
closed bool
45+
readDeadline time.Time
46+
writeDeadline time.Time
47+
deadlineCh chan struct{} // closed when deadline is set/changed
3548
}
3649

3750
func (c *Conn) Read(b []byte) (int, error) {
51+
c.readMu.Lock()
52+
defer c.readMu.Unlock()
53+
3854
// Drain leftover first
3955
if len(c.recvBuf) > 0 {
4056
n := copy(b, c.recvBuf)
@@ -78,17 +94,36 @@ func (c *Conn) Read(b []byte) (int, error) {
7894
}
7995
}
8096

97+
// Write enqueues b to the local daemon over IPC, splitting it into
98+
// maxSendChunk-sized cmdSend frames.
99+
//
100+
// Send semantics: a nil error and n == len(b) mean every chunk was handed
101+
// to the local daemon over IPC — NOT that the bytes were transmitted on the
102+
// wire or acknowledged by the peer. The Pilot stream layer in the daemon
103+
// handles retransmission/ordering after this point; Write does not block on
104+
// it. Errors reported here are local IPC write failures or a passed
105+
// write deadline.
81106
func (c *Conn) Write(b []byte) (int, error) {
82107
c.mu.Lock()
83108
if c.closed {
84109
c.mu.Unlock()
85110
return 0, protocol.ErrConnClosed
86111
}
112+
wdl := c.writeDeadline
87113
c.mu.Unlock()
88114

115+
if !wdl.IsZero() && !time.Now().Before(wdl) {
116+
return 0, os.ErrDeadlineExceeded
117+
}
118+
89119
total := len(b)
90120
written := 0
91121
for written < total {
122+
// Honour the write deadline between chunks so a large, slow write
123+
// to a backed-up IPC socket cannot block past the deadline.
124+
if !wdl.IsZero() && !time.Now().Before(wdl) {
125+
return written, os.ErrDeadlineExceeded
126+
}
92127
chunk := total - written
93128
if chunk > maxSendChunk {
94129
chunk = maxSendChunk
@@ -125,7 +160,15 @@ func (c *Conn) LocalAddr() net.Addr { return pilotAddr(c.localAddr) }
125160
func (c *Conn) RemoteAddr() net.Addr { return pilotAddr(c.remoteAddr) }
126161

127162
func (c *Conn) SetDeadline(t time.Time) error {
128-
c.SetReadDeadline(t)
163+
c.mu.Lock()
164+
c.readDeadline = t
165+
c.writeDeadline = t
166+
// Signal any blocked Read to re-check.
167+
if c.deadlineCh != nil {
168+
close(c.deadlineCh)
169+
}
170+
c.deadlineCh = make(chan struct{})
171+
c.mu.Unlock()
129172
return nil
130173
}
131174

@@ -141,7 +184,18 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
141184
return nil
142185
}
143186

144-
func (c *Conn) SetWriteDeadline(t time.Time) error { return nil }
187+
// SetWriteDeadline sets a deadline for Write. A passed deadline causes Write
188+
// to return os.ErrDeadlineExceeded. Because Write never blocks waiting on a
189+
// remote peer (it only enqueues chunks to the local daemon over IPC), the
190+
// deadline is enforced before each chunk rather than via an interrupt — a
191+
// zero time clears it. This satisfies the net.Conn contract instead of the
192+
// previous silent no-op.
193+
func (c *Conn) SetWriteDeadline(t time.Time) error {
194+
c.mu.Lock()
195+
c.writeDeadline = t
196+
c.mu.Unlock()
197+
return nil
198+
}
145199

146200
// pilotAddr wraps SocketAddr to satisfy net.Addr.
147201
type pilotAddr protocol.SocketAddr

driver/driver.go

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ func DefaultSocketPath() string {
2727
return "/tmp/pilot.sock"
2828
}
2929

30+
// defaultDialTimeout bounds DialAddr / Listen / Broadcast so a wedged or
31+
// non-responsive daemon can't block the caller forever. The daemon resolves
32+
// + dials within this window in the normal case (direct punch or relay
33+
// fallback both complete well under it); callers needing a tighter bound use
34+
// DialAddrTimeout. Operations that legitimately block in the daemon
35+
// (WaitForTrust) deliberately keep the unbounded sendAndWait path.
36+
const defaultDialTimeout = 30 * time.Second
37+
3038
// Handshake sub-commands (must match daemon SubHandshake* constants)
3139
const (
3240
subHandshakeSend byte = 0x01
@@ -83,32 +91,11 @@ func (d *Driver) Dial(addr string) (*Conn, error) {
8391
return d.DialAddr(sa.Addr, sa.Port)
8492
}
8593

86-
// DialAddr opens a stream connection to a remote Addr + port.
94+
// DialAddr opens a stream connection to a remote Addr + port. It applies
95+
// defaultDialTimeout so a non-responsive daemon cannot block the caller
96+
// indefinitely; use DialAddrTimeout to supply an explicit bound.
8797
func (d *Driver) DialAddr(dst protocol.Addr, port uint16) (*Conn, error) {
88-
msg := make([]byte, 1+protocol.AddrSize+2)
89-
msg[0] = cmdDial
90-
dst.MarshalTo(msg, 1)
91-
binary.BigEndian.PutUint16(msg[1+protocol.AddrSize:], port)
92-
93-
resp, err := d.ipc.sendAndWait(msg, cmdDialOK)
94-
if err != nil {
95-
return nil, fmt.Errorf("dial: %w", err)
96-
}
97-
98-
if len(resp) < 4 {
99-
return nil, fmt.Errorf("invalid dial response")
100-
}
101-
102-
connID := binary.BigEndian.Uint32(resp[0:4])
103-
recvCh := d.ipc.registerRecvCh(connID)
104-
105-
return &Conn{
106-
id: connID,
107-
remoteAddr: protocol.SocketAddr{Addr: dst, Port: port},
108-
ipc: d.ipc,
109-
recvCh: recvCh,
110-
deadlineCh: make(chan struct{}),
111-
}, nil
98+
return d.DialAddrTimeout(dst, port, defaultDialTimeout)
11299
}
113100

114101
// DialAddrTimeout opens a stream connection with a client-side timeout.
@@ -146,7 +133,7 @@ func (d *Driver) Listen(port uint16) (*Listener, error) {
146133
msg[0] = cmdBind
147134
binary.BigEndian.PutUint16(msg[1:3], port)
148135

149-
resp, err := d.ipc.sendAndWait(msg, cmdBindOK)
136+
resp, err := d.ipc.sendAndWaitTimeout(msg, cmdBindOK, defaultDialTimeout)
150137
if err != nil {
151138
return nil, fmt.Errorf("bind: %w", err)
152139
}
@@ -167,6 +154,13 @@ func (d *Driver) Listen(port uint16) (*Listener, error) {
167154
// SendTo sends an unreliable unicast datagram to the given address:port.
168155
// Broadcast addresses (Node=0xFFFFFFFF) are not accepted on this path; use
169156
// Broadcast, which requires the daemon's admin token.
157+
//
158+
// Send semantics: this is fire-and-forget. A nil return means only that the
159+
// frame was successfully enqueued to the local daemon over IPC — it does NOT
160+
// indicate the datagram was transmitted on the wire, routed, or delivered to
161+
// the peer. Datagrams are unreliable; there is no acknowledgement. The only
162+
// errors reported are local IPC failures (empty/oversized frame, socket
163+
// write error).
170164
func (d *Driver) SendTo(dst protocol.Addr, port uint16, data []byte) error {
171165
if dst.IsBroadcast() {
172166
return fmt.Errorf("broadcast address requires admin token: use Driver.Broadcast")
@@ -192,7 +186,7 @@ func (d *Driver) Broadcast(netID uint16, port uint16, data []byte, adminToken st
192186
binary.BigEndian.PutUint16(msg[5:7], uint16(len(tokenBytes)))
193187
copy(msg[7:7+len(tokenBytes)], tokenBytes)
194188
copy(msg[7+len(tokenBytes):], data)
195-
if _, err := d.ipc.sendAndWait(msg, cmdBroadcastOK); err != nil {
189+
if _, err := d.ipc.sendAndWaitTimeout(msg, cmdBroadcastOK, defaultDialTimeout); err != nil {
196190
return err
197191
}
198192
return nil

0 commit comments

Comments
 (0)