mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-28 18:41:30 +02:00
On a network switch (e.g. cellular to WiFi) the management, signal and relay sockets stay bound to the old network and look alive until the OS tears them down — measured at 5 seconds of dead air on Android, while the UI kept claiming Connected. The Android client papered over this with a full engine restart, paying for it with a torn-down TUN device and discarded peer state. Introduce client/netsweep: connections register on dial and deregister on close, and a sweep closes everything registered while aborting in-flight dials through sweep-cancellable dial contexts. The aborted dials matter: a relay dial started on the dying network would otherwise hold the reconnect loop hostage for the QUIC handshake timeout. After a sweep every failure surfaces as an ordinary read/write error and the existing retry loops redial immediately on the new network. The sweeper reaches the three long-lived connections through the same options that carry the netstate gate: a gRPC dial option wraps the management and signal transports (reconnects included), and the relay client wraps its connection in one place for the picker, the guard and foreign relays alike. Everything is nil-safe; platforms that inject no sweeper are untouched. Mobile clients expose the sweep as NotifyNetworkChange. Measured on Android against the engine restart it replaces: recovery in 1.6s instead of 3.2s, no Disconnected flash, and the TUN device, WireGuard config and peer state survive.
63 lines
1.6 KiB
Go
63 lines
1.6 KiB
Go
//go:build !js
|
|
|
|
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os/user"
|
|
"runtime"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc"
|
|
|
|
nbnet "github.com/netbirdio/netbird/client/net"
|
|
"github.com/netbirdio/netbird/client/netsweep"
|
|
)
|
|
|
|
func WithCustomDialer(_ bool, _ string) grpc.DialOption {
|
|
return grpc.WithContextDialer(dialContext)
|
|
}
|
|
|
|
// WithSweeper dials like WithCustomDialer but registers connections and
|
|
// dials with the sweeper. Append it after WithCustomDialer: gRPC applies
|
|
// dial options in order, so the later context dialer wins.
|
|
func WithSweeper(sweeper *netsweep.Sweeper) grpc.DialOption {
|
|
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
|
ctx, releaseDial := sweeper.WrapDialContext(ctx)
|
|
defer releaseDial()
|
|
|
|
conn, err := dialContext(ctx, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sweeper.WrapConn(conn), nil
|
|
})
|
|
}
|
|
|
|
func dialContext(ctx context.Context, addr string) (net.Conn, error) {
|
|
if runtime.GOOS == "linux" {
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.FailedPrecondition, "failed to get current user: %v", err)
|
|
}
|
|
|
|
// the custom dialer requires root permissions which are not required for use cases run as non-root
|
|
if currentUser.Uid != "0" {
|
|
log.Debug("Not running as root, using standard dialer")
|
|
dialer := &net.Dialer{}
|
|
return dialer.DialContext(ctx, "tcp", addr)
|
|
}
|
|
}
|
|
|
|
conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
|
|
}
|
|
return conn, nil
|
|
}
|