diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7be52259b..e9741f541 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,7 +9,7 @@ on:
pull_request:
env:
- SIGN_PIPE_VER: "v0.0.22"
+ SIGN_PIPE_VER: "v0.0.23"
GORELEASER_VER: "v2.3.2"
PRODUCT_NAME: "NetBird"
COPYRIGHT: "NetBird GmbH"
diff --git a/README.md b/README.md
index ea7655869..2c5ee2ab6 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+
@@ -52,7 +53,7 @@
### Open Source Network Security in a Single Platform
-

+https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
### NetBird on Lawrence Systems (Video)
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
diff --git a/client/Dockerfile b/client/Dockerfile
index e19a09909..b2f627409 100644
--- a/client/Dockerfile
+++ b/client/Dockerfile
@@ -18,7 +18,7 @@ ENV \
NB_LOG_FILE="console,/var/log/netbird/client.log" \
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
- NB_ENTRYPOINT_LOGIN_TIMEOUT="1"
+ NB_ENTRYPOINT_LOGIN_TIMEOUT="5"
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
diff --git a/client/android/client.go b/client/android/client.go
index 4b4fcc9be..d2d0c37f6 100644
--- a/client/android/client.go
+++ b/client/android/client.go
@@ -19,7 +19,7 @@ import (
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
- "github.com/netbirdio/netbird/util/net"
+ "github.com/netbirdio/netbird/client/net"
)
// ConnectionListener export internal Listener for mobile
diff --git a/client/android/login.go b/client/android/login.go
index d8ac645e2..0df78dbc3 100644
--- a/client/android/login.go
+++ b/client/android/login.go
@@ -33,6 +33,7 @@ type ErrListener interface {
// the backend want to show an url for the user
type URLOpener interface {
Open(string)
+ OnLoginSuccess()
}
// Auth can register or login new client
@@ -181,6 +182,11 @@ func (a *Auth) login(urlOpener URLOpener) error {
err = a.withBackOff(a.ctx, func() error {
err := internal.Login(a.ctx, a.config, "", jwtToken)
+
+ if err == nil {
+ go urlOpener.OnLoginSuccess()
+ }
+
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
return nil
}
diff --git a/client/cmd/down.go b/client/cmd/down.go
index 3ce51c678..17c152d22 100644
--- a/client/cmd/down.go
+++ b/client/cmd/down.go
@@ -27,7 +27,7 @@ var downCmd = &cobra.Command{
return err
}
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*7)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
conn, err := DialClientGRPCServer(ctx, daemonAddr)
diff --git a/client/cmd/root.go b/client/cmd/root.go
index 5084bd38a..11e5228f1 100644
--- a/client/cmd/root.go
+++ b/client/cmd/root.go
@@ -231,7 +231,7 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
// DialClientGRPCServer returns client connection to the daemon server.
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
- ctx, cancel := context.WithTimeout(ctx, time.Second*3)
+ ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
return grpc.DialContext(
diff --git a/client/cmd/up.go b/client/cmd/up.go
index e686625d6..d047c041e 100644
--- a/client/cmd/up.go
+++ b/client/cmd/up.go
@@ -230,7 +230,9 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
client := proto.NewDaemonServiceClient(conn)
- status, err := client.Status(ctx, &proto.StatusRequest{})
+ status, err := client.Status(ctx, &proto.StatusRequest{
+ WaitForReady: func() *bool { b := true; return &b }(),
+ })
if err != nil {
return fmt.Errorf("unable to get daemon status: %v", err)
}
diff --git a/client/embed/embed.go b/client/embed/embed.go
index de83f9d96..0bfc7a37c 100644
--- a/client/embed/embed.go
+++ b/client/embed/embed.go
@@ -135,7 +135,7 @@ func (c *Client) Start(startCtx context.Context) error {
// either startup error (permanent backoff err) or nil err (successful engine up)
// TODO: make after-startup backoff err available
- run := make(chan struct{}, 1)
+ run := make(chan struct{})
clientErr := make(chan error, 1)
go func() {
if err := client.Run(run); err != nil {
diff --git a/client/firewall/iptables/acl_linux.go b/client/firewall/iptables/acl_linux.go
index 7b90000a8..ed8a7403b 100644
--- a/client/firewall/iptables/acl_linux.go
+++ b/client/firewall/iptables/acl_linux.go
@@ -12,7 +12,7 @@ import (
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/statemanager"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
const (
diff --git a/client/firewall/iptables/router_linux.go b/client/firewall/iptables/router_linux.go
index 1e44c7a4d..081991235 100644
--- a/client/firewall/iptables/router_linux.go
+++ b/client/firewall/iptables/router_linux.go
@@ -19,7 +19,7 @@ import (
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
"github.com/netbirdio/netbird/client/internal/statemanager"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
// constants needed to manage and create iptable rules
diff --git a/client/firewall/iptables/router_linux_test.go b/client/firewall/iptables/router_linux_test.go
index e9eeff863..3490c5dad 100644
--- a/client/firewall/iptables/router_linux_test.go
+++ b/client/firewall/iptables/router_linux_test.go
@@ -14,7 +14,7 @@ import (
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/firewall/test"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
func isIptablesSupported() bool {
diff --git a/client/firewall/nftables/acl_linux.go b/client/firewall/nftables/acl_linux.go
index 52979d257..9ff5b8c92 100644
--- a/client/firewall/nftables/acl_linux.go
+++ b/client/firewall/nftables/acl_linux.go
@@ -16,7 +16,7 @@ import (
"golang.org/x/sys/unix"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
const (
diff --git a/client/firewall/nftables/router_linux.go b/client/firewall/nftables/router_linux.go
index f8fed4d80..e918d0524 100644
--- a/client/firewall/nftables/router_linux.go
+++ b/client/firewall/nftables/router_linux.go
@@ -22,7 +22,7 @@ import (
nbid "github.com/netbirdio/netbird/client/internal/acl/id"
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
const (
diff --git a/util/grpc/dialer.go b/client/grpc/dialer.go
similarity index 91%
rename from util/grpc/dialer.go
rename to client/grpc/dialer.go
index f6d6d2f04..69e3f088c 100644
--- a/util/grpc/dialer.go
+++ b/client/grpc/dialer.go
@@ -20,8 +20,9 @@ import (
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
+ nbnet "github.com/netbirdio/netbird/client/net"
+
"github.com/netbirdio/netbird/util/embeddedroots"
- nbnet "github.com/netbirdio/netbird/util/net"
)
func WithCustomDialer() grpc.DialOption {
@@ -57,7 +58,7 @@ func Backoff(ctx context.Context) backoff.BackOff {
return backoff.WithContext(b, ctx)
}
-func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
+func CreateConnection(ctx context.Context, addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
if tlsEnabled {
certPool, err := x509.SystemCertPool()
@@ -71,7 +72,7 @@ func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
}))
}
- connCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ connCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
conn, err := grpc.DialContext(
diff --git a/client/iface/bind/control.go b/client/iface/bind/control.go
index 89bddf12c..32b07c330 100644
--- a/client/iface/bind/control.go
+++ b/client/iface/bind/control.go
@@ -3,7 +3,7 @@ package bind
import (
wireguard "golang.zx2c4.com/wireguard/conn"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
// TODO: This is most likely obsolete since the control fns should be called by the wrapped udpconn (ice_bind.go)
diff --git a/client/iface/bind/ice_bind.go b/client/iface/bind/ice_bind.go
index b74f90d6c..577c7c0c4 100644
--- a/client/iface/bind/ice_bind.go
+++ b/client/iface/bind/ice_bind.go
@@ -17,7 +17,7 @@ import (
"github.com/netbirdio/netbird/client/iface/udpmux"
"github.com/netbirdio/netbird/client/iface/wgaddr"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type RecvMessage struct {
diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go
index 945f1a162..f744e0127 100644
--- a/client/iface/configurer/usp.go
+++ b/client/iface/configurer/usp.go
@@ -17,8 +17,8 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/iface/bind"
+ nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/monotime"
- nbnet "github.com/netbirdio/netbird/util/net"
)
const (
@@ -409,7 +409,7 @@ func toBytes(s string) (int64, error) {
}
func getFwmark() int {
- if nbnet.AdvancedRouting() {
+ if nbnet.AdvancedRouting() && runtime.GOOS == "linux" {
return nbnet.ControlPlaneMark
}
return 0
diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go
index 2ef6f6b22..cdac43a53 100644
--- a/client/iface/device/device_kernel_unix.go
+++ b/client/iface/device/device_kernel_unix.go
@@ -15,8 +15,8 @@ import (
"github.com/netbirdio/netbird/client/iface/configurer"
"github.com/netbirdio/netbird/client/iface/udpmux"
"github.com/netbirdio/netbird/client/iface/wgaddr"
+ nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/sharedsock"
- nbnet "github.com/netbirdio/netbird/util/net"
)
type TunKernelDevice struct {
@@ -101,13 +101,8 @@ func (t *TunKernelDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
return nil, err
}
- var udpConn net.PacketConn = rawSock
- if !nbnet.AdvancedRouting() {
- udpConn = nbnet.WrapPacketConn(rawSock)
- }
-
bindParams := udpmux.UniversalUDPMuxParams{
- UDPConn: udpConn,
+ UDPConn: nbnet.WrapPacketConn(rawSock),
Net: t.transportNet,
FilterFn: t.filterFn,
WGAddress: t.address,
diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go
index 2fcc74809..a6ef47027 100644
--- a/client/iface/device/device_netstack.go
+++ b/client/iface/device/device_netstack.go
@@ -12,7 +12,7 @@ import (
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/iface/udpmux"
"github.com/netbirdio/netbird/client/iface/wgaddr"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type TunNetstackDevice struct {
diff --git a/client/iface/udpmux/mux_generic.go b/client/iface/udpmux/mux_generic.go
index cf3043be0..29fc2d834 100644
--- a/client/iface/udpmux/mux_generic.go
+++ b/client/iface/udpmux/mux_generic.go
@@ -3,7 +3,7 @@
package udpmux
import (
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
func (m *SingleSocketUDPMux) notifyAddressRemoval(addr string) {
diff --git a/client/iface/wgproxy/ebpf/proxy.go b/client/iface/wgproxy/ebpf/proxy.go
index fcdc0189d..b899f1694 100644
--- a/client/iface/wgproxy/ebpf/proxy.go
+++ b/client/iface/wgproxy/ebpf/proxy.go
@@ -20,7 +20,7 @@ import (
"github.com/netbirdio/netbird/client/iface/bufsize"
"github.com/netbirdio/netbird/client/internal/ebpf"
ebpfMgr "github.com/netbirdio/netbird/client/internal/ebpf/manager"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
const (
diff --git a/client/internal/connect.go b/client/internal/connect.go
index 33cd4b4a1..c9331baf5 100644
--- a/client/internal/connect.go
+++ b/client/internal/connect.go
@@ -34,7 +34,7 @@ import (
relayClient "github.com/netbirdio/netbird/shared/relay/client"
signal "github.com/netbirdio/netbird/shared/signal/client"
"github.com/netbirdio/netbird/util"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/version"
)
diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go
index 89d637686..6ef0ab526 100644
--- a/client/internal/dns/service_memory.go
+++ b/client/internal/dns/service_memory.go
@@ -10,7 +10,7 @@ import (
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type ServiceViaMemory struct {
diff --git a/client/internal/dns/upstream_android.go b/client/internal/dns/upstream_android.go
index 6b7dcc05e..def281f28 100644
--- a/client/internal/dns/upstream_android.go
+++ b/client/internal/dns/upstream_android.go
@@ -10,7 +10,7 @@ import (
"github.com/miekg/dns"
"github.com/netbirdio/netbird/client/internal/peer"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type upstreamResolver struct {
diff --git a/client/internal/engine.go b/client/internal/engine.go
index 9dc744434..d4c465efb 100644
--- a/client/internal/engine.go
+++ b/client/internal/engine.go
@@ -446,6 +446,8 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
return fmt.Errorf("up wg interface: %w", err)
}
+
+
// if inbound conns are blocked there is no need to create the ACL manager
if e.firewall != nil && !e.config.BlockInbound {
e.acl = acl.NewDefaultManager(e.firewall)
diff --git a/client/internal/netflow/conntrack/conntrack.go b/client/internal/netflow/conntrack/conntrack.go
index dbb4747a5..a4ffa3a25 100644
--- a/client/internal/netflow/conntrack/conntrack.go
+++ b/client/internal/netflow/conntrack/conntrack.go
@@ -14,7 +14,7 @@ import (
"github.com/ti-mo/netfilter"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
const defaultChannelSize = 100
diff --git a/client/internal/relay/relay.go b/client/internal/relay/relay.go
index 8c3d5a571..fa208716f 100644
--- a/client/internal/relay/relay.go
+++ b/client/internal/relay/relay.go
@@ -12,7 +12,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/stdnet"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
// ProbeResult holds the info about the result of a relay probe request
diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go
index a6775c45a..04513bbe4 100644
--- a/client/internal/routemanager/manager.go
+++ b/client/internal/routemanager/manager.go
@@ -36,9 +36,9 @@ import (
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
"github.com/netbirdio/netbird/client/internal/routeselector"
"github.com/netbirdio/netbird/client/internal/statemanager"
+ nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/route"
relayClient "github.com/netbirdio/netbird/shared/relay/client"
- nbnet "github.com/netbirdio/netbird/util/net"
"github.com/netbirdio/netbird/version"
)
@@ -108,6 +108,10 @@ func NewManager(config ManagerConfig) *DefaultManager {
notifier := notifier.NewNotifier()
sysOps := systemops.NewSysOps(config.WGInterface, notifier)
+ if runtime.GOOS == "windows" && config.WGInterface != nil {
+ nbnet.SetVPNInterfaceName(config.WGInterface.Name())
+ }
+
dm := &DefaultManager{
ctx: mCTX,
stop: cancel,
@@ -208,7 +212,7 @@ func (m *DefaultManager) Init() error {
return nil
}
- if err := m.sysOps.CleanupRouting(nil); err != nil {
+ if err := m.sysOps.CleanupRouting(nil, nbnet.AdvancedRouting()); err != nil {
log.Warnf("Failed cleaning up routing: %v", err)
}
@@ -219,7 +223,7 @@ func (m *DefaultManager) Init() error {
ips := resolveURLsToIPs(initialAddresses)
- if err := m.sysOps.SetupRouting(ips, m.stateManager); err != nil {
+ if err := m.sysOps.SetupRouting(ips, m.stateManager, nbnet.AdvancedRouting()); err != nil {
return fmt.Errorf("setup routing: %w", err)
}
@@ -285,11 +289,15 @@ func (m *DefaultManager) Stop(stateManager *statemanager.Manager) {
}
if !nbnet.CustomRoutingDisabled() && !m.disableClientRoutes {
- if err := m.sysOps.CleanupRouting(stateManager); err != nil {
+ if err := m.sysOps.CleanupRouting(stateManager, nbnet.AdvancedRouting()); err != nil {
log.Errorf("Error cleaning up routing: %v", err)
} else {
log.Info("Routing cleanup complete")
}
+
+ if runtime.GOOS == "windows" {
+ nbnet.SetVPNInterfaceName("")
+ }
}
m.mux.Lock()
diff --git a/client/internal/routemanager/systemops/systemops_android.go b/client/internal/routemanager/systemops/systemops_android.go
index a375ce832..7cb8dae93 100644
--- a/client/internal/routemanager/systemops/systemops_android.go
+++ b/client/internal/routemanager/systemops/systemops_android.go
@@ -12,11 +12,11 @@ import (
"github.com/netbirdio/netbird/client/internal/statemanager"
)
-func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager) error {
+func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager, bool) error {
return nil
}
-func (r *SysOps) CleanupRouting(*statemanager.Manager) error {
+func (r *SysOps) CleanupRouting(*statemanager.Manager, bool) error {
return nil
}
diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go
index 128afa2a5..26a548634 100644
--- a/client/internal/routemanager/systemops/systemops_generic.go
+++ b/client/internal/routemanager/systemops/systemops_generic.go
@@ -3,7 +3,6 @@
package systemops
import (
- "context"
"errors"
"fmt"
"net"
@@ -22,7 +21,7 @@ import (
"github.com/netbirdio/netbird/client/internal/routemanager/util"
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
"github.com/netbirdio/netbird/client/internal/statemanager"
- nbnet "github.com/netbirdio/netbird/util/net"
+ "github.com/netbirdio/netbird/client/net/hooks"
)
const localSubnetsCacheTTL = 15 * time.Minute
@@ -96,9 +95,9 @@ func (r *SysOps) cleanupRefCounter(stateManager *statemanager.Manager) error {
return nil
}
- // TODO: Remove hooks selectively
- nbnet.RemoveDialerHooks()
- nbnet.RemoveListenerHooks()
+ hooks.RemoveWriteHooks()
+ hooks.RemoveCloseHooks()
+ hooks.RemoveAddressRemoveHooks()
if err := r.refCounter.Flush(); err != nil {
return fmt.Errorf("flush route manager: %w", err)
@@ -290,12 +289,7 @@ func (r *SysOps) genericRemoveVPNRoute(prefix netip.Prefix, intf *net.Interface)
}
func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.Manager) error {
- beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error {
- prefix, err := util.GetPrefixFromIP(ip)
- if err != nil {
- return fmt.Errorf("convert ip to prefix: %w", err)
- }
-
+ beforeHook := func(connID hooks.ConnectionID, prefix netip.Prefix) error {
if _, err := r.refCounter.IncrementWithID(string(connID), prefix, struct{}{}); err != nil {
return fmt.Errorf("adding route reference: %v", err)
}
@@ -304,7 +298,7 @@ func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.M
return nil
}
- afterHook := func(connID nbnet.ConnectionID) error {
+ afterHook := func(connID hooks.ConnectionID) error {
if err := r.refCounter.DecrementWithID(string(connID)); err != nil {
return fmt.Errorf("remove route reference: %w", err)
}
@@ -317,36 +311,20 @@ func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.M
var merr *multierror.Error
for _, ip := range initAddresses {
- if err := beforeHook("init", ip); err != nil {
- merr = multierror.Append(merr, fmt.Errorf("add initial route for %s: %w", ip, err))
+ prefix, err := util.GetPrefixFromIP(ip)
+ if err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("invalid IP address %s: %w", ip, err))
+ continue
+ }
+ if err := beforeHook("init", prefix); err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("add initial route for %s: %w", prefix, err))
}
}
- nbnet.AddDialerHook(func(ctx context.Context, connID nbnet.ConnectionID, resolvedIPs []net.IPAddr) error {
- if ctx.Err() != nil {
- return ctx.Err()
- }
+ hooks.AddWriteHook(beforeHook)
+ hooks.AddCloseHook(afterHook)
- var merr *multierror.Error
- for _, ip := range resolvedIPs {
- merr = multierror.Append(merr, beforeHook(connID, ip.IP))
- }
- return nberrors.FormatErrorOrNil(merr)
- })
-
- nbnet.AddDialerCloseHook(func(connID nbnet.ConnectionID, conn *net.Conn) error {
- return afterHook(connID)
- })
-
- nbnet.AddListenerWriteHook(func(connID nbnet.ConnectionID, ip *net.IPAddr, data []byte) error {
- return beforeHook(connID, ip.IP)
- })
-
- nbnet.AddListenerCloseHook(func(connID nbnet.ConnectionID, conn net.PacketConn) error {
- return afterHook(connID)
- })
-
- nbnet.AddListenerAddressRemoveHook(func(connID nbnet.ConnectionID, prefix netip.Prefix) error {
+ hooks.AddAddressRemoveHook(func(connID hooks.ConnectionID, prefix netip.Prefix) error {
if _, err := r.refCounter.Decrement(prefix); err != nil {
return fmt.Errorf("remove route reference: %w", err)
}
diff --git a/client/internal/routemanager/systemops/systemops_generic_test.go b/client/internal/routemanager/systemops/systemops_generic_test.go
index c1c1182bc..32ea38a7a 100644
--- a/client/internal/routemanager/systemops/systemops_generic_test.go
+++ b/client/internal/routemanager/systemops/systemops_generic_test.go
@@ -22,6 +22,7 @@ import (
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type dialer interface {
@@ -143,10 +144,11 @@ func TestAddVPNRoute(t *testing.T) {
wgInterface := createWGInterface(t, fmt.Sprintf("utun53%d", n), "100.65.75.2/24", 33100+n)
r := NewSysOps(wgInterface, nil)
- err := r.SetupRouting(nil, nil)
+ advancedRouting := nbnet.AdvancedRouting()
+ err := r.SetupRouting(nil, nil, advancedRouting)
require.NoError(t, err)
t.Cleanup(func() {
- assert.NoError(t, r.CleanupRouting(nil))
+ assert.NoError(t, r.CleanupRouting(nil, advancedRouting))
})
intf, err := net.InterfaceByName(wgInterface.Name())
@@ -341,10 +343,11 @@ func TestAddRouteToNonVPNIntf(t *testing.T) {
wgInterface := createWGInterface(t, fmt.Sprintf("utun54%d", n), "100.65.75.2/24", 33200+n)
r := NewSysOps(wgInterface, nil)
- err := r.SetupRouting(nil, nil)
+ advancedRouting := nbnet.AdvancedRouting()
+ err := r.SetupRouting(nil, nil, advancedRouting)
require.NoError(t, err)
t.Cleanup(func() {
- assert.NoError(t, r.CleanupRouting(nil))
+ assert.NoError(t, r.CleanupRouting(nil, advancedRouting))
})
initialNextHopV4, err := GetNextHop(netip.IPv4Unspecified())
@@ -484,10 +487,11 @@ func setupTestEnv(t *testing.T) {
})
r := NewSysOps(wgInterface, nil)
- err := r.SetupRouting(nil, nil)
+ advancedRouting := nbnet.AdvancedRouting()
+ err := r.SetupRouting(nil, nil, advancedRouting)
require.NoError(t, err, "setupRouting should not return err")
t.Cleanup(func() {
- assert.NoError(t, r.CleanupRouting(nil))
+ assert.NoError(t, r.CleanupRouting(nil, advancedRouting))
})
index, err := net.InterfaceByName(wgInterface.Name())
diff --git a/client/internal/routemanager/systemops/systemops_ios.go b/client/internal/routemanager/systemops/systemops_ios.go
index 10356eae0..99a363371 100644
--- a/client/internal/routemanager/systemops/systemops_ios.go
+++ b/client/internal/routemanager/systemops/systemops_ios.go
@@ -12,14 +12,14 @@ import (
"github.com/netbirdio/netbird/client/internal/statemanager"
)
-func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager) error {
+func (r *SysOps) SetupRouting([]net.IP, *statemanager.Manager, bool) error {
r.mu.Lock()
defer r.mu.Unlock()
r.prefixes = make(map[netip.Prefix]struct{})
return nil
}
-func (r *SysOps) CleanupRouting(*statemanager.Manager) error {
+func (r *SysOps) CleanupRouting(*statemanager.Manager, bool) error {
r.mu.Lock()
defer r.mu.Unlock()
diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go
index c0cef94ba..bd10f131f 100644
--- a/client/internal/routemanager/systemops/systemops_linux.go
+++ b/client/internal/routemanager/systemops/systemops_linux.go
@@ -20,7 +20,7 @@ import (
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
"github.com/netbirdio/netbird/client/internal/statemanager"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
// IPRule contains IP rule information for debugging
@@ -94,15 +94,15 @@ func getSetupRules() []ruleParams {
// Rule 2 (VPN Traffic Routing): Directs all remaining traffic to the 'NetbirdVPNTableID' custom routing table.
// This table is where a default route or other specific routes received from the management server are configured,
// enabling VPN connectivity.
-func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (err error) {
- if !nbnet.AdvancedRouting() {
+func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager, advancedRouting bool) (err error) {
+ if !advancedRouting {
log.Infof("Using legacy routing setup")
return r.setupRefCounter(initAddresses, stateManager)
}
defer func() {
if err != nil {
- if cleanErr := r.CleanupRouting(stateManager); cleanErr != nil {
+ if cleanErr := r.CleanupRouting(stateManager, advancedRouting); cleanErr != nil {
log.Errorf("Error cleaning up routing: %v", cleanErr)
}
}
@@ -132,8 +132,8 @@ func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager
// CleanupRouting performs a thorough cleanup of the routing configuration established by 'setupRouting'.
// It systematically removes the three rules and any associated routing table entries to ensure a clean state.
// The function uses error aggregation to report any errors encountered during the cleanup process.
-func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error {
- if !nbnet.AdvancedRouting() {
+func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager, advancedRouting bool) error {
+ if !advancedRouting {
return r.cleanupRefCounter(stateManager)
}
diff --git a/client/internal/routemanager/systemops/systemops_unix.go b/client/internal/routemanager/systemops/systemops_unix.go
index f165f7779..d43c2d5bf 100644
--- a/client/internal/routemanager/systemops/systemops_unix.go
+++ b/client/internal/routemanager/systemops/systemops_unix.go
@@ -20,11 +20,11 @@ import (
"github.com/netbirdio/netbird/client/internal/statemanager"
)
-func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) error {
+func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager, advancedRouting bool) error {
return r.setupRefCounter(initAddresses, stateManager)
}
-func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error {
+func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager, advancedRouting bool) error {
return r.cleanupRefCounter(stateManager)
}
diff --git a/client/internal/routemanager/systemops/systemops_unix_test.go b/client/internal/routemanager/systemops/systemops_unix_test.go
index ad37f611f..959c697e4 100644
--- a/client/internal/routemanager/systemops/systemops_unix_test.go
+++ b/client/internal/routemanager/systemops/systemops_unix_test.go
@@ -17,7 +17,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type PacketExpectation struct {
diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go
index 4f836897b..95645329e 100644
--- a/client/internal/routemanager/systemops/systemops_windows.go
+++ b/client/internal/routemanager/systemops/systemops_windows.go
@@ -8,6 +8,7 @@ import (
"net/netip"
"os"
"runtime/debug"
+ "sort"
"strconv"
"sync"
"syscall"
@@ -19,9 +20,16 @@ import (
"golang.org/x/sys/windows"
"github.com/netbirdio/netbird/client/internal/statemanager"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
-const InfiniteLifetime = 0xffffffff
+func init() {
+ nbnet.GetBestInterfaceFunc = GetBestInterface
+}
+
+const (
+ InfiniteLifetime = 0xffffffff
+)
type RouteUpdateType int
@@ -77,6 +85,14 @@ type MIB_IPFORWARD_TABLE2 struct {
Table [1]MIB_IPFORWARD_ROW2 // Flexible array member
}
+// candidateRoute represents a potential route for selection during route lookup
+type candidateRoute struct {
+ interfaceIndex uint32
+ prefixLength uint8
+ routeMetric uint32
+ interfaceMetric int
+}
+
// IP_ADDRESS_PREFIX is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-ip_address_prefix
type IP_ADDRESS_PREFIX struct {
Prefix SOCKADDR_INET
@@ -177,11 +193,20 @@ const (
RouteDeleted
)
-func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) error {
+func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager, advancedRouting bool) error {
+ if advancedRouting {
+ return nil
+ }
+
+ log.Infof("Using legacy routing setup with ref counters")
return r.setupRefCounter(initAddresses, stateManager)
}
-func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error {
+func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager, advancedRouting bool) error {
+ if advancedRouting {
+ return nil
+ }
+
return r.cleanupRefCounter(stateManager)
}
@@ -635,10 +660,7 @@ func getWindowsRoutingTable() (*MIB_IPFORWARD_TABLE2, error) {
func freeWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) {
if table != nil {
- ret, _, _ := procFreeMibTable.Call(uintptr(unsafe.Pointer(table)))
- if ret != 0 {
- log.Warnf("FreeMibTable failed with return code: %d", ret)
- }
+ _, _, _ = procFreeMibTable.Call(uintptr(unsafe.Pointer(table)))
}
}
@@ -652,8 +674,7 @@ func parseWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) []DetailedRoute {
entryPtr := basePtr + uintptr(i)*entrySize
entry := (*MIB_IPFORWARD_ROW2)(unsafe.Pointer(entryPtr))
- detailed := buildWindowsDetailedRoute(entry)
- if detailed != nil {
+ if detailed := buildWindowsDetailedRoute(entry); detailed != nil {
detailedRoutes = append(detailedRoutes, *detailed)
}
}
@@ -802,6 +823,46 @@ func addZone(ip netip.Addr, interfaceIndex int) netip.Addr {
return ip
}
+// parseCandidatesFromTable extracts all matching candidate routes from the routing table
+func parseCandidatesFromTable(table *MIB_IPFORWARD_TABLE2, dest netip.Addr, skipInterfaceIndex int) []candidateRoute {
+ var candidates []candidateRoute
+ entrySize := unsafe.Sizeof(MIB_IPFORWARD_ROW2{})
+ basePtr := uintptr(unsafe.Pointer(&table.Table[0]))
+
+ for i := uint32(0); i < table.NumEntries; i++ {
+ entryPtr := basePtr + uintptr(i)*entrySize
+ entry := (*MIB_IPFORWARD_ROW2)(unsafe.Pointer(entryPtr))
+
+ if candidate := parseCandidateRoute(entry, dest, skipInterfaceIndex); candidate != nil {
+ candidates = append(candidates, *candidate)
+ }
+ }
+
+ return candidates
+}
+
+// parseCandidateRoute extracts candidate route information from a MIB_IPFORWARD_ROW2 entry
+// Returns nil if the route doesn't match the destination or should be skipped
+func parseCandidateRoute(entry *MIB_IPFORWARD_ROW2, dest netip.Addr, skipInterfaceIndex int) *candidateRoute {
+ if skipInterfaceIndex > 0 && int(entry.InterfaceIndex) == skipInterfaceIndex {
+ return nil
+ }
+
+ destPrefix := parseIPPrefix(entry.DestinationPrefix, int(entry.InterfaceIndex))
+ if !destPrefix.IsValid() || !destPrefix.Contains(dest) {
+ return nil
+ }
+
+ interfaceMetric := getInterfaceMetric(entry.InterfaceIndex, entry.DestinationPrefix.Prefix.sin6_family)
+
+ return &candidateRoute{
+ interfaceIndex: entry.InterfaceIndex,
+ prefixLength: entry.DestinationPrefix.PrefixLength,
+ routeMetric: entry.Metric,
+ interfaceMetric: interfaceMetric,
+ }
+}
+
// getInterfaceMetric retrieves the interface metric for a given interface and address family
func getInterfaceMetric(interfaceIndex uint32, family int16) int {
if interfaceIndex == 0 {
@@ -821,6 +882,75 @@ func getInterfaceMetric(interfaceIndex uint32, family int16) int {
return int(ipInterfaceRow.Metric)
}
+// sortRouteCandidates sorts route candidates by priority: prefix length -> route metric -> interface metric
+func sortRouteCandidates(candidates []candidateRoute) {
+ sort.Slice(candidates, func(i, j int) bool {
+ if candidates[i].prefixLength != candidates[j].prefixLength {
+ return candidates[i].prefixLength > candidates[j].prefixLength
+ }
+ if candidates[i].routeMetric != candidates[j].routeMetric {
+ return candidates[i].routeMetric < candidates[j].routeMetric
+ }
+ return candidates[i].interfaceMetric < candidates[j].interfaceMetric
+ })
+}
+
+// GetBestInterface finds the best interface for reaching a destination,
+// excluding the VPN interface to avoid routing loops.
+//
+// Route selection priority:
+// 1. Longest prefix match (most specific route)
+// 2. Lowest route metric
+// 3. Lowest interface metric
+func GetBestInterface(dest netip.Addr, vpnIntf string) (*net.Interface, error) {
+ var skipInterfaceIndex int
+ if vpnIntf != "" {
+ if iface, err := net.InterfaceByName(vpnIntf); err == nil {
+ skipInterfaceIndex = iface.Index
+ } else {
+ return nil, fmt.Errorf("get VPN interface %s: %w", vpnIntf, err)
+ }
+ }
+
+ table, err := getWindowsRoutingTable()
+ if err != nil {
+ return nil, fmt.Errorf("get routing table: %w", err)
+ }
+ defer freeWindowsRoutingTable(table)
+
+ candidates := parseCandidatesFromTable(table, dest, skipInterfaceIndex)
+
+ if len(candidates) == 0 {
+ return nil, fmt.Errorf("no route to %s", dest)
+ }
+
+ // Sort routes: prefix length -> route metric -> interface metric
+ sortRouteCandidates(candidates)
+
+ for _, candidate := range candidates {
+ iface, err := net.InterfaceByIndex(int(candidate.interfaceIndex))
+ if err != nil {
+ log.Warnf("failed to get interface by index %d: %v", candidate.interfaceIndex, err)
+ continue
+ }
+
+ if iface.Flags&net.FlagLoopback != 0 && !dest.IsLoopback() {
+ continue
+ }
+
+ if iface.Flags&net.FlagUp == 0 {
+ log.Debugf("interface %s is down, trying next route", iface.Name)
+ continue
+ }
+
+ log.Debugf("route lookup for %s: selected interface %s (index %d), route metric %d, interface metric %d",
+ dest, iface.Name, iface.Index, candidate.routeMetric, candidate.interfaceMetric)
+ return iface, nil
+ }
+
+ return nil, fmt.Errorf("no usable interface found for %s", dest)
+}
+
// formatRouteAge formats the route age in seconds to a human-readable string
func formatRouteAge(ageSeconds uint32) string {
if ageSeconds == 0 {
diff --git a/client/internal/routemanager/systemops/systemops_windows_test.go b/client/internal/routemanager/systemops/systemops_windows_test.go
index 523bd0b0d..3561adec4 100644
--- a/client/internal/routemanager/systemops/systemops_windows_test.go
+++ b/client/internal/routemanager/systemops/systemops_windows_test.go
@@ -15,7 +15,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
var (
diff --git a/client/internal/routemanager/util/ip.go b/client/internal/routemanager/util/ip.go
index ac5a48e37..57ea32f69 100644
--- a/client/internal/routemanager/util/ip.go
+++ b/client/internal/routemanager/util/ip.go
@@ -12,18 +12,8 @@ func GetPrefixFromIP(ip net.IP) (netip.Prefix, error) {
if !ok {
return netip.Prefix{}, fmt.Errorf("parse IP address: %s", ip)
}
+
addr = addr.Unmap()
-
- var prefixLength int
- switch {
- case addr.Is4():
- prefixLength = 32
- case addr.Is6():
- prefixLength = 128
- default:
- return netip.Prefix{}, fmt.Errorf("invalid IP address: %s", addr)
- }
-
- prefix := netip.PrefixFrom(addr, prefixLength)
+ prefix := netip.PrefixFrom(addr, addr.BitLen())
return prefix, nil
}
diff --git a/client/internal/stdnet/dialer.go b/client/internal/stdnet/dialer.go
index e80adb42b..8961eaa69 100644
--- a/client/internal/stdnet/dialer.go
+++ b/client/internal/stdnet/dialer.go
@@ -5,7 +5,7 @@ import (
"github.com/pion/transport/v3"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
// Dial connects to the address on the named network.
diff --git a/client/internal/stdnet/listener.go b/client/internal/stdnet/listener.go
index 9ce0a5556..d3be1896f 100644
--- a/client/internal/stdnet/listener.go
+++ b/client/internal/stdnet/listener.go
@@ -6,7 +6,7 @@ import (
"github.com/pion/transport/v3"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
// ListenPacket listens for incoming packets on the given network and address.
diff --git a/client/net/conn.go b/client/net/conn.go
new file mode 100644
index 000000000..918e7f628
--- /dev/null
+++ b/client/net/conn.go
@@ -0,0 +1,49 @@
+//go:build !ios
+
+package net
+
+import (
+ "io"
+ "net"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/net/hooks"
+)
+
+// Conn wraps a net.Conn to override the Close method
+type Conn struct {
+ net.Conn
+ ID hooks.ConnectionID
+}
+
+// Close overrides the net.Conn Close method to execute all registered hooks after closing the connection
+// Close overrides the net.Conn Close method to execute all registered hooks before closing the connection.
+func (c *Conn) Close() error {
+ return closeConn(c.ID, c.Conn)
+}
+
+// TCPConn wraps net.TCPConn to override its Close method to include hook functionality.
+type TCPConn struct {
+ *net.TCPConn
+ ID hooks.ConnectionID
+}
+
+// Close overrides the net.TCPConn Close method to execute all registered hooks before closing the connection.
+func (c *TCPConn) Close() error {
+ return closeConn(c.ID, c.TCPConn)
+}
+
+// closeConn is a helper function to close connections and execute close hooks.
+func closeConn(id hooks.ConnectionID, conn io.Closer) error {
+ err := conn.Close()
+
+ closeHooks := hooks.GetCloseHooks()
+ for _, hook := range closeHooks {
+ if err := hook(id); err != nil {
+ log.Errorf("Error executing close hook: %v", err)
+ }
+ }
+
+ return err
+}
diff --git a/client/net/dial.go b/client/net/dial.go
new file mode 100644
index 000000000..041a00e5d
--- /dev/null
+++ b/client/net/dial.go
@@ -0,0 +1,82 @@
+//go:build !ios
+
+package net
+
+import (
+ "fmt"
+ "net"
+ "sync"
+
+ "github.com/pion/transport/v3"
+ log "github.com/sirupsen/logrus"
+)
+
+func DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) {
+ if CustomRoutingDisabled() {
+ return net.DialUDP(network, laddr, raddr)
+ }
+
+ dialer := NewDialer()
+ dialer.LocalAddr = laddr
+
+ conn, err := dialer.Dial(network, raddr.String())
+ if err != nil {
+ return nil, fmt.Errorf("dialing UDP %s: %w", raddr.String(), err)
+ }
+
+ switch c := conn.(type) {
+ case *net.UDPConn:
+ // Advanced routing: plain connection
+ return c, nil
+ case *Conn:
+ // Legacy routing: wrapped connection preserves close hooks
+ udpConn, ok := c.Conn.(*net.UDPConn)
+ if !ok {
+ if err := conn.Close(); err != nil {
+ log.Errorf("Failed to close connection: %v", err)
+ }
+ return nil, fmt.Errorf("expected UDP connection, got %T", c.Conn)
+ }
+ return &UDPConn{UDPConn: udpConn, ID: c.ID, seenAddrs: &sync.Map{}}, nil
+ }
+
+ if err := conn.Close(); err != nil {
+ log.Errorf("failed to close connection: %v", err)
+ }
+ return nil, fmt.Errorf("unexpected connection type: %T", conn)
+}
+
+func DialTCP(network string, laddr, raddr *net.TCPAddr) (transport.TCPConn, error) {
+ if CustomRoutingDisabled() {
+ return net.DialTCP(network, laddr, raddr)
+ }
+
+ dialer := NewDialer()
+ dialer.LocalAddr = laddr
+
+ conn, err := dialer.Dial(network, raddr.String())
+ if err != nil {
+ return nil, fmt.Errorf("dialing TCP %s: %w", raddr.String(), err)
+ }
+
+ switch c := conn.(type) {
+ case *net.TCPConn:
+ // Advanced routing: plain connection
+ return c, nil
+ case *Conn:
+ // Legacy routing: wrapped connection preserves close hooks
+ tcpConn, ok := c.Conn.(*net.TCPConn)
+ if !ok {
+ if err := conn.Close(); err != nil {
+ log.Errorf("Failed to close connection: %v", err)
+ }
+ return nil, fmt.Errorf("expected TCP connection, got %T", c.Conn)
+ }
+ return &TCPConn{TCPConn: tcpConn, ID: c.ID}, nil
+ }
+
+ if err := conn.Close(); err != nil {
+ log.Errorf("failed to close connection: %v", err)
+ }
+ return nil, fmt.Errorf("unexpected connection type: %T", conn)
+}
diff --git a/util/net/dial_ios.go b/client/net/dial_ios.go
similarity index 100%
rename from util/net/dial_ios.go
rename to client/net/dial_ios.go
diff --git a/util/net/dialer.go b/client/net/dialer.go
similarity index 99%
rename from util/net/dialer.go
rename to client/net/dialer.go
index 0786c667e..29bec05a7 100644
--- a/util/net/dialer.go
+++ b/client/net/dialer.go
@@ -16,6 +16,5 @@ func NewDialer() *Dialer {
Dialer: &net.Dialer{},
}
dialer.init()
-
return dialer
}
diff --git a/client/net/dialer_dial.go b/client/net/dialer_dial.go
new file mode 100644
index 000000000..2e1eb53d8
--- /dev/null
+++ b/client/net/dialer_dial.go
@@ -0,0 +1,87 @@
+//go:build !ios
+
+package net
+
+import (
+ "context"
+ "fmt"
+ "net"
+
+ "github.com/hashicorp/go-multierror"
+ log "github.com/sirupsen/logrus"
+
+ nberrors "github.com/netbirdio/netbird/client/errors"
+ "github.com/netbirdio/netbird/client/internal/routemanager/util"
+ "github.com/netbirdio/netbird/client/net/hooks"
+)
+
+// DialContext wraps the net.Dialer's DialContext method to use the custom connection
+func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
+ log.Debugf("Dialing %s %s", network, address)
+
+ if CustomRoutingDisabled() || AdvancedRouting() {
+ return d.Dialer.DialContext(ctx, network, address)
+ }
+
+ connID := hooks.GenerateConnID()
+ if err := callDialerHooks(ctx, connID, address, d.Resolver); err != nil {
+ log.Errorf("Failed to call dialer hooks: %v", err)
+ }
+
+ conn, err := d.Dialer.DialContext(ctx, network, address)
+ if err != nil {
+ return nil, fmt.Errorf("d.Dialer.DialContext: %w", err)
+ }
+
+ // Wrap the connection in Conn to handle Close with hooks
+ return &Conn{Conn: conn, ID: connID}, nil
+}
+
+// Dial wraps the net.Dialer's Dial method to use the custom connection
+func (d *Dialer) Dial(network, address string) (net.Conn, error) {
+ return d.DialContext(context.Background(), network, address)
+}
+
+func callDialerHooks(ctx context.Context, connID hooks.ConnectionID, address string, customResolver *net.Resolver) error {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ writeHooks := hooks.GetWriteHooks()
+ if len(writeHooks) == 0 {
+ return nil
+ }
+
+ host, _, err := net.SplitHostPort(address)
+ if err != nil {
+ return fmt.Errorf("split host and port: %w", err)
+ }
+
+ resolver := customResolver
+ if resolver == nil {
+ resolver = net.DefaultResolver
+ }
+
+ ips, err := resolver.LookupIPAddr(ctx, host)
+ if err != nil {
+ return fmt.Errorf("failed to resolve address %s: %w", address, err)
+ }
+
+ log.Debugf("Dialer resolved IPs for %s: %v", address, ips)
+
+ var merr *multierror.Error
+ for _, ip := range ips {
+ prefix, err := util.GetPrefixFromIP(ip.IP)
+ if err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("convert IP %s to prefix: %w", ip.IP, err))
+ continue
+ }
+ for _, hook := range writeHooks {
+ if err := hook(connID, prefix); err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("executing dial hook for IP %s: %w", ip.IP, err))
+ }
+ }
+ }
+
+ return nberrors.FormatErrorOrNil(merr)
+}
diff --git a/util/net/dialer_init_android.go b/client/net/dialer_init_android.go
similarity index 100%
rename from util/net/dialer_init_android.go
rename to client/net/dialer_init_android.go
diff --git a/client/net/dialer_init_generic.go b/client/net/dialer_init_generic.go
new file mode 100644
index 000000000..18ebc6ad1
--- /dev/null
+++ b/client/net/dialer_init_generic.go
@@ -0,0 +1,7 @@
+//go:build !linux && !windows
+
+package net
+
+func (d *Dialer) init() {
+ // implemented on Linux, Android, and Windows only
+}
diff --git a/util/net/dialer_init_linux.go b/client/net/dialer_init_linux.go
similarity index 100%
rename from util/net/dialer_init_linux.go
rename to client/net/dialer_init_linux.go
diff --git a/client/net/dialer_init_windows.go b/client/net/dialer_init_windows.go
new file mode 100644
index 000000000..6eefe5b1e
--- /dev/null
+++ b/client/net/dialer_init_windows.go
@@ -0,0 +1,5 @@
+package net
+
+func (d *Dialer) init() {
+ d.Dialer.Control = applyUnicastIFToSocket
+}
diff --git a/util/net/env.go b/client/net/env.go
similarity index 94%
rename from util/net/env.go
rename to client/net/env.go
index 32425665d..8f326ca88 100644
--- a/util/net/env.go
+++ b/client/net/env.go
@@ -11,6 +11,7 @@ import (
const (
envDisableCustomRouting = "NB_DISABLE_CUSTOM_ROUTING"
+ envUseLegacyRouting = "NB_USE_LEGACY_ROUTING"
)
// CustomRoutingDisabled returns true if custom routing is disabled.
diff --git a/client/net/env_android.go b/client/net/env_android.go
new file mode 100644
index 000000000..9d89951a1
--- /dev/null
+++ b/client/net/env_android.go
@@ -0,0 +1,24 @@
+//go:build android
+
+package net
+
+// Init initializes the network environment for Android
+func Init() {
+ // No initialization needed on Android
+}
+
+// AdvancedRouting reports whether routing loops can be avoided without using exclusion routes.
+// Always returns true on Android since we cannot handle routes dynamically.
+func AdvancedRouting() bool {
+ return true
+}
+
+// SetVPNInterfaceName is a no-op on Android
+func SetVPNInterfaceName(name string) {
+ // No-op on Android - not needed for Android VPN service
+}
+
+// GetVPNInterfaceName returns empty string on Android
+func GetVPNInterfaceName() string {
+ return ""
+}
diff --git a/client/net/env_generic.go b/client/net/env_generic.go
new file mode 100644
index 000000000..f467930c3
--- /dev/null
+++ b/client/net/env_generic.go
@@ -0,0 +1,23 @@
+//go:build !linux && !windows && !android
+
+package net
+
+// Init initializes the network environment (no-op on non-Linux/Windows platforms)
+func Init() {
+ // No-op on non-Linux/Windows platforms
+}
+
+// AdvancedRouting returns false on non-Linux/Windows platforms
+func AdvancedRouting() bool {
+ return false
+}
+
+// SetVPNInterfaceName is a no-op on non-Windows platforms
+func SetVPNInterfaceName(name string) {
+ // No-op on non-Windows platforms
+}
+
+// GetVPNInterfaceName returns empty string on non-Windows platforms
+func GetVPNInterfaceName() string {
+ return ""
+}
diff --git a/util/net/env_linux.go b/client/net/env_linux.go
similarity index 86%
rename from util/net/env_linux.go
rename to client/net/env_linux.go
index 3159f6462..82d9a74a8 100644
--- a/util/net/env_linux.go
+++ b/client/net/env_linux.go
@@ -17,8 +17,7 @@ import (
const (
// these have the same effect, skip socket env supported for backward compatibility
- envSkipSocketMark = "NB_SKIP_SOCKET_MARK"
- envUseLegacyRouting = "NB_USE_LEGACY_ROUTING"
+ envSkipSocketMark = "NB_SKIP_SOCKET_MARK"
)
var advancedRoutingSupported bool
@@ -27,6 +26,7 @@ func Init() {
advancedRoutingSupported = checkAdvancedRoutingSupport()
}
+// AdvancedRouting reports whether routing loops can be avoided without using exclusion routes
func AdvancedRouting() bool {
return advancedRoutingSupported
}
@@ -73,7 +73,7 @@ func checkAdvancedRoutingSupport() bool {
}
func CheckFwmarkSupport() bool {
- // temporarily enable advanced routing to check fwmarks are supported
+ // temporarily enable advanced routing to check if fwmarks are supported
old := advancedRoutingSupported
advancedRoutingSupported = true
defer func() {
@@ -129,3 +129,13 @@ func CheckRuleOperationsSupport() bool {
}
return true
}
+
+// SetVPNInterfaceName is a no-op on Linux
+func SetVPNInterfaceName(name string) {
+ // No-op on Linux - not needed for fwmark-based routing
+}
+
+// GetVPNInterfaceName returns empty string on Linux
+func GetVPNInterfaceName() string {
+ return ""
+}
diff --git a/client/net/env_windows.go b/client/net/env_windows.go
new file mode 100644
index 000000000..7e8868ba5
--- /dev/null
+++ b/client/net/env_windows.go
@@ -0,0 +1,67 @@
+//go:build windows
+
+package net
+
+import (
+ "os"
+ "strconv"
+ "sync"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/iface/netstack"
+)
+
+var (
+ vpnInterfaceName string
+ vpnInitMutex sync.RWMutex
+
+ advancedRoutingSupported bool
+)
+
+func Init() {
+ advancedRoutingSupported = checkAdvancedRoutingSupport()
+}
+
+func checkAdvancedRoutingSupport() bool {
+ var err error
+ var legacyRouting bool
+ if val := os.Getenv(envUseLegacyRouting); val != "" {
+ legacyRouting, err = strconv.ParseBool(val)
+ if err != nil {
+ log.Warnf("failed to parse %s: %v", envUseLegacyRouting, err)
+ }
+ }
+
+ if legacyRouting || netstack.IsEnabled() {
+ log.Info("advanced routing has been requested to be disabled")
+ return false
+ }
+
+ log.Info("system supports advanced routing")
+
+ return true
+}
+
+// AdvancedRouting reports whether routing loops can be avoided without using exclusion routes
+func AdvancedRouting() bool {
+ return advancedRoutingSupported
+}
+
+// GetVPNInterfaceName returns the stored VPN interface name
+func GetVPNInterfaceName() string {
+ vpnInitMutex.RLock()
+ defer vpnInitMutex.RUnlock()
+ return vpnInterfaceName
+}
+
+// SetVPNInterfaceName sets the VPN interface name for lazy initialization
+func SetVPNInterfaceName(name string) {
+ vpnInitMutex.Lock()
+ defer vpnInitMutex.Unlock()
+ vpnInterfaceName = name
+
+ if name != "" {
+ log.Infof("VPN interface name set to %s for route exclusion", name)
+ }
+}
diff --git a/client/net/hooks/hooks.go b/client/net/hooks/hooks.go
new file mode 100644
index 000000000..93d8e18ef
--- /dev/null
+++ b/client/net/hooks/hooks.go
@@ -0,0 +1,93 @@
+package hooks
+
+import (
+ "net/netip"
+ "slices"
+ "sync"
+
+ "github.com/google/uuid"
+)
+
+// ConnectionID provides a globally unique identifier for network connections.
+// It's used to track connections throughout their lifecycle so the close hook can correlate with the dial hook.
+type ConnectionID string
+
+// GenerateConnID generates a unique identifier for each connection.
+func GenerateConnID() ConnectionID {
+ return ConnectionID(uuid.NewString())
+}
+
+type WriteHookFunc func(connID ConnectionID, prefix netip.Prefix) error
+type CloseHookFunc func(connID ConnectionID) error
+type AddressRemoveHookFunc func(connID ConnectionID, prefix netip.Prefix) error
+
+var (
+ hooksMutex sync.RWMutex
+
+ writeHooks []WriteHookFunc
+ closeHooks []CloseHookFunc
+ addressRemoveHooks []AddressRemoveHookFunc
+)
+
+// AddWriteHook allows adding a new hook to be executed before writing/dialing.
+func AddWriteHook(hook WriteHookFunc) {
+ hooksMutex.Lock()
+ defer hooksMutex.Unlock()
+ writeHooks = append(writeHooks, hook)
+}
+
+// AddCloseHook allows adding a new hook to be executed on connection close.
+func AddCloseHook(hook CloseHookFunc) {
+ hooksMutex.Lock()
+ defer hooksMutex.Unlock()
+ closeHooks = append(closeHooks, hook)
+}
+
+// RemoveWriteHooks removes all write hooks.
+func RemoveWriteHooks() {
+ hooksMutex.Lock()
+ defer hooksMutex.Unlock()
+ writeHooks = nil
+}
+
+// RemoveCloseHooks removes all close hooks.
+func RemoveCloseHooks() {
+ hooksMutex.Lock()
+ defer hooksMutex.Unlock()
+ closeHooks = nil
+}
+
+// AddAddressRemoveHook allows adding a new hook to be executed when an address is removed.
+func AddAddressRemoveHook(hook AddressRemoveHookFunc) {
+ hooksMutex.Lock()
+ defer hooksMutex.Unlock()
+ addressRemoveHooks = append(addressRemoveHooks, hook)
+}
+
+// RemoveAddressRemoveHooks removes all listener address hooks.
+func RemoveAddressRemoveHooks() {
+ hooksMutex.Lock()
+ defer hooksMutex.Unlock()
+ addressRemoveHooks = nil
+}
+
+// GetWriteHooks returns a copy of the current write hooks.
+func GetWriteHooks() []WriteHookFunc {
+ hooksMutex.RLock()
+ defer hooksMutex.RUnlock()
+ return slices.Clone(writeHooks)
+}
+
+// GetCloseHooks returns a copy of the current close hooks.
+func GetCloseHooks() []CloseHookFunc {
+ hooksMutex.RLock()
+ defer hooksMutex.RUnlock()
+ return slices.Clone(closeHooks)
+}
+
+// GetAddressRemoveHooks returns a copy of the current listener address remove hooks.
+func GetAddressRemoveHooks() []AddressRemoveHookFunc {
+ hooksMutex.RLock()
+ defer hooksMutex.RUnlock()
+ return slices.Clone(addressRemoveHooks)
+}
diff --git a/client/net/listen.go b/client/net/listen.go
new file mode 100644
index 000000000..da7262806
--- /dev/null
+++ b/client/net/listen.go
@@ -0,0 +1,47 @@
+//go:build !ios
+
+package net
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "sync"
+
+ "github.com/pion/transport/v3"
+ log "github.com/sirupsen/logrus"
+)
+
+// ListenUDP listens on the network address and returns a transport.UDPConn
+// which includes support for write and close hooks.
+func ListenUDP(network string, laddr *net.UDPAddr) (transport.UDPConn, error) {
+ if CustomRoutingDisabled() {
+ return net.ListenUDP(network, laddr)
+ }
+
+ conn, err := NewListener().ListenPacket(context.Background(), network, laddr.String())
+ if err != nil {
+ return nil, fmt.Errorf("listen UDP: %w", err)
+ }
+
+ switch c := conn.(type) {
+ case *net.UDPConn:
+ // Advanced routing: plain connection
+ return c, nil
+ case *PacketConn:
+ // Legacy routing: wrapped connection for hooks
+ udpConn, ok := c.PacketConn.(*net.UDPConn)
+ if !ok {
+ if err := c.Close(); err != nil {
+ log.Errorf("Failed to close connection: %v", err)
+ }
+ return nil, fmt.Errorf("expected UDPConn, got %T", c.PacketConn)
+ }
+ return &UDPConn{UDPConn: udpConn, ID: c.ID, seenAddrs: &sync.Map{}}, nil
+ }
+
+ if err := conn.Close(); err != nil {
+ log.Errorf("failed to close connection: %v", err)
+ }
+ return nil, fmt.Errorf("unexpected connection type: %T", conn)
+}
diff --git a/util/net/listen_ios.go b/client/net/listen_ios.go
similarity index 100%
rename from util/net/listen_ios.go
rename to client/net/listen_ios.go
diff --git a/util/net/listener.go b/client/net/listener.go
similarity index 81%
rename from util/net/listener.go
rename to client/net/listener.go
index f4d769f58..4c2f53c05 100644
--- a/util/net/listener.go
+++ b/client/net/listener.go
@@ -7,14 +7,12 @@ import (
// ListenerConfig extends the standard net.ListenConfig with the ability to execute hooks before
// responding via the socket and after closing. This can be used to bypass the VPN for listeners.
type ListenerConfig struct {
- *net.ListenConfig
+ net.ListenConfig
}
// NewListener creates a new ListenerConfig instance.
func NewListener() *ListenerConfig {
- listener := &ListenerConfig{
- ListenConfig: &net.ListenConfig{},
- }
+ listener := &ListenerConfig{}
listener.init()
return listener
diff --git a/util/net/listener_init_android.go b/client/net/listener_init_android.go
similarity index 100%
rename from util/net/listener_init_android.go
rename to client/net/listener_init_android.go
diff --git a/client/net/listener_init_generic.go b/client/net/listener_init_generic.go
new file mode 100644
index 000000000..4f8f17ab2
--- /dev/null
+++ b/client/net/listener_init_generic.go
@@ -0,0 +1,7 @@
+//go:build !linux && !windows
+
+package net
+
+func (l *ListenerConfig) init() {
+ // implemented on Linux, Android, and Windows only
+}
diff --git a/util/net/listener_init_linux.go b/client/net/listener_init_linux.go
similarity index 100%
rename from util/net/listener_init_linux.go
rename to client/net/listener_init_linux.go
diff --git a/client/net/listener_init_windows.go b/client/net/listener_init_windows.go
new file mode 100644
index 000000000..a9399b5f1
--- /dev/null
+++ b/client/net/listener_init_windows.go
@@ -0,0 +1,8 @@
+package net
+
+func (l *ListenerConfig) init() {
+ // TODO: this will select a single source interface, but for UDP we can have various source interfaces and IP addresses.
+ // For now we stick to the one that matches the request IP address, which can be the unspecified IP. In this case
+ // the interface will be selected that serves the default route.
+ l.ListenConfig.Control = applyUnicastIFToSocket
+}
diff --git a/client/net/listener_listen.go b/client/net/listener_listen.go
new file mode 100644
index 000000000..0bb5ad67d
--- /dev/null
+++ b/client/net/listener_listen.go
@@ -0,0 +1,153 @@
+//go:build !ios
+
+package net
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/netip"
+ "sync"
+
+ "github.com/hashicorp/go-multierror"
+ log "github.com/sirupsen/logrus"
+
+ nberrors "github.com/netbirdio/netbird/client/errors"
+ "github.com/netbirdio/netbird/client/internal/routemanager/util"
+ "github.com/netbirdio/netbird/client/net/hooks"
+)
+
+// ListenPacket listens on the network address and returns a PacketConn
+// which includes support for write hooks.
+func (l *ListenerConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
+ if CustomRoutingDisabled() || AdvancedRouting() {
+ return l.ListenConfig.ListenPacket(ctx, network, address)
+ }
+
+ pc, err := l.ListenConfig.ListenPacket(ctx, network, address)
+ if err != nil {
+ return nil, fmt.Errorf("listen packet: %w", err)
+ }
+ connID := hooks.GenerateConnID()
+
+ return &PacketConn{PacketConn: pc, ID: connID, seenAddrs: &sync.Map{}}, nil
+}
+
+// PacketConn wraps net.PacketConn to override its WriteTo and Close methods to include hook functionality.
+type PacketConn struct {
+ net.PacketConn
+ ID hooks.ConnectionID
+ seenAddrs *sync.Map
+}
+
+// WriteTo writes a packet with payload b to addr, executing registered write hooks beforehand.
+func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
+ if err := callWriteHooks(c.ID, c.seenAddrs, addr); err != nil {
+ log.Errorf("Failed to call write hooks: %v", err)
+ }
+ return c.PacketConn.WriteTo(b, addr)
+}
+
+// Close overrides the net.PacketConn Close method to execute all registered hooks before closing the connection.
+func (c *PacketConn) Close() error {
+ defer c.seenAddrs.Clear()
+ return closeConn(c.ID, c.PacketConn)
+}
+
+// UDPConn wraps net.UDPConn to override its WriteTo and Close methods to include hook functionality.
+type UDPConn struct {
+ *net.UDPConn
+ ID hooks.ConnectionID
+ seenAddrs *sync.Map
+}
+
+// WriteTo writes a packet with payload b to addr, executing registered write hooks beforehand.
+func (c *UDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
+ if err := callWriteHooks(c.ID, c.seenAddrs, addr); err != nil {
+ log.Errorf("Failed to call write hooks: %v", err)
+ }
+ return c.UDPConn.WriteTo(b, addr)
+}
+
+// Close overrides the net.UDPConn Close method to execute all registered hooks before closing the connection.
+func (c *UDPConn) Close() error {
+ defer c.seenAddrs.Clear()
+ return closeConn(c.ID, c.UDPConn)
+}
+
+// RemoveAddress removes an address from the seen cache and triggers removal hooks.
+func (c *PacketConn) RemoveAddress(addr string) {
+ if _, exists := c.seenAddrs.LoadAndDelete(addr); !exists {
+ return
+ }
+
+ ipStr, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ log.Errorf("Error splitting IP address and port: %v", err)
+ return
+ }
+
+ ipAddr, err := netip.ParseAddr(ipStr)
+ if err != nil {
+ log.Errorf("Error parsing IP address %s: %v", ipStr, err)
+ return
+ }
+
+ prefix := netip.PrefixFrom(ipAddr.Unmap(), ipAddr.BitLen())
+
+ addressRemoveHooks := hooks.GetAddressRemoveHooks()
+ if len(addressRemoveHooks) == 0 {
+ return
+ }
+
+ for _, hook := range addressRemoveHooks {
+ if err := hook(c.ID, prefix); err != nil {
+ log.Errorf("Error executing listener address remove hook: %v", err)
+ }
+ }
+}
+
+// WrapPacketConn wraps an existing net.PacketConn with nbnet hook functionality
+func WrapPacketConn(conn net.PacketConn) net.PacketConn {
+ if AdvancedRouting() {
+ // hooks not required for advanced routing
+ return conn
+ }
+ return &PacketConn{
+ PacketConn: conn,
+ ID: hooks.GenerateConnID(),
+ seenAddrs: &sync.Map{},
+ }
+}
+
+func callWriteHooks(id hooks.ConnectionID, seenAddrs *sync.Map, addr net.Addr) error {
+ if _, loaded := seenAddrs.LoadOrStore(addr.String(), true); loaded {
+ return nil
+ }
+
+ writeHooks := hooks.GetWriteHooks()
+ if len(writeHooks) == 0 {
+ return nil
+ }
+
+ udpAddr, ok := addr.(*net.UDPAddr)
+ if !ok {
+ return fmt.Errorf("expected *net.UDPAddr for packet connection, got %T", addr)
+ }
+
+ prefix, err := util.GetPrefixFromIP(udpAddr.IP)
+ if err != nil {
+ return fmt.Errorf("convert UDP IP %s to prefix: %w", udpAddr.IP, err)
+ }
+
+ log.Debugf("Listener resolved IP for %s: %s", addr, prefix)
+
+ var merr *multierror.Error
+ for _, hook := range writeHooks {
+ if err := hook(id, prefix); err != nil {
+ merr = multierror.Append(merr, fmt.Errorf("execute write hook: %w", err))
+ }
+ }
+
+ return nberrors.FormatErrorOrNil(merr)
+}
diff --git a/util/net/listener_listen_ios.go b/client/net/listener_listen_ios.go
similarity index 100%
rename from util/net/listener_listen_ios.go
rename to client/net/listener_listen_ios.go
diff --git a/util/net/net.go b/client/net/net.go
similarity index 81%
rename from util/net/net.go
rename to client/net/net.go
index fdcf4ee6a..a97de9d59 100644
--- a/util/net/net.go
+++ b/client/net/net.go
@@ -5,8 +5,6 @@ import (
"math/big"
"net"
"net/netip"
-
- "github.com/google/uuid"
)
const (
@@ -44,18 +42,6 @@ func IsDataPlaneMark(fwmark uint32) bool {
return fwmark >= DataPlaneMarkLower && fwmark <= DataPlaneMarkUpper
}
-// ConnectionID provides a globally unique identifier for network connections.
-// It's used to track connections throughout their lifecycle so the close hook can correlate with the dial hook.
-type ConnectionID string
-
-type AddHookFunc func(connID ConnectionID, IP net.IP) error
-type RemoveHookFunc func(connID ConnectionID) error
-
-// GenerateConnID generates a unique identifier for each connection.
-func GenerateConnID() ConnectionID {
- return ConnectionID(uuid.NewString())
-}
-
func GetLastIPFromNetwork(network netip.Prefix, fromEnd int) (netip.Addr, error) {
var endIP net.IP
addr := network.Addr().AsSlice()
diff --git a/util/net/net_linux.go b/client/net/net_linux.go
similarity index 100%
rename from util/net/net_linux.go
rename to client/net/net_linux.go
diff --git a/util/net/net_test.go b/client/net/net_test.go
similarity index 100%
rename from util/net/net_test.go
rename to client/net/net_test.go
diff --git a/client/net/net_windows.go b/client/net/net_windows.go
new file mode 100644
index 000000000..649d83aaf
--- /dev/null
+++ b/client/net/net_windows.go
@@ -0,0 +1,284 @@
+package net
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "net/netip"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+ "unsafe"
+
+ log "github.com/sirupsen/logrus"
+ "golang.org/x/sys/windows"
+)
+
+const (
+ // https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
+ IpUnicastIf = 31
+ Ipv6UnicastIf = 31
+
+ // https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-ipv6-socket-options
+ Ipv6V6only = 27
+)
+
+// GetBestInterfaceFunc is set at runtime to avoid import cycle
+var GetBestInterfaceFunc func(dest netip.Addr, vpnIntf string) (*net.Interface, error)
+
+// nativeToBigEndian converts a uint32 from native byte order to big-endian
+func nativeToBigEndian(v uint32) uint32 {
+ return (v&0xff)<<24 | (v&0xff00)<<8 | (v&0xff0000)>>8 | (v&0xff000000)>>24
+}
+
+// parseDestinationAddress parses the destination address from various formats
+func parseDestinationAddress(network, address string) (netip.Addr, error) {
+ if address == "" {
+ if strings.HasSuffix(network, "6") {
+ return netip.IPv6Unspecified(), nil
+ }
+ return netip.IPv4Unspecified(), nil
+ }
+
+ if addrPort, err := netip.ParseAddrPort(address); err == nil {
+ return addrPort.Addr(), nil
+ }
+
+ if dest, err := netip.ParseAddr(address); err == nil {
+ return dest, nil
+ }
+
+ host, _, err := net.SplitHostPort(address)
+ if err != nil {
+ // No port, treat whole string as host
+ host = address
+ }
+
+ if host == "" {
+ if strings.HasSuffix(network, "6") {
+ return netip.IPv6Unspecified(), nil
+ }
+ return netip.IPv4Unspecified(), nil
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
+ if err != nil || len(ips) == 0 {
+ return netip.Addr{}, fmt.Errorf("resolve destination %s: %w", host, err)
+ }
+
+ dest, ok := netip.AddrFromSlice(ips[0].IP)
+ if !ok {
+ return netip.Addr{}, fmt.Errorf("convert IP %v to netip.Addr", ips[0].IP)
+ }
+
+ if ips[0].Zone != "" {
+ dest = dest.WithZone(ips[0].Zone)
+ }
+
+ return dest, nil
+}
+
+func getInterfaceFromZone(zone string) *net.Interface {
+ if zone == "" {
+ return nil
+ }
+
+ idx, err := strconv.Atoi(zone)
+ if err != nil {
+ log.Debugf("invalid zone format for Windows (expected numeric): %s", zone)
+ return nil
+ }
+
+ iface, err := net.InterfaceByIndex(idx)
+ if err != nil {
+ log.Debugf("failed to get interface by index %d from zone: %v", idx, err)
+ return nil
+ }
+
+ return iface
+}
+
+type interfaceSelection struct {
+ iface4 *net.Interface
+ iface6 *net.Interface
+}
+
+func selectInterfaceForZone(dest netip.Addr, zone string) *interfaceSelection {
+ iface := getInterfaceFromZone(zone)
+ if iface == nil {
+ return nil
+ }
+
+ if dest.Is6() {
+ return &interfaceSelection{iface6: iface}
+ }
+ return &interfaceSelection{iface4: iface}
+}
+
+func selectInterfaceForUnspecified() (*interfaceSelection, error) {
+ if GetBestInterfaceFunc == nil {
+ return nil, errors.New("GetBestInterfaceFunc not initialized")
+ }
+
+ var result interfaceSelection
+ vpnIfaceName := GetVPNInterfaceName()
+
+ if iface4, err := GetBestInterfaceFunc(netip.IPv4Unspecified(), vpnIfaceName); err == nil {
+ result.iface4 = iface4
+ } else {
+ log.Debugf("No IPv4 default route found: %v", err)
+ }
+
+ if iface6, err := GetBestInterfaceFunc(netip.IPv6Unspecified(), vpnIfaceName); err == nil {
+ result.iface6 = iface6
+ } else {
+ log.Debugf("No IPv6 default route found: %v", err)
+ }
+
+ if result.iface4 == nil && result.iface6 == nil {
+ return nil, errors.New("no default routes found")
+ }
+
+ return &result, nil
+}
+
+func selectInterface(dest netip.Addr) (*interfaceSelection, error) {
+ if zone := dest.Zone(); zone != "" {
+ if selection := selectInterfaceForZone(dest, zone); selection != nil {
+ return selection, nil
+ }
+ }
+
+ if dest.IsUnspecified() {
+ return selectInterfaceForUnspecified()
+ }
+
+ if GetBestInterfaceFunc == nil {
+ return nil, errors.New("GetBestInterfaceFunc not initialized")
+ }
+
+ iface, err := GetBestInterfaceFunc(dest, GetVPNInterfaceName())
+ if err != nil {
+ return nil, fmt.Errorf("find route for %s: %w", dest, err)
+ }
+
+ if dest.Is6() {
+ return &interfaceSelection{iface6: iface}, nil
+ }
+ return &interfaceSelection{iface4: iface}, nil
+}
+
+func setIPv4UnicastIF(fd uintptr, iface *net.Interface) error {
+ ifaceIndexBE := nativeToBigEndian(uint32(iface.Index))
+ if err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IpUnicastIf, int(ifaceIndexBE)); err != nil {
+ return fmt.Errorf("set IP_UNICAST_IF: %w (interface: %s, index: %d)", err, iface.Name, iface.Index)
+ }
+ return nil
+}
+
+func setIPv6UnicastIF(fd uintptr, iface *net.Interface) error {
+ if err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, Ipv6UnicastIf, iface.Index); err != nil {
+ return fmt.Errorf("set IPV6_UNICAST_IF: %w (interface: %s, index: %d)", err, iface.Name, iface.Index)
+ }
+ return nil
+}
+
+func setUnicastIf(fd uintptr, network string, selection *interfaceSelection, address string) error {
+ // The Go runtime always passes specific network types to Control (udp4, udp6, tcp4, tcp6, etc.)
+ // Never generic ones (udp, tcp, ip)
+
+ switch {
+ case strings.HasSuffix(network, "4"):
+ // IPv4-only socket (udp4, tcp4, ip4)
+ return setUnicastIfIPv4(fd, network, selection, address)
+
+ case strings.HasSuffix(network, "6"):
+ // IPv6 socket (udp6, tcp6, ip6) - could be dual-stack or IPv6-only
+ return setUnicastIfIPv6(fd, network, selection, address)
+ }
+
+ // Shouldn't reach here based on Go's documented behavior
+ return fmt.Errorf("unexpected network type: %s", network)
+}
+
+func setUnicastIfIPv4(fd uintptr, network string, selection *interfaceSelection, address string) error {
+ if selection.iface4 == nil {
+ return nil
+ }
+
+ if err := setIPv4UnicastIF(fd, selection.iface4); err != nil {
+ return err
+ }
+
+ log.Debugf("Set IP_UNICAST_IF=%d on %s for %s to %s", selection.iface4.Index, selection.iface4.Name, network, address)
+ return nil
+}
+
+func setUnicastIfIPv6(fd uintptr, network string, selection *interfaceSelection, address string) error {
+ isDualStack := checkDualStack(fd)
+
+ // For dual-stack sockets, also set the IPv4 option
+ if isDualStack && selection.iface4 != nil {
+ if err := setIPv4UnicastIF(fd, selection.iface4); err != nil {
+ return err
+ }
+ log.Debugf("Set IP_UNICAST_IF=%d on %s for %s to %s (dual-stack)", selection.iface4.Index, selection.iface4.Name, network, address)
+ }
+
+ if selection.iface6 == nil {
+ return nil
+ }
+
+ if err := setIPv6UnicastIF(fd, selection.iface6); err != nil {
+ return err
+ }
+
+ log.Debugf("Set IPV6_UNICAST_IF=%d on %s for %s to %s", selection.iface6.Index, selection.iface6.Name, network, address)
+ return nil
+}
+
+func checkDualStack(fd uintptr) bool {
+ var v6Only int
+ v6OnlyLen := int32(unsafe.Sizeof(v6Only))
+ err := windows.Getsockopt(windows.Handle(fd), windows.IPPROTO_IPV6, Ipv6V6only, (*byte)(unsafe.Pointer(&v6Only)), &v6OnlyLen)
+ return err == nil && v6Only == 0
+}
+
+// applyUnicastIFToSocket applies IpUnicastIf to a socket based on the destination address
+func applyUnicastIFToSocket(network string, address string, c syscall.RawConn) error {
+ if !AdvancedRouting() {
+ return nil
+ }
+
+ dest, err := parseDestinationAddress(network, address)
+ if err != nil {
+ return err
+ }
+
+ dest = dest.Unmap()
+
+ if !dest.IsValid() {
+ return fmt.Errorf("invalid destination address for %s", address)
+ }
+
+ selection, err := selectInterface(dest)
+ if err != nil {
+ return err
+ }
+
+ var controlErr error
+ err = c.Control(func(fd uintptr) {
+ controlErr = setUnicastIf(fd, network, selection, address)
+ })
+
+ if err != nil {
+ return fmt.Errorf("control: %w", err)
+ }
+
+ return controlErr
+}
diff --git a/util/net/protectsocket_android.go b/client/net/protectsocket_android.go
similarity index 100%
rename from util/net/protectsocket_android.go
rename to client/net/protectsocket_android.go
diff --git a/client/netbird-entrypoint.sh b/client/netbird-entrypoint.sh
index 2422d2683..7c9fa021a 100755
--- a/client/netbird-entrypoint.sh
+++ b/client/netbird-entrypoint.sh
@@ -2,7 +2,7 @@
set -eEuo pipefail
: ${NB_ENTRYPOINT_SERVICE_TIMEOUT:="5"}
-: ${NB_ENTRYPOINT_LOGIN_TIMEOUT:="1"}
+: ${NB_ENTRYPOINT_LOGIN_TIMEOUT:="5"}
NETBIRD_BIN="${NETBIRD_BIN:-"netbird"}"
export NB_LOG_FILE="${NB_LOG_FILE:-"console,/var/log/netbird/client.log"}"
service_pids=()
@@ -39,7 +39,7 @@ wait_for_message() {
info "not waiting for log line ${message@Q} due to zero timeout."
elif test -n "${log_file_path}"; then
info "waiting for log line ${message@Q} for ${timeout} seconds..."
- grep -q "${message}" <(timeout "${timeout}" tail -F "${log_file_path}" 2>/dev/null)
+ grep -E -q "${message}" <(timeout "${timeout}" tail -F "${log_file_path}" 2>/dev/null)
else
info "log file unsupported, sleeping for ${timeout} seconds..."
sleep "${timeout}"
@@ -81,7 +81,7 @@ wait_for_daemon_startup() {
login_if_needed() {
local timeout="${1}"
- if test -n "${log_file_path}" && wait_for_message "${timeout}" 'peer has been successfully registered'; then
+ if test -n "${log_file_path}" && wait_for_message "${timeout}" 'peer has been successfully registered|management connection state READY'; then
info "already logged in, skipping 'netbird up'..."
else
info "logging in..."
diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
index c633afc83..841e3c0f7 100644
--- a/client/proto/daemon.pb.go
+++ b/client/proto/daemon.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
-// protoc v5.29.3
+// protoc v6.32.1
// source: daemon.proto
package proto
@@ -794,8 +794,10 @@ type StatusRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
GetFullPeerStatus bool `protobuf:"varint,1,opt,name=getFullPeerStatus,proto3" json:"getFullPeerStatus,omitempty"`
ShouldRunProbes bool `protobuf:"varint,2,opt,name=shouldRunProbes,proto3" json:"shouldRunProbes,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ // the UI do not using this yet, but CLIs could use it to wait until the status is ready
+ WaitForReady *bool `protobuf:"varint,3,opt,name=waitForReady,proto3,oneof" json:"waitForReady,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *StatusRequest) Reset() {
@@ -842,6 +844,13 @@ func (x *StatusRequest) GetShouldRunProbes() bool {
return false
}
+func (x *StatusRequest) GetWaitForReady() bool {
+ if x != nil && x.WaitForReady != nil {
+ return *x.WaitForReady
+ }
+ return false
+}
+
type StatusResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// status of the server.
@@ -4673,10 +4682,12 @@ const file_daemon_proto_rawDesc = "" +
"\f_profileNameB\v\n" +
"\t_username\"\f\n" +
"\n" +
- "UpResponse\"g\n" +
+ "UpResponse\"\xa1\x01\n" +
"\rStatusRequest\x12,\n" +
"\x11getFullPeerStatus\x18\x01 \x01(\bR\x11getFullPeerStatus\x12(\n" +
- "\x0fshouldRunProbes\x18\x02 \x01(\bR\x0fshouldRunProbes\"\x82\x01\n" +
+ "\x0fshouldRunProbes\x18\x02 \x01(\bR\x0fshouldRunProbes\x12'\n" +
+ "\fwaitForReady\x18\x03 \x01(\bH\x00R\fwaitForReady\x88\x01\x01B\x0f\n" +
+ "\r_waitForReady\"\x82\x01\n" +
"\x0eStatusResponse\x12\x16\n" +
"\x06status\x18\x01 \x01(\tR\x06status\x122\n" +
"\n" +
@@ -5231,6 +5242,7 @@ func file_daemon_proto_init() {
}
file_daemon_proto_msgTypes[1].OneofWrappers = []any{}
file_daemon_proto_msgTypes[5].OneofWrappers = []any{}
+ file_daemon_proto_msgTypes[7].OneofWrappers = []any{}
file_daemon_proto_msgTypes[26].OneofWrappers = []any{
(*PortInfo_Port)(nil),
(*PortInfo_Range_)(nil),
diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto
index 0cd3579b9..5b27b4d98 100644
--- a/client/proto/daemon.proto
+++ b/client/proto/daemon.proto
@@ -186,6 +186,8 @@ message UpResponse {}
message StatusRequest{
bool getFullPeerStatus = 1;
bool shouldRunProbes = 2;
+ // the UI do not using this yet, but CLIs could use it to wait until the status is ready
+ optional bool waitForReady = 3;
}
message StatusResponse{
diff --git a/client/server/server.go b/client/server/server.go
index fae342f78..e6de608c5 100644
--- a/client/server/server.go
+++ b/client/server/server.go
@@ -67,6 +67,7 @@ type Server struct {
proto.UnimplementedDaemonServiceServer
clientRunning bool // protected by mutex
clientRunningChan chan struct{}
+ clientGiveUpChan chan struct{}
connectClient *internal.ConnectClient
@@ -106,6 +107,10 @@ func (s *Server) Start() error {
s.mutex.Lock()
defer s.mutex.Unlock()
+ if s.clientRunning {
+ return nil
+ }
+
state := internal.CtxGetState(s.rootCtx)
if err := handlePanicLog(); err != nil {
@@ -175,12 +180,10 @@ func (s *Server) Start() error {
return nil
}
- if s.clientRunning {
- return nil
- }
s.clientRunning = true
- s.clientRunningChan = make(chan struct{}, 1)
- go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.clientRunningChan)
+ s.clientRunningChan = make(chan struct{})
+ s.clientGiveUpChan = make(chan struct{})
+ go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan)
return nil
}
@@ -211,7 +214,7 @@ func (s *Server) setDefaultConfigIfNotExists(ctx context.Context) error {
// connectWithRetryRuns runs the client connection with a backoff strategy where we retry the operation as additional
// mechanism to keep the client connected even when the connection is lost.
// we cancel retry if the client receive a stop or down command, or if disable auto connect is configured.
-func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profilemanager.Config, statusRecorder *peer.Status, runningChan chan struct{}) {
+func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profilemanager.Config, statusRecorder *peer.Status, runningChan chan struct{}, giveUpChan chan struct{}) {
defer func() {
s.mutex.Lock()
s.clientRunning = false
@@ -261,6 +264,10 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, profileConfig *profil
if err := backoff.Retry(runOperation, backOff); err != nil {
log.Errorf("operation failed: %v", err)
}
+
+ if giveUpChan != nil {
+ close(giveUpChan)
+ }
}
// loginAttempt attempts to login using the provided information. it returns a status in case something fails
@@ -379,7 +386,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
if s.actCancel != nil {
s.actCancel()
}
- ctx, cancel := context.WithCancel(s.rootCtx)
+ ctx, cancel := context.WithCancel(callerCtx)
md, ok := metadata.FromIncomingContext(callerCtx)
if ok {
@@ -389,11 +396,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
s.actCancel = cancel
s.mutex.Unlock()
- if err := restoreResidualState(ctx, s.profileManager.GetStatePath()); err != nil {
+ if err := restoreResidualState(s.rootCtx, s.profileManager.GetStatePath()); err != nil {
log.Warnf(errRestoreResidualState, err)
}
- state := internal.CtxGetState(ctx)
+ state := internal.CtxGetState(s.rootCtx)
defer func() {
status, err := state.Status()
if err != nil || (status != internal.StatusNeedsLogin && status != internal.StatusLoginFailed) {
@@ -606,6 +613,20 @@ func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLogin
// Up starts engine work in the daemon.
func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpResponse, error) {
s.mutex.Lock()
+ if s.clientRunning {
+ state := internal.CtxGetState(s.rootCtx)
+ status, err := state.Status()
+ if err != nil {
+ s.mutex.Unlock()
+ return nil, err
+ }
+ if status == internal.StatusNeedsLogin {
+ s.actCancel()
+ }
+ s.mutex.Unlock()
+
+ return s.waitForUp(callerCtx)
+ }
defer s.mutex.Unlock()
if err := restoreResidualState(callerCtx, s.profileManager.GetStatePath()); err != nil {
@@ -621,16 +642,16 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
if err != nil {
return nil, err
}
+
if status != internal.StatusIdle {
return nil, fmt.Errorf("up already in progress: current status %s", status)
}
- // it should be nil here, but .
+ // it should be nil here, but in case it isn't we cancel it.
if s.actCancel != nil {
s.actCancel()
}
ctx, cancel := context.WithCancel(s.rootCtx)
-
md, ok := metadata.FromIncomingContext(callerCtx)
if ok {
ctx = metadata.NewOutgoingContext(ctx, md)
@@ -673,26 +694,31 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
s.statusRecorder.UpdateManagementAddress(s.config.ManagementURL.String())
s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive)
+ s.clientRunning = true
+ s.clientRunningChan = make(chan struct{})
+ s.clientGiveUpChan = make(chan struct{})
+ go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan)
+
+ return s.waitForUp(callerCtx)
+}
+
+// todo: handle potential race conditions
+func (s *Server) waitForUp(callerCtx context.Context) (*proto.UpResponse, error) {
timeoutCtx, cancel := context.WithTimeout(callerCtx, 50*time.Second)
defer cancel()
- if !s.clientRunning {
- s.clientRunning = true
- s.clientRunningChan = make(chan struct{}, 1)
- go s.connectWithRetryRuns(ctx, s.config, s.statusRecorder, s.clientRunningChan)
- }
- for {
- select {
- case <-s.clientRunningChan:
- s.isSessionActive.Store(true)
- return &proto.UpResponse{}, nil
- case <-callerCtx.Done():
- log.Debug("context done, stopping the wait for engine to become ready")
- return nil, callerCtx.Err()
- case <-timeoutCtx.Done():
- log.Debug("up is timed out, stopping the wait for engine to become ready")
- return nil, timeoutCtx.Err()
- }
+ select {
+ case <-s.clientGiveUpChan:
+ return nil, fmt.Errorf("client gave up to connect")
+ case <-s.clientRunningChan:
+ s.isSessionActive.Store(true)
+ return &proto.UpResponse{}, nil
+ case <-callerCtx.Done():
+ log.Debug("context done, stopping the wait for engine to become ready")
+ return nil, callerCtx.Err()
+ case <-timeoutCtx.Done():
+ log.Debug("up is timed out, stopping the wait for engine to become ready")
+ return nil, timeoutCtx.Err()
}
}
@@ -966,12 +992,46 @@ func (s *Server) Status(
ctx context.Context,
msg *proto.StatusRequest,
) (*proto.StatusResponse, error) {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
s.mutex.Lock()
- defer s.mutex.Unlock()
+ clientRunning := s.clientRunning
+ s.mutex.Unlock()
+
+ if msg.WaitForReady != nil && *msg.WaitForReady && clientRunning {
+ state := internal.CtxGetState(s.rootCtx)
+ status, err := state.Status()
+ if err != nil {
+ return nil, err
+ }
+
+ if status != internal.StatusIdle && status != internal.StatusConnected && status != internal.StatusConnecting {
+ s.actCancel()
+ }
+
+ ticker := time.NewTicker(1 * time.Second)
+ defer ticker.Stop()
+ loop:
+ for {
+ select {
+ case <-s.clientGiveUpChan:
+ ticker.Stop()
+ break loop
+ case <-s.clientRunningChan:
+ ticker.Stop()
+ break loop
+ case <-ticker.C:
+ status, err := state.Status()
+ if err != nil {
+ continue
+ }
+ if status != internal.StatusIdle && status != internal.StatusConnected && status != internal.StatusConnecting {
+ s.actCancel()
+ }
+ continue
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+ }
+ }
status, err := internal.CtxGetState(s.rootCtx).Status()
if err != nil {
diff --git a/client/server/server_test.go b/client/server/server_test.go
index 45a1aa5c7..755925003 100644
--- a/client/server/server_test.go
+++ b/client/server/server_test.go
@@ -105,7 +105,7 @@ func TestConnectWithRetryRuns(t *testing.T) {
t.Setenv(maxRetryTimeVar, "5s")
t.Setenv(retryMultiplierVar, "1")
- s.connectWithRetryRuns(ctx, config, s.statusRecorder, nil)
+ s.connectWithRetryRuns(ctx, config, s.statusRecorder, nil, nil)
if counter < 3 {
t.Fatalf("expected counter > 2, got %d", counter)
}
@@ -134,8 +134,12 @@ func TestServer_Up(t *testing.T) {
profName := "default"
+ u, err := url.Parse("http://non-existent-url-for-testing.invalid:12345")
+ require.NoError(t, err)
+
ic := profilemanager.ConfigInput{
- ConfigPath: filepath.Join(tempDir, profName+".json"),
+ ConfigPath: filepath.Join(tempDir, profName+".json"),
+ ManagementURL: u.String(),
}
_, err = profilemanager.UpdateOrCreateConfig(ic)
@@ -153,16 +157,9 @@ func TestServer_Up(t *testing.T) {
}
s := New(ctx, "console", "", false, false)
-
err = s.Start()
require.NoError(t, err)
- u, err := url.Parse("http://non-existent-url-for-testing.invalid:12345")
- require.NoError(t, err)
- s.config = &profilemanager.Config{
- ManagementURL: u,
- }
-
upCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
@@ -171,6 +168,7 @@ func TestServer_Up(t *testing.T) {
Username: &currUser.Username,
}
_, err = s.Up(upCtx, upReq)
+ log.Errorf("error from Up: %v", err)
assert.Contains(t, err.Error(), "context deadline exceeded")
}
diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
index 2403b5d05..25d7380a9 100644
--- a/client/ui/client_ui.go
+++ b/client/ui/client_ui.go
@@ -529,7 +529,7 @@ func (s *serviceClient) getSettingsForm() *widget.Form {
var req proto.SetConfigRequest
req.ProfileName = activeProf.Name
req.Username = currUser.Username
-
+
if iMngURL != "" {
req.ManagementUrl = iMngURL
}
@@ -563,27 +563,28 @@ func (s *serviceClient) getSettingsForm() *widget.Form {
return
}
- status, err := conn.Status(s.ctx, &proto.StatusRequest{})
- if err != nil {
- log.Errorf("get service status: %v", err)
- dialog.ShowError(fmt.Errorf("Failed to get service status: %v", err), s.wSettings)
- return
- }
- if status.Status == string(internal.StatusConnected) {
- // run down & up
- _, err = conn.Down(s.ctx, &proto.DownRequest{})
+ go func() {
+ status, err := conn.Status(s.ctx, &proto.StatusRequest{})
if err != nil {
- log.Errorf("down service: %v", err)
- }
-
- _, err = conn.Up(s.ctx, &proto.UpRequest{})
- if err != nil {
- log.Errorf("up service: %v", err)
- dialog.ShowError(fmt.Errorf("Failed to reconnect: %v", err), s.wSettings)
+ log.Errorf("get service status: %v", err)
+ dialog.ShowError(fmt.Errorf("Failed to get service status: %v", err), s.wSettings)
return
}
- }
+ if status.Status == string(internal.StatusConnected) {
+ // run down & up
+ _, err = conn.Down(s.ctx, &proto.DownRequest{})
+ if err != nil {
+ log.Errorf("down service: %v", err)
+ }
+ _, err = conn.Up(s.ctx, &proto.UpRequest{})
+ if err != nil {
+ log.Errorf("up service: %v", err)
+ dialog.ShowError(fmt.Errorf("Failed to reconnect: %v", err), s.wSettings)
+ return
+ }
+ }
+ }()
}
},
OnCancel: func() {
diff --git a/flow/client/client.go b/flow/client/client.go
index 949824065..603fd6882 100644
--- a/flow/client/client.go
+++ b/flow/client/client.go
@@ -20,9 +20,9 @@ import (
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/status"
+ nbgrpc "github.com/netbirdio/netbird/client/grpc"
"github.com/netbirdio/netbird/flow/proto"
"github.com/netbirdio/netbird/util/embeddedroots"
- nbgrpc "github.com/netbirdio/netbird/util/grpc"
)
type GRPCClient struct {
diff --git a/management/server/types/account.go b/management/server/types/account.go
index 9ac2568a0..ca075b9f6 100644
--- a/management/server/types/account.go
+++ b/management/server/types/account.go
@@ -302,7 +302,11 @@ func (a *Account) GetPeerNetworkMap(
var zones []nbdns.CustomZone
if peersCustomZone.Domain != "" {
- zones = append(zones, peersCustomZone)
+ records := filterZoneRecordsForPeers(peer, peersCustomZone, peersToConnect)
+ zones = append(zones, nbdns.CustomZone{
+ Domain: peersCustomZone.Domain,
+ Records: records,
+ })
}
dnsUpdate.CustomZones = zones
dnsUpdate.NameServerGroups = getPeerNSGroups(a, peerID)
@@ -1651,3 +1655,24 @@ func peerSupportsPortRanges(peerVer string) bool {
meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
return err == nil && meetMinVer
}
+
+// filterZoneRecordsForPeers filters DNS records to only include peers to connect.
+func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, peersToConnect []*nbpeer.Peer) []nbdns.SimpleRecord {
+ filteredRecords := make([]nbdns.SimpleRecord, 0, len(customZone.Records))
+ peerIPs := make(map[string]struct{})
+
+ // Add peer's own IP to include its own DNS records
+ peerIPs[peer.IP.String()] = struct{}{}
+
+ for _, peerToConnect := range peersToConnect {
+ peerIPs[peerToConnect.IP.String()] = struct{}{}
+ }
+
+ for _, record := range customZone.Records {
+ if _, exists := peerIPs[record.RData]; exists {
+ filteredRecords = append(filteredRecords, record)
+ }
+ }
+
+ return filteredRecords
+}
diff --git a/management/server/types/account_test.go b/management/server/types/account_test.go
index f8ab1d627..cd221b590 100644
--- a/management/server/types/account_test.go
+++ b/management/server/types/account_test.go
@@ -2,14 +2,17 @@ package types
import (
"context"
+ "fmt"
"net"
"net/netip"
"slices"
"testing"
+ "github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ nbdns "github.com/netbirdio/netbird/dns"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
@@ -835,3 +838,109 @@ func Test_NetworksNetMapGenShouldExcludeOtherRouters(t *testing.T) {
assert.Len(t, networkResourcesRoutes, 1, "expected network resource route don't match")
assert.Len(t, sourcePeers, 2, "expected source peers don't match")
}
+
+func Test_FilterZoneRecordsForPeers(t *testing.T) {
+ tests := []struct {
+ name string
+ peer *nbpeer.Peer
+ customZone nbdns.CustomZone
+ peersToConnect []*nbpeer.Peer
+ expectedRecords []nbdns.SimpleRecord
+ }{
+ {
+ name: "empty peers to connect",
+ customZone: nbdns.CustomZone{
+ Domain: "netbird.cloud.",
+ Records: []nbdns.SimpleRecord{
+ {Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
+ {Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
+ },
+ },
+ peersToConnect: []*nbpeer.Peer{},
+ peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
+ expectedRecords: []nbdns.SimpleRecord{
+ {Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
+ },
+ },
+ {
+ name: "multiple peers multiple records match",
+ customZone: nbdns.CustomZone{
+ Domain: "netbird.cloud.",
+ Records: func() []nbdns.SimpleRecord {
+ var records []nbdns.SimpleRecord
+ for i := 1; i <= 100; i++ {
+ records = append(records, nbdns.SimpleRecord{
+ Name: fmt.Sprintf("peer%d.netbird.cloud", i),
+ Type: int(dns.TypeA),
+ Class: nbdns.DefaultClass,
+ TTL: 300,
+ RData: fmt.Sprintf("10.0.%d.%d", i/256, i%256),
+ })
+ }
+ return records
+ }(),
+ },
+ peersToConnect: func() []*nbpeer.Peer {
+ var peers []*nbpeer.Peer
+ for _, i := range []int{1, 5, 10, 25, 50, 75, 100} {
+ peers = append(peers, &nbpeer.Peer{
+ ID: fmt.Sprintf("peer%d", i),
+ IP: net.ParseIP(fmt.Sprintf("10.0.%d.%d", i/256, i%256)),
+ })
+ }
+ return peers
+ }(),
+ peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
+ expectedRecords: func() []nbdns.SimpleRecord {
+ var records []nbdns.SimpleRecord
+ for _, i := range []int{1, 5, 10, 25, 50, 75, 100} {
+ records = append(records, nbdns.SimpleRecord{
+ Name: fmt.Sprintf("peer%d.netbird.cloud", i),
+ Type: int(dns.TypeA),
+ Class: nbdns.DefaultClass,
+ TTL: 300,
+ RData: fmt.Sprintf("10.0.%d.%d", i/256, i%256),
+ })
+ }
+ return records
+ }(),
+ },
+ {
+ name: "peers with multiple DNS labels",
+ customZone: nbdns.CustomZone{
+ Domain: "netbird.cloud.",
+ Records: []nbdns.SimpleRecord{
+ {Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
+ {Name: "peer1-alt.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
+ {Name: "peer1-backup.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
+ {Name: "peer2.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
+ {Name: "peer2-service.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
+ {Name: "peer3.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.3"},
+ {Name: "peer3-alt.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.3"},
+ {Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
+ },
+ },
+ peersToConnect: []*nbpeer.Peer{
+ {ID: "peer1", IP: net.ParseIP("10.0.0.1"), DNSLabel: "peer1", ExtraDNSLabels: []string{"peer1-alt", "peer1-backup"}},
+ {ID: "peer2", IP: net.ParseIP("10.0.0.2"), DNSLabel: "peer2", ExtraDNSLabels: []string{"peer2-service"}},
+ },
+ peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
+ expectedRecords: []nbdns.SimpleRecord{
+ {Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
+ {Name: "peer1-alt.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
+ {Name: "peer1-backup.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
+ {Name: "peer2.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
+ {Name: "peer2-service.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2"},
+ {Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := filterZoneRecordsForPeers(tt.peer, tt.customZone, tt.peersToConnect)
+ assert.Equal(t, len(tt.expectedRecords), len(result))
+ assert.ElementsMatch(t, tt.expectedRecords, result)
+ })
+ }
+}
diff --git a/shared/management/client/grpc.go b/shared/management/client/grpc.go
index dc26253e9..f30e965be 100644
--- a/shared/management/client/grpc.go
+++ b/shared/management/client/grpc.go
@@ -17,11 +17,11 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
+ nbgrpc "github.com/netbirdio/netbird/client/grpc"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/proto"
- nbgrpc "github.com/netbirdio/netbird/util/grpc"
)
const ConnectTimeout = 10 * time.Second
@@ -52,7 +52,7 @@ func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsE
operation := func() error {
var err error
- conn, err = nbgrpc.CreateConnection(addr, tlsEnabled)
+ conn, err = nbgrpc.CreateConnection(ctx, addr, tlsEnabled)
if err != nil {
log.Printf("createConnection error: %v", err)
return err
diff --git a/shared/relay/client/dialer/quic/quic.go b/shared/relay/client/dialer/quic/quic.go
index b496f6a9b..967e18d79 100644
--- a/shared/relay/client/dialer/quic/quic.go
+++ b/shared/relay/client/dialer/quic/quic.go
@@ -12,7 +12,7 @@ import (
log "github.com/sirupsen/logrus"
quictls "github.com/netbirdio/netbird/shared/relay/tls"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type Dialer struct {
diff --git a/shared/relay/client/dialer/ws/ws.go b/shared/relay/client/dialer/ws/ws.go
index 109651f5d..ef6bd6b3c 100644
--- a/shared/relay/client/dialer/ws/ws.go
+++ b/shared/relay/client/dialer/ws/ws.go
@@ -16,7 +16,7 @@ import (
"github.com/netbirdio/netbird/shared/relay"
"github.com/netbirdio/netbird/util/embeddedroots"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
type Dialer struct {
diff --git a/shared/signal/client/grpc.go b/shared/signal/client/grpc.go
index 82ab678f4..5ca0c0282 100644
--- a/shared/signal/client/grpc.go
+++ b/shared/signal/client/grpc.go
@@ -16,10 +16,10 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
+ nbgrpc "github.com/netbirdio/netbird/client/grpc"
"github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/shared/management/client"
"github.com/netbirdio/netbird/shared/signal/proto"
- nbgrpc "github.com/netbirdio/netbird/util/grpc"
)
// ConnStateNotifier is a wrapper interface of the status recorder
@@ -57,7 +57,7 @@ func NewClient(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled boo
operation := func() error {
var err error
- conn, err = nbgrpc.CreateConnection(addr, tlsEnabled)
+ conn, err = nbgrpc.CreateConnection(ctx, addr, tlsEnabled)
if err != nil {
log.Printf("createConnection error: %v", err)
return err
diff --git a/sharedsock/sock_linux.go b/sharedsock/sock_linux.go
index d4fedc492..bc2d4d1be 100644
--- a/sharedsock/sock_linux.go
+++ b/sharedsock/sock_linux.go
@@ -22,7 +22,7 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/sys/unix"
- nbnet "github.com/netbirdio/netbird/util/net"
+ nbnet "github.com/netbirdio/netbird/client/net"
)
// ErrSharedSockStopped indicates that shared socket has been stopped
@@ -93,7 +93,7 @@ func Listen(port int, filter BPFFilter, mtu uint16) (_ net.PacketConn, err error
}
if err = nbnet.SetSocketMark(rawSock.conn4); err != nil {
- return nil, fmt.Errorf("failed to set SO_MARK on ipv4 socket: %w", err)
+ return nil, fmt.Errorf("set SO_MARK on ipv4 socket: %w", err)
}
var sockErr error
@@ -102,7 +102,7 @@ func Listen(port int, filter BPFFilter, mtu uint16) (_ net.PacketConn, err error
log.Errorf("Failed to create ipv6 raw socket: %v", err)
} else {
if err = nbnet.SetSocketMark(rawSock.conn6); err != nil {
- return nil, fmt.Errorf("failed to set SO_MARK on ipv6 socket: %w", err)
+ return nil, fmt.Errorf("set SO_MARK on ipv6 socket: %w", err)
}
}
diff --git a/util/net/conn.go b/util/net/conn.go
deleted file mode 100644
index 26693f841..000000000
--- a/util/net/conn.go
+++ /dev/null
@@ -1,31 +0,0 @@
-//go:build !ios
-
-package net
-
-import (
- "net"
-
- log "github.com/sirupsen/logrus"
-)
-
-// Conn wraps a net.Conn to override the Close method
-type Conn struct {
- net.Conn
- ID ConnectionID
-}
-
-// Close overrides the net.Conn Close method to execute all registered hooks after closing the connection
-func (c *Conn) Close() error {
- err := c.Conn.Close()
-
- dialerCloseHooksMutex.RLock()
- defer dialerCloseHooksMutex.RUnlock()
-
- for _, hook := range dialerCloseHooks {
- if err := hook(c.ID, &c.Conn); err != nil {
- log.Errorf("Error executing dialer close hook: %v", err)
- }
- }
-
- return err
-}
diff --git a/util/net/dial.go b/util/net/dial.go
deleted file mode 100644
index 595311492..000000000
--- a/util/net/dial.go
+++ /dev/null
@@ -1,58 +0,0 @@
-//go:build !ios
-
-package net
-
-import (
- "fmt"
- "net"
-
- log "github.com/sirupsen/logrus"
-)
-
-func DialUDP(network string, laddr, raddr *net.UDPAddr) (*net.UDPConn, error) {
- if CustomRoutingDisabled() {
- return net.DialUDP(network, laddr, raddr)
- }
-
- dialer := NewDialer()
- dialer.LocalAddr = laddr
-
- conn, err := dialer.Dial(network, raddr.String())
- if err != nil {
- return nil, fmt.Errorf("dialing UDP %s: %w", raddr.String(), err)
- }
-
- udpConn, ok := conn.(*Conn).Conn.(*net.UDPConn)
- if !ok {
- if err := conn.Close(); err != nil {
- log.Errorf("Failed to close connection: %v", err)
- }
- return nil, fmt.Errorf("expected UDP connection, got different type: %T", conn)
- }
-
- return udpConn, nil
-}
-
-func DialTCP(network string, laddr, raddr *net.TCPAddr) (*net.TCPConn, error) {
- if CustomRoutingDisabled() {
- return net.DialTCP(network, laddr, raddr)
- }
-
- dialer := NewDialer()
- dialer.LocalAddr = laddr
-
- conn, err := dialer.Dial(network, raddr.String())
- if err != nil {
- return nil, fmt.Errorf("dialing TCP %s: %w", raddr.String(), err)
- }
-
- tcpConn, ok := conn.(*Conn).Conn.(*net.TCPConn)
- if !ok {
- if err := conn.Close(); err != nil {
- log.Errorf("Failed to close connection: %v", err)
- }
- return nil, fmt.Errorf("expected TCP connection, got different type: %T", conn)
- }
-
- return tcpConn, nil
-}
diff --git a/util/net/dialer_dial.go b/util/net/dialer_dial.go
deleted file mode 100644
index 1659b6220..000000000
--- a/util/net/dialer_dial.go
+++ /dev/null
@@ -1,107 +0,0 @@
-//go:build !ios
-
-package net
-
-import (
- "context"
- "fmt"
- "net"
- "sync"
-
- "github.com/hashicorp/go-multierror"
- log "github.com/sirupsen/logrus"
-)
-
-type DialerDialHookFunc func(ctx context.Context, connID ConnectionID, resolvedAddresses []net.IPAddr) error
-type DialerCloseHookFunc func(connID ConnectionID, conn *net.Conn) error
-
-var (
- dialerDialHooksMutex sync.RWMutex
- dialerDialHooks []DialerDialHookFunc
- dialerCloseHooksMutex sync.RWMutex
- dialerCloseHooks []DialerCloseHookFunc
-)
-
-// AddDialerHook allows adding a new hook to be executed before dialing.
-func AddDialerHook(hook DialerDialHookFunc) {
- dialerDialHooksMutex.Lock()
- defer dialerDialHooksMutex.Unlock()
- dialerDialHooks = append(dialerDialHooks, hook)
-}
-
-// AddDialerCloseHook allows adding a new hook to be executed on connection close.
-func AddDialerCloseHook(hook DialerCloseHookFunc) {
- dialerCloseHooksMutex.Lock()
- defer dialerCloseHooksMutex.Unlock()
- dialerCloseHooks = append(dialerCloseHooks, hook)
-}
-
-// RemoveDialerHooks removes all dialer hooks.
-func RemoveDialerHooks() {
- dialerDialHooksMutex.Lock()
- defer dialerDialHooksMutex.Unlock()
- dialerDialHooks = nil
-
- dialerCloseHooksMutex.Lock()
- defer dialerCloseHooksMutex.Unlock()
- dialerCloseHooks = nil
-}
-
-// DialContext wraps the net.Dialer's DialContext method to use the custom connection
-func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
- log.Debugf("Dialing %s %s", network, address)
-
- if CustomRoutingDisabled() {
- return d.Dialer.DialContext(ctx, network, address)
- }
-
- var resolver *net.Resolver
- if d.Resolver != nil {
- resolver = d.Resolver
- }
-
- connID := GenerateConnID()
- if dialerDialHooks != nil {
- if err := callDialerHooks(ctx, connID, address, resolver); err != nil {
- log.Errorf("Failed to call dialer hooks: %v", err)
- }
- }
-
- conn, err := d.Dialer.DialContext(ctx, network, address)
- if err != nil {
- return nil, fmt.Errorf("d.Dialer.DialContext: %w", err)
- }
-
- // Wrap the connection in Conn to handle Close with hooks
- return &Conn{Conn: conn, ID: connID}, nil
-}
-
-// Dial wraps the net.Dialer's Dial method to use the custom connection
-func (d *Dialer) Dial(network, address string) (net.Conn, error) {
- return d.DialContext(context.Background(), network, address)
-}
-
-func callDialerHooks(ctx context.Context, connID ConnectionID, address string, resolver *net.Resolver) error {
- host, _, err := net.SplitHostPort(address)
- if err != nil {
- return fmt.Errorf("split host and port: %w", err)
- }
- ips, err := resolver.LookupIPAddr(ctx, host)
- if err != nil {
- return fmt.Errorf("failed to resolve address %s: %w", address, err)
- }
-
- log.Debugf("Dialer resolved IPs for %s: %v", address, ips)
-
- var result *multierror.Error
-
- dialerDialHooksMutex.RLock()
- defer dialerDialHooksMutex.RUnlock()
- for _, hook := range dialerDialHooks {
- if err := hook(ctx, connID, ips); err != nil {
- result = multierror.Append(result, fmt.Errorf("executing dial hook: %w", err))
- }
- }
-
- return result.ErrorOrNil()
-}
diff --git a/util/net/dialer_init_nonlinux.go b/util/net/dialer_init_nonlinux.go
deleted file mode 100644
index 8c57ebbaa..000000000
--- a/util/net/dialer_init_nonlinux.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build !linux
-
-package net
-
-func (d *Dialer) init() {
- // implemented on Linux and Android only
-}
diff --git a/util/net/env_generic.go b/util/net/env_generic.go
deleted file mode 100644
index 6d142a838..000000000
--- a/util/net/env_generic.go
+++ /dev/null
@@ -1,12 +0,0 @@
-//go:build !linux || android
-
-package net
-
-func Init() {
- // nothing to do on non-linux
-}
-
-func AdvancedRouting() bool {
- // non-linux currently doesn't support advanced routing
- return false
-}
diff --git a/util/net/listen.go b/util/net/listen.go
deleted file mode 100644
index 3ae8a9435..000000000
--- a/util/net/listen.go
+++ /dev/null
@@ -1,37 +0,0 @@
-//go:build !ios
-
-package net
-
-import (
- "context"
- "fmt"
- "net"
- "sync"
-
- "github.com/pion/transport/v3"
- log "github.com/sirupsen/logrus"
-)
-
-// ListenUDP listens on the network address and returns a transport.UDPConn
-// which includes support for write and close hooks.
-func ListenUDP(network string, laddr *net.UDPAddr) (transport.UDPConn, error) {
- if CustomRoutingDisabled() {
- return net.ListenUDP(network, laddr)
- }
-
- conn, err := NewListener().ListenPacket(context.Background(), network, laddr.String())
- if err != nil {
- return nil, fmt.Errorf("listen UDP: %w", err)
- }
-
- packetConn := conn.(*PacketConn)
- udpConn, ok := packetConn.PacketConn.(*net.UDPConn)
- if !ok {
- if err := packetConn.Close(); err != nil {
- log.Errorf("Failed to close connection: %v", err)
- }
- return nil, fmt.Errorf("expected UDPConn, got different type: %T", udpConn)
- }
-
- return &UDPConn{UDPConn: udpConn, ID: packetConn.ID, seenAddrs: &sync.Map{}}, nil
-}
diff --git a/util/net/listener_init_nonlinux.go b/util/net/listener_init_nonlinux.go
deleted file mode 100644
index 80f6f7f1a..000000000
--- a/util/net/listener_init_nonlinux.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build !linux
-
-package net
-
-func (l *ListenerConfig) init() {
- // implemented on Linux and Android only
-}
diff --git a/util/net/listener_listen.go b/util/net/listener_listen.go
deleted file mode 100644
index 4060ab49a..000000000
--- a/util/net/listener_listen.go
+++ /dev/null
@@ -1,205 +0,0 @@
-//go:build !ios
-
-package net
-
-import (
- "context"
- "fmt"
- "net"
- "net/netip"
- "sync"
-
- log "github.com/sirupsen/logrus"
-)
-
-// ListenerWriteHookFunc defines the function signature for write hooks for PacketConn.
-type ListenerWriteHookFunc func(connID ConnectionID, ip *net.IPAddr, data []byte) error
-
-// ListenerCloseHookFunc defines the function signature for close hooks for PacketConn.
-type ListenerCloseHookFunc func(connID ConnectionID, conn net.PacketConn) error
-
-// ListenerAddressRemoveHookFunc defines the function signature for hooks called when addresses are removed.
-type ListenerAddressRemoveHookFunc func(connID ConnectionID, prefix netip.Prefix) error
-
-var (
- listenerWriteHooksMutex sync.RWMutex
- listenerWriteHooks []ListenerWriteHookFunc
- listenerCloseHooksMutex sync.RWMutex
- listenerCloseHooks []ListenerCloseHookFunc
- listenerAddressRemoveHooksMutex sync.RWMutex
- listenerAddressRemoveHooks []ListenerAddressRemoveHookFunc
-)
-
-// AddListenerWriteHook allows adding a new write hook to be executed before a UDP packet is sent.
-func AddListenerWriteHook(hook ListenerWriteHookFunc) {
- listenerWriteHooksMutex.Lock()
- defer listenerWriteHooksMutex.Unlock()
- listenerWriteHooks = append(listenerWriteHooks, hook)
-}
-
-// AddListenerCloseHook allows adding a new hook to be executed upon closing a UDP connection.
-func AddListenerCloseHook(hook ListenerCloseHookFunc) {
- listenerCloseHooksMutex.Lock()
- defer listenerCloseHooksMutex.Unlock()
- listenerCloseHooks = append(listenerCloseHooks, hook)
-}
-
-// AddListenerAddressRemoveHook allows adding a new hook to be executed when an address is removed.
-func AddListenerAddressRemoveHook(hook ListenerAddressRemoveHookFunc) {
- listenerAddressRemoveHooksMutex.Lock()
- defer listenerAddressRemoveHooksMutex.Unlock()
- listenerAddressRemoveHooks = append(listenerAddressRemoveHooks, hook)
-}
-
-// RemoveListenerHooks removes all listener hooks.
-func RemoveListenerHooks() {
- listenerWriteHooksMutex.Lock()
- defer listenerWriteHooksMutex.Unlock()
- listenerWriteHooks = nil
-
- listenerCloseHooksMutex.Lock()
- defer listenerCloseHooksMutex.Unlock()
- listenerCloseHooks = nil
-
- listenerAddressRemoveHooksMutex.Lock()
- defer listenerAddressRemoveHooksMutex.Unlock()
- listenerAddressRemoveHooks = nil
-}
-
-// ListenPacket listens on the network address and returns a PacketConn
-// which includes support for write hooks.
-func (l *ListenerConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
- if CustomRoutingDisabled() {
- return l.ListenConfig.ListenPacket(ctx, network, address)
- }
-
- pc, err := l.ListenConfig.ListenPacket(ctx, network, address)
- if err != nil {
- return nil, fmt.Errorf("listen packet: %w", err)
- }
- connID := GenerateConnID()
-
- return &PacketConn{PacketConn: pc, ID: connID, seenAddrs: &sync.Map{}}, nil
-}
-
-// PacketConn wraps net.PacketConn to override its WriteTo and Close methods to include hook functionality.
-type PacketConn struct {
- net.PacketConn
- ID ConnectionID
- seenAddrs *sync.Map
-}
-
-// WriteTo writes a packet with payload b to addr, executing registered write hooks beforehand.
-func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
- callWriteHooks(c.ID, c.seenAddrs, b, addr)
- return c.PacketConn.WriteTo(b, addr)
-}
-
-// Close overrides the net.PacketConn Close method to execute all registered hooks before closing the connection.
-func (c *PacketConn) Close() error {
- c.seenAddrs = &sync.Map{}
- return closeConn(c.ID, c.PacketConn)
-}
-
-// UDPConn wraps net.UDPConn to override its WriteTo and Close methods to include hook functionality.
-type UDPConn struct {
- *net.UDPConn
- ID ConnectionID
- seenAddrs *sync.Map
-}
-
-// WriteTo writes a packet with payload b to addr, executing registered write hooks beforehand.
-func (c *UDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
- callWriteHooks(c.ID, c.seenAddrs, b, addr)
- return c.UDPConn.WriteTo(b, addr)
-}
-
-// Close overrides the net.UDPConn Close method to execute all registered hooks before closing the connection.
-func (c *UDPConn) Close() error {
- c.seenAddrs = &sync.Map{}
- return closeConn(c.ID, c.UDPConn)
-}
-
-// RemoveAddress removes an address from the seen cache and triggers removal hooks.
-func (c *PacketConn) RemoveAddress(addr string) {
- if _, exists := c.seenAddrs.LoadAndDelete(addr); !exists {
- return
- }
-
- ipStr, _, err := net.SplitHostPort(addr)
- if err != nil {
- log.Errorf("Error splitting IP address and port: %v", err)
- return
- }
-
- ipAddr, err := netip.ParseAddr(ipStr)
- if err != nil {
- log.Errorf("Error parsing IP address %s: %v", ipStr, err)
- return
- }
-
- prefix := netip.PrefixFrom(ipAddr, ipAddr.BitLen())
-
- listenerAddressRemoveHooksMutex.RLock()
- defer listenerAddressRemoveHooksMutex.RUnlock()
-
- for _, hook := range listenerAddressRemoveHooks {
- if err := hook(c.ID, prefix); err != nil {
- log.Errorf("Error executing listener address remove hook: %v", err)
- }
- }
-}
-
-
-// WrapPacketConn wraps an existing net.PacketConn with nbnet functionality
-func WrapPacketConn(conn net.PacketConn) *PacketConn {
- return &PacketConn{
- PacketConn: conn,
- ID: GenerateConnID(),
- seenAddrs: &sync.Map{},
- }
-}
-
-func callWriteHooks(id ConnectionID, seenAddrs *sync.Map, b []byte, addr net.Addr) {
- // Lookup the address in the seenAddrs map to avoid calling the hooks for every write
- if _, loaded := seenAddrs.LoadOrStore(addr.String(), true); !loaded {
- ipStr, _, splitErr := net.SplitHostPort(addr.String())
- if splitErr != nil {
- log.Errorf("Error splitting IP address and port: %v", splitErr)
- return
- }
-
- ip, err := net.ResolveIPAddr("ip", ipStr)
- if err != nil {
- log.Errorf("Error resolving IP address: %v", err)
- return
- }
- log.Debugf("Listener resolved IP for %s: %s", addr, ip)
-
- func() {
- listenerWriteHooksMutex.RLock()
- defer listenerWriteHooksMutex.RUnlock()
-
- for _, hook := range listenerWriteHooks {
- if err := hook(id, ip, b); err != nil {
- log.Errorf("Error executing listener write hook: %v", err)
- }
- }
- }()
- }
-}
-
-func closeConn(id ConnectionID, conn net.PacketConn) error {
- err := conn.Close()
-
- listenerCloseHooksMutex.RLock()
- defer listenerCloseHooksMutex.RUnlock()
-
- for _, hook := range listenerCloseHooks {
- if err := hook(id, conn); err != nil {
- log.Errorf("Error executing listener close hook: %v", err)
- }
- }
-
- return err
-}