mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 06:49:07 +01:00
[client] Fix peers not being notified when the relay connection drops (#7490)
* [relay] Signal relay disconnects through the conn context AddCloseListener deduplicated listeners by comparing reflect.ValueOf(callback).Pointer(). For a method value that pointer is the address of the compiler-generated wrapper, not an identity bound to the receiver, so every peer's w.onRelayClientDisconnected compared equal. All peers on the home relay register under the same connectionURL key, so only the first registration survived and the rest were silently dropped. On a relay disconnect those peers were never notified: statusRelay stayed connected and the reconnect guard never fired. The relayed net.Conn itself was closed by closeAllConns, so nothing leaked, but the peer state machine did not learn about it. Foreign relays had the same defect scoped to the peers sharing that server. Rather than fixing the deduplication, drop the peer-level listener registry entirely. A relayed Conn now exposes Context(), cancelled when the connection is torn down, with a cancellation cause naming the reason. This is the same shape quic-go uses for its Conn and Stream types, and it removes the whole class of problems around listener identity, lifetime and deregistration: the signal belongs to the resource instead of a side table. WorkerRelay watches that context in a goroutine whose lifetime matches the connection. A watcher that wakes up for a superseded connection compares the conn pointer against the current one and returns without touching the state machine, so a fast relay reconnect cannot have a stale watcher tear down the connection that replaced it. Client.SetOnDisconnectListener stays: it is server-level and drives the reconnect guard and foreign relay eviction, unrelated to peers. handleRelayReady also checks the conn context, closing the race where the relay dies between OpenConn and the readiness handoff and the peer would otherwise build a WireGuard endpoint over a dead connection. TestNotifierDoubleAdd covered the removed mechanism and is gone. TestForeignAutoClose asserted nothing (both branches logged); it now waits for the relay to leave the client map and fails if it does not. * [relay] Fix build: return the concrete conn from Client.OpenConn OpenConn now returns *Conn, but it still went through connContainer.netConn(), which widens to net.Conn. The helper had one caller and only existed to produce the interface value the signature no longer wants, so return container.conn directly and drop it. * [relay] Assert the local-close cancellation cause explicitly The local-close test only rejected ErrServerDisconnected, so it would also have passed for ErrPeerDisconnected or a bare context.Canceled. closeConn cancels with net.ErrClosed, so assert that. * [client] Ignore relay disconnects from superseded connections The relayed conn watcher compared the conn pointer under relayLock, released it, and only then tore the connection down. A new offer could install its replacement in that window, so a watcher that validated the old pointer went on to close the proxy of the connection that had already replaced it and report the peer as disconnected while it was up. Move the decision to where the teardown happens. Conn records which relayed connection the current proxy was built from, and onRelayDisconnected takes the connection the signal belongs to and drops it under conn.mu when it is no longer the current one. Check and effect are now in the same critical section, so the verdict cannot go stale before it is acted on. This also covers the proxy read loops, whose disconnect listener took no argument and had the same defect: it now names the connection it belongs to. The WG timeout path keeps passing nil, since it deliberately tears down whatever is current. * [client] Bind the relayed conn reference to the proxy swap relayedConnRef was set at the top of the readiness path, but wgProxyRelay only changes at the end, in setRelayedProxy. The two failure returns in between — newProxy and ConfigureWGEndpoint — left the reference pointing at a connection that never became active while the old proxy was still installed. A disconnect of that old, live relay would then be dismissed as belonging to a superseded connection and never cleaned up. Set the reference in setRelayedProxy, next to the proxy it belongs to. Both success paths go through it and neither failure path does, so no failure branch has to remember to roll anything back.
This commit is contained in:
@@ -30,6 +30,12 @@ const (
|
||||
|
||||
var (
|
||||
ErrConnAlreadyExists = fmt.Errorf("connection already exists")
|
||||
// ErrServerDisconnected is the cancellation cause of a relayed Conn when the
|
||||
// client lost the connection to the relay server.
|
||||
ErrServerDisconnected = fmt.Errorf("relay server disconnected")
|
||||
// ErrPeerDisconnected is the cancellation cause of a relayed Conn when the
|
||||
// remote peer went offline.
|
||||
ErrPeerDisconnected = fmt.Errorf("remote peer disconnected")
|
||||
)
|
||||
|
||||
type internalStopFlag struct {
|
||||
@@ -74,16 +80,17 @@ type connContainer struct {
|
||||
msgChanLock sync.Mutex
|
||||
closed bool // flag to check if channel is closed
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
cancel context.CancelCauseFunc
|
||||
}
|
||||
|
||||
func newConnContainer(log *log.Entry, c *Client, peerID messages.PeerID, instanceURL *RelayAddr) *connContainer {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
msgChan := make(chan Msg, connChannelSize)
|
||||
cn := &Conn{
|
||||
dstID: peerID,
|
||||
messageChan: msgChan,
|
||||
instanceURL: instanceURL,
|
||||
ctx: ctx,
|
||||
}
|
||||
cc := &connContainer{
|
||||
log: log,
|
||||
@@ -106,10 +113,6 @@ func newConnContainer(log *log.Entry, c *Client, peerID messages.PeerID, instanc
|
||||
return cc
|
||||
}
|
||||
|
||||
func (cc *connContainer) netConn() net.Conn {
|
||||
return cc.conn
|
||||
}
|
||||
|
||||
func (cc *connContainer) writeMsg(msg Msg) {
|
||||
cc.msgChanLock.Lock()
|
||||
defer cc.msgChanLock.Unlock()
|
||||
@@ -128,8 +131,8 @@ func (cc *connContainer) writeMsg(msg Msg) {
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *connContainer) close() {
|
||||
cc.cancel()
|
||||
func (cc *connContainer) close(cause error) {
|
||||
cc.cancel(cause)
|
||||
|
||||
cc.msgChanLock.Lock()
|
||||
defer cc.msgChanLock.Unlock()
|
||||
@@ -293,12 +296,12 @@ func (c *Client) Connect(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenConn create a new net.Conn for the destination peer ID. In case if the connection is in progress
|
||||
// OpenConn create a new Conn for the destination peer ID. In case if the connection is in progress
|
||||
// to the relay server, the function will block until the connection is established or timed out. Otherwise,
|
||||
// it will return immediately.
|
||||
// It block until the server confirm the peer is online.
|
||||
// todo: what should happen if call with the same peerID with multiple times?
|
||||
func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, error) {
|
||||
func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (*Conn, error) {
|
||||
peerID := messages.HashID(dstPeerID)
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -335,7 +338,7 @@ func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, erro
|
||||
delete(c.conns, peerID)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
container.close()
|
||||
container.close(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -345,13 +348,13 @@ func (c *Client) OpenConn(ctx context.Context, dstPeerID string) (net.Conn, erro
|
||||
delete(c.conns, peerID)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
container.close()
|
||||
container.close(ErrServerDisconnected)
|
||||
return nil, fmt.Errorf("relay connection is not established")
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.log.Infof("remote peer is available: %s", peerID)
|
||||
return container.netConn(), nil
|
||||
return container.conn, nil
|
||||
}
|
||||
|
||||
// ServerInstanceURL returns the address of the relay server. It could change after the close and reopen the connection.
|
||||
@@ -773,7 +776,7 @@ func (c *Client) serverInstanceAddress() (string, netip.Addr, error) {
|
||||
|
||||
func (c *Client) closeAllConns() {
|
||||
for _, container := range c.conns {
|
||||
container.close()
|
||||
container.close(ErrServerDisconnected)
|
||||
}
|
||||
c.conns = make(map[messages.PeerID]*connContainer)
|
||||
|
||||
@@ -793,7 +796,7 @@ func (c *Client) closeConnsByPeerID(peerIDs []messages.PeerID) {
|
||||
}
|
||||
|
||||
container.log.Infof("remote peer has been disconnected, free up connection: %s", peerID)
|
||||
container.close()
|
||||
container.close(ErrPeerDisconnected)
|
||||
delete(c.conns, peerID)
|
||||
}
|
||||
|
||||
@@ -821,7 +824,7 @@ func (c *Client) closeConn(containerRef *connContainer, id messages.PeerID) erro
|
||||
|
||||
c.log.Infof("free up connection to peer: %s", id)
|
||||
delete(c.conns, id)
|
||||
current.close()
|
||||
current.close(net.ErrClosed)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user