mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 00:36:38 +00:00
Compare commits
35 Commits
dependabot
...
v0.58.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
998fb30e1e | ||
|
|
e254b4cde5 | ||
|
|
ead1c618ba | ||
|
|
55126f990c | ||
|
|
90577682e4 | ||
|
|
dc30dcacce | ||
|
|
2c87fa6236 | ||
|
|
ec8d83ade4 | ||
|
|
3130cce72d | ||
|
|
bd23ab925e | ||
|
|
0c6f671a7c | ||
|
|
cf7f6c355f | ||
|
|
47e64d72db | ||
|
|
9e81e782e5 | ||
|
|
7aef0f67df | ||
|
|
dba7ef667d | ||
|
|
69d87343d2 | ||
|
|
5113c70943 | ||
|
|
ad8fcda67b | ||
|
|
d33f88df82 | ||
|
|
786ca6fc79 | ||
|
|
dfebdf1444 | ||
|
|
a8dcff69c2 | ||
|
|
71e944fa57 | ||
|
|
d39fcfd62a | ||
|
|
21368b38d9 | ||
|
|
d817584f52 | ||
|
|
4d3dc3475d | ||
|
|
6fc50a438f | ||
|
|
149559a06b | ||
|
|
e14c6de203 | ||
|
|
d4c067f0af | ||
|
|
dbefa8bd9f | ||
|
|
4fd10b9447 | ||
|
|
aa595c3073 |
37
.github/workflows/golang-test-linux.yml
vendored
37
.github/workflows/golang-test-linux.yml
vendored
@@ -217,7 +217,7 @@ jobs:
|
|||||||
- arch: "386"
|
- arch: "386"
|
||||||
raceFlag: ""
|
raceFlag: ""
|
||||||
- arch: "amd64"
|
- arch: "amd64"
|
||||||
raceFlag: ""
|
raceFlag: "-race"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
@@ -382,6 +382,32 @@ jobs:
|
|||||||
store: [ 'sqlite', 'postgres' ]
|
store: [ 'sqlite', 'postgres' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Create Docker network
|
||||||
|
run: docker network create promnet
|
||||||
|
|
||||||
|
- name: Start Prometheus Pushgateway
|
||||||
|
run: docker run -d --name pushgateway --network promnet -p 9091:9091 prom/pushgateway
|
||||||
|
|
||||||
|
- name: Start Prometheus (for Pushgateway forwarding)
|
||||||
|
run: |
|
||||||
|
echo '
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: "pushgateway"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["pushgateway:9091"]
|
||||||
|
remote_write:
|
||||||
|
- url: ${{ secrets.GRAFANA_URL }}
|
||||||
|
basic_auth:
|
||||||
|
username: ${{ secrets.GRAFANA_USER }}
|
||||||
|
password: ${{ secrets.GRAFANA_API_KEY }}
|
||||||
|
' > prometheus.yml
|
||||||
|
|
||||||
|
docker run -d --name prometheus --network promnet \
|
||||||
|
-v $PWD/prometheus.yml:/etc/prometheus/prometheus.yml \
|
||||||
|
-p 9090:9090 \
|
||||||
|
prom/prometheus
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
@@ -428,9 +454,10 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
|
GIT_BRANCH=${{ github.ref_name }} \
|
||||||
go test -tags devcert -run=^$ -bench=. \
|
go test -tags devcert -run=^$ -bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
@@ -521,7 +548,7 @@ jobs:
|
|||||||
-run=^$ \
|
-run=^$ \
|
||||||
-bench=. \
|
-bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
name: "Management / Integration"
|
name: "Management / Integration"
|
||||||
@@ -571,4 +598,4 @@ jobs:
|
|||||||
CI=true \
|
CI=true \
|
||||||
go test -tags=integration \
|
go test -tags=integration \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
|||||||
2
.github/workflows/golang-test-windows.yml
vendored
2
.github/workflows/golang-test-windows.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=${{ env.cache }}
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=${{ env.cache }}
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
||||||
- run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' })" >> $env:GITHUB_ENV
|
- run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' })" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
|
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.22"
|
SIGN_PIPE_VER: "v0.0.23"
|
||||||
GORELEASER_VER: "v2.3.2"
|
GORELEASER_VER: "v2.3.2"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
|
|
||||||
### Open Source Network Security in a Single Platform
|
### Open Source Network Security in a Single Platform
|
||||||
|
|
||||||
<img width="1188" alt="centralized-network-management 1" src="https://github.com/user-attachments/assets/c28cc8e4-15d2-4d2f-bb97-a6433db39d56" />
|
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
||||||
|
|
||||||
### NetBird on Lawrence Systems (Video)
|
### NetBird on Lawrence Systems (Video)
|
||||||
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
|
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ ENV \
|
|||||||
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
||||||
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
|
||||||
NB_ENTRYPOINT_LOGIN_TIMEOUT="1"
|
NB_ENTRYPOINT_LOGIN_TIMEOUT="5"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package android
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/formatter"
|
"github.com/netbirdio/netbird/formatter"
|
||||||
"github.com/netbirdio/netbird/util/net"
|
"github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionListener export internal Listener for mobile
|
// ConnectionListener export internal Listener for mobile
|
||||||
@@ -83,7 +84,8 @@ func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run start the internal client. It is a blocker function
|
// Run start the internal client. It is a blocker function
|
||||||
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener) error {
|
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
||||||
|
exportEnvList(envList)
|
||||||
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
||||||
ConfigPath: c.cfgFile,
|
ConfigPath: c.cfgFile,
|
||||||
})
|
})
|
||||||
@@ -118,7 +120,8 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
|
|||||||
|
|
||||||
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
||||||
// In this case make no sense handle registration steps.
|
// In this case make no sense handle registration steps.
|
||||||
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error {
|
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
||||||
|
exportEnvList(envList)
|
||||||
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
||||||
ConfigPath: c.cfgFile,
|
ConfigPath: c.cfgFile,
|
||||||
})
|
})
|
||||||
@@ -249,3 +252,14 @@ func (c *Client) SetConnectionListener(listener ConnectionListener) {
|
|||||||
func (c *Client) RemoveConnectionListener() {
|
func (c *Client) RemoveConnectionListener() {
|
||||||
c.recorder.RemoveConnectionListener()
|
c.recorder.RemoveConnectionListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exportEnvList(list *EnvList) {
|
||||||
|
if list == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range list.AllItems() {
|
||||||
|
if err := os.Setenv(k, v); err != nil {
|
||||||
|
log.Errorf("could not set env variable %s: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
32
client/android/env_list.go
Normal file
32
client/android/env_list.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package android
|
||||||
|
|
||||||
|
import "github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// EnvKeyNBForceRelay Exported for Android java client
|
||||||
|
EnvKeyNBForceRelay = peer.EnvKeyNBForceRelay
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvList wraps a Go map for export to Java
|
||||||
|
type EnvList struct {
|
||||||
|
data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEnvList creates a new EnvList
|
||||||
|
func NewEnvList() *EnvList {
|
||||||
|
return &EnvList{data: make(map[string]string)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put adds a key-value pair
|
||||||
|
func (el *EnvList) Put(key, value string) {
|
||||||
|
el.data[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value by key
|
||||||
|
func (el *EnvList) Get(key string) string {
|
||||||
|
return el.data[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EnvList) AllItems() map[string]string {
|
||||||
|
return el.data
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ type ErrListener interface {
|
|||||||
// the backend want to show an url for the user
|
// the backend want to show an url for the user
|
||||||
type URLOpener interface {
|
type URLOpener interface {
|
||||||
Open(string)
|
Open(string)
|
||||||
|
OnLoginSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth can register or login new client
|
// 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 = a.withBackOff(a.ctx, func() error {
|
||||||
err := internal.Login(a.ctx, a.config, "", jwtToken)
|
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) {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ var downCmd = &cobra.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*7)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update host's static platform and system information
|
// update host's static platform and system information
|
||||||
system.UpdateStaticInfo()
|
system.UpdateStaticInfoAsync()
|
||||||
|
|
||||||
configFilePath, err := activeProf.FilePath()
|
configFilePath, err := activeProf.FilePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
|
|||||||
|
|
||||||
// DialClientGRPCServer returns client connection to the daemon server.
|
// DialClientGRPCServer returns client connection to the daemon server.
|
||||||
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
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()
|
defer cancel()
|
||||||
|
|
||||||
return grpc.DialContext(
|
return grpc.DialContext(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
log.Info("starting NetBird service") //nolint
|
log.Info("starting NetBird service") //nolint
|
||||||
|
|
||||||
// Collect static system and platform information
|
// Collect static system and platform information
|
||||||
system.UpdateStaticInfo()
|
system.UpdateStaticInfoAsync()
|
||||||
|
|
||||||
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
||||||
p.serv = grpc.NewServer()
|
p.serv = grpc.NewServer()
|
||||||
|
|||||||
@@ -9,29 +9,26 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
clientProto "github.com/netbirdio/netbird/client/proto"
|
||||||
|
client "github.com/netbirdio/netbird/client/server"
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
mgmt "github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"github.com/netbirdio/netbird/management/server/peers"
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
|
||||||
|
|
||||||
clientProto "github.com/netbirdio/netbird/client/proto"
|
|
||||||
client "github.com/netbirdio/netbird/client/server"
|
|
||||||
mgmt "github.com/netbirdio/netbird/management/server"
|
|
||||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
sigProto "github.com/netbirdio/netbird/shared/signal/proto"
|
sigProto "github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
sig "github.com/netbirdio/netbird/signal/server"
|
sig "github.com/netbirdio/netbird/signal/server"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startTestingServices(t *testing.T) string {
|
func startTestingServices(t *testing.T) string {
|
||||||
@@ -90,15 +87,20 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
iv, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
t.Cleanup(ctrl.Finish)
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
settingsMockManager := settings.NewMockManager(ctrl)
|
|
||||||
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
||||||
|
peersmanager := peers.NewManager(store, permissionsManagerMock)
|
||||||
|
settingsManagerMock := settings.NewMockManager(ctrl)
|
||||||
|
|
||||||
|
iv, _ := integrations.NewIntegratedValidator(context.Background(), peersmanager, settingsManagerMock, eventStore)
|
||||||
|
|
||||||
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
settingsMockManager := settings.NewMockManager(ctrl)
|
||||||
groupsManager := groups.NewManagerMock()
|
groupsManager := groups.NewManagerMock()
|
||||||
|
|
||||||
settingsMockManager.EXPECT().
|
settingsMockManager.EXPECT().
|
||||||
|
|||||||
@@ -230,7 +230,9 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
|
|||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get daemon status: %v", err)
|
return fmt.Errorf("unable to get daemon status: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
|
|
||||||
// either startup error (permanent backoff err) or nil err (successful engine up)
|
// either startup error (permanent backoff err) or nil err (successful engine up)
|
||||||
// TODO: make after-startup backoff err available
|
// TODO: make after-startup backoff err available
|
||||||
run := make(chan struct{}, 1)
|
run := make(chan struct{})
|
||||||
clientErr := make(chan error, 1)
|
clientErr := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
if err := client.Run(run); err != nil {
|
if err := client.Run(run); err != nil {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"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
|
// constants needed to manage and create iptable rules
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/test"
|
"github.com/netbirdio/netbird/client/firewall/test"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isIptablesSupported() bool {
|
func isIptablesSupported() bool {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
nbid "github.com/netbirdio/netbird/client/internal/acl/id"
|
nbid "github.com/netbirdio/netbird/client/internal/acl/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import (
|
|||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util/embeddedroots"
|
"github.com/netbirdio/netbird/util/embeddedroots"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithCustomDialer() grpc.DialOption {
|
func WithCustomDialer() grpc.DialOption {
|
||||||
@@ -57,7 +58,7 @@ func Backoff(ctx context.Context) backoff.BackOff {
|
|||||||
return backoff.WithContext(b, ctx)
|
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())
|
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||||
if tlsEnabled {
|
if tlsEnabled {
|
||||||
certPool, err := x509.SystemCertPool()
|
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()
|
defer cancel()
|
||||||
|
|
||||||
conn, err := grpc.DialContext(
|
conn, err := grpc.DialContext(
|
||||||
@@ -3,7 +3,7 @@ package bind
|
|||||||
import (
|
import (
|
||||||
wireguard "golang.zx2c4.com/wireguard/conn"
|
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)
|
// TODO: This is most likely obsolete since the control fns should be called by the wrapped udpconn (ice_bind.go)
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
"github.com/pion/transport/v3"
|
"github.com/pion/transport/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
wgConn "golang.zx2c4.com/wireguard/conn"
|
wgConn "golang.zx2c4.com/wireguard/conn"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecvMessage struct {
|
type RecvMessage struct {
|
||||||
@@ -44,7 +45,7 @@ type ICEBind struct {
|
|||||||
RecvChan chan RecvMessage
|
RecvChan chan RecvMessage
|
||||||
|
|
||||||
transportNet transport.Net
|
transportNet transport.Net
|
||||||
filterFn FilterFn
|
filterFn udpmux.FilterFn
|
||||||
endpoints map[netip.Addr]net.Conn
|
endpoints map[netip.Addr]net.Conn
|
||||||
endpointsMu sync.Mutex
|
endpointsMu sync.Mutex
|
||||||
// every time when Close() is called (i.e. BindUpdate()) we need to close exit from the receiveRelayed and create a
|
// every time when Close() is called (i.e. BindUpdate()) we need to close exit from the receiveRelayed and create a
|
||||||
@@ -54,13 +55,13 @@ type ICEBind struct {
|
|||||||
closed bool
|
closed bool
|
||||||
|
|
||||||
muUDPMux sync.Mutex
|
muUDPMux sync.Mutex
|
||||||
udpMux *UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
mtu uint16
|
mtu uint16
|
||||||
activityRecorder *ActivityRecorder
|
activityRecorder *ActivityRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewICEBind(transportNet transport.Net, filterFn FilterFn, address wgaddr.Address, mtu uint16) *ICEBind {
|
func NewICEBind(transportNet transport.Net, filterFn udpmux.FilterFn, address wgaddr.Address, mtu uint16) *ICEBind {
|
||||||
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
||||||
ib := &ICEBind{
|
ib := &ICEBind{
|
||||||
StdNetBind: b,
|
StdNetBind: b,
|
||||||
@@ -115,7 +116,7 @@ func (s *ICEBind) ActivityRecorder() *ActivityRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetICEMux returns the ICE UDPMux that was created and used by ICEBind
|
// GetICEMux returns the ICE UDPMux that was created and used by ICEBind
|
||||||
func (s *ICEBind) GetICEMux() (*UniversalUDPMuxDefault, error) {
|
func (s *ICEBind) GetICEMux() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
s.muUDPMux.Lock()
|
s.muUDPMux.Lock()
|
||||||
defer s.muUDPMux.Unlock()
|
defer s.muUDPMux.Unlock()
|
||||||
if s.udpMux == nil {
|
if s.udpMux == nil {
|
||||||
@@ -158,8 +159,8 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r
|
|||||||
s.muUDPMux.Lock()
|
s.muUDPMux.Lock()
|
||||||
defer s.muUDPMux.Unlock()
|
defer s.muUDPMux.Unlock()
|
||||||
|
|
||||||
s.udpMux = NewUniversalUDPMuxDefault(
|
s.udpMux = udpmux.NewUniversalUDPMuxDefault(
|
||||||
UniversalUDPMuxParams{
|
udpmux.UniversalUDPMuxParams{
|
||||||
UDPConn: nbnet.WrapPacketConn(conn),
|
UDPConn: nbnet.WrapPacketConn(conn),
|
||||||
Net: s.transportNet,
|
Net: s.transportNet,
|
||||||
FilterFn: s.filterFn,
|
FilterFn: s.filterFn,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build ios
|
|
||||||
|
|
||||||
package bind
|
|
||||||
|
|
||||||
func (m *UDPMuxDefault) notifyAddressRemoval(addr string) {
|
|
||||||
// iOS doesn't support nbnet hooks, so this is a no-op
|
|
||||||
}
|
|
||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
"github.com/netbirdio/netbird/monotime"
|
"github.com/netbirdio/netbird/monotime"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -394,6 +394,13 @@ func toLastHandshake(stringVar string) (time.Time, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, fmt.Errorf("parse handshake sec: %w", err)
|
return time.Time{}, fmt.Errorf("parse handshake sec: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If sec is 0 (Unix epoch), return zero time instead
|
||||||
|
// This indicates no handshake has occurred
|
||||||
|
if sec == 0 {
|
||||||
|
return time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
return time.Unix(sec, 0), nil
|
return time.Unix(sec, 0), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +409,7 @@ func toBytes(s string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFwmark() int {
|
func getFwmark() int {
|
||||||
if nbnet.AdvancedRouting() {
|
if nbnet.AdvancedRouting() && runtime.GOOS == "linux" {
|
||||||
return nbnet.ControlPlaneMark
|
return nbnet.ControlPlaneMark
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import (
|
|||||||
|
|
||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WGTunDevice interface {
|
type WGTunDevice interface {
|
||||||
Create() (device.WGConfigurer, error)
|
Create() (device.WGConfigurer, error)
|
||||||
Up() (*bind.UniversalUDPMuxDefault, error)
|
Up() (*udpmux.UniversalUDPMuxDefault, error)
|
||||||
UpdateAddr(address wgaddr.Address) error
|
UpdateAddr(address wgaddr.Address) error
|
||||||
WgAddress() wgaddr.Address
|
WgAddress() wgaddr.Address
|
||||||
MTU() uint16
|
MTU() uint16
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ type WGTunDevice struct {
|
|||||||
name string
|
name string
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string
|
|||||||
}
|
}
|
||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
func (t *WGTunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *WGTunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ type TunDevice struct {
|
|||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ type TunDevice struct {
|
|||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/device"
|
"golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
"github.com/netbirdio/netbird/sharedsock"
|
"github.com/netbirdio/netbird/sharedsock"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TunKernelDevice struct {
|
type TunKernelDevice struct {
|
||||||
@@ -31,9 +31,9 @@ type TunKernelDevice struct {
|
|||||||
|
|
||||||
link *wgLink
|
link *wgLink
|
||||||
udpMuxConn net.PacketConn
|
udpMuxConn net.PacketConn
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
|
|
||||||
filterFn bind.FilterFn
|
filterFn udpmux.FilterFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice {
|
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice {
|
||||||
@@ -79,7 +79,7 @@ func (t *TunKernelDevice) Create() (WGConfigurer, error) {
|
|||||||
return configurer, nil
|
return configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunKernelDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
if t.udpMux != nil {
|
if t.udpMux != nil {
|
||||||
return t.udpMux, nil
|
return t.udpMux, nil
|
||||||
}
|
}
|
||||||
@@ -101,19 +101,14 @@ func (t *TunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var udpConn net.PacketConn = rawSock
|
bindParams := udpmux.UniversalUDPMuxParams{
|
||||||
if !nbnet.AdvancedRouting() {
|
UDPConn: nbnet.WrapPacketConn(rawSock),
|
||||||
udpConn = nbnet.WrapPacketConn(rawSock)
|
|
||||||
}
|
|
||||||
|
|
||||||
bindParams := bind.UniversalUDPMuxParams{
|
|
||||||
UDPConn: udpConn,
|
|
||||||
Net: t.transportNet,
|
Net: t.transportNet,
|
||||||
FilterFn: t.filterFn,
|
FilterFn: t.filterFn,
|
||||||
WGAddress: t.address,
|
WGAddress: t.address,
|
||||||
MTU: t.mtu,
|
MTU: t.mtu,
|
||||||
}
|
}
|
||||||
mux := bind.NewUniversalUDPMuxDefault(bindParams)
|
mux := udpmux.NewUniversalUDPMuxDefault(bindParams)
|
||||||
go mux.ReadFromConn(t.ctx)
|
go mux.ReadFromConn(t.ctx)
|
||||||
t.udpMuxConn = rawSock
|
t.udpMuxConn = rawSock
|
||||||
t.udpMux = mux
|
t.udpMux = mux
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TunNetstackDevice struct {
|
type TunNetstackDevice struct {
|
||||||
@@ -26,7 +27,7 @@ type TunNetstackDevice struct {
|
|||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
nsTun *nbnetstack.NetStackTun
|
nsTun *nbnetstack.NetStackTun
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
|
|
||||||
net *netstack.Net
|
net *netstack.Net
|
||||||
@@ -80,7 +81,7 @@ func (t *TunNetstackDevice) create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunNetstackDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunNetstackDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
if t.device == nil {
|
if t.device == nil {
|
||||||
return nil, fmt.Errorf("device is not ready yet")
|
return nil, fmt.Errorf("device is not ready yet")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ type USPDevice struct {
|
|||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ func (t *USPDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *USPDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *USPDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
if t.device == nil {
|
if t.device == nil {
|
||||||
return nil, fmt.Errorf("device is not ready yet")
|
return nil, fmt.Errorf("device is not ready yet")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ type TunDevice struct {
|
|||||||
device *device.Device
|
device *device.Device
|
||||||
nativeTunDevice *tun.NativeTun
|
nativeTunDevice *tun.NativeTun
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import (
|
|||||||
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WGTunDevice interface {
|
type WGTunDevice interface {
|
||||||
Create(routes []string, dns string, searchDomains []string) (device.WGConfigurer, error)
|
Create(routes []string, dns string, searchDomains []string) (device.WGConfigurer, error)
|
||||||
Up() (*bind.UniversalUDPMuxDefault, error)
|
Up() (*udpmux.UniversalUDPMuxDefault, error)
|
||||||
UpdateAddr(address wgaddr.Address) error
|
UpdateAddr(address wgaddr.Address) error
|
||||||
WgAddress() wgaddr.Address
|
WgAddress() wgaddr.Address
|
||||||
MTU() uint16
|
MTU() uint16
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import (
|
|||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/errors"
|
"github.com/netbirdio/netbird/client/errors"
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
||||||
"github.com/netbirdio/netbird/monotime"
|
"github.com/netbirdio/netbird/monotime"
|
||||||
@@ -61,7 +61,7 @@ type WGIFaceOpts struct {
|
|||||||
MTU uint16
|
MTU uint16
|
||||||
MobileArgs *device.MobileIFaceArguments
|
MobileArgs *device.MobileIFaceArguments
|
||||||
TransportNet transport.Net
|
TransportNet transport.Net
|
||||||
FilterFn bind.FilterFn
|
FilterFn udpmux.FilterFn
|
||||||
DisableDNS bool
|
DisableDNS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ func (r *WGIface) ToInterface() *net.Interface {
|
|||||||
|
|
||||||
// Up configures a Wireguard interface
|
// Up configures a Wireguard interface
|
||||||
// The interface must exist before calling this method (e.g. call interface.Create() before)
|
// The interface must exist before calling this method (e.g. call interface.Create() before)
|
||||||
func (w *WGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (w *WGIface) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package bind
|
package udpmux
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Most of this code was copied from https://github.com/pion/ice and modified to fulfill NetBird's requirements
|
Most of this code was copied from https://github.com/pion/ice and modified to fulfill NetBird's requirements
|
||||||
@@ -16,11 +16,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type udpMuxedConnParams struct {
|
type udpMuxedConnParams struct {
|
||||||
Mux *UDPMuxDefault
|
Mux *SingleSocketUDPMux
|
||||||
AddrPool *sync.Pool
|
AddrPool *sync.Pool
|
||||||
Key string
|
Key string
|
||||||
LocalAddr net.Addr
|
LocalAddr net.Addr
|
||||||
Logger logging.LeveledLogger
|
Logger logging.LeveledLogger
|
||||||
|
CandidateID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag
|
// udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag
|
||||||
@@ -119,6 +120,10 @@ func (c *udpMuxedConn) Close() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *udpMuxedConn) GetCandidateID() string {
|
||||||
|
return c.params.CandidateID
|
||||||
|
}
|
||||||
|
|
||||||
func (c *udpMuxedConn) isClosed() bool {
|
func (c *udpMuxedConn) isClosed() bool {
|
||||||
select {
|
select {
|
||||||
case <-c.closedChan:
|
case <-c.closedChan:
|
||||||
64
client/iface/udpmux/doc.go
Normal file
64
client/iface/udpmux/doc.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Package udpmux provides a custom implementation of a UDP multiplexer
|
||||||
|
// that allows multiple logical ICE connections to share a single underlying
|
||||||
|
// UDP socket. This is based on Pion's ICE library, with modifications for
|
||||||
|
// NetBird's requirements.
|
||||||
|
//
|
||||||
|
// # Background
|
||||||
|
//
|
||||||
|
// In WebRTC and NAT traversal scenarios, ICE (Interactive Connectivity
|
||||||
|
// Establishment) is responsible for discovering candidate network paths
|
||||||
|
// and maintaining connectivity between peers. Each ICE connection
|
||||||
|
// normally requires a dedicated UDP socket. However, using one socket
|
||||||
|
// per candidate can be inefficient and difficult to manage.
|
||||||
|
//
|
||||||
|
// This package introduces SingleSocketUDPMux, which allows multiple ICE
|
||||||
|
// candidate connections (muxed connections) to share a single UDP socket.
|
||||||
|
// It handles demultiplexing of packets based on ICE ufrag values, STUN
|
||||||
|
// attributes, and candidate IDs.
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// The typical flow is:
|
||||||
|
//
|
||||||
|
// 1. Create a UDP socket (net.PacketConn).
|
||||||
|
// 2. Construct Params with the socket and optional logger/net stack.
|
||||||
|
// 3. Call NewSingleSocketUDPMux(params).
|
||||||
|
// 4. For each ICE candidate ufrag, call GetConn(ufrag, addr, candidateID)
|
||||||
|
// to obtain a logical PacketConn.
|
||||||
|
// 5. Use the returned PacketConn just like a normal UDP connection.
|
||||||
|
//
|
||||||
|
// # STUN Message Routing Logic
|
||||||
|
//
|
||||||
|
// When a STUN packet arrives, the mux decides which connection should
|
||||||
|
// receive it using this routing logic:
|
||||||
|
//
|
||||||
|
// Primary Routing: Candidate Pair ID
|
||||||
|
// - Extract the candidate pair ID from the STUN message using
|
||||||
|
// ice.CandidatePairIDFromSTUN(msg)
|
||||||
|
// - The target candidate is the locally generated candidate that
|
||||||
|
// corresponds to the connection that should handle this STUN message
|
||||||
|
// - If found, use the target candidate ID to lookup the specific
|
||||||
|
// connection in candidateConnMap
|
||||||
|
// - Route the message directly to that connection
|
||||||
|
//
|
||||||
|
// Fallback Routing: Broadcasting
|
||||||
|
// When candidate pair ID is not available or lookup fails:
|
||||||
|
// - Collect connections from addressMap based on source address
|
||||||
|
// - Find connection using username attribute (ufrag) from STUN message
|
||||||
|
// - Remove duplicate connections from the list
|
||||||
|
// - Send the STUN message to all collected connections
|
||||||
|
//
|
||||||
|
// # Peer Reflexive Candidate Discovery
|
||||||
|
//
|
||||||
|
// When a remote peer sends a STUN message from an unknown source address
|
||||||
|
// (from a candidate that has not been exchanged via signal), the ICE
|
||||||
|
// library will:
|
||||||
|
// - Generate a new peer reflexive candidate for this source address
|
||||||
|
// - Extract or assign a candidate ID based on the STUN message attributes
|
||||||
|
// - Create a mapping between the new peer reflexive candidate ID and
|
||||||
|
// the appropriate local connection
|
||||||
|
//
|
||||||
|
// This discovery mechanism ensures that STUN messages from newly discovered
|
||||||
|
// peer reflexive candidates can be properly routed to the correct local
|
||||||
|
// connection without requiring fallback broadcasting.
|
||||||
|
package udpmux
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package bind
|
package udpmux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
"github.com/pion/logging"
|
"github.com/pion/logging"
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
"github.com/pion/transport/v3"
|
"github.com/pion/transport/v3"
|
||||||
"github.com/pion/transport/v3/stdnet"
|
"github.com/pion/transport/v3/stdnet"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -22,9 +22,9 @@ import (
|
|||||||
|
|
||||||
const receiveMTU = 8192
|
const receiveMTU = 8192
|
||||||
|
|
||||||
// UDPMuxDefault is an implementation of the interface
|
// SingleSocketUDPMux is an implementation of the interface
|
||||||
type UDPMuxDefault struct {
|
type SingleSocketUDPMux struct {
|
||||||
params UDPMuxParams
|
params Params
|
||||||
|
|
||||||
closedChan chan struct{}
|
closedChan chan struct{}
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
@@ -32,6 +32,9 @@ type UDPMuxDefault struct {
|
|||||||
// connsIPv4 and connsIPv6 are maps of all udpMuxedConn indexed by ufrag|network|candidateType
|
// connsIPv4 and connsIPv6 are maps of all udpMuxedConn indexed by ufrag|network|candidateType
|
||||||
connsIPv4, connsIPv6 map[string]*udpMuxedConn
|
connsIPv4, connsIPv6 map[string]*udpMuxedConn
|
||||||
|
|
||||||
|
// candidateConnMap maps local candidate IDs to their corresponding connection.
|
||||||
|
candidateConnMap map[string]*udpMuxedConn
|
||||||
|
|
||||||
addressMapMu sync.RWMutex
|
addressMapMu sync.RWMutex
|
||||||
addressMap map[string][]*udpMuxedConn
|
addressMap map[string][]*udpMuxedConn
|
||||||
|
|
||||||
@@ -46,8 +49,8 @@ type UDPMuxDefault struct {
|
|||||||
|
|
||||||
const maxAddrSize = 512
|
const maxAddrSize = 512
|
||||||
|
|
||||||
// UDPMuxParams are parameters for UDPMux.
|
// Params are parameters for UDPMux.
|
||||||
type UDPMuxParams struct {
|
type Params struct {
|
||||||
Logger logging.LeveledLogger
|
Logger logging.LeveledLogger
|
||||||
UDPConn net.PacketConn
|
UDPConn net.PacketConn
|
||||||
|
|
||||||
@@ -147,18 +150,19 @@ func isZeros(ip net.IP) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUDPMuxDefault creates an implementation of UDPMux
|
// NewSingleSocketUDPMux creates an implementation of UDPMux
|
||||||
func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault {
|
func NewSingleSocketUDPMux(params Params) *SingleSocketUDPMux {
|
||||||
if params.Logger == nil {
|
if params.Logger == nil {
|
||||||
params.Logger = getLogger()
|
params.Logger = getLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := &UDPMuxDefault{
|
mux := &SingleSocketUDPMux{
|
||||||
addressMap: map[string][]*udpMuxedConn{},
|
addressMap: map[string][]*udpMuxedConn{},
|
||||||
params: params,
|
params: params,
|
||||||
connsIPv4: make(map[string]*udpMuxedConn),
|
connsIPv4: make(map[string]*udpMuxedConn),
|
||||||
connsIPv6: make(map[string]*udpMuxedConn),
|
connsIPv6: make(map[string]*udpMuxedConn),
|
||||||
closedChan: make(chan struct{}, 1),
|
candidateConnMap: make(map[string]*udpMuxedConn),
|
||||||
|
closedChan: make(chan struct{}, 1),
|
||||||
pool: &sync.Pool{
|
pool: &sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
// big enough buffer to fit both packet and address
|
// big enough buffer to fit both packet and address
|
||||||
@@ -171,15 +175,15 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault {
|
|||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *UDPMuxDefault) updateLocalAddresses() {
|
func (m *SingleSocketUDPMux) updateLocalAddresses() {
|
||||||
var localAddrsForUnspecified []net.Addr
|
var localAddrsForUnspecified []net.Addr
|
||||||
if addr, ok := m.params.UDPConn.LocalAddr().(*net.UDPAddr); !ok {
|
if addr, ok := m.params.UDPConn.LocalAddr().(*net.UDPAddr); !ok {
|
||||||
m.params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", m.params.UDPConn.LocalAddr())
|
m.params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", m.params.UDPConn.LocalAddr())
|
||||||
} else if ok && addr.IP.IsUnspecified() {
|
} else if ok && addr.IP.IsUnspecified() {
|
||||||
// For unspecified addresses, the correct behavior is to return errListenUnspecified, but
|
// For unspecified addresses, the correct behavior is to return errListenUnspecified, but
|
||||||
// it will break the applications that are already using unspecified UDP connection
|
// it will break the applications that are already using unspecified UDP connection
|
||||||
// with UDPMuxDefault, so print a warn log and create a local address list for mux.
|
// with SingleSocketUDPMux, so print a warn log and create a local address list for mux.
|
||||||
m.params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead")
|
m.params.Logger.Warn("SingleSocketUDPMux should not listening on unspecified address, use NewMultiUDPMuxFromPort instead")
|
||||||
var networks []ice.NetworkType
|
var networks []ice.NetworkType
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
@@ -216,13 +220,13 @@ func (m *UDPMuxDefault) updateLocalAddresses() {
|
|||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalAddr returns the listening address of this UDPMuxDefault
|
// LocalAddr returns the listening address of this SingleSocketUDPMux
|
||||||
func (m *UDPMuxDefault) LocalAddr() net.Addr {
|
func (m *SingleSocketUDPMux) LocalAddr() net.Addr {
|
||||||
return m.params.UDPConn.LocalAddr()
|
return m.params.UDPConn.LocalAddr()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetListenAddresses returns the list of addresses that this mux is listening on
|
// GetListenAddresses returns the list of addresses that this mux is listening on
|
||||||
func (m *UDPMuxDefault) GetListenAddresses() []net.Addr {
|
func (m *SingleSocketUDPMux) GetListenAddresses() []net.Addr {
|
||||||
m.updateLocalAddresses()
|
m.updateLocalAddresses()
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -236,7 +240,7 @@ func (m *UDPMuxDefault) GetListenAddresses() []net.Addr {
|
|||||||
|
|
||||||
// GetConn returns a PacketConn given the connection's ufrag and network address
|
// GetConn returns a PacketConn given the connection's ufrag and network address
|
||||||
// creates the connection if an existing one can't be found
|
// creates the connection if an existing one can't be found
|
||||||
func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) {
|
func (m *SingleSocketUDPMux) GetConn(ufrag string, addr net.Addr, candidateID string) (net.PacketConn, error) {
|
||||||
// don't check addr for mux using unspecified address
|
// don't check addr for mux using unspecified address
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
lenLocalAddrs := len(m.localAddrsForUnspecified)
|
lenLocalAddrs := len(m.localAddrsForUnspecified)
|
||||||
@@ -260,12 +264,14 @@ func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, er
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c := m.createMuxedConn(ufrag)
|
c := m.createMuxedConn(ufrag, candidateID)
|
||||||
go func() {
|
go func() {
|
||||||
<-c.CloseChannel()
|
<-c.CloseChannel()
|
||||||
m.RemoveConnByUfrag(ufrag)
|
m.RemoveConnByUfrag(ufrag)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
m.candidateConnMap[candidateID] = c
|
||||||
|
|
||||||
if isIPv6 {
|
if isIPv6 {
|
||||||
m.connsIPv6[ufrag] = c
|
m.connsIPv6[ufrag] = c
|
||||||
} else {
|
} else {
|
||||||
@@ -276,7 +282,7 @@ func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveConnByUfrag stops and removes the muxed packet connection
|
// RemoveConnByUfrag stops and removes the muxed packet connection
|
||||||
func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) {
|
func (m *SingleSocketUDPMux) RemoveConnByUfrag(ufrag string) {
|
||||||
removedConns := make([]*udpMuxedConn, 0, 2)
|
removedConns := make([]*udpMuxedConn, 0, 2)
|
||||||
|
|
||||||
// Keep lock section small to avoid deadlock with conn lock
|
// Keep lock section small to avoid deadlock with conn lock
|
||||||
@@ -284,10 +290,12 @@ func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) {
|
|||||||
if c, ok := m.connsIPv4[ufrag]; ok {
|
if c, ok := m.connsIPv4[ufrag]; ok {
|
||||||
delete(m.connsIPv4, ufrag)
|
delete(m.connsIPv4, ufrag)
|
||||||
removedConns = append(removedConns, c)
|
removedConns = append(removedConns, c)
|
||||||
|
delete(m.candidateConnMap, c.GetCandidateID())
|
||||||
}
|
}
|
||||||
if c, ok := m.connsIPv6[ufrag]; ok {
|
if c, ok := m.connsIPv6[ufrag]; ok {
|
||||||
delete(m.connsIPv6, ufrag)
|
delete(m.connsIPv6, ufrag)
|
||||||
removedConns = append(removedConns, c)
|
removedConns = append(removedConns, c)
|
||||||
|
delete(m.candidateConnMap, c.GetCandidateID())
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
@@ -314,7 +322,7 @@ func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsClosed returns true if the mux had been closed
|
// IsClosed returns true if the mux had been closed
|
||||||
func (m *UDPMuxDefault) IsClosed() bool {
|
func (m *SingleSocketUDPMux) IsClosed() bool {
|
||||||
select {
|
select {
|
||||||
case <-m.closedChan:
|
case <-m.closedChan:
|
||||||
return true
|
return true
|
||||||
@@ -324,7 +332,7 @@ func (m *UDPMuxDefault) IsClosed() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close the mux, no further connections could be created
|
// Close the mux, no further connections could be created
|
||||||
func (m *UDPMuxDefault) Close() error {
|
func (m *SingleSocketUDPMux) Close() error {
|
||||||
var err error
|
var err error
|
||||||
m.closeOnce.Do(func() {
|
m.closeOnce.Do(func() {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -347,11 +355,11 @@ func (m *UDPMuxDefault) Close() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *UDPMuxDefault) writeTo(buf []byte, rAddr net.Addr) (n int, err error) {
|
func (m *SingleSocketUDPMux) writeTo(buf []byte, rAddr net.Addr) (n int, err error) {
|
||||||
return m.params.UDPConn.WriteTo(buf, rAddr)
|
return m.params.UDPConn.WriteTo(buf, rAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *UDPMuxDefault) registerConnForAddress(conn *udpMuxedConn, addr string) {
|
func (m *SingleSocketUDPMux) registerConnForAddress(conn *udpMuxedConn, addr string) {
|
||||||
if m.IsClosed() {
|
if m.IsClosed() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -368,81 +376,109 @@ func (m *UDPMuxDefault) registerConnForAddress(conn *udpMuxedConn, addr string)
|
|||||||
log.Debugf("ICE: registered %s for %s", addr, conn.params.Key)
|
log.Debugf("ICE: registered %s for %s", addr, conn.params.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *UDPMuxDefault) createMuxedConn(key string) *udpMuxedConn {
|
func (m *SingleSocketUDPMux) createMuxedConn(key string, candidateID string) *udpMuxedConn {
|
||||||
c := newUDPMuxedConn(&udpMuxedConnParams{
|
c := newUDPMuxedConn(&udpMuxedConnParams{
|
||||||
Mux: m,
|
Mux: m,
|
||||||
Key: key,
|
Key: key,
|
||||||
AddrPool: m.pool,
|
AddrPool: m.pool,
|
||||||
LocalAddr: m.LocalAddr(),
|
LocalAddr: m.LocalAddr(),
|
||||||
Logger: m.params.Logger,
|
Logger: m.params.Logger,
|
||||||
|
CandidateID: candidateID,
|
||||||
})
|
})
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSTUNMessage handles STUN packets and forwards them to underlying pion/ice library
|
// HandleSTUNMessage handles STUN packets and forwards them to underlying pion/ice library
|
||||||
func (m *UDPMuxDefault) HandleSTUNMessage(msg *stun.Message, addr net.Addr) error {
|
func (m *SingleSocketUDPMux) HandleSTUNMessage(msg *stun.Message, addr net.Addr) error {
|
||||||
|
|
||||||
remoteAddr, ok := addr.(*net.UDPAddr)
|
remoteAddr, ok := addr.(*net.UDPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("underlying PacketConn did not return a UDPAddr")
|
return fmt.Errorf("underlying PacketConn did not return a UDPAddr")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have already seen this address dispatch to the appropriate destination
|
// Try to route to specific candidate connection first
|
||||||
// If you are using the same socket for the Host and SRFLX candidates, it might be that there are more than one
|
if conn := m.findCandidateConnection(msg); conn != nil {
|
||||||
// muxed connection - one for the SRFLX candidate and the other one for the HOST one.
|
return conn.writePacket(msg.Raw, remoteAddr)
|
||||||
// We will then forward STUN packets to each of these connections.
|
}
|
||||||
m.addressMapMu.RLock()
|
|
||||||
|
// Fallback: route to all possible connections
|
||||||
|
return m.forwardToAllConnections(msg, addr, remoteAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findCandidateConnection attempts to find the specific connection for a STUN message
|
||||||
|
func (m *SingleSocketUDPMux) findCandidateConnection(msg *stun.Message) *udpMuxedConn {
|
||||||
|
candidatePairID, ok, err := ice.CandidatePairIDFromSTUN(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
} else if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
conn, exists := m.candidateConnMap[candidatePairID.TargetCandidateID()]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardToAllConnections forwards STUN message to all relevant connections
|
||||||
|
func (m *SingleSocketUDPMux) forwardToAllConnections(msg *stun.Message, addr net.Addr, remoteAddr *net.UDPAddr) error {
|
||||||
var destinationConnList []*udpMuxedConn
|
var destinationConnList []*udpMuxedConn
|
||||||
|
|
||||||
|
// Add connections from address map
|
||||||
|
m.addressMapMu.RLock()
|
||||||
if storedConns, ok := m.addressMap[addr.String()]; ok {
|
if storedConns, ok := m.addressMap[addr.String()]; ok {
|
||||||
destinationConnList = append(destinationConnList, storedConns...)
|
destinationConnList = append(destinationConnList, storedConns...)
|
||||||
}
|
}
|
||||||
m.addressMapMu.RUnlock()
|
m.addressMapMu.RUnlock()
|
||||||
|
|
||||||
var isIPv6 bool
|
if conn, ok := m.findConnectionByUsername(msg, addr); ok {
|
||||||
if udpAddr, _ := addr.(*net.UDPAddr); udpAddr != nil && udpAddr.IP.To4() == nil {
|
// If we have already seen this address dispatch to the appropriate destination
|
||||||
isIPv6 = true
|
// If you are using the same socket for the Host and SRFLX candidates, it might be that there are more than one
|
||||||
|
// muxed connection - one for the SRFLX candidate and the other one for the HOST one.
|
||||||
|
// We will then forward STUN packets to each of these connections.
|
||||||
|
if !m.connectionExists(conn, destinationConnList) {
|
||||||
|
destinationConnList = append(destinationConnList, conn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This block is needed to discover Peer Reflexive Candidates for which we don't know the Endpoint upfront.
|
// Forward to all found connections
|
||||||
// However, we can take a username attribute from the STUN message which contains ufrag.
|
|
||||||
// We can use ufrag to identify the destination conn to route packet to.
|
|
||||||
attr, stunAttrErr := msg.Get(stun.AttrUsername)
|
|
||||||
if stunAttrErr == nil {
|
|
||||||
ufrag := strings.Split(string(attr), ":")[0]
|
|
||||||
|
|
||||||
m.mu.Lock()
|
|
||||||
destinationConn := m.connsIPv4[ufrag]
|
|
||||||
if isIPv6 {
|
|
||||||
destinationConn = m.connsIPv6[ufrag]
|
|
||||||
}
|
|
||||||
|
|
||||||
if destinationConn != nil {
|
|
||||||
exists := false
|
|
||||||
for _, conn := range destinationConnList {
|
|
||||||
if conn.params.Key == destinationConn.params.Key {
|
|
||||||
exists = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
destinationConnList = append(destinationConnList, destinationConn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward STUN packets to each destination connections even thought the STUN packet might not belong there.
|
|
||||||
// It will be discarded by the further ICE candidate logic if so.
|
|
||||||
for _, conn := range destinationConnList {
|
for _, conn := range destinationConnList {
|
||||||
if err := conn.writePacket(msg.Raw, remoteAddr); err != nil {
|
if err := conn.writePacket(msg.Raw, remoteAddr); err != nil {
|
||||||
log.Errorf("could not write packet: %v", err)
|
log.Errorf("could not write packet: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *UDPMuxDefault) getConn(ufrag string, isIPv6 bool) (val *udpMuxedConn, ok bool) {
|
// findConnectionByUsername finds connection using username attribute from STUN message
|
||||||
|
func (m *SingleSocketUDPMux) findConnectionByUsername(msg *stun.Message, addr net.Addr) (*udpMuxedConn, bool) {
|
||||||
|
attr, err := msg.Get(stun.AttrUsername)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
ufrag := strings.Split(string(attr), ":")[0]
|
||||||
|
isIPv6 := isIPv6Address(addr)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
return m.getConn(ufrag, isIPv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectionExists checks if a connection already exists in the list
|
||||||
|
func (m *SingleSocketUDPMux) connectionExists(target *udpMuxedConn, conns []*udpMuxedConn) bool {
|
||||||
|
for _, conn := range conns {
|
||||||
|
if conn.params.Key == target.params.Key {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SingleSocketUDPMux) getConn(ufrag string, isIPv6 bool) (val *udpMuxedConn, ok bool) {
|
||||||
if isIPv6 {
|
if isIPv6 {
|
||||||
val, ok = m.connsIPv6[ufrag]
|
val, ok = m.connsIPv6[ufrag]
|
||||||
} else {
|
} else {
|
||||||
@@ -451,6 +487,13 @@ func (m *UDPMuxDefault) getConn(ufrag string, isIPv6 bool) (val *udpMuxedConn, o
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isIPv6Address(addr net.Addr) bool {
|
||||||
|
if udpAddr, ok := addr.(*net.UDPAddr); ok {
|
||||||
|
return udpAddr.IP.To4() == nil
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type bufferHolder struct {
|
type bufferHolder struct {
|
||||||
buf []byte
|
buf []byte
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
//go:build !ios
|
//go:build !ios
|
||||||
|
|
||||||
package bind
|
package udpmux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *UDPMuxDefault) notifyAddressRemoval(addr string) {
|
func (m *SingleSocketUDPMux) notifyAddressRemoval(addr string) {
|
||||||
// Kernel mode: direct nbnet.PacketConn (SharedSocket wrapped with nbnet)
|
// Kernel mode: direct nbnet.PacketConn (SharedSocket wrapped with nbnet)
|
||||||
if conn, ok := m.params.UDPConn.(*nbnet.PacketConn); ok {
|
if conn, ok := m.params.UDPConn.(*nbnet.PacketConn); ok {
|
||||||
conn.RemoveAddress(addr)
|
conn.RemoveAddress(addr)
|
||||||
7
client/iface/udpmux/mux_ios.go
Normal file
7
client/iface/udpmux/mux_ios.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build ios
|
||||||
|
|
||||||
|
package udpmux
|
||||||
|
|
||||||
|
func (m *SingleSocketUDPMux) notifyAddressRemoval(addr string) {
|
||||||
|
// iOS doesn't support nbnet hooks, so this is a no-op
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package bind
|
package udpmux
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Most of this code was copied from https://github.com/pion/ice and modified to fulfill NetBird's requirements.
|
Most of this code was copied from https://github.com/pion/ice and modified to fulfill NetBird's requirements.
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/pion/logging"
|
"github.com/pion/logging"
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
"github.com/pion/transport/v3"
|
"github.com/pion/transport/v3"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bufsize"
|
"github.com/netbirdio/netbird/client/iface/bufsize"
|
||||||
@@ -29,7 +29,7 @@ type FilterFn func(address netip.Addr) (bool, netip.Prefix, error)
|
|||||||
// UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn
|
// UniversalUDPMuxDefault handles STUN and TURN servers packets by wrapping the original UDPConn
|
||||||
// It then passes packets to the UDPMux that does the actual connection muxing.
|
// It then passes packets to the UDPMux that does the actual connection muxing.
|
||||||
type UniversalUDPMuxDefault struct {
|
type UniversalUDPMuxDefault struct {
|
||||||
*UDPMuxDefault
|
*SingleSocketUDPMux
|
||||||
params UniversalUDPMuxParams
|
params UniversalUDPMuxParams
|
||||||
|
|
||||||
// since we have a shared socket, for srflx candidates it makes sense to have a shared mapped address across all the agents
|
// since we have a shared socket, for srflx candidates it makes sense to have a shared mapped address across all the agents
|
||||||
@@ -72,12 +72,12 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef
|
|||||||
address: params.WGAddress,
|
address: params.WGAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
udpMuxParams := UDPMuxParams{
|
udpMuxParams := Params{
|
||||||
Logger: params.Logger,
|
Logger: params.Logger,
|
||||||
UDPConn: m.params.UDPConn,
|
UDPConn: m.params.UDPConn,
|
||||||
Net: m.params.Net,
|
Net: m.params.Net,
|
||||||
}
|
}
|
||||||
m.UDPMuxDefault = NewUDPMuxDefault(udpMuxParams)
|
m.SingleSocketUDPMux = NewSingleSocketUDPMux(udpMuxParams)
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@@ -211,8 +211,8 @@ func (m *UniversalUDPMuxDefault) GetRelayedAddr(turnAddr net.Addr, deadline time
|
|||||||
|
|
||||||
// GetConnForURL add uniques to the muxed connection by concatenating ufrag and URL (e.g. STUN URL) to be able to support multiple STUN/TURN servers
|
// GetConnForURL add uniques to the muxed connection by concatenating ufrag and URL (e.g. STUN URL) to be able to support multiple STUN/TURN servers
|
||||||
// and return a unique connection per server.
|
// and return a unique connection per server.
|
||||||
func (m *UniversalUDPMuxDefault) GetConnForURL(ufrag string, url string, addr net.Addr) (net.PacketConn, error) {
|
func (m *UniversalUDPMuxDefault) GetConnForURL(ufrag string, url string, addr net.Addr, candidateID string) (net.PacketConn, error) {
|
||||||
return m.UDPMuxDefault.GetConn(fmt.Sprintf("%s%s", ufrag, url), addr)
|
return m.SingleSocketUDPMux.GetConn(fmt.Sprintf("%s%s", ufrag, url), addr, candidateID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleSTUNMessage discovers STUN packets that carry a XOR mapped address from a STUN server.
|
// HandleSTUNMessage discovers STUN packets that carry a XOR mapped address from a STUN server.
|
||||||
@@ -233,7 +233,7 @@ func (m *UniversalUDPMuxDefault) HandleSTUNMessage(msg *stun.Message, addr net.A
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return m.UDPMuxDefault.HandleSTUNMessage(msg, addr)
|
return m.SingleSocketUDPMux.HandleSTUNMessage(msg, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isXORMappedResponse indicates whether the message is a XORMappedAddress and is coming from the known STUN server.
|
// isXORMappedResponse indicates whether the message is a XORMappedAddress and is coming from the known STUN server.
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/bufsize"
|
"github.com/netbirdio/netbird/client/iface/bufsize"
|
||||||
"github.com/netbirdio/netbird/client/internal/ebpf"
|
"github.com/netbirdio/netbird/client/internal/ebpf"
|
||||||
ebpfMgr "github.com/netbirdio/netbird/client/internal/ebpf/manager"
|
ebpfMgr "github.com/netbirdio/netbird/client/internal/ebpf/manager"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import (
|
|||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
signal "github.com/netbirdio/netbird/shared/signal/client"
|
signal "github.com/netbirdio/netbird/shared/signal/client"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
c.engine.SetSyncResponsePersistence(c.persistSyncResponse)
|
c.engine.SetSyncResponsePersistence(c.persistSyncResponse)
|
||||||
c.engineMutex.Unlock()
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
if err := c.engine.Start(); err != nil {
|
if err := c.engine.Start(loginResp.GetNetbirdConfig(), c.config.ManagementURL); err != nil {
|
||||||
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
@@ -284,10 +284,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
|||||||
state.Set(StatusConnected)
|
state.Set(StatusConnected)
|
||||||
|
|
||||||
if runningChan != nil {
|
if runningChan != nil {
|
||||||
select {
|
close(runningChan)
|
||||||
case runningChan <- struct{}{}:
|
runningChan = nil
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<-engineCtx.Done()
|
<-engineCtx.Done()
|
||||||
|
|||||||
201
client/internal/dns/config/domains.go
Normal file
201
client/internal/dns/config/domains.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrEmptyURL = errors.New("empty URL")
|
||||||
|
ErrEmptyHost = errors.New("empty host")
|
||||||
|
ErrIPNotAllowed = errors.New("IP address not allowed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerDomains represents the management server domains extracted from NetBird configuration
|
||||||
|
type ServerDomains struct {
|
||||||
|
Signal domain.Domain
|
||||||
|
Relay []domain.Domain
|
||||||
|
Flow domain.Domain
|
||||||
|
Stuns []domain.Domain
|
||||||
|
Turns []domain.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractFromNetbirdConfig extracts domain information from NetBird protobuf configuration
|
||||||
|
func ExtractFromNetbirdConfig(config *mgmProto.NetbirdConfig) ServerDomains {
|
||||||
|
if config == nil {
|
||||||
|
return ServerDomains{}
|
||||||
|
}
|
||||||
|
|
||||||
|
domains := ServerDomains{}
|
||||||
|
|
||||||
|
domains.Signal = extractSignalDomain(config)
|
||||||
|
domains.Relay = extractRelayDomains(config)
|
||||||
|
domains.Flow = extractFlowDomain(config)
|
||||||
|
domains.Stuns = extractStunDomains(config)
|
||||||
|
domains.Turns = extractTurnDomains(config)
|
||||||
|
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractValidDomain extracts a valid domain from a URL, filtering out IP addresses
|
||||||
|
func ExtractValidDomain(rawURL string) (domain.Domain, error) {
|
||||||
|
if rawURL == "" {
|
||||||
|
return "", ErrEmptyURL
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err == nil {
|
||||||
|
if domain, err := extractFromParsedURL(parsedURL); err != nil || domain != "" {
|
||||||
|
return domain, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractFromRawString(rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFromParsedURL handles domain extraction from successfully parsed URLs
|
||||||
|
func extractFromParsedURL(parsedURL *url.URL) (domain.Domain, error) {
|
||||||
|
if parsedURL.Hostname() != "" {
|
||||||
|
return extractDomainFromHost(parsedURL.Hostname())
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL.Opaque == "" || parsedURL.Scheme == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle URLs with opaque content (e.g., stun:host:port)
|
||||||
|
if strings.Contains(parsedURL.Scheme, ".") {
|
||||||
|
// This is likely "domain.com:port" being parsed as scheme:opaque
|
||||||
|
reconstructed := parsedURL.Scheme + ":" + parsedURL.Opaque
|
||||||
|
if host, _, err := net.SplitHostPort(reconstructed); err == nil {
|
||||||
|
return extractDomainFromHost(host)
|
||||||
|
}
|
||||||
|
return extractDomainFromHost(parsedURL.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid scheme with opaque content (e.g., stun:host:port)
|
||||||
|
host := parsedURL.Opaque
|
||||||
|
if queryIndex := strings.Index(host, "?"); queryIndex > 0 {
|
||||||
|
host = host[:queryIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostOnly, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
return extractDomainFromHost(hostOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractDomainFromHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFromRawString handles domain extraction when URL parsing fails or returns no results
|
||||||
|
func extractFromRawString(rawURL string) (domain.Domain, error) {
|
||||||
|
if host, _, err := net.SplitHostPort(rawURL); err == nil {
|
||||||
|
return extractDomainFromHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractDomainFromHost(rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractDomainFromHost extracts domain from a host string, filtering out IP addresses
|
||||||
|
func extractDomainFromHost(host string) (domain.Domain, error) {
|
||||||
|
if host == "" {
|
||||||
|
return "", ErrEmptyHost
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := netip.ParseAddr(host); err == nil {
|
||||||
|
return "", fmt.Errorf("%w: %s", ErrIPNotAllowed, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := domain.FromString(host)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid domain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractSingleDomain extracts a single domain from a URL with error logging
|
||||||
|
func extractSingleDomain(url, serviceType string) domain.Domain {
|
||||||
|
if url == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := ExtractValidDomain(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Skipping %s: %v", serviceType, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractMultipleDomains extracts multiple domains from URLs with error logging
|
||||||
|
func extractMultipleDomains(urls []string, serviceType string) []domain.Domain {
|
||||||
|
var domains []domain.Domain
|
||||||
|
for _, url := range urls {
|
||||||
|
if url == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d, err := ExtractValidDomain(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Skipping %s: %v", serviceType, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
domains = append(domains, d)
|
||||||
|
}
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractSignalDomain extracts the signal domain from NetBird configuration.
|
||||||
|
func extractSignalDomain(config *mgmProto.NetbirdConfig) domain.Domain {
|
||||||
|
if config.Signal != nil {
|
||||||
|
return extractSingleDomain(config.Signal.Uri, "signal")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractRelayDomains extracts relay server domains from NetBird configuration.
|
||||||
|
func extractRelayDomains(config *mgmProto.NetbirdConfig) []domain.Domain {
|
||||||
|
if config.Relay != nil {
|
||||||
|
return extractMultipleDomains(config.Relay.Urls, "relay")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFlowDomain extracts the traffic flow domain from NetBird configuration.
|
||||||
|
func extractFlowDomain(config *mgmProto.NetbirdConfig) domain.Domain {
|
||||||
|
if config.Flow != nil {
|
||||||
|
return extractSingleDomain(config.Flow.Url, "flow")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractStunDomains extracts STUN server domains from NetBird configuration.
|
||||||
|
func extractStunDomains(config *mgmProto.NetbirdConfig) []domain.Domain {
|
||||||
|
var urls []string
|
||||||
|
for _, stun := range config.Stuns {
|
||||||
|
if stun != nil && stun.Uri != "" {
|
||||||
|
urls = append(urls, stun.Uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extractMultipleDomains(urls, "STUN")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractTurnDomains extracts TURN server domains from NetBird configuration.
|
||||||
|
func extractTurnDomains(config *mgmProto.NetbirdConfig) []domain.Domain {
|
||||||
|
var urls []string
|
||||||
|
for _, turn := range config.Turns {
|
||||||
|
if turn != nil && turn.HostConfig != nil && turn.HostConfig.Uri != "" {
|
||||||
|
urls = append(urls, turn.HostConfig.Uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extractMultipleDomains(urls, "TURN")
|
||||||
|
}
|
||||||
213
client/internal/dns/config/domains_test.go
Normal file
213
client/internal/dns/config/domains_test.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractValidDomain(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
expected string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "HTTPS URL with port",
|
||||||
|
url: "https://api.netbird.io:443",
|
||||||
|
expected: "api.netbird.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTP URL without port",
|
||||||
|
url: "http://signal.example.com",
|
||||||
|
expected: "signal.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Host with port (no scheme)",
|
||||||
|
url: "signal.netbird.io:443",
|
||||||
|
expected: "signal.netbird.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "STUN URL",
|
||||||
|
url: "stun:stun.netbird.io:443",
|
||||||
|
expected: "stun.netbird.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "STUN URL with different port",
|
||||||
|
url: "stun:stun.netbird.io:5555",
|
||||||
|
expected: "stun.netbird.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TURNS URL with query params",
|
||||||
|
url: "turns:turn.netbird.io:443?transport=tcp",
|
||||||
|
expected: "turn.netbird.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TURN URL",
|
||||||
|
url: "turn:turn.example.com:3478",
|
||||||
|
expected: "turn.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "REL URL",
|
||||||
|
url: "rel://relay.example.com:443",
|
||||||
|
expected: "relay.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RELS URL",
|
||||||
|
url: "rels://relay.netbird.io:443",
|
||||||
|
expected: "relay.netbird.io",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Raw hostname",
|
||||||
|
url: "example.org",
|
||||||
|
expected: "example.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP address should be rejected",
|
||||||
|
url: "192.168.1.1",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IP address with port should be rejected",
|
||||||
|
url: "192.168.1.1:443",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address should be rejected",
|
||||||
|
url: "2001:db8::1",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTP URL with IPv4 should be rejected",
|
||||||
|
url: "http://192.168.1.1:8080",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS URL with IPv4 should be rejected",
|
||||||
|
url: "https://10.0.0.1:443",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "STUN URL with IPv4 should be rejected",
|
||||||
|
url: "stun:192.168.1.1:3478",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TURN URL with IPv4 should be rejected",
|
||||||
|
url: "turn:10.0.0.1:3478",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TURNS URL with IPv4 should be rejected",
|
||||||
|
url: "turns:172.16.0.1:5349",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTP URL with IPv6 should be rejected",
|
||||||
|
url: "http://[2001:db8::1]:8080",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS URL with IPv6 should be rejected",
|
||||||
|
url: "https://[::1]:443",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "STUN URL with IPv6 should be rejected",
|
||||||
|
url: "stun:[2001:db8::1]:3478",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 with port should be rejected",
|
||||||
|
url: "[2001:db8::1]:443",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Localhost IPv4 should be rejected",
|
||||||
|
url: "127.0.0.1:8080",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Localhost IPv6 should be rejected",
|
||||||
|
url: "[::1]:443",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "REL URL with IPv4 should be rejected",
|
||||||
|
url: "rel://192.168.1.1:443",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RELS URL with IPv4 should be rejected",
|
||||||
|
url: "rels://10.0.0.1:443",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty URL",
|
||||||
|
url: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := ExtractValidDomain(tt.url)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err, "Expected error for URL: %s", tt.url)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err, "Unexpected error for URL: %s", tt.url)
|
||||||
|
assert.Equal(t, tt.expected, result.SafeString(), "Domain mismatch for URL: %s", tt.url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractDomainFromHost(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
host string
|
||||||
|
expected string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid domain",
|
||||||
|
host: "example.com",
|
||||||
|
expected: "example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Subdomain",
|
||||||
|
host: "api.example.com",
|
||||||
|
expected: "api.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 address",
|
||||||
|
host: "192.168.1.1",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address",
|
||||||
|
host: "2001:db8::1",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty host",
|
||||||
|
host: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := extractDomainFromHost(tt.host)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err, "Expected error for host: %s", tt.host)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err, "Unexpected error for host: %s", tt.host)
|
||||||
|
assert.Equal(t, tt.expected, result.SafeString(), "Domain mismatch for host: %s", tt.host)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,11 +11,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PriorityLocal = 100
|
PriorityMgmtCache = 150
|
||||||
PriorityDNSRoute = 75
|
PriorityLocal = 100
|
||||||
PriorityUpstream = 50
|
PriorityDNSRoute = 75
|
||||||
PriorityDefault = 1
|
PriorityUpstream = 50
|
||||||
PriorityFallback = -100
|
PriorityDefault = 1
|
||||||
|
PriorityFallback = -100
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubdomainMatcher interface {
|
type SubdomainMatcher interface {
|
||||||
@@ -182,7 +183,10 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
|
|
||||||
// If handler wants to continue, try next handler
|
// If handler wants to continue, try next handler
|
||||||
if chainWriter.shouldContinue {
|
if chainWriter.shouldContinue {
|
||||||
log.Tracef("handler requested continue to next handler for domain=%s", qname)
|
// Only log continue for non-management cache handlers to reduce noise
|
||||||
|
if entry.Priority != PriorityMgmtCache {
|
||||||
|
log.Tracef("handler requested continue to next handler for domain=%s", qname)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (d *Resolver) MatchSubdomains() bool {
|
|||||||
|
|
||||||
// String returns a string representation of the local resolver
|
// String returns a string representation of the local resolver
|
||||||
func (d *Resolver) String() string {
|
func (d *Resolver) String() string {
|
||||||
return fmt.Sprintf("local resolver [%d records]", len(d.records))
|
return fmt.Sprintf("LocalResolver [%d records]", len(d.records))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Resolver) Stop() {}
|
func (d *Resolver) Stop() {}
|
||||||
|
|||||||
360
client/internal/dns/mgmt/mgmt.go
Normal file
360
client/internal/dns/mgmt/mgmt.go
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
package mgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dnsTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
// Resolver caches critical NetBird infrastructure domains
|
||||||
|
type Resolver struct {
|
||||||
|
records map[dns.Question][]dns.RR
|
||||||
|
mgmtDomain *domain.Domain
|
||||||
|
serverDomains *dnsconfig.ServerDomains
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResolver creates a new management domains cache resolver.
|
||||||
|
func NewResolver() *Resolver {
|
||||||
|
return &Resolver{
|
||||||
|
records: make(map[dns.Question][]dns.RR),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the resolver.
|
||||||
|
func (m *Resolver) String() string {
|
||||||
|
return "MgmtCacheResolver"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeDNS implements dns.Handler interface.
|
||||||
|
func (m *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
if len(r.Question) == 0 {
|
||||||
|
m.continueToNext(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
question := r.Question[0]
|
||||||
|
question.Name = strings.ToLower(dns.Fqdn(question.Name))
|
||||||
|
|
||||||
|
if question.Qtype != dns.TypeA && question.Qtype != dns.TypeAAAA {
|
||||||
|
m.continueToNext(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.RLock()
|
||||||
|
records, found := m.records[question]
|
||||||
|
m.mutex.RUnlock()
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
m.continueToNext(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &dns.Msg{}
|
||||||
|
resp.SetReply(r)
|
||||||
|
resp.Authoritative = false
|
||||||
|
resp.RecursionAvailable = true
|
||||||
|
|
||||||
|
resp.Answer = append(resp.Answer, records...)
|
||||||
|
|
||||||
|
log.Debugf("serving %d cached records for domain=%s", len(resp.Answer), question.Name)
|
||||||
|
|
||||||
|
if err := w.WriteMsg(resp); err != nil {
|
||||||
|
log.Errorf("failed to write response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchSubdomains returns false since this resolver only handles exact domain matches
|
||||||
|
// for NetBird infrastructure domains (signal, relay, flow, etc.), not their subdomains.
|
||||||
|
func (m *Resolver) MatchSubdomains() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// continueToNext signals the handler chain to continue to the next handler.
|
||||||
|
func (m *Resolver) continueToNext(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
resp := &dns.Msg{}
|
||||||
|
resp.SetRcode(r, dns.RcodeNameError)
|
||||||
|
resp.MsgHdr.Zero = true
|
||||||
|
if err := w.WriteMsg(resp); err != nil {
|
||||||
|
log.Errorf("failed to write continue signal: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDomain manually adds a domain to cache by resolving it.
|
||||||
|
func (m *Resolver) AddDomain(ctx context.Context, d domain.Domain) error {
|
||||||
|
dnsName := strings.ToLower(dns.Fqdn(d.PunycodeString()))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, dnsTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", d.PunycodeString())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve domain %s: %w", d.SafeString(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var aRecords, aaaaRecords []dns.RR
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.Is4() {
|
||||||
|
rr := &dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: dnsName,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: 300,
|
||||||
|
},
|
||||||
|
A: ip.AsSlice(),
|
||||||
|
}
|
||||||
|
aRecords = append(aRecords, rr)
|
||||||
|
} else if ip.Is6() {
|
||||||
|
rr := &dns.AAAA{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: dnsName,
|
||||||
|
Rrtype: dns.TypeAAAA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: 300,
|
||||||
|
},
|
||||||
|
AAAA: ip.AsSlice(),
|
||||||
|
}
|
||||||
|
aaaaRecords = append(aaaaRecords, rr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
|
||||||
|
if len(aRecords) > 0 {
|
||||||
|
aQuestion := dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}
|
||||||
|
m.records[aQuestion] = aRecords
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aaaaRecords) > 0 {
|
||||||
|
aaaaQuestion := dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeAAAA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}
|
||||||
|
m.records[aaaaQuestion] = aaaaRecords
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
log.Debugf("added domain=%s with %d A records and %d AAAA records",
|
||||||
|
d.SafeString(), len(aRecords), len(aaaaRecords))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopulateFromConfig extracts and caches domains from the client configuration.
|
||||||
|
func (m *Resolver) PopulateFromConfig(ctx context.Context, mgmtURL *url.URL) error {
|
||||||
|
if mgmtURL == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := dnsconfig.ExtractValidDomain(mgmtURL.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("extract domain from URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
m.mgmtDomain = &d
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
if err := m.AddDomain(ctx, d); err != nil {
|
||||||
|
return fmt.Errorf("add domain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveDomain removes a domain from the cache.
|
||||||
|
func (m *Resolver) RemoveDomain(d domain.Domain) error {
|
||||||
|
dnsName := strings.ToLower(dns.Fqdn(d.PunycodeString()))
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
aQuestion := dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}
|
||||||
|
delete(m.records, aQuestion)
|
||||||
|
|
||||||
|
aaaaQuestion := dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeAAAA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}
|
||||||
|
delete(m.records, aaaaQuestion)
|
||||||
|
|
||||||
|
log.Debugf("removed domain=%s from cache", d.SafeString())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCachedDomains returns a list of all cached domains.
|
||||||
|
func (m *Resolver) GetCachedDomains() domain.List {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
domainSet := make(map[domain.Domain]struct{})
|
||||||
|
for question := range m.records {
|
||||||
|
domainName := strings.TrimSuffix(question.Name, ".")
|
||||||
|
domainSet[domain.Domain(domainName)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
domains := make(domain.List, 0, len(domainSet))
|
||||||
|
for d := range domainSet {
|
||||||
|
domains = append(domains, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFromServerDomains updates the cache with server domains from network configuration.
|
||||||
|
// It merges new domains with existing ones, replacing entire domain types when updated.
|
||||||
|
// Empty updates are ignored to prevent clearing infrastructure domains during partial updates.
|
||||||
|
func (m *Resolver) UpdateFromServerDomains(ctx context.Context, serverDomains dnsconfig.ServerDomains) (domain.List, error) {
|
||||||
|
newDomains := m.extractDomainsFromServerDomains(serverDomains)
|
||||||
|
var removedDomains domain.List
|
||||||
|
|
||||||
|
if len(newDomains) > 0 {
|
||||||
|
m.mutex.Lock()
|
||||||
|
if m.serverDomains == nil {
|
||||||
|
m.serverDomains = &dnsconfig.ServerDomains{}
|
||||||
|
}
|
||||||
|
updatedServerDomains := m.mergeServerDomains(*m.serverDomains, serverDomains)
|
||||||
|
m.serverDomains = &updatedServerDomains
|
||||||
|
m.mutex.Unlock()
|
||||||
|
|
||||||
|
allDomains := m.extractDomainsFromServerDomains(updatedServerDomains)
|
||||||
|
currentDomains := m.GetCachedDomains()
|
||||||
|
removedDomains = m.removeStaleDomains(currentDomains, allDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.addNewDomains(ctx, newDomains)
|
||||||
|
|
||||||
|
return removedDomains, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeStaleDomains removes cached domains not present in the target domain list.
|
||||||
|
// Management domains are preserved and never removed during server domain updates.
|
||||||
|
func (m *Resolver) removeStaleDomains(currentDomains, newDomains domain.List) domain.List {
|
||||||
|
var removedDomains domain.List
|
||||||
|
|
||||||
|
for _, currentDomain := range currentDomains {
|
||||||
|
if m.isDomainInList(currentDomain, newDomains) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.isManagementDomain(currentDomain) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
removedDomains = append(removedDomains, currentDomain)
|
||||||
|
if err := m.RemoveDomain(currentDomain); err != nil {
|
||||||
|
log.Warnf("failed to remove domain=%s: %v", currentDomain.SafeString(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removedDomains
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeServerDomains merges new server domains with existing ones.
|
||||||
|
// When a domain type is provided in the new domains, it completely replaces that type.
|
||||||
|
func (m *Resolver) mergeServerDomains(existing, incoming dnsconfig.ServerDomains) dnsconfig.ServerDomains {
|
||||||
|
merged := existing
|
||||||
|
|
||||||
|
if incoming.Signal != "" {
|
||||||
|
merged.Signal = incoming.Signal
|
||||||
|
}
|
||||||
|
if len(incoming.Relay) > 0 {
|
||||||
|
merged.Relay = incoming.Relay
|
||||||
|
}
|
||||||
|
if incoming.Flow != "" {
|
||||||
|
merged.Flow = incoming.Flow
|
||||||
|
}
|
||||||
|
if len(incoming.Stuns) > 0 {
|
||||||
|
merged.Stuns = incoming.Stuns
|
||||||
|
}
|
||||||
|
if len(incoming.Turns) > 0 {
|
||||||
|
merged.Turns = incoming.Turns
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDomainInList checks if domain exists in the list
|
||||||
|
func (m *Resolver) isDomainInList(domain domain.Domain, list domain.List) bool {
|
||||||
|
for _, d := range list {
|
||||||
|
if domain.SafeString() == d.SafeString() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isManagementDomain checks if domain is the protected management domain
|
||||||
|
func (m *Resolver) isManagementDomain(domain domain.Domain) bool {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
return m.mgmtDomain != nil && domain == *m.mgmtDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewDomains resolves and caches all domains from the update
|
||||||
|
func (m *Resolver) addNewDomains(ctx context.Context, newDomains domain.List) {
|
||||||
|
for _, newDomain := range newDomains {
|
||||||
|
if err := m.AddDomain(ctx, newDomain); err != nil {
|
||||||
|
log.Warnf("failed to add/update domain=%s: %v", newDomain.SafeString(), err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("added/updated management cache domain=%s", newDomain.SafeString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Resolver) extractDomainsFromServerDomains(serverDomains dnsconfig.ServerDomains) domain.List {
|
||||||
|
var domains domain.List
|
||||||
|
|
||||||
|
if serverDomains.Signal != "" {
|
||||||
|
domains = append(domains, serverDomains.Signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relay := range serverDomains.Relay {
|
||||||
|
if relay != "" {
|
||||||
|
domains = append(domains, relay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverDomains.Flow != "" {
|
||||||
|
domains = append(domains, serverDomains.Flow)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stun := range serverDomains.Stuns {
|
||||||
|
if stun != "" {
|
||||||
|
domains = append(domains, stun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, turn := range serverDomains.Turns {
|
||||||
|
if turn != "" {
|
||||||
|
domains = append(domains, turn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains
|
||||||
|
}
|
||||||
416
client/internal/dns/mgmt/mgmt_test.go
Normal file
416
client/internal/dns/mgmt/mgmt_test.go
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
package mgmt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolver_NewResolver(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
|
||||||
|
assert.NotNil(t, resolver)
|
||||||
|
assert.NotNil(t, resolver.records)
|
||||||
|
assert.False(t, resolver.MatchSubdomains())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_ExtractDomainFromURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
urlStr string
|
||||||
|
expectedDom string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "HTTPS URL with port",
|
||||||
|
urlStr: "https://api.netbird.io:443",
|
||||||
|
expectedDom: "api.netbird.io",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTP URL without port",
|
||||||
|
urlStr: "http://signal.example.com",
|
||||||
|
expectedDom: "signal.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with path",
|
||||||
|
urlStr: "https://relay.netbird.io/status",
|
||||||
|
expectedDom: "relay.netbird.io",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid URL",
|
||||||
|
urlStr: "not-a-valid-url",
|
||||||
|
expectedDom: "not-a-valid-url",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty URL",
|
||||||
|
urlStr: "",
|
||||||
|
expectedDom: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "STUN URL",
|
||||||
|
urlStr: "stun:stun.example.com:3478",
|
||||||
|
expectedDom: "stun.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TURN URL",
|
||||||
|
urlStr: "turn:turn.example.com:3478",
|
||||||
|
expectedDom: "turn.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "REL URL",
|
||||||
|
urlStr: "rel://relay.example.com:443",
|
||||||
|
expectedDom: "relay.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RELS URL",
|
||||||
|
urlStr: "rels://relay.example.com:443",
|
||||||
|
expectedDom: "relay.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var parsedURL *url.URL
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if tt.urlStr != "" {
|
||||||
|
parsedURL, err = url.Parse(tt.urlStr)
|
||||||
|
if err != nil && !tt.expectError {
|
||||||
|
t.Fatalf("Failed to parse URL: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := extractDomainFromURL(parsedURL)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expectedDom, domain.SafeString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_PopulateFromConfig(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resolver := NewResolver()
|
||||||
|
|
||||||
|
// Test with IP address - should return error since IP addresses are rejected
|
||||||
|
mgmtURL, _ := url.Parse("https://127.0.0.1")
|
||||||
|
|
||||||
|
err := resolver.PopulateFromConfig(ctx, mgmtURL)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, dnsconfig.ErrIPNotAllowed)
|
||||||
|
|
||||||
|
// No domains should be cached when using IP addresses
|
||||||
|
domains := resolver.GetCachedDomains()
|
||||||
|
assert.Equal(t, 0, len(domains), "No domains should be cached when using IP addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_ServeDNS(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Add a test domain to the cache - use example.org which is reserved for testing
|
||||||
|
testDomain, err := domain.FromString("example.org")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create domain: %v", err)
|
||||||
|
}
|
||||||
|
err = resolver.AddDomain(ctx, testDomain)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test A record query for cached domain
|
||||||
|
t.Run("Cached domain A record", func(t *testing.T) {
|
||||||
|
var capturedMsg *dns.Msg
|
||||||
|
mockWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
capturedMsg = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(dns.Msg)
|
||||||
|
req.SetQuestion("example.org.", dns.TypeA)
|
||||||
|
|
||||||
|
resolver.ServeDNS(mockWriter, req)
|
||||||
|
|
||||||
|
assert.NotNil(t, capturedMsg)
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, capturedMsg.Rcode)
|
||||||
|
assert.True(t, len(capturedMsg.Answer) > 0, "Should have at least one answer")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test uncached domain signals to continue to next handler
|
||||||
|
t.Run("Uncached domain signals continue to next handler", func(t *testing.T) {
|
||||||
|
var capturedMsg *dns.Msg
|
||||||
|
mockWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
capturedMsg = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(dns.Msg)
|
||||||
|
req.SetQuestion("unknown.example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
resolver.ServeDNS(mockWriter, req)
|
||||||
|
|
||||||
|
assert.NotNil(t, capturedMsg)
|
||||||
|
assert.Equal(t, dns.RcodeNameError, capturedMsg.Rcode)
|
||||||
|
// Zero flag set to true signals the handler chain to continue to next handler
|
||||||
|
assert.True(t, capturedMsg.MsgHdr.Zero, "Zero flag should be set to signal continuation to next handler")
|
||||||
|
assert.Empty(t, capturedMsg.Answer, "Should have no answers for uncached domain")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that subdomains of cached domains are NOT resolved
|
||||||
|
t.Run("Subdomains of cached domains are not resolved", func(t *testing.T) {
|
||||||
|
var capturedMsg *dns.Msg
|
||||||
|
mockWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
capturedMsg = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for a subdomain of our cached domain
|
||||||
|
req := new(dns.Msg)
|
||||||
|
req.SetQuestion("sub.example.org.", dns.TypeA)
|
||||||
|
|
||||||
|
resolver.ServeDNS(mockWriter, req)
|
||||||
|
|
||||||
|
assert.NotNil(t, capturedMsg)
|
||||||
|
assert.Equal(t, dns.RcodeNameError, capturedMsg.Rcode)
|
||||||
|
assert.True(t, capturedMsg.MsgHdr.Zero, "Should signal continuation to next handler for subdomains")
|
||||||
|
assert.Empty(t, capturedMsg.Answer, "Should have no answers for subdomains")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test case-insensitive matching
|
||||||
|
t.Run("Case-insensitive domain matching", func(t *testing.T) {
|
||||||
|
var capturedMsg *dns.Msg
|
||||||
|
mockWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
capturedMsg = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query with different casing
|
||||||
|
req := new(dns.Msg)
|
||||||
|
req.SetQuestion("EXAMPLE.ORG.", dns.TypeA)
|
||||||
|
|
||||||
|
resolver.ServeDNS(mockWriter, req)
|
||||||
|
|
||||||
|
assert.NotNil(t, capturedMsg)
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, capturedMsg.Rcode)
|
||||||
|
assert.True(t, len(capturedMsg.Answer) > 0, "Should resolve regardless of case")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_GetCachedDomains(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
testDomain, err := domain.FromString("example.org")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create domain: %v", err)
|
||||||
|
}
|
||||||
|
err = resolver.AddDomain(ctx, testDomain)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedDomains := resolver.GetCachedDomains()
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(cachedDomains), "Should return exactly one domain for single added domain")
|
||||||
|
assert.Equal(t, testDomain.SafeString(), cachedDomains[0].SafeString(), "Cached domain should match original")
|
||||||
|
assert.False(t, strings.HasSuffix(cachedDomains[0].PunycodeString(), "."), "Domain should not have trailing dot")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_ManagementDomainProtection(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mgmtURL, _ := url.Parse("https://example.org")
|
||||||
|
err := resolver.PopulateFromConfig(ctx, mgmtURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDomains := resolver.GetCachedDomains()
|
||||||
|
if len(initialDomains) == 0 {
|
||||||
|
t.Skip("Management domain failed to resolve, skipping test")
|
||||||
|
}
|
||||||
|
assert.Equal(t, 1, len(initialDomains), "Should have management domain cached")
|
||||||
|
assert.Equal(t, "example.org", initialDomains[0].SafeString())
|
||||||
|
|
||||||
|
serverDomains := dnsconfig.ServerDomains{
|
||||||
|
Signal: "google.com",
|
||||||
|
Relay: []domain.Domain{"cloudflare.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = resolver.UpdateFromServerDomains(ctx, serverDomains)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Server domains update failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalDomains := resolver.GetCachedDomains()
|
||||||
|
|
||||||
|
managementStillCached := false
|
||||||
|
for _, d := range finalDomains {
|
||||||
|
if d.SafeString() == "example.org" {
|
||||||
|
managementStillCached = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, managementStillCached, "Management domain should never be removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractDomainFromURL extracts a domain from a URL - test helper function
|
||||||
|
func extractDomainFromURL(u *url.URL) (domain.Domain, error) {
|
||||||
|
if u == nil {
|
||||||
|
return "", fmt.Errorf("URL is nil")
|
||||||
|
}
|
||||||
|
return dnsconfig.ExtractValidDomain(u.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_EmptyUpdateDoesNotRemoveDomains(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Set up initial domains using resolvable domains
|
||||||
|
initialDomains := dnsconfig.ServerDomains{
|
||||||
|
Signal: "example.org",
|
||||||
|
Stuns: []domain.Domain{"google.com"},
|
||||||
|
Turns: []domain.Domain{"cloudflare.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add initial domains
|
||||||
|
_, err := resolver.UpdateFromServerDomains(ctx, initialDomains)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify domains were added
|
||||||
|
cachedDomains := resolver.GetCachedDomains()
|
||||||
|
assert.Len(t, cachedDomains, 3)
|
||||||
|
|
||||||
|
// Update with empty ServerDomains (simulating partial network map update)
|
||||||
|
emptyDomains := dnsconfig.ServerDomains{}
|
||||||
|
removedDomains, err := resolver.UpdateFromServerDomains(ctx, emptyDomains)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify no domains were removed
|
||||||
|
assert.Len(t, removedDomains, 0, "No domains should be removed when update is empty")
|
||||||
|
|
||||||
|
// Verify all original domains are still cached
|
||||||
|
finalDomains := resolver.GetCachedDomains()
|
||||||
|
assert.Len(t, finalDomains, 3, "All original domains should still be cached")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_PartialUpdateReplacesOnlyUpdatedTypes(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Set up initial complete domains using resolvable domains
|
||||||
|
initialDomains := dnsconfig.ServerDomains{
|
||||||
|
Signal: "example.org",
|
||||||
|
Stuns: []domain.Domain{"google.com"},
|
||||||
|
Turns: []domain.Domain{"cloudflare.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add initial domains
|
||||||
|
_, err := resolver.UpdateFromServerDomains(ctx, initialDomains)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
assert.Len(t, resolver.GetCachedDomains(), 3)
|
||||||
|
|
||||||
|
// Update with partial ServerDomains (only signal domain - this should replace signal but preserve stun/turn)
|
||||||
|
partialDomains := dnsconfig.ServerDomains{
|
||||||
|
Signal: "github.com",
|
||||||
|
}
|
||||||
|
removedDomains, err := resolver.UpdateFromServerDomains(ctx, partialDomains)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should remove only the old signal domain
|
||||||
|
assert.Len(t, removedDomains, 1, "Should remove only the old signal domain")
|
||||||
|
assert.Equal(t, "example.org", removedDomains[0].SafeString())
|
||||||
|
|
||||||
|
finalDomains := resolver.GetCachedDomains()
|
||||||
|
assert.Len(t, finalDomains, 3, "Should have new signal plus preserved stun/turn domains")
|
||||||
|
|
||||||
|
domainStrings := make([]string, len(finalDomains))
|
||||||
|
for i, d := range finalDomains {
|
||||||
|
domainStrings[i] = d.SafeString()
|
||||||
|
}
|
||||||
|
assert.Contains(t, domainStrings, "github.com")
|
||||||
|
assert.Contains(t, domainStrings, "google.com")
|
||||||
|
assert.Contains(t, domainStrings, "cloudflare.com")
|
||||||
|
assert.NotContains(t, domainStrings, "example.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver_PartialUpdateAddsNewTypePreservesExisting(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Set up initial complete domains using resolvable domains
|
||||||
|
initialDomains := dnsconfig.ServerDomains{
|
||||||
|
Signal: "example.org",
|
||||||
|
Stuns: []domain.Domain{"google.com"},
|
||||||
|
Turns: []domain.Domain{"cloudflare.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add initial domains
|
||||||
|
_, err := resolver.UpdateFromServerDomains(ctx, initialDomains)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
assert.Len(t, resolver.GetCachedDomains(), 3)
|
||||||
|
|
||||||
|
// Update with partial ServerDomains (only flow domain - new type, should preserve all existing)
|
||||||
|
partialDomains := dnsconfig.ServerDomains{
|
||||||
|
Flow: "github.com",
|
||||||
|
}
|
||||||
|
removedDomains, err := resolver.UpdateFromServerDomains(ctx, partialDomains)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Skipping test due to DNS resolution failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, removedDomains, 0, "Should not remove any domains when adding new type")
|
||||||
|
|
||||||
|
finalDomains := resolver.GetCachedDomains()
|
||||||
|
assert.Len(t, finalDomains, 4, "Should have all original domains plus new flow domain")
|
||||||
|
|
||||||
|
domainStrings := make([]string, len(finalDomains))
|
||||||
|
for i, d := range finalDomains {
|
||||||
|
domainStrings[i] = d.SafeString()
|
||||||
|
}
|
||||||
|
assert.Contains(t, domainStrings, "example.org")
|
||||||
|
assert.Contains(t, domainStrings, "google.com")
|
||||||
|
assert.Contains(t, domainStrings, "cloudflare.com")
|
||||||
|
assert.Contains(t, domainStrings, "github.com")
|
||||||
|
}
|
||||||
@@ -3,20 +3,23 @@ package dns
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockServer is the mock instance of a dns server
|
// MockServer is the mock instance of a dns server
|
||||||
type MockServer struct {
|
type MockServer struct {
|
||||||
InitializeFunc func() error
|
InitializeFunc func() error
|
||||||
StopFunc func()
|
StopFunc func()
|
||||||
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
|
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
|
||||||
RegisterHandlerFunc func(domain.List, dns.Handler, int)
|
RegisterHandlerFunc func(domain.List, dns.Handler, int)
|
||||||
DeregisterHandlerFunc func(domain.List, int)
|
DeregisterHandlerFunc func(domain.List, int)
|
||||||
|
UpdateServerConfigFunc func(domains dnsconfig.ServerDomains) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockServer) RegisterHandler(domains domain.List, handler dns.Handler, priority int) {
|
func (m *MockServer) RegisterHandler(domains domain.List, handler dns.Handler, priority int) {
|
||||||
@@ -70,3 +73,14 @@ func (m *MockServer) SearchDomains() []string {
|
|||||||
// ProbeAvailability mocks implementation of ProbeAvailability from the Server interface
|
// ProbeAvailability mocks implementation of ProbeAvailability from the Server interface
|
||||||
func (m *MockServer) ProbeAvailability() {
|
func (m *MockServer) ProbeAvailability() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) UpdateServerConfig(domains dnsconfig.ServerDomains) error {
|
||||||
|
if m.UpdateServerConfigFunc != nil {
|
||||||
|
return m.UpdateServerConfigFunc(domains)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -15,7 +16,9 @@ import (
|
|||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns/local"
|
"github.com/netbirdio/netbird/client/internal/dns/local"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/mgmt"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns/types"
|
"github.com/netbirdio/netbird/client/internal/dns/types"
|
||||||
"github.com/netbirdio/netbird/client/internal/listener"
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
@@ -45,6 +48,8 @@ type Server interface {
|
|||||||
OnUpdatedHostDNSServer(addrs []netip.AddrPort)
|
OnUpdatedHostDNSServer(addrs []netip.AddrPort)
|
||||||
SearchDomains() []string
|
SearchDomains() []string
|
||||||
ProbeAvailability()
|
ProbeAvailability()
|
||||||
|
UpdateServerConfig(domains dnsconfig.ServerDomains) error
|
||||||
|
PopulateManagementDomain(mgmtURL *url.URL) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type nsGroupsByDomain struct {
|
type nsGroupsByDomain struct {
|
||||||
@@ -77,6 +82,8 @@ type DefaultServer struct {
|
|||||||
handlerChain *HandlerChain
|
handlerChain *HandlerChain
|
||||||
extraDomains map[domain.Domain]int
|
extraDomains map[domain.Domain]int
|
||||||
|
|
||||||
|
mgmtCacheResolver *mgmt.Resolver
|
||||||
|
|
||||||
// permanent related properties
|
// permanent related properties
|
||||||
permanent bool
|
permanent bool
|
||||||
hostsDNSHolder *hostsDNSHolder
|
hostsDNSHolder *hostsDNSHolder
|
||||||
@@ -104,18 +111,20 @@ type handlerWrapper struct {
|
|||||||
|
|
||||||
type registeredHandlerMap map[types.HandlerID]handlerWrapper
|
type registeredHandlerMap map[types.HandlerID]handlerWrapper
|
||||||
|
|
||||||
|
// DefaultServerConfig holds configuration parameters for NewDefaultServer
|
||||||
|
type DefaultServerConfig struct {
|
||||||
|
WgInterface WGIface
|
||||||
|
CustomAddress string
|
||||||
|
StatusRecorder *peer.Status
|
||||||
|
StateManager *statemanager.Manager
|
||||||
|
DisableSys bool
|
||||||
|
}
|
||||||
|
|
||||||
// NewDefaultServer returns a new dns server
|
// NewDefaultServer returns a new dns server
|
||||||
func NewDefaultServer(
|
func NewDefaultServer(ctx context.Context, config DefaultServerConfig) (*DefaultServer, error) {
|
||||||
ctx context.Context,
|
|
||||||
wgInterface WGIface,
|
|
||||||
customAddress string,
|
|
||||||
statusRecorder *peer.Status,
|
|
||||||
stateManager *statemanager.Manager,
|
|
||||||
disableSys bool,
|
|
||||||
) (*DefaultServer, error) {
|
|
||||||
var addrPort *netip.AddrPort
|
var addrPort *netip.AddrPort
|
||||||
if customAddress != "" {
|
if config.CustomAddress != "" {
|
||||||
parsedAddrPort, err := netip.ParseAddrPort(customAddress)
|
parsedAddrPort, err := netip.ParseAddrPort(config.CustomAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to parse the custom dns address, got error: %s", err)
|
return nil, fmt.Errorf("unable to parse the custom dns address, got error: %s", err)
|
||||||
}
|
}
|
||||||
@@ -123,13 +132,14 @@ func NewDefaultServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dnsService service
|
var dnsService service
|
||||||
if wgInterface.IsUserspaceBind() {
|
if config.WgInterface.IsUserspaceBind() {
|
||||||
dnsService = NewServiceViaMemory(wgInterface)
|
dnsService = NewServiceViaMemory(config.WgInterface)
|
||||||
} else {
|
} else {
|
||||||
dnsService = newServiceViaListener(wgInterface, addrPort)
|
dnsService = newServiceViaListener(config.WgInterface, addrPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newDefaultServer(ctx, wgInterface, dnsService, statusRecorder, stateManager, disableSys), nil
|
server := newDefaultServer(ctx, config.WgInterface, dnsService, config.StatusRecorder, config.StateManager, config.DisableSys)
|
||||||
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems
|
// NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems
|
||||||
@@ -178,20 +188,24 @@ func newDefaultServer(
|
|||||||
) *DefaultServer {
|
) *DefaultServer {
|
||||||
handlerChain := NewHandlerChain()
|
handlerChain := NewHandlerChain()
|
||||||
ctx, stop := context.WithCancel(ctx)
|
ctx, stop := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
mgmtCacheResolver := mgmt.NewResolver()
|
||||||
|
|
||||||
defaultServer := &DefaultServer{
|
defaultServer := &DefaultServer{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
ctxCancel: stop,
|
ctxCancel: stop,
|
||||||
disableSys: disableSys,
|
disableSys: disableSys,
|
||||||
service: dnsService,
|
service: dnsService,
|
||||||
handlerChain: handlerChain,
|
handlerChain: handlerChain,
|
||||||
extraDomains: make(map[domain.Domain]int),
|
extraDomains: make(map[domain.Domain]int),
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
localResolver: local.NewResolver(),
|
localResolver: local.NewResolver(),
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
stateManager: stateManager,
|
stateManager: stateManager,
|
||||||
hostsDNSHolder: newHostsDNSHolder(),
|
hostsDNSHolder: newHostsDNSHolder(),
|
||||||
hostManager: &noopHostConfigurator{},
|
hostManager: &noopHostConfigurator{},
|
||||||
|
mgmtCacheResolver: mgmtCacheResolver,
|
||||||
}
|
}
|
||||||
|
|
||||||
// register with root zone, handler chain takes care of the routing
|
// register with root zone, handler chain takes care of the routing
|
||||||
@@ -217,7 +231,7 @@ func (s *DefaultServer) RegisterHandler(domains domain.List, handler dns.Handler
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
||||||
log.Debugf("registering handler %s with priority %d", handler, priority)
|
log.Debugf("registering handler %s with priority %d for %v", handler, priority, domains)
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
@@ -246,7 +260,7 @@ func (s *DefaultServer) DeregisterHandler(domains domain.List, priority int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) deregisterHandler(domains []string, priority int) {
|
func (s *DefaultServer) deregisterHandler(domains []string, priority int) {
|
||||||
log.Debugf("deregistering handler %v with priority %d", domains, priority)
|
log.Debugf("deregistering handler with priority %d for %v", priority, domains)
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
@@ -432,6 +446,29 @@ func (s *DefaultServer) ProbeAvailability() {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) UpdateServerConfig(domains dnsconfig.ServerDomains) error {
|
||||||
|
s.mux.Lock()
|
||||||
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
|
if s.mgmtCacheResolver != nil {
|
||||||
|
removedDomains, err := s.mgmtCacheResolver.UpdateFromServerDomains(s.ctx, domains)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update management cache resolver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(removedDomains) > 0 {
|
||||||
|
s.deregisterHandler(removedDomains.ToPunycodeList(), PriorityMgmtCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
newDomains := s.mgmtCacheResolver.GetCachedDomains()
|
||||||
|
if len(newDomains) > 0 {
|
||||||
|
s.registerHandler(newDomains.ToPunycodeList(), s.mgmtCacheResolver, PriorityMgmtCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
||||||
// is the service should be Disabled, we stop the listener or fake resolver
|
// is the service should be Disabled, we stop the listener or fake resolver
|
||||||
if update.ServiceEnable {
|
if update.ServiceEnable {
|
||||||
@@ -961,3 +998,11 @@ func toZone(d domain.Domain) domain.Domain {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PopulateManagementDomain populates the DNS cache with management domain
|
||||||
|
func (s *DefaultServer) PopulateManagementDomain(mgmtURL *url.URL) error {
|
||||||
|
if s.mgmtCacheResolver != nil {
|
||||||
|
return s.mgmtCacheResolver.PopulateFromConfig(s.ctx, mgmtURL)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -363,7 +363,13 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
t.Log(err)
|
t.Log(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false)
|
dnsServer, err := NewDefaultServer(context.Background(), DefaultServerConfig{
|
||||||
|
WgInterface: wgIface,
|
||||||
|
CustomAddress: "",
|
||||||
|
StatusRecorder: peer.NewRecorder("mgm"),
|
||||||
|
StateManager: nil,
|
||||||
|
DisableSys: false,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -473,7 +479,13 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", peer.NewRecorder("mgm"), nil, false)
|
dnsServer, err := NewDefaultServer(context.Background(), DefaultServerConfig{
|
||||||
|
WgInterface: wgIface,
|
||||||
|
CustomAddress: "",
|
||||||
|
StatusRecorder: peer.NewRecorder("mgm"),
|
||||||
|
StateManager: nil,
|
||||||
|
DisableSys: false,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("create DNS server: %v", err)
|
t.Errorf("create DNS server: %v", err)
|
||||||
return
|
return
|
||||||
@@ -575,7 +587,13 @@ func TestDNSServerStartStop(t *testing.T) {
|
|||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
dnsServer, err := NewDefaultServer(context.Background(), &mocWGIface{}, testCase.addrPort, peer.NewRecorder("mgm"), nil, false)
|
dnsServer, err := NewDefaultServer(context.Background(), DefaultServerConfig{
|
||||||
|
WgInterface: &mocWGIface{},
|
||||||
|
CustomAddress: testCase.addrPort,
|
||||||
|
StatusRecorder: peer.NewRecorder("mgm"),
|
||||||
|
StateManager: nil,
|
||||||
|
DisableSys: false,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v", err)
|
t.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServiceViaMemory struct {
|
type ServiceViaMemory struct {
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ func SetCurrentMTU(mtu uint16) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UpstreamTimeout = 15 * time.Second
|
UpstreamTimeout = 4 * time.Second
|
||||||
|
// ClientTimeout is the timeout for the dns.Client.
|
||||||
|
// Set longer than UpstreamTimeout to ensure context timeout takes precedence
|
||||||
|
ClientTimeout = 5 * time.Second
|
||||||
|
|
||||||
failsTillDeact = int32(5)
|
|
||||||
reactivatePeriod = 30 * time.Second
|
reactivatePeriod = 30 * time.Second
|
||||||
probeTimeout = 2 * time.Second
|
probeTimeout = 2 * time.Second
|
||||||
)
|
)
|
||||||
@@ -58,9 +60,7 @@ type upstreamResolverBase struct {
|
|||||||
upstreamServers []netip.AddrPort
|
upstreamServers []netip.AddrPort
|
||||||
domain string
|
domain string
|
||||||
disabled bool
|
disabled bool
|
||||||
failsCount atomic.Int32
|
|
||||||
successCount atomic.Int32
|
successCount atomic.Int32
|
||||||
failsTillDeact int32
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
reactivatePeriod time.Duration
|
reactivatePeriod time.Duration
|
||||||
upstreamTimeout time.Duration
|
upstreamTimeout time.Duration
|
||||||
@@ -79,14 +79,13 @@ func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, d
|
|||||||
domain: domain,
|
domain: domain,
|
||||||
upstreamTimeout: UpstreamTimeout,
|
upstreamTimeout: UpstreamTimeout,
|
||||||
reactivatePeriod: reactivatePeriod,
|
reactivatePeriod: reactivatePeriod,
|
||||||
failsTillDeact: failsTillDeact,
|
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a string representation of the upstream resolver
|
// String returns a string representation of the upstream resolver
|
||||||
func (u *upstreamResolverBase) String() string {
|
func (u *upstreamResolverBase) String() string {
|
||||||
return fmt.Sprintf("upstream %s", u.upstreamServers)
|
return fmt.Sprintf("Upstream %s", u.upstreamServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the unique handler ID
|
// ID returns the unique handler ID
|
||||||
@@ -116,58 +115,102 @@ func (u *upstreamResolverBase) Stop() {
|
|||||||
func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
requestID := GenerateRequestID()
|
requestID := GenerateRequestID()
|
||||||
logger := log.WithField("request_id", requestID)
|
logger := log.WithField("request_id", requestID)
|
||||||
var err error
|
|
||||||
defer func() {
|
|
||||||
u.checkUpstreamFails(err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
logger.Tracef("received upstream question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
logger.Tracef("received upstream question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
||||||
|
|
||||||
|
u.prepareRequest(r)
|
||||||
|
|
||||||
|
if u.ctx.Err() != nil {
|
||||||
|
logger.Tracef("%s has been stopped", u)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.tryUpstreamServers(w, r, logger) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.writeErrorResponse(w, r, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) prepareRequest(r *dns.Msg) {
|
||||||
if r.Extra == nil {
|
if r.Extra == nil {
|
||||||
r.MsgHdr.AuthenticatedData = true
|
r.MsgHdr.AuthenticatedData = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
func (u *upstreamResolverBase) tryUpstreamServers(w dns.ResponseWriter, r *dns.Msg, logger *log.Entry) bool {
|
||||||
case <-u.ctx.Done():
|
timeout := u.upstreamTimeout
|
||||||
logger.Tracef("%s has been stopped", u)
|
if len(u.upstreamServers) > 1 {
|
||||||
return
|
maxTotal := 5 * time.Second
|
||||||
default:
|
minPerUpstream := 2 * time.Second
|
||||||
|
scaledTimeout := maxTotal / time.Duration(len(u.upstreamServers))
|
||||||
|
if scaledTimeout > minPerUpstream {
|
||||||
|
timeout = scaledTimeout
|
||||||
|
} else {
|
||||||
|
timeout = minPerUpstream
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, upstream := range u.upstreamServers {
|
for _, upstream := range u.upstreamServers {
|
||||||
var rm *dns.Msg
|
if u.queryUpstream(w, r, upstream, timeout, logger) {
|
||||||
var t time.Duration
|
return true
|
||||||
|
|
||||||
func() {
|
|
||||||
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
|
|
||||||
defer cancel()
|
|
||||||
rm, t, err = u.upstreamClient.exchange(ctx, upstream.String(), r)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, context.DeadlineExceeded) || isTimeout(err) {
|
|
||||||
logger.Warnf("upstream %s timed out for question domain=%s", upstream, r.Question[0].Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Warnf("failed to query upstream %s for question domain=%s: %s", upstream, r.Question[0].Name, err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if rm == nil || !rm.Response {
|
func (u *upstreamResolverBase) queryUpstream(w dns.ResponseWriter, r *dns.Msg, upstream netip.AddrPort, timeout time.Duration, logger *log.Entry) bool {
|
||||||
logger.Warnf("no response from upstream %s for question domain=%s", upstream, r.Question[0].Name)
|
var rm *dns.Msg
|
||||||
continue
|
var t time.Duration
|
||||||
}
|
var err error
|
||||||
|
|
||||||
u.successCount.Add(1)
|
var startTime time.Time
|
||||||
logger.Tracef("took %s to query the upstream %s for question domain=%s", t, upstream, r.Question[0].Name)
|
func() {
|
||||||
|
ctx, cancel := context.WithTimeout(u.ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
startTime = time.Now()
|
||||||
|
rm, t, err = u.upstreamClient.exchange(ctx, upstream.String(), r)
|
||||||
|
}()
|
||||||
|
|
||||||
if err = w.WriteMsg(rm); err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to write DNS response for question domain=%s: %s", r.Question[0].Name, err)
|
u.handleUpstreamError(err, upstream, r.Question[0].Name, startTime, timeout, logger)
|
||||||
}
|
return false
|
||||||
// count the fails only if they happen sequentially
|
}
|
||||||
u.failsCount.Store(0)
|
|
||||||
|
if rm == nil || !rm.Response {
|
||||||
|
logger.Warnf("no response from upstream %s for question domain=%s", upstream, r.Question[0].Name)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.writeSuccessResponse(w, rm, upstream, r.Question[0].Name, t, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) handleUpstreamError(err error, upstream netip.AddrPort, domain string, startTime time.Time, timeout time.Duration, logger *log.Entry) {
|
||||||
|
if !errors.Is(err, context.DeadlineExceeded) && !isTimeout(err) {
|
||||||
|
logger.Warnf("failed to query upstream %s for question domain=%s: %s", upstream, domain, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
u.failsCount.Add(1)
|
|
||||||
|
elapsed := time.Since(startTime)
|
||||||
|
timeoutMsg := fmt.Sprintf("upstream %s timed out for question domain=%s after %v (timeout=%v)", upstream, domain, elapsed.Truncate(time.Millisecond), timeout)
|
||||||
|
if peerInfo := u.debugUpstreamTimeout(upstream); peerInfo != "" {
|
||||||
|
timeoutMsg += " " + peerInfo
|
||||||
|
}
|
||||||
|
timeoutMsg += fmt.Sprintf(" - error: %v", err)
|
||||||
|
logger.Warnf(timeoutMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) writeSuccessResponse(w dns.ResponseWriter, rm *dns.Msg, upstream netip.AddrPort, domain string, t time.Duration, logger *log.Entry) bool {
|
||||||
|
u.successCount.Add(1)
|
||||||
|
logger.Tracef("took %s to query the upstream %s for question domain=%s", t, upstream, domain)
|
||||||
|
|
||||||
|
if err := w.WriteMsg(rm); err != nil {
|
||||||
|
logger.Errorf("failed to write DNS response for question domain=%s: %s", domain, err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) writeErrorResponse(w dns.ResponseWriter, r *dns.Msg, logger *log.Entry) {
|
||||||
logger.Errorf("all queries to the %s failed for question domain=%s", u, r.Question[0].Name)
|
logger.Errorf("all queries to the %s failed for question domain=%s", u, r.Question[0].Name)
|
||||||
|
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
@@ -177,41 +220,6 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkUpstreamFails counts fails and disables or enables upstream resolving
|
|
||||||
//
|
|
||||||
// If fails count is greater that failsTillDeact, upstream resolving
|
|
||||||
// will be disabled for reactivatePeriod, after that time period fails counter
|
|
||||||
// will be reset and upstream will be reactivated.
|
|
||||||
func (u *upstreamResolverBase) checkUpstreamFails(err error) {
|
|
||||||
u.mutex.Lock()
|
|
||||||
defer u.mutex.Unlock()
|
|
||||||
|
|
||||||
if u.failsCount.Load() < u.failsTillDeact || u.disabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-u.ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
u.disable(err)
|
|
||||||
|
|
||||||
if u.statusRecorder == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
u.statusRecorder.PublishEvent(
|
|
||||||
proto.SystemEvent_WARNING,
|
|
||||||
proto.SystemEvent_DNS,
|
|
||||||
"All upstream servers failed (fail count exceeded)",
|
|
||||||
"Unable to reach one or more DNS servers. This might affect your ability to connect to some services.",
|
|
||||||
map[string]string{"upstreams": u.upstreamServersString()},
|
|
||||||
// TODO add domain meta
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProbeAvailability tests all upstream servers simultaneously and
|
// ProbeAvailability tests all upstream servers simultaneously and
|
||||||
// disables the resolver if none work
|
// disables the resolver if none work
|
||||||
func (u *upstreamResolverBase) ProbeAvailability() {
|
func (u *upstreamResolverBase) ProbeAvailability() {
|
||||||
@@ -224,8 +232,8 @@ func (u *upstreamResolverBase) ProbeAvailability() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid probe if upstreams could resolve at least one query and fails count is less than failsTillDeact
|
// avoid probe if upstreams could resolve at least one query
|
||||||
if u.successCount.Load() > 0 && u.failsCount.Load() < u.failsTillDeact {
|
if u.successCount.Load() > 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +320,6 @@ func (u *upstreamResolverBase) waitUntilResponse() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServersString())
|
log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServersString())
|
||||||
u.failsCount.Store(0)
|
|
||||||
u.successCount.Add(1)
|
u.successCount.Add(1)
|
||||||
u.reactivate()
|
u.reactivate()
|
||||||
u.disabled = false
|
u.disabled = false
|
||||||
@@ -416,3 +423,80 @@ func GenerateRequestID() string {
|
|||||||
}
|
}
|
||||||
return hex.EncodeToString(bytes)
|
return hex.EncodeToString(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormatPeerStatus formats peer connection status information for debugging DNS timeouts
|
||||||
|
func FormatPeerStatus(peerState *peer.State) string {
|
||||||
|
isConnected := peerState.ConnStatus == peer.StatusConnected
|
||||||
|
hasRecentHandshake := !peerState.LastWireguardHandshake.IsZero() &&
|
||||||
|
time.Since(peerState.LastWireguardHandshake) < 3*time.Minute
|
||||||
|
|
||||||
|
statusInfo := fmt.Sprintf("%s:%s", peerState.FQDN, peerState.IP)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !isConnected:
|
||||||
|
statusInfo += " DISCONNECTED"
|
||||||
|
case !hasRecentHandshake:
|
||||||
|
statusInfo += " NO_RECENT_HANDSHAKE"
|
||||||
|
default:
|
||||||
|
statusInfo += " connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !peerState.LastWireguardHandshake.IsZero() {
|
||||||
|
timeSinceHandshake := time.Since(peerState.LastWireguardHandshake)
|
||||||
|
statusInfo += fmt.Sprintf(" last_handshake=%v_ago", timeSinceHandshake.Truncate(time.Second))
|
||||||
|
} else {
|
||||||
|
statusInfo += " no_handshake"
|
||||||
|
}
|
||||||
|
|
||||||
|
if peerState.Relayed {
|
||||||
|
statusInfo += " via_relay"
|
||||||
|
}
|
||||||
|
|
||||||
|
if peerState.Latency > 0 {
|
||||||
|
statusInfo += fmt.Sprintf(" latency=%v", peerState.Latency)
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// findPeerForIP finds which peer handles the given IP address
|
||||||
|
func findPeerForIP(ip netip.Addr, statusRecorder *peer.Status) *peer.State {
|
||||||
|
if statusRecorder == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fullStatus := statusRecorder.GetFullStatus()
|
||||||
|
var bestMatch *peer.State
|
||||||
|
var bestPrefixLen int
|
||||||
|
|
||||||
|
for _, peerState := range fullStatus.Peers {
|
||||||
|
routes := peerState.GetRoutes()
|
||||||
|
for route := range routes {
|
||||||
|
prefix, err := netip.ParsePrefix(route)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix.Contains(ip) && prefix.Bits() > bestPrefixLen {
|
||||||
|
peerStateCopy := peerState
|
||||||
|
bestMatch = &peerStateCopy
|
||||||
|
bestPrefixLen = prefix.Bits()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *upstreamResolverBase) debugUpstreamTimeout(upstream netip.AddrPort) string {
|
||||||
|
if u.statusRecorder == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
peerInfo := findPeerForIP(upstream.Addr(), u.statusRecorder)
|
||||||
|
if peerInfo == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("(routes through NetBird peer %s)", FormatPeerStatus(peerInfo))
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type upstreamResolver struct {
|
type upstreamResolver struct {
|
||||||
@@ -50,7 +50,9 @@ func (u *upstreamResolver) exchange(ctx context.Context, upstream string, r *dns
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolver) exchangeWithinVPN(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
func (u *upstreamResolver) exchangeWithinVPN(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
||||||
upstreamExchangeClient := &dns.Client{}
|
upstreamExchangeClient := &dns.Client{
|
||||||
|
Timeout: ClientTimeout,
|
||||||
|
}
|
||||||
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +74,11 @@ func (u *upstreamResolver) exchangeWithoutVPN(ctx context.Context, upstream stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
upstreamExchangeClient := &dns.Client{
|
upstreamExchangeClient := &dns.Client{
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
return upstreamExchangeClient.Exchange(r, upstream)
|
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolver) isLocalResolver(upstream string) bool {
|
func (u *upstreamResolver) isLocalResolver(upstream string) bool {
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ func newUpstreamResolver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolver) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
func (u *upstreamResolver) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
||||||
return ExchangeWithFallback(ctx, &dns.Client{}, r, upstream)
|
client := &dns.Client{
|
||||||
|
Timeout: ClientTimeout,
|
||||||
|
}
|
||||||
|
return ExchangeWithFallback(ctx, client, r, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) {
|
func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) {
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ func newUpstreamResolver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *dns.Msg) (rm *dns.Msg, t time.Duration, err error) {
|
||||||
client := &dns.Client{}
|
client := &dns.Client{
|
||||||
|
Timeout: ClientTimeout,
|
||||||
|
}
|
||||||
upstreamHost, _, err := net.SplitHostPort(upstream)
|
upstreamHost, _, err := net.SplitHostPort(upstream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("error while parsing upstream host: %s", err)
|
return nil, 0, fmt.Errorf("error while parsing upstream host: %s", err)
|
||||||
@@ -110,7 +112,8 @@ func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Dura
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
client := &dns.Client{
|
client := &dns.Client{
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
|
Timeout: dialTimeout,
|
||||||
}
|
}
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,29 +124,26 @@ func (c mockUpstreamResolver) exchange(_ context.Context, _ string, _ *dns.Msg)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
|
func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
|
||||||
|
mockClient := &mockUpstreamResolver{
|
||||||
|
err: dns.ErrTime,
|
||||||
|
r: new(dns.Msg),
|
||||||
|
rtt: time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
resolver := &upstreamResolverBase{
|
resolver := &upstreamResolverBase{
|
||||||
ctx: context.TODO(),
|
ctx: context.TODO(),
|
||||||
upstreamClient: &mockUpstreamResolver{
|
upstreamClient: mockClient,
|
||||||
err: nil,
|
|
||||||
r: new(dns.Msg),
|
|
||||||
rtt: time.Millisecond,
|
|
||||||
},
|
|
||||||
upstreamTimeout: UpstreamTimeout,
|
upstreamTimeout: UpstreamTimeout,
|
||||||
reactivatePeriod: reactivatePeriod,
|
reactivatePeriod: time.Microsecond * 100,
|
||||||
failsTillDeact: failsTillDeact,
|
|
||||||
}
|
}
|
||||||
addrPort, _ := netip.ParseAddrPort("0.0.0.0:1") // Use valid port for parsing, test will still fail on connection
|
addrPort, _ := netip.ParseAddrPort("0.0.0.0:1") // Use valid port for parsing, test will still fail on connection
|
||||||
resolver.upstreamServers = []netip.AddrPort{netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())}
|
resolver.upstreamServers = []netip.AddrPort{netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())}
|
||||||
resolver.failsTillDeact = 0
|
|
||||||
resolver.reactivatePeriod = time.Microsecond * 100
|
|
||||||
|
|
||||||
responseWriter := &test.MockResponseWriter{
|
|
||||||
WriteMsgFunc: func(m *dns.Msg) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
failed := false
|
failed := false
|
||||||
resolver.deactivate = func(error) {
|
resolver.deactivate = func(error) {
|
||||||
failed = true
|
failed = true
|
||||||
|
// After deactivation, make the mock client work again
|
||||||
|
mockClient.err = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
reactivated := false
|
reactivated := false
|
||||||
@@ -154,7 +151,7 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
|
|||||||
reactivated = true
|
reactivated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resolver.ServeDNS(responseWriter, new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA))
|
resolver.ProbeAvailability()
|
||||||
|
|
||||||
if !failed {
|
if !failed {
|
||||||
t.Errorf("expected that resolving was deactivated")
|
t.Errorf("expected that resolving was deactivated")
|
||||||
@@ -173,11 +170,6 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resolver.failsCount.Load() != 0 {
|
|
||||||
t.Errorf("fails count after reactivation should be 0")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if resolver.disabled {
|
if resolver.disabled {
|
||||||
t.Errorf("should be enabled")
|
t.Errorf("should be enabled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -17,8 +18,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
@@ -28,11 +29,12 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/firewall"
|
"github.com/netbirdio/netbird/client/firewall"
|
||||||
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
|
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/internal/acl"
|
"github.com/netbirdio/netbird/client/internal/acl"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config"
|
||||||
"github.com/netbirdio/netbird/client/internal/dnsfwd"
|
"github.com/netbirdio/netbird/client/internal/dnsfwd"
|
||||||
"github.com/netbirdio/netbird/client/internal/ingressgw"
|
"github.com/netbirdio/netbird/client/internal/ingressgw"
|
||||||
"github.com/netbirdio/netbird/client/internal/netflow"
|
"github.com/netbirdio/netbird/client/internal/netflow"
|
||||||
@@ -164,7 +166,7 @@ type Engine struct {
|
|||||||
|
|
||||||
wgInterface WGIface
|
wgInterface WGIface
|
||||||
|
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
|
|
||||||
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
||||||
networkSerial uint64
|
networkSerial uint64
|
||||||
@@ -345,7 +347,7 @@ func (e *Engine) Stop() error {
|
|||||||
// Start creates a new WireGuard tunnel interface and listens to events from Signal and Management services
|
// Start creates a new WireGuard tunnel interface and listens to events from Signal and Management services
|
||||||
// Connections to remote peers are not established here.
|
// Connections to remote peers are not established here.
|
||||||
// However, they will be established once an event with a list of peers to connect to will be received from Management Service
|
// However, they will be established once an event with a list of peers to connect to will be received from Management Service
|
||||||
func (e *Engine) Start() error {
|
func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) error {
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
@@ -401,6 +403,11 @@ func (e *Engine) Start() error {
|
|||||||
}
|
}
|
||||||
e.dnsServer = dnsServer
|
e.dnsServer = dnsServer
|
||||||
|
|
||||||
|
// Populate DNS cache with NetbirdConfig and management URL for early resolution
|
||||||
|
if err := e.PopulateNetbirdConfig(netbirdConfig, mgmtURL); err != nil {
|
||||||
|
log.Warnf("failed to populate DNS cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
e.routeManager = routemanager.NewManager(routemanager.ManagerConfig{
|
e.routeManager = routemanager.NewManager(routemanager.ManagerConfig{
|
||||||
Context: e.ctx,
|
Context: e.ctx,
|
||||||
PublicKey: e.config.WgPrivateKey.PublicKey().String(),
|
PublicKey: e.config.WgPrivateKey.PublicKey().String(),
|
||||||
@@ -439,6 +446,8 @@ func (e *Engine) Start() error {
|
|||||||
return fmt.Errorf("up wg interface: %w", err)
|
return fmt.Errorf("up wg interface: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if inbound conns are blocked there is no need to create the ACL manager
|
// if inbound conns are blocked there is no need to create the ACL manager
|
||||||
if e.firewall != nil && !e.config.BlockInbound {
|
if e.firewall != nil && !e.config.BlockInbound {
|
||||||
e.acl = acl.NewDefaultManager(e.firewall)
|
e.acl = acl.NewDefaultManager(e.firewall)
|
||||||
@@ -454,7 +463,7 @@ func (e *Engine) Start() error {
|
|||||||
StunTurn: &e.stunTurn,
|
StunTurn: &e.stunTurn,
|
||||||
InterfaceBlackList: e.config.IFaceBlackList,
|
InterfaceBlackList: e.config.IFaceBlackList,
|
||||||
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
|
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
|
||||||
UDPMux: e.udpMux.UDPMuxDefault,
|
UDPMux: e.udpMux.SingleSocketUDPMux,
|
||||||
UDPMuxSrflx: e.udpMux,
|
UDPMuxSrflx: e.udpMux,
|
||||||
NATExternalIPs: e.parseNATExternalIPMappings(),
|
NATExternalIPs: e.parseNATExternalIPMappings(),
|
||||||
}
|
}
|
||||||
@@ -661,6 +670,30 @@ func (e *Engine) removePeer(peerKey string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PopulateNetbirdConfig populates the DNS cache with infrastructure domains from login response
|
||||||
|
func (e *Engine) PopulateNetbirdConfig(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) error {
|
||||||
|
if e.dnsServer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate management URL if provided
|
||||||
|
if mgmtURL != nil {
|
||||||
|
if err := e.dnsServer.PopulateManagementDomain(mgmtURL); err != nil {
|
||||||
|
log.Warnf("failed to populate DNS cache with management URL: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate NetbirdConfig domains if provided
|
||||||
|
if netbirdConfig != nil {
|
||||||
|
serverDomains := dnsconfig.ExtractFromNetbirdConfig(netbirdConfig)
|
||||||
|
if err := e.dnsServer.UpdateServerConfig(serverDomains); err != nil {
|
||||||
|
return fmt.Errorf("update DNS server config from NetbirdConfig: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
@@ -692,6 +725,10 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return fmt.Errorf("handle the flow configuration: %w", err)
|
return fmt.Errorf("handle the flow configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
|
||||||
|
log.Warnf("Failed to update DNS server config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// todo update signal
|
// todo update signal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,7 +951,6 @@ func (e *Engine) receiveManagementEvents() {
|
|||||||
e.config.LazyConnectionEnabled,
|
e.config.LazyConnectionEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
// err = e.mgmClient.Sync(info, e.handleSync)
|
|
||||||
err = e.mgmClient.Sync(e.ctx, info, e.handleSync)
|
err = e.mgmClient.Sync(e.ctx, info, e.handleSync)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// happens if management is unavailable for a long time.
|
// happens if management is unavailable for a long time.
|
||||||
@@ -925,7 +961,7 @@ func (e *Engine) receiveManagementEvents() {
|
|||||||
}
|
}
|
||||||
log.Debugf("stopped receiving updates from Management Service")
|
log.Debugf("stopped receiving updates from Management Service")
|
||||||
}()
|
}()
|
||||||
log.Debugf("connecting to Management Service updates stream")
|
log.Infof("connecting to Management Service updates stream")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) updateSTUNs(stuns []*mgmProto.HostConfig) error {
|
func (e *Engine) updateSTUNs(stuns []*mgmProto.HostConfig) error {
|
||||||
@@ -1292,7 +1328,7 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
|
|||||||
StunTurn: &e.stunTurn,
|
StunTurn: &e.stunTurn,
|
||||||
InterfaceBlackList: e.config.IFaceBlackList,
|
InterfaceBlackList: e.config.IFaceBlackList,
|
||||||
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
|
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
|
||||||
UDPMux: e.udpMux.UDPMuxDefault,
|
UDPMux: e.udpMux.SingleSocketUDPMux,
|
||||||
UDPMuxSrflx: e.udpMux,
|
UDPMuxSrflx: e.udpMux,
|
||||||
NATExternalIPs: e.parseNATExternalIPMappings(),
|
NATExternalIPs: e.parseNATExternalIPMappings(),
|
||||||
},
|
},
|
||||||
@@ -1557,7 +1593,14 @@ func (e *Engine) newDnsServer(dnsConfig *nbdns.Config) (dns.Server, error) {
|
|||||||
return dnsServer, nil
|
return dnsServer, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, e.statusRecorder, e.stateManager, e.config.DisableDNS)
|
|
||||||
|
dnsServer, err := dns.NewDefaultServer(e.ctx, dns.DefaultServerConfig{
|
||||||
|
WgInterface: e.wgInterface,
|
||||||
|
CustomAddress: e.config.CustomDNSAddress,
|
||||||
|
StatusRecorder: e.statusRecorder,
|
||||||
|
StateManager: e.stateManager,
|
||||||
|
DisableSys: e.config.DisableDNS,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,21 +19,18 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
|
||||||
"github.com/netbirdio/netbird/management/server/groups"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
@@ -45,9 +42,12 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/ssh"
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"github.com/netbirdio/netbird/management/server/peers"
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
@@ -85,7 +85,7 @@ type MockWGIface struct {
|
|||||||
NameFunc func() string
|
NameFunc func() string
|
||||||
AddressFunc func() wgaddr.Address
|
AddressFunc func() wgaddr.Address
|
||||||
ToInterfaceFunc func() *net.Interface
|
ToInterfaceFunc func() *net.Interface
|
||||||
UpFunc func() (*bind.UniversalUDPMuxDefault, error)
|
UpFunc func() (*udpmux.UniversalUDPMuxDefault, error)
|
||||||
UpdateAddrFunc func(newAddr string) error
|
UpdateAddrFunc func(newAddr string) error
|
||||||
UpdatePeerFunc func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
|
UpdatePeerFunc func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
|
||||||
RemovePeerFunc func(peerKey string) error
|
RemovePeerFunc func(peerKey string) error
|
||||||
@@ -135,7 +135,7 @@ func (m *MockWGIface) ToInterface() *net.Interface {
|
|||||||
return m.ToInterfaceFunc()
|
return m.ToInterfaceFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockWGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (m *MockWGIface) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
return m.UpFunc()
|
return m.UpFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ func TestEngine_SSH(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
err = engine.Start()
|
err = engine.Start(nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -414,7 +414,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
engine.udpMux = bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: conn, MTU: 1280})
|
engine.udpMux = udpmux.NewUniversalUDPMuxDefault(udpmux.UniversalUDPMuxParams{UDPConn: conn, MTU: 1280})
|
||||||
engine.ctx = ctx
|
engine.ctx = ctx
|
||||||
engine.srWatcher = guard.NewSRWatcher(nil, nil, nil, icemaker.Config{})
|
engine.srWatcher = guard.NewSRWatcher(nil, nil, nil, icemaker.Config{})
|
||||||
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface)
|
engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface)
|
||||||
@@ -612,7 +612,7 @@ func TestEngine_Sync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = engine.Start()
|
err = engine.Start(nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
return
|
return
|
||||||
@@ -1069,7 +1069,7 @@ func TestEngine_MultiplePeers(t *testing.T) {
|
|||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
guid := fmt.Sprintf("{%s}", uuid.New().String())
|
guid := fmt.Sprintf("{%s}", uuid.New().String())
|
||||||
device.CustomWindowsGUIDString = strings.ToLower(guid)
|
device.CustomWindowsGUIDString = strings.ToLower(guid)
|
||||||
err = engine.Start()
|
err = engine.Start(nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to start engine for peer %d with error %v", j, err)
|
t.Errorf("unable to start engine for peer %d with error %v", j, err)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
@@ -1555,7 +1555,11 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
ia, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
|
||||||
|
permissionsManager := permissions.NewManager(store)
|
||||||
|
peersManager := peers.NewManager(store, permissionsManager)
|
||||||
|
|
||||||
|
ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore)
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1572,7 +1576,6 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
|||||||
Return(&types.ExtraSettings{}, nil).
|
Return(&types.ExtraSettings{}, nil).
|
||||||
AnyTimes()
|
AnyTimes()
|
||||||
|
|
||||||
permissionsManager := permissions.NewManager(store)
|
|
||||||
groupsManager := groups.NewManagerMock()
|
groupsManager := groups.NewManagerMock()
|
||||||
|
|
||||||
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false)
|
accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
||||||
"github.com/netbirdio/netbird/monotime"
|
"github.com/netbirdio/netbird/monotime"
|
||||||
@@ -24,7 +24,7 @@ type wgIfaceBase interface {
|
|||||||
Name() string
|
Name() string
|
||||||
Address() wgaddr.Address
|
Address() wgaddr.Address
|
||||||
ToInterface() *net.Interface
|
ToInterface() *net.Interface
|
||||||
Up() (*bind.UniversalUDPMuxDefault, error)
|
Up() (*udpmux.UniversalUDPMuxDefault, error)
|
||||||
UpdateAddr(newAddr string) error
|
UpdateAddr(newAddr string) error
|
||||||
GetProxy() wgproxy.Proxy
|
GetProxy() wgproxy.Proxy
|
||||||
UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
|
UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func IsLoginRequired(ctx context.Context, config *profilemanager.Config) (bool,
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = doMgmLogin(ctx, mgmClient, pubSSHKey, config)
|
_, _, err = doMgmLogin(ctx, mgmClient, pubSSHKey, config)
|
||||||
if isLoginNeeded(err) {
|
if isLoginNeeded(err) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@@ -69,14 +69,18 @@ func Login(ctx context.Context, config *profilemanager.Config, setupKey string,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
serverKey, err := doMgmLogin(ctx, mgmClient, pubSSHKey, config)
|
serverKey, _, err := doMgmLogin(ctx, mgmClient, pubSSHKey, config)
|
||||||
if serverKey != nil && isRegistrationNeeded(err) {
|
if serverKey != nil && isRegistrationNeeded(err) {
|
||||||
log.Debugf("peer registration required")
|
log.Debugf("peer registration required")
|
||||||
_, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey, config)
|
_, err = registerPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm.GrpcClient, error) {
|
func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm.GrpcClient, error) {
|
||||||
@@ -101,11 +105,11 @@ func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm
|
|||||||
return mgmClient, err
|
return mgmClient, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *profilemanager.Config) (*wgtypes.Key, error) {
|
func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *profilemanager.Config) (*wgtypes.Key, *mgmProto.LoginResponse, error) {
|
||||||
serverKey, err := mgmClient.GetServerPublicKey()
|
serverKey, err := mgmClient.GetServerPublicKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed while getting Management Service public key: %v", err)
|
log.Errorf("failed while getting Management Service public key: %v", err)
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sysInfo := system.GetInfo(ctx)
|
sysInfo := system.GetInfo(ctx)
|
||||||
@@ -121,8 +125,8 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte
|
|||||||
config.BlockInbound,
|
config.BlockInbound,
|
||||||
config.LazyConnectionEnabled,
|
config.LazyConnectionEnabled,
|
||||||
)
|
)
|
||||||
_, err = mgmClient.Login(*serverKey, sysInfo, pubSSHKey, config.DNSLabels)
|
loginResp, err := mgmClient.Login(*serverKey, sysInfo, pubSSHKey, config.DNSLabels)
|
||||||
return serverKey, err
|
return serverKey, loginResp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
|
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/ti-mo/netfilter"
|
"github.com/ti-mo/netfilter"
|
||||||
|
|
||||||
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
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
|
const defaultChannelSize = 100
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
@@ -174,7 +173,7 @@ func (conn *Conn) Open(engineCtx context.Context) error {
|
|||||||
conn.handshaker = NewHandshaker(conn.Log, conn.config, conn.signaler, conn.workerICE, conn.workerRelay)
|
conn.handshaker = NewHandshaker(conn.Log, conn.config, conn.signaler, conn.workerICE, conn.workerRelay)
|
||||||
|
|
||||||
conn.handshaker.AddOnNewOfferListener(conn.workerRelay.OnNewOffer)
|
conn.handshaker.AddOnNewOfferListener(conn.workerRelay.OnNewOffer)
|
||||||
if os.Getenv("NB_FORCE_RELAY") != "true" {
|
if !isForceRelayed() {
|
||||||
conn.handshaker.AddOnNewOfferListener(conn.workerICE.OnNewOffer)
|
conn.handshaker.AddOnNewOfferListener(conn.workerICE.OnNewOffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
client/internal/peer/env.go
Normal file
14
client/internal/peer/env.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isForceRelayed() bool {
|
||||||
|
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
|
|||||||
@@ -43,13 +43,6 @@ type OfferAnswer struct {
|
|||||||
SessionID *ICESessionID
|
SessionID *ICESessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oa *OfferAnswer) SessionIDString() string {
|
|
||||||
if oa.SessionID == nil {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
return oa.SessionID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handshaker struct {
|
type Handshaker struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
@@ -57,7 +50,7 @@ type Handshaker struct {
|
|||||||
signaler *Signaler
|
signaler *Signaler
|
||||||
ice *WorkerICE
|
ice *WorkerICE
|
||||||
relay *WorkerRelay
|
relay *WorkerRelay
|
||||||
onNewOfferListeners []func(*OfferAnswer)
|
onNewOfferListeners []*OfferListener
|
||||||
|
|
||||||
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
||||||
remoteOffersCh chan OfferAnswer
|
remoteOffersCh chan OfferAnswer
|
||||||
@@ -78,7 +71,8 @@ func NewHandshaker(log *log.Entry, config ConnConfig, signaler *Signaler, ice *W
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAnswer)) {
|
func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAnswer)) {
|
||||||
h.onNewOfferListeners = append(h.onNewOfferListeners, offer)
|
l := NewOfferListener(offer)
|
||||||
|
h.onNewOfferListeners = append(h.onNewOfferListeners, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handshaker) Listen(ctx context.Context) {
|
func (h *Handshaker) Listen(ctx context.Context) {
|
||||||
@@ -91,13 +85,13 @@ func (h *Handshaker) Listen(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, listener := range h.onNewOfferListeners {
|
for _, listener := range h.onNewOfferListeners {
|
||||||
listener(&remoteOfferAnswer)
|
listener.Notify(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
h.log.Infof("received offer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
h.log.Infof("received offer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
||||||
case remoteOfferAnswer := <-h.remoteAnswerCh:
|
case remoteOfferAnswer := <-h.remoteAnswerCh:
|
||||||
h.log.Infof("received answer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
h.log.Infof("received answer, running version %s, remote WireGuard listen port %d, session id: %s", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString())
|
||||||
for _, listener := range h.onNewOfferListeners {
|
for _, listener := range h.onNewOfferListeners {
|
||||||
listener(&remoteOfferAnswer)
|
listener.Notify(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
h.log.Infof("stop listening for remote offers and answers")
|
h.log.Infof("stop listening for remote offers and answers")
|
||||||
|
|||||||
62
client/internal/peer/handshaker_listener.go
Normal file
62
client/internal/peer/handshaker_listener.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type callbackFunc func(remoteOfferAnswer *OfferAnswer)
|
||||||
|
|
||||||
|
func (oa *OfferAnswer) SessionIDString() string {
|
||||||
|
if oa.SessionID == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return oa.SessionID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type OfferListener struct {
|
||||||
|
fn callbackFunc
|
||||||
|
running bool
|
||||||
|
latest *OfferAnswer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOfferListener(fn callbackFunc) *OfferListener {
|
||||||
|
return &OfferListener{
|
||||||
|
fn: fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OfferListener) Notify(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
o.mu.Lock()
|
||||||
|
defer o.mu.Unlock()
|
||||||
|
|
||||||
|
// Store the latest offer
|
||||||
|
o.latest = remoteOfferAnswer
|
||||||
|
|
||||||
|
// If already running, the running goroutine will pick up this latest value
|
||||||
|
if o.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing
|
||||||
|
o.running = true
|
||||||
|
|
||||||
|
// Process in a goroutine to avoid blocking the caller
|
||||||
|
go func(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
for {
|
||||||
|
o.fn(remoteOfferAnswer)
|
||||||
|
|
||||||
|
o.mu.Lock()
|
||||||
|
if o.latest == nil {
|
||||||
|
// No more work to do
|
||||||
|
o.running = false
|
||||||
|
o.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remoteOfferAnswer = o.latest
|
||||||
|
// Clear the latest to mark it as being processed
|
||||||
|
o.latest = nil
|
||||||
|
o.mu.Unlock()
|
||||||
|
}
|
||||||
|
}(remoteOfferAnswer)
|
||||||
|
}
|
||||||
39
client/internal/peer/handshaker_listener_test.go
Normal file
39
client/internal/peer/handshaker_listener_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newOfferListener(t *testing.T) {
|
||||||
|
dummyOfferAnswer := &OfferAnswer{}
|
||||||
|
runChan := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
longRunningFn := func(remoteOfferAnswer *OfferAnswer) {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
runChan <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
hl := NewOfferListener(longRunningFn)
|
||||||
|
|
||||||
|
hl.Notify(dummyOfferAnswer)
|
||||||
|
hl.Notify(dummyOfferAnswer)
|
||||||
|
hl.Notify(dummyOfferAnswer)
|
||||||
|
|
||||||
|
// Wait for exactly 2 callbacks
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
select {
|
||||||
|
case <-runChan:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("Timeout waiting for callback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no additional callbacks happen
|
||||||
|
select {
|
||||||
|
case <-runChan:
|
||||||
|
t.Fatal("Unexpected additional callback")
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Log("Correctly received exactly 2 callbacks")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package ice
|
|||||||
import (
|
import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StunTurn atomic.Value
|
type StunTurn atomic.Value
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
"github.com/pion/logging"
|
"github.com/pion/logging"
|
||||||
"github.com/pion/randutil"
|
"github.com/pion/randutil"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package ice
|
package ice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ type WGWatcher struct {
|
|||||||
peerKey string
|
peerKey string
|
||||||
stateDump *stateDump
|
stateDump *stateDump
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
ctxLock sync.Mutex
|
ctxLock sync.Mutex
|
||||||
|
enabledTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string, stateDump *stateDump) *WGWatcher {
|
func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string, stateDump *stateDump) *WGWatcher {
|
||||||
@@ -48,6 +49,7 @@ func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey strin
|
|||||||
func (w *WGWatcher) EnableWgWatcher(parentCtx context.Context, onDisconnectedFn func()) {
|
func (w *WGWatcher) EnableWgWatcher(parentCtx context.Context, onDisconnectedFn func()) {
|
||||||
w.log.Debugf("enable WireGuard watcher")
|
w.log.Debugf("enable WireGuard watcher")
|
||||||
w.ctxLock.Lock()
|
w.ctxLock.Lock()
|
||||||
|
w.enabledTime = time.Now()
|
||||||
|
|
||||||
if w.ctx != nil && w.ctx.Err() == nil {
|
if w.ctx != nil && w.ctx.Err() == nil {
|
||||||
w.log.Errorf("WireGuard watcher already enabled")
|
w.log.Errorf("WireGuard watcher already enabled")
|
||||||
@@ -101,6 +103,11 @@ func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, ctxCancel contex
|
|||||||
onDisconnectedFn()
|
onDisconnectedFn()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if lastHandshake.IsZero() {
|
||||||
|
elapsed := handshake.Sub(w.enabledTime).Seconds()
|
||||||
|
w.log.Infof("first wg handshake detected within: %.2fsec, (%s)", elapsed, handshake)
|
||||||
|
}
|
||||||
|
|
||||||
lastHandshake = *handshake
|
lastHandshake = *handshake
|
||||||
|
|
||||||
resetTime := time.Until(handshake.Add(checkPeriod))
|
resetTime := time.Until(handshake.Add(checkPeriod))
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v4"
|
||||||
"github.com/pion/stun/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/conntype"
|
"github.com/netbirdio/netbird/client/internal/peer/conntype"
|
||||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
@@ -55,10 +54,6 @@ type WorkerICE struct {
|
|||||||
sessionID ICESessionID
|
sessionID ICESessionID
|
||||||
muxAgent sync.Mutex
|
muxAgent sync.Mutex
|
||||||
|
|
||||||
StunTurn []*stun.URI
|
|
||||||
|
|
||||||
sentExtraSrflx bool
|
|
||||||
|
|
||||||
localUfrag string
|
localUfrag string
|
||||||
localPwd string
|
localPwd string
|
||||||
|
|
||||||
@@ -122,7 +117,6 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
w.log.Warnf("failed to close ICE agent: %s", err)
|
w.log.Warnf("failed to close ICE agent: %s", err)
|
||||||
}
|
}
|
||||||
w.agent = nil
|
w.agent = nil
|
||||||
// todo consider to switch to Relay connection while establishing a new ICE connection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var preferredCandidateTypes []ice.CandidateType
|
var preferredCandidateTypes []ice.CandidateType
|
||||||
@@ -140,7 +134,6 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
w.muxAgent.Unlock()
|
w.muxAgent.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.sentExtraSrflx = false
|
|
||||||
w.agent = agent
|
w.agent = agent
|
||||||
w.agentDialerCancel = dialerCancel
|
w.agentDialerCancel = dialerCancel
|
||||||
w.agentConnecting = true
|
w.agentConnecting = true
|
||||||
@@ -167,6 +160,21 @@ func (w *WorkerICE) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HA
|
|||||||
w.log.Errorf("error while handling remote candidate")
|
w.log.Errorf("error while handling remote candidate")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldAddExtraCandidate(candidate) {
|
||||||
|
// sends an extra server reflexive candidate to the remote peer with our related port (usually the wireguard port)
|
||||||
|
// this is useful when network has an existing port forwarding rule for the wireguard port and this peer
|
||||||
|
extraSrflx, err := extraSrflxCandidate(candidate)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("failed creating extra server reflexive candidate %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.agent.AddRemoteCandidate(extraSrflx); err != nil {
|
||||||
|
w.log.Errorf("error while handling remote candidate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) GetLocalUserCredentials() (frag string, pwd string) {
|
func (w *WorkerICE) GetLocalUserCredentials() (frag string, pwd string) {
|
||||||
@@ -210,14 +218,12 @@ func (w *WorkerICE) reCreateAgent(dialerCancel context.CancelFunc, candidates []
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := agent.OnSelectedCandidatePairChange(w.onICESelectedCandidatePair); err != nil {
|
if err := agent.OnSelectedCandidatePairChange(func(c1, c2 ice.Candidate) {
|
||||||
|
w.onICESelectedCandidatePair(agent, c1, c2)
|
||||||
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := agent.OnSuccessfulSelectedPairBindingResponse(w.onSuccessfulSelectedPairBindingResponse); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed setting binding response callback: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return agent, nil
|
return agent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +259,11 @@ func (w *WorkerICE) connect(ctx context.Context, agent *icemaker.ThreadSafeAgent
|
|||||||
w.closeAgent(agent, w.agentDialerCancel)
|
w.closeAgent(agent, w.agentDialerCancel)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if pair == nil {
|
||||||
|
w.log.Warnf("selected candidate pair is nil, cannot proceed")
|
||||||
|
w.closeAgent(agent, w.agentDialerCancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !isRelayCandidate(pair.Local) {
|
if !isRelayCandidate(pair.Local) {
|
||||||
// dynamically set remote WireGuard port if other side specified a different one from the default one
|
// dynamically set remote WireGuard port if other side specified a different one from the default one
|
||||||
@@ -327,7 +338,7 @@ func (w *WorkerICE) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mux, ok := w.config.ICEConfig.UDPMuxSrflx.(*bind.UniversalUDPMuxDefault)
|
mux, ok := w.config.ICEConfig.UDPMuxSrflx.(*udpmux.UniversalUDPMuxDefault)
|
||||||
if !ok {
|
if !ok {
|
||||||
w.log.Warn("invalid udp mux conversion")
|
w.log.Warn("invalid udp mux conversion")
|
||||||
return
|
return
|
||||||
@@ -354,31 +365,23 @@ func (w *WorkerICE) onICECandidate(candidate ice.Candidate) {
|
|||||||
w.log.Errorf("failed signaling candidate to the remote peer %s %s", w.config.Key, err)
|
w.log.Errorf("failed signaling candidate to the remote peer %s %s", w.config.Key, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !w.shouldSendExtraSrflxCandidate(candidate) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// sends an extra server reflexive candidate to the remote peer with our related port (usually the wireguard port)
|
|
||||||
// this is useful when network has an existing port forwarding rule for the wireguard port and this peer
|
|
||||||
extraSrflx, err := extraSrflxCandidate(candidate)
|
|
||||||
if err != nil {
|
|
||||||
w.log.Errorf("failed creating extra server reflexive candidate %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.sentExtraSrflx = true
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err = w.signaler.SignalICECandidate(extraSrflx, w.config.Key)
|
|
||||||
if err != nil {
|
|
||||||
w.log.Errorf("failed signaling the extra server reflexive candidate: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) onICESelectedCandidatePair(c1 ice.Candidate, c2 ice.Candidate) {
|
func (w *WorkerICE) onICESelectedCandidatePair(agent *icemaker.ThreadSafeAgent, c1, c2 ice.Candidate) {
|
||||||
w.log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", c1.String(), c2.String(),
|
w.log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", c1.String(), c2.String(),
|
||||||
w.config.Key)
|
w.config.Key)
|
||||||
|
|
||||||
|
pairStat, ok := agent.GetSelectedCandidatePairStats()
|
||||||
|
if !ok {
|
||||||
|
w.log.Warnf("failed to get selected candidate pair stats")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Duration(pairStat.CurrentRoundTripTime * float64(time.Second))
|
||||||
|
if err := w.statusRecorder.UpdateLatency(w.config.Key, duration); err != nil {
|
||||||
|
w.log.Debugf("failed to update latency for peer: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dialerCancel context.CancelFunc) func(ice.ConnectionState) {
|
func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dialerCancel context.CancelFunc) func(ice.ConnectionState) {
|
||||||
@@ -388,7 +391,10 @@ func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dia
|
|||||||
case ice.ConnectionStateConnected:
|
case ice.ConnectionStateConnected:
|
||||||
w.lastKnownState = ice.ConnectionStateConnected
|
w.lastKnownState = ice.ConnectionStateConnected
|
||||||
return
|
return
|
||||||
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected:
|
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected, ice.ConnectionStateClosed:
|
||||||
|
// ice.ConnectionStateClosed happens when we recreate the agent. For the P2P to TURN switch important to
|
||||||
|
// notify the conn.onICEStateDisconnected changes to update the current used priority
|
||||||
|
|
||||||
if w.lastKnownState == ice.ConnectionStateConnected {
|
if w.lastKnownState == ice.ConnectionStateConnected {
|
||||||
w.lastKnownState = ice.ConnectionStateDisconnected
|
w.lastKnownState = ice.ConnectionStateDisconnected
|
||||||
w.conn.onICEStateDisconnected()
|
w.conn.onICEStateDisconnected()
|
||||||
@@ -400,32 +406,34 @@ func (w *WorkerICE) onConnectionStateChange(agent *icemaker.ThreadSafeAgent, dia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) onSuccessfulSelectedPairBindingResponse(pair *ice.CandidatePair) {
|
|
||||||
if err := w.statusRecorder.UpdateLatency(w.config.Key, pair.Latency()); err != nil {
|
|
||||||
w.log.Debugf("failed to update latency for peer: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WorkerICE) shouldSendExtraSrflxCandidate(candidate ice.Candidate) bool {
|
|
||||||
if !w.sentExtraSrflx && candidate.Type() == ice.CandidateTypeServerReflexive && candidate.Port() != candidate.RelatedAddress().Port {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WorkerICE) turnAgentDial(ctx context.Context, agent *icemaker.ThreadSafeAgent, remoteOfferAnswer *OfferAnswer) (*ice.Conn, error) {
|
func (w *WorkerICE) turnAgentDial(ctx context.Context, agent *icemaker.ThreadSafeAgent, remoteOfferAnswer *OfferAnswer) (*ice.Conn, error) {
|
||||||
isControlling := w.config.LocalKey > w.config.Key
|
if isController(w.config) {
|
||||||
if isControlling {
|
return w.agent.Dial(ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
return agent.Dial(ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
|
||||||
} else {
|
} else {
|
||||||
return agent.Accept(ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
return agent.Accept(ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldAddExtraCandidate(candidate ice.Candidate) bool {
|
||||||
|
if candidate.Type() != ice.CandidateTypeServerReflexive {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if candidate.Port() == candidate.RelatedAddress().Port {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the older version when we didn't set candidate ID extension the remote peer sent the extra candidates
|
||||||
|
// in newer version we generate locally the extra candidate
|
||||||
|
if _, ok := candidate.GetExtension(ice.ExtensionKeyCandidateID); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func extraSrflxCandidate(candidate ice.Candidate) (*ice.CandidateServerReflexive, error) {
|
func extraSrflxCandidate(candidate ice.Candidate) (*ice.CandidateServerReflexive, error) {
|
||||||
relatedAdd := candidate.RelatedAddress()
|
relatedAdd := candidate.RelatedAddress()
|
||||||
return ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
|
ec, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
|
||||||
Network: candidate.NetworkType().String(),
|
Network: candidate.NetworkType().String(),
|
||||||
Address: candidate.Address(),
|
Address: candidate.Address(),
|
||||||
Port: relatedAdd.Port,
|
Port: relatedAdd.Port,
|
||||||
@@ -433,6 +441,21 @@ func extraSrflxCandidate(candidate ice.Candidate) (*ice.CandidateServerReflexive
|
|||||||
RelAddr: relatedAdd.Address,
|
RelAddr: relatedAdd.Address,
|
||||||
RelPort: relatedAdd.Port,
|
RelPort: relatedAdd.Port,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range candidate.Extensions() {
|
||||||
|
// overwrite the original candidate ID with the new one to avoid candidate duplication
|
||||||
|
if e.Key == ice.ExtensionKeyCandidateID {
|
||||||
|
e.Value = candidate.ID()
|
||||||
|
}
|
||||||
|
if err := ec.AddExtension(e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool {
|
func candidateViaRoutes(candidate ice.Candidate, clientRoutes route.HAMap) bool {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
"github.com/pion/turn/v3"
|
"github.com/pion/turn/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"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
|
// ProbeResult holds the info about the result of a relay probe request
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package dnsinterceptor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -26,6 +28,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const dnsTimeout = 8 * time.Second
|
||||||
|
|
||||||
type domainMap map[domain.Domain][]netip.Prefix
|
type domainMap map[domain.Domain][]netip.Prefix
|
||||||
|
|
||||||
type internalDNATer interface {
|
type internalDNATer interface {
|
||||||
@@ -243,7 +247,7 @@ func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := nbdns.GetClientPrivate(d.wgInterface.Address().IP, d.wgInterface.Name(), nbdns.UpstreamTimeout)
|
client, err := nbdns.GetClientPrivate(d.wgInterface.Address().IP, d.wgInterface.Name(), dnsTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.writeDNSError(w, r, logger, fmt.Sprintf("create DNS client: %v", err))
|
d.writeDNSError(w, r, logger, fmt.Sprintf("create DNS client: %v", err))
|
||||||
return
|
return
|
||||||
@@ -254,9 +258,20 @@ func (d *DnsInterceptor) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upstream := fmt.Sprintf("%s:%d", upstreamIP.String(), dnsfwd.ListenPort)
|
upstream := fmt.Sprintf("%s:%d", upstreamIP.String(), dnsfwd.ListenPort)
|
||||||
reply, _, err := nbdns.ExchangeWithFallback(context.TODO(), client, r, upstream)
|
ctx, cancel := context.WithTimeout(context.Background(), dnsTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
reply, _, err := nbdns.ExchangeWithFallback(ctx, client, r, upstream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to exchange DNS request with %s (%s) for domain=%s: %v", upstreamIP.String(), peerKey, r.Question[0].Name, err)
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
elapsed := time.Since(startTime)
|
||||||
|
peerInfo := d.debugPeerTimeout(upstreamIP, peerKey)
|
||||||
|
logger.Errorf("peer DNS timeout after %v (timeout=%v) for domain=%s to peer %s (%s)%s - error: %v",
|
||||||
|
elapsed.Truncate(time.Millisecond), dnsTimeout, r.Question[0].Name, upstreamIP.String(), peerKey, peerInfo, err)
|
||||||
|
} else {
|
||||||
|
logger.Errorf("failed to exchange DNS request with %s (%s) for domain=%s: %v", upstreamIP.String(), peerKey, r.Question[0].Name, err)
|
||||||
|
}
|
||||||
if err := w.WriteMsg(&dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure, Id: r.Id}}); err != nil {
|
if err := w.WriteMsg(&dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeServerFailure, Id: r.Id}}); err != nil {
|
||||||
logger.Errorf("failed writing DNS response: %v", err)
|
logger.Errorf("failed writing DNS response: %v", err)
|
||||||
}
|
}
|
||||||
@@ -568,3 +583,16 @@ func determinePrefixChanges(oldPrefixes, newPrefixes []netip.Prefix) (toAdd, toR
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DnsInterceptor) debugPeerTimeout(peerIP netip.Addr, peerKey string) string {
|
||||||
|
if d.statusRecorder == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
peerState, err := d.statusRecorder.GetPeer(peerKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf(" (peer %s state error: %v)", peerKey[:8], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(" (peer %s)", nbdns.FormatPeerStatus(&peerState))
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
||||||
"github.com/netbirdio/netbird/client/internal/routeselector"
|
"github.com/netbirdio/netbird/client/internal/routeselector"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,6 +108,10 @@ func NewManager(config ManagerConfig) *DefaultManager {
|
|||||||
notifier := notifier.NewNotifier()
|
notifier := notifier.NewNotifier()
|
||||||
sysOps := systemops.NewSysOps(config.WGInterface, notifier)
|
sysOps := systemops.NewSysOps(config.WGInterface, notifier)
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" && config.WGInterface != nil {
|
||||||
|
nbnet.SetVPNInterfaceName(config.WGInterface.Name())
|
||||||
|
}
|
||||||
|
|
||||||
dm := &DefaultManager{
|
dm := &DefaultManager{
|
||||||
ctx: mCTX,
|
ctx: mCTX,
|
||||||
stop: cancel,
|
stop: cancel,
|
||||||
@@ -208,7 +212,7 @@ func (m *DefaultManager) Init() error {
|
|||||||
return nil
|
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)
|
log.Warnf("Failed cleaning up routing: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +223,7 @@ func (m *DefaultManager) Init() error {
|
|||||||
|
|
||||||
ips := resolveURLsToIPs(initialAddresses)
|
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)
|
return fmt.Errorf("setup routing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,11 +289,15 @@ func (m *DefaultManager) Stop(stateManager *statemanager.Manager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !nbnet.CustomRoutingDisabled() && !m.disableClientRoutes {
|
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)
|
log.Errorf("Error cleaning up routing: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("Routing cleanup complete")
|
log.Info("Routing cleanup complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
nbnet.SetVPNInterfaceName("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mux.Lock()
|
m.mux.Lock()
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SysOps) CleanupRouting(*statemanager.Manager) error {
|
func (r *SysOps) CleanupRouting(*statemanager.Manager, bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package systemops
|
package systemops
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -22,7 +21,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/routemanager/util"
|
"github.com/netbirdio/netbird/client/internal/routemanager/util"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"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
|
const localSubnetsCacheTTL = 15 * time.Minute
|
||||||
@@ -96,9 +95,9 @@ func (r *SysOps) cleanupRefCounter(stateManager *statemanager.Manager) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove hooks selectively
|
hooks.RemoveWriteHooks()
|
||||||
nbnet.RemoveDialerHooks()
|
hooks.RemoveCloseHooks()
|
||||||
nbnet.RemoveListenerHooks()
|
hooks.RemoveAddressRemoveHooks()
|
||||||
|
|
||||||
if err := r.refCounter.Flush(); err != nil {
|
if err := r.refCounter.Flush(); err != nil {
|
||||||
return fmt.Errorf("flush route manager: %w", err)
|
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 {
|
func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.Manager) error {
|
||||||
beforeHook := func(connID nbnet.ConnectionID, ip net.IP) error {
|
beforeHook := func(connID hooks.ConnectionID, prefix netip.Prefix) error {
|
||||||
prefix, err := util.GetPrefixFromIP(ip)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("convert ip to prefix: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := r.refCounter.IncrementWithID(string(connID), prefix, struct{}{}); err != nil {
|
if _, err := r.refCounter.IncrementWithID(string(connID), prefix, struct{}{}); err != nil {
|
||||||
return fmt.Errorf("adding route reference: %v", err)
|
return fmt.Errorf("adding route reference: %v", err)
|
||||||
}
|
}
|
||||||
@@ -304,7 +298,7 @@ func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.M
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
afterHook := func(connID nbnet.ConnectionID) error {
|
afterHook := func(connID hooks.ConnectionID) error {
|
||||||
if err := r.refCounter.DecrementWithID(string(connID)); err != nil {
|
if err := r.refCounter.DecrementWithID(string(connID)); err != nil {
|
||||||
return fmt.Errorf("remove route reference: %w", err)
|
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
|
var merr *multierror.Error
|
||||||
|
|
||||||
for _, ip := range initAddresses {
|
for _, ip := range initAddresses {
|
||||||
if err := beforeHook("init", ip); err != nil {
|
prefix, err := util.GetPrefixFromIP(ip)
|
||||||
merr = multierror.Append(merr, fmt.Errorf("add initial route for %s: %w", ip, err))
|
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 {
|
hooks.AddWriteHook(beforeHook)
|
||||||
if ctx.Err() != nil {
|
hooks.AddCloseHook(afterHook)
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
var merr *multierror.Error
|
hooks.AddAddressRemoveHook(func(connID hooks.ConnectionID, prefix netip.Prefix) 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 {
|
|
||||||
if _, err := r.refCounter.Decrement(prefix); err != nil {
|
if _, err := r.refCounter.Decrement(prefix); err != nil {
|
||||||
return fmt.Errorf("remove route reference: %w", err)
|
return fmt.Errorf("remove route reference: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
||||||
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dialer interface {
|
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)
|
wgInterface := createWGInterface(t, fmt.Sprintf("utun53%d", n), "100.65.75.2/24", 33100+n)
|
||||||
|
|
||||||
r := NewSysOps(wgInterface, nil)
|
r := NewSysOps(wgInterface, nil)
|
||||||
err := r.SetupRouting(nil, nil)
|
advancedRouting := nbnet.AdvancedRouting()
|
||||||
|
err := r.SetupRouting(nil, nil, advancedRouting)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, r.CleanupRouting(nil))
|
assert.NoError(t, r.CleanupRouting(nil, advancedRouting))
|
||||||
})
|
})
|
||||||
|
|
||||||
intf, err := net.InterfaceByName(wgInterface.Name())
|
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)
|
wgInterface := createWGInterface(t, fmt.Sprintf("utun54%d", n), "100.65.75.2/24", 33200+n)
|
||||||
|
|
||||||
r := NewSysOps(wgInterface, nil)
|
r := NewSysOps(wgInterface, nil)
|
||||||
err := r.SetupRouting(nil, nil)
|
advancedRouting := nbnet.AdvancedRouting()
|
||||||
|
err := r.SetupRouting(nil, nil, advancedRouting)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, r.CleanupRouting(nil))
|
assert.NoError(t, r.CleanupRouting(nil, advancedRouting))
|
||||||
})
|
})
|
||||||
|
|
||||||
initialNextHopV4, err := GetNextHop(netip.IPv4Unspecified())
|
initialNextHopV4, err := GetNextHop(netip.IPv4Unspecified())
|
||||||
@@ -484,10 +487,11 @@ func setupTestEnv(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
r := NewSysOps(wgInterface, nil)
|
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")
|
require.NoError(t, err, "setupRouting should not return err")
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
assert.NoError(t, r.CleanupRouting(nil))
|
assert.NoError(t, r.CleanupRouting(nil, advancedRouting))
|
||||||
})
|
})
|
||||||
|
|
||||||
index, err := net.InterfaceByName(wgInterface.Name())
|
index, err := net.InterfaceByName(wgInterface.Name())
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"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()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
r.prefixes = make(map[netip.Prefix]struct{})
|
r.prefixes = make(map[netip.Prefix]struct{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SysOps) CleanupRouting(*statemanager.Manager) error {
|
func (r *SysOps) CleanupRouting(*statemanager.Manager, bool) error {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
|
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
"github.com/netbirdio/netbird/client/internal/routemanager/vars"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"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
|
// 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.
|
// 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,
|
// This table is where a default route or other specific routes received from the management server are configured,
|
||||||
// enabling VPN connectivity.
|
// enabling VPN connectivity.
|
||||||
func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager) (err error) {
|
func (r *SysOps) SetupRouting(initAddresses []net.IP, stateManager *statemanager.Manager, advancedRouting bool) (err error) {
|
||||||
if !nbnet.AdvancedRouting() {
|
if !advancedRouting {
|
||||||
log.Infof("Using legacy routing setup")
|
log.Infof("Using legacy routing setup")
|
||||||
return r.setupRefCounter(initAddresses, stateManager)
|
return r.setupRefCounter(initAddresses, stateManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
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)
|
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'.
|
// 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.
|
// 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.
|
// The function uses error aggregation to report any errors encountered during the cleanup process.
|
||||||
func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error {
|
func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager, advancedRouting bool) error {
|
||||||
if !nbnet.AdvancedRouting() {
|
if !advancedRouting {
|
||||||
return r.cleanupRefCounter(stateManager)
|
return r.cleanupRefCounter(stateManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"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)
|
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)
|
return r.cleanupRefCounter(stateManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PacketExpectation struct {
|
type PacketExpectation struct {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -19,9 +20,16 @@ import (
|
|||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"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
|
type RouteUpdateType int
|
||||||
|
|
||||||
@@ -77,6 +85,14 @@ type MIB_IPFORWARD_TABLE2 struct {
|
|||||||
Table [1]MIB_IPFORWARD_ROW2 // Flexible array member
|
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
|
// 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 {
|
type IP_ADDRESS_PREFIX struct {
|
||||||
Prefix SOCKADDR_INET
|
Prefix SOCKADDR_INET
|
||||||
@@ -177,11 +193,20 @@ const (
|
|||||||
RouteDeleted
|
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)
|
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)
|
return r.cleanupRefCounter(stateManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,10 +660,7 @@ func getWindowsRoutingTable() (*MIB_IPFORWARD_TABLE2, error) {
|
|||||||
|
|
||||||
func freeWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) {
|
func freeWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) {
|
||||||
if table != nil {
|
if table != nil {
|
||||||
ret, _, _ := procFreeMibTable.Call(uintptr(unsafe.Pointer(table)))
|
_, _, _ = procFreeMibTable.Call(uintptr(unsafe.Pointer(table)))
|
||||||
if ret != 0 {
|
|
||||||
log.Warnf("FreeMibTable failed with return code: %d", ret)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,8 +674,7 @@ func parseWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) []DetailedRoute {
|
|||||||
entryPtr := basePtr + uintptr(i)*entrySize
|
entryPtr := basePtr + uintptr(i)*entrySize
|
||||||
entry := (*MIB_IPFORWARD_ROW2)(unsafe.Pointer(entryPtr))
|
entry := (*MIB_IPFORWARD_ROW2)(unsafe.Pointer(entryPtr))
|
||||||
|
|
||||||
detailed := buildWindowsDetailedRoute(entry)
|
if detailed := buildWindowsDetailedRoute(entry); detailed != nil {
|
||||||
if detailed != nil {
|
|
||||||
detailedRoutes = append(detailedRoutes, *detailed)
|
detailedRoutes = append(detailedRoutes, *detailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -802,6 +823,46 @@ func addZone(ip netip.Addr, interfaceIndex int) netip.Addr {
|
|||||||
return ip
|
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
|
// getInterfaceMetric retrieves the interface metric for a given interface and address family
|
||||||
func getInterfaceMetric(interfaceIndex uint32, family int16) int {
|
func getInterfaceMetric(interfaceIndex uint32, family int16) int {
|
||||||
if interfaceIndex == 0 {
|
if interfaceIndex == 0 {
|
||||||
@@ -821,6 +882,75 @@ func getInterfaceMetric(interfaceIndex uint32, family int16) int {
|
|||||||
return int(ipInterfaceRow.Metric)
|
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
|
// formatRouteAge formats the route age in seconds to a human-readable string
|
||||||
func formatRouteAge(ageSeconds uint32) string {
|
func formatRouteAge(ageSeconds uint32) string {
|
||||||
if ageSeconds == 0 {
|
if ageSeconds == 0 {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -12,18 +12,8 @@ func GetPrefixFromIP(ip net.IP) (netip.Prefix, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return netip.Prefix{}, fmt.Errorf("parse IP address: %s", ip)
|
return netip.Prefix{}, fmt.Errorf("parse IP address: %s", ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = addr.Unmap()
|
addr = addr.Unmap()
|
||||||
|
prefix := netip.PrefixFrom(addr, addr.BitLen())
|
||||||
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)
|
|
||||||
return prefix, nil
|
return prefix, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/transport/v3"
|
"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.
|
// Dial connects to the address on the named network.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/transport/v3"
|
"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.
|
// ListenPacket listens for incoming packets on the given network and address.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func NewNetWithDiscover(iFaceDiscover ExternalIFaceDiscover, disallowList []stri
|
|||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
n.iFaceDiscover = pionDiscover{}
|
n.iFaceDiscover = pionDiscover{}
|
||||||
} else {
|
} else {
|
||||||
newMobileIFaceDiscover(iFaceDiscover)
|
n.iFaceDiscover = newMobileIFaceDiscover(iFaceDiscover)
|
||||||
}
|
}
|
||||||
return n, n.UpdateInterfaces()
|
return n, n.UpdateInterfaces()
|
||||||
}
|
}
|
||||||
|
|||||||
49
client/net/conn.go
Normal file
49
client/net/conn.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
82
client/net/dial.go
Normal file
82
client/net/dial.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -16,6 +16,5 @@ func NewDialer() *Dialer {
|
|||||||
Dialer: &net.Dialer{},
|
Dialer: &net.Dialer{},
|
||||||
}
|
}
|
||||||
dialer.init()
|
dialer.init()
|
||||||
|
|
||||||
return dialer
|
return dialer
|
||||||
}
|
}
|
||||||
87
client/net/dialer_dial.go
Normal file
87
client/net/dialer_dial.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
7
client/net/dialer_init_generic.go
Normal file
7
client/net/dialer_init_generic.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !linux && !windows
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
func (d *Dialer) init() {
|
||||||
|
// implemented on Linux, Android, and Windows only
|
||||||
|
}
|
||||||
5
client/net/dialer_init_windows.go
Normal file
5
client/net/dialer_init_windows.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
func (d *Dialer) init() {
|
||||||
|
d.Dialer.Control = applyUnicastIFToSocket
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user