mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-21 17:56:39 +00:00
Compare commits
6 Commits
v0.69.0
...
fix-crowds
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a07c254f32 | ||
|
|
064ec1c832 | ||
|
|
75e408f51c | ||
|
|
5a89e6621b | ||
|
|
06dfa9d4a5 | ||
|
|
45d9ee52c0 |
11
client/firewall/firewalld/firewalld.go
Normal file
11
client/firewall/firewalld/firewalld.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Package firewalld integrates with the firewalld daemon so NetBird can place
|
||||||
|
// its wg interface into firewalld's "trusted" zone. This is required because
|
||||||
|
// firewalld's nftables chains are created with NFT_CHAIN_OWNER on recent
|
||||||
|
// versions, which returns EPERM to any other process that tries to insert
|
||||||
|
// rules into them. The workaround mirrors what Tailscale does: let firewalld
|
||||||
|
// itself add the accept rules to its own chains by trusting the interface.
|
||||||
|
package firewalld
|
||||||
|
|
||||||
|
// TrustedZone is the firewalld zone name used for interfaces whose traffic
|
||||||
|
// should bypass firewalld filtering.
|
||||||
|
const TrustedZone = "trusted"
|
||||||
260
client/firewall/firewalld/firewalld_linux.go
Normal file
260
client/firewall/firewalld/firewalld_linux.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package firewalld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dbusDest = "org.fedoraproject.FirewallD1"
|
||||||
|
dbusPath = "/org/fedoraproject/FirewallD1"
|
||||||
|
dbusRootIface = "org.fedoraproject.FirewallD1"
|
||||||
|
dbusZoneIface = "org.fedoraproject.FirewallD1.zone"
|
||||||
|
|
||||||
|
errZoneAlreadySet = "ZONE_ALREADY_SET"
|
||||||
|
errAlreadyEnabled = "ALREADY_ENABLED"
|
||||||
|
errUnknownIface = "UNKNOWN_INTERFACE"
|
||||||
|
errNotEnabled = "NOT_ENABLED"
|
||||||
|
|
||||||
|
// callTimeout bounds each individual DBus or firewall-cmd invocation.
|
||||||
|
// A fresh context is created for each call so a slow DBus probe can't
|
||||||
|
// exhaust the deadline before the firewall-cmd fallback gets to run.
|
||||||
|
callTimeout = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errDBusUnavailable = errors.New("firewalld dbus unavailable")
|
||||||
|
|
||||||
|
// trustLogOnce ensures the "added to trusted zone" message is logged at
|
||||||
|
// Info level only for the first successful add per process; repeat adds
|
||||||
|
// from other init paths are quieter.
|
||||||
|
trustLogOnce sync.Once
|
||||||
|
|
||||||
|
parentCtxMu sync.RWMutex
|
||||||
|
parentCtx context.Context = context.Background()
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetParentContext installs a parent context whose cancellation aborts any
|
||||||
|
// in-flight TrustInterface call. It does not affect UntrustInterface, which
|
||||||
|
// always uses a fresh Background-rooted timeout so cleanup can still run
|
||||||
|
// during engine shutdown when the engine context is already cancelled.
|
||||||
|
func SetParentContext(ctx context.Context) {
|
||||||
|
parentCtxMu.Lock()
|
||||||
|
parentCtx = ctx
|
||||||
|
parentCtxMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParentContext() context.Context {
|
||||||
|
parentCtxMu.RLock()
|
||||||
|
defer parentCtxMu.RUnlock()
|
||||||
|
return parentCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrustInterface places iface into firewalld's trusted zone if firewalld is
|
||||||
|
// running. It is idempotent and best-effort: errors are returned so callers
|
||||||
|
// can log, but a non-running firewalld is not an error. Only the first
|
||||||
|
// successful call per process logs at Info. Respects the parent context set
|
||||||
|
// via SetParentContext so startup-time cancellation unblocks it.
|
||||||
|
func TrustInterface(iface string) error {
|
||||||
|
parent := getParentContext()
|
||||||
|
if !isRunning(parent) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := addTrusted(parent, iface); err != nil {
|
||||||
|
return fmt.Errorf("add %s to firewalld trusted zone: %w", iface, err)
|
||||||
|
}
|
||||||
|
trustLogOnce.Do(func() {
|
||||||
|
log.Infof("added %s to firewalld trusted zone", iface)
|
||||||
|
})
|
||||||
|
log.Debugf("firewalld: ensured %s is in trusted zone", iface)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UntrustInterface removes iface from firewalld's trusted zone if firewalld
|
||||||
|
// is running. Idempotent. Uses a Background-rooted timeout so it still runs
|
||||||
|
// during shutdown after the engine context has been cancelled.
|
||||||
|
func UntrustInterface(iface string) error {
|
||||||
|
if !isRunning(context.Background()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := removeTrusted(context.Background(), iface); err != nil {
|
||||||
|
return fmt.Errorf("remove %s from firewalld trusted zone: %w", iface, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCallContext(parent context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(parent, callTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRunning(parent context.Context) bool {
|
||||||
|
ctx, cancel := newCallContext(parent)
|
||||||
|
ok, err := isRunningDBus(ctx)
|
||||||
|
cancel()
|
||||||
|
if err == nil {
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
if errors.Is(err, errDBusUnavailable) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
ctx, cancel = newCallContext(parent)
|
||||||
|
defer cancel()
|
||||||
|
return isRunningCLI(ctx)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTrusted(parent context.Context, iface string) error {
|
||||||
|
ctx, cancel := newCallContext(parent)
|
||||||
|
err := addDBus(ctx, iface)
|
||||||
|
cancel()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, errDBusUnavailable) {
|
||||||
|
log.Debugf("firewalld: dbus add failed, falling back to firewall-cmd: %v", err)
|
||||||
|
}
|
||||||
|
ctx, cancel = newCallContext(parent)
|
||||||
|
defer cancel()
|
||||||
|
return addCLI(ctx, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeTrusted(parent context.Context, iface string) error {
|
||||||
|
ctx, cancel := newCallContext(parent)
|
||||||
|
err := removeDBus(ctx, iface)
|
||||||
|
cancel()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, errDBusUnavailable) {
|
||||||
|
log.Debugf("firewalld: dbus remove failed, falling back to firewall-cmd: %v", err)
|
||||||
|
}
|
||||||
|
ctx, cancel = newCallContext(parent)
|
||||||
|
defer cancel()
|
||||||
|
return removeCLI(ctx, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRunningDBus(ctx context.Context) (bool, error) {
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("%w: %v", errDBusUnavailable, err)
|
||||||
|
}
|
||||||
|
obj := conn.Object(dbusDest, dbusPath)
|
||||||
|
|
||||||
|
var zone string
|
||||||
|
if err := obj.CallWithContext(ctx, dbusRootIface+".getDefaultZone", 0).Store(&zone); err != nil {
|
||||||
|
return false, fmt.Errorf("firewalld getDefaultZone: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRunningCLI(ctx context.Context) bool {
|
||||||
|
if _, err := exec.LookPath("firewall-cmd"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return exec.CommandContext(ctx, "firewall-cmd", "--state").Run() == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDBus(ctx context.Context, iface string) error {
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", errDBusUnavailable, err)
|
||||||
|
}
|
||||||
|
obj := conn.Object(dbusDest, dbusPath)
|
||||||
|
|
||||||
|
call := obj.CallWithContext(ctx, dbusZoneIface+".addInterface", 0, TrustedZone, iface)
|
||||||
|
if call.Err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbusErrContains(call.Err, errAlreadyEnabled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbusErrContains(call.Err, errZoneAlreadySet) {
|
||||||
|
move := obj.CallWithContext(ctx, dbusZoneIface+".changeZoneOfInterface", 0, TrustedZone, iface)
|
||||||
|
if move.Err != nil {
|
||||||
|
return fmt.Errorf("firewalld changeZoneOfInterface: %w", move.Err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("firewalld addInterface: %w", call.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDBus(ctx context.Context, iface string) error {
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", errDBusUnavailable, err)
|
||||||
|
}
|
||||||
|
obj := conn.Object(dbusDest, dbusPath)
|
||||||
|
|
||||||
|
call := obj.CallWithContext(ctx, dbusZoneIface+".removeInterface", 0, TrustedZone, iface)
|
||||||
|
if call.Err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbusErrContains(call.Err, errUnknownIface) || dbusErrContains(call.Err, errNotEnabled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("firewalld removeInterface: %w", call.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCLI(ctx context.Context, iface string) error {
|
||||||
|
if _, err := exec.LookPath("firewall-cmd"); err != nil {
|
||||||
|
return fmt.Errorf("firewall-cmd not available: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --change-interface (no --permanent) binds the interface for the
|
||||||
|
// current runtime only; we do not want membership to persist across
|
||||||
|
// reboots because netbird re-asserts it on every startup.
|
||||||
|
out, err := exec.CommandContext(ctx,
|
||||||
|
"firewall-cmd", "--zone="+TrustedZone, "--change-interface="+iface,
|
||||||
|
).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("firewall-cmd change-interface: %w: %s", err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCLI(ctx context.Context, iface string) error {
|
||||||
|
if _, err := exec.LookPath("firewall-cmd"); err != nil {
|
||||||
|
return fmt.Errorf("firewall-cmd not available: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := exec.CommandContext(ctx,
|
||||||
|
"firewall-cmd", "--zone="+TrustedZone, "--remove-interface="+iface,
|
||||||
|
).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
msg := strings.TrimSpace(string(out))
|
||||||
|
if strings.Contains(msg, errUnknownIface) || strings.Contains(msg, errNotEnabled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("firewall-cmd remove-interface: %w: %s", err, msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbusErrContains(err error, code string) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var de dbus.Error
|
||||||
|
if errors.As(err, &de) {
|
||||||
|
for _, b := range de.Body {
|
||||||
|
if s, ok := b.(string); ok && strings.Contains(s, code) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Contains(err.Error(), code)
|
||||||
|
}
|
||||||
49
client/firewall/firewalld/firewalld_linux_test.go
Normal file
49
client/firewall/firewalld/firewalld_linux_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package firewalld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDBusErrContains(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
code string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"nil error", nil, errZoneAlreadySet, false},
|
||||||
|
{"plain error match", errors.New("ZONE_ALREADY_SET: wt0"), errZoneAlreadySet, true},
|
||||||
|
{"plain error miss", errors.New("something else"), errZoneAlreadySet, false},
|
||||||
|
{
|
||||||
|
"dbus.Error body match",
|
||||||
|
dbus.Error{Name: "org.fedoraproject.FirewallD1.Exception", Body: []any{"ZONE_ALREADY_SET: wt0"}},
|
||||||
|
errZoneAlreadySet,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dbus.Error body miss",
|
||||||
|
dbus.Error{Name: "org.fedoraproject.FirewallD1.Exception", Body: []any{"INVALID_INTERFACE"}},
|
||||||
|
errAlreadyEnabled,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dbus.Error non-string body falls back to Error()",
|
||||||
|
dbus.Error{Name: "x", Body: []any{123}},
|
||||||
|
"x",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := dbusErrContains(tc.err, tc.code)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Fatalf("dbusErrContains(%v, %q) = %v; want %v", tc.err, tc.code, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
25
client/firewall/firewalld/firewalld_other.go
Normal file
25
client/firewall/firewalld/firewalld_other.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package firewalld
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// SetParentContext is a no-op on non-Linux platforms because firewalld only
|
||||||
|
// runs on Linux.
|
||||||
|
func SetParentContext(context.Context) {
|
||||||
|
// intentionally empty: firewalld is a Linux-only daemon
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrustInterface is a no-op on non-Linux platforms because firewalld only
|
||||||
|
// runs on Linux.
|
||||||
|
func TrustInterface(string) error {
|
||||||
|
// intentionally empty: firewalld is a Linux-only daemon
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UntrustInterface is a no-op on non-Linux platforms because firewalld only
|
||||||
|
// runs on Linux.
|
||||||
|
func UntrustInterface(string) error {
|
||||||
|
// intentionally empty: firewalld is a Linux-only daemon
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
@@ -86,6 +87,12 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
|||||||
log.Warnf("raw table not available, notrack rules will be disabled: %v", err)
|
log.Warnf("raw table not available, notrack rules will be disabled: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trust after all fatal init steps so a later failure doesn't leave the
|
||||||
|
// interface in firewalld's trusted zone without a corresponding Close.
|
||||||
|
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
|
||||||
|
log.Warnf("failed to trust interface in firewalld: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// persist early to ensure cleanup of chains
|
// persist early to ensure cleanup of chains
|
||||||
go func() {
|
go func() {
|
||||||
if err := stateManager.PersistState(context.Background()); err != nil {
|
if err := stateManager.PersistState(context.Background()); err != nil {
|
||||||
@@ -191,6 +198,12 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
|||||||
merr = multierror.Append(merr, fmt.Errorf("reset router: %w", err))
|
merr = multierror.Append(merr, fmt.Errorf("reset router: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Appending to merr intentionally blocks DeleteState below so ShutdownState
|
||||||
|
// stays persisted and the crash-recovery path retries firewalld cleanup.
|
||||||
|
if err := firewalld.UntrustInterface(m.wgIface.Name()); err != nil {
|
||||||
|
merr = multierror.Append(merr, err)
|
||||||
|
}
|
||||||
|
|
||||||
// attempt to delete state only if all other operations succeeded
|
// attempt to delete state only if all other operations succeeded
|
||||||
if merr == nil {
|
if merr == nil {
|
||||||
if err := stateManager.DeleteState(&ShutdownState{}); err != nil {
|
if err := stateManager.DeleteState(&ShutdownState{}); err != nil {
|
||||||
@@ -217,6 +230,11 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("allow netbird interface traffic: %w", err)
|
return fmt.Errorf("allow netbird interface traffic: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
|
||||||
|
log.Warnf("failed to trust interface in firewalld: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
@@ -217,6 +218,10 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
return fmt.Errorf("flush allow input netbird rules: %w", err)
|
return fmt.Errorf("flush allow input netbird rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
|
||||||
|
log.Warnf("failed to trust interface in firewalld: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
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"
|
||||||
@@ -40,6 +41,8 @@ const (
|
|||||||
chainNameForward = "FORWARD"
|
chainNameForward = "FORWARD"
|
||||||
chainNameMangleForward = "netbird-mangle-forward"
|
chainNameMangleForward = "netbird-mangle-forward"
|
||||||
|
|
||||||
|
firewalldTableName = "firewalld"
|
||||||
|
|
||||||
userDataAcceptForwardRuleIif = "frwacceptiif"
|
userDataAcceptForwardRuleIif = "frwacceptiif"
|
||||||
userDataAcceptForwardRuleOif = "frwacceptoif"
|
userDataAcceptForwardRuleOif = "frwacceptoif"
|
||||||
userDataAcceptInputRule = "inputaccept"
|
userDataAcceptInputRule = "inputaccept"
|
||||||
@@ -133,6 +136,10 @@ func (r *router) Reset() error {
|
|||||||
merr = multierror.Append(merr, fmt.Errorf("remove accept filter rules: %w", err))
|
merr = multierror.Append(merr, fmt.Errorf("remove accept filter rules: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := firewalld.UntrustInterface(r.wgIface.Name()); err != nil {
|
||||||
|
merr = multierror.Append(merr, err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.removeNatPreroutingRules(); err != nil {
|
if err := r.removeNatPreroutingRules(); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("remove filter prerouting rules: %w", err))
|
merr = multierror.Append(merr, fmt.Errorf("remove filter prerouting rules: %w", err))
|
||||||
}
|
}
|
||||||
@@ -280,6 +287,10 @@ func (r *router) createContainers() error {
|
|||||||
log.Errorf("failed to add accept rules for the forward chain: %s", err)
|
log.Errorf("failed to add accept rules for the forward chain: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := firewalld.TrustInterface(r.wgIface.Name()); err != nil {
|
||||||
|
log.Warnf("failed to trust interface in firewalld: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
log.Errorf("failed to refresh rules: %s", err)
|
log.Errorf("failed to refresh rules: %s", err)
|
||||||
}
|
}
|
||||||
@@ -1319,6 +1330,13 @@ func (r *router) isExternalChain(chain *nftables.Chain) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip firewalld-owned chains. Firewalld creates its chains with the
|
||||||
|
// NFT_CHAIN_OWNER flag, so inserting rules into them returns EPERM.
|
||||||
|
// We delegate acceptance to firewalld by trusting the interface instead.
|
||||||
|
if chain.Table.Name == firewalldTableName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Skip all iptables-managed tables in the ip family
|
// Skip all iptables-managed tables in the ip family
|
||||||
if chain.Table.Family == nftables.TableFamilyIPv4 && isIptablesTable(chain.Table.Name) {
|
if chain.Table.Family == nftables.TableFamilyIPv4 && isIptablesTable(chain.Table.Name) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
package uspfilter
|
package uspfilter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,6 +19,9 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
|||||||
if m.nativeFirewall != nil {
|
if m.nativeFirewall != nil {
|
||||||
return m.nativeFirewall.Close(stateManager)
|
return m.nativeFirewall.Close(stateManager)
|
||||||
}
|
}
|
||||||
|
if err := firewalld.UntrustInterface(m.wgIface.Name()); err != nil {
|
||||||
|
log.Warnf("failed to untrust interface in firewalld: %v", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,5 +30,8 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
if m.nativeFirewall != nil {
|
if m.nativeFirewall != nil {
|
||||||
return m.nativeFirewall.AllowNetbird()
|
return m.nativeFirewall.AllowNetbird()
|
||||||
}
|
}
|
||||||
|
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
|
||||||
|
log.Warnf("failed to trust interface in firewalld: %v", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
// IFaceMapper defines subset methods of interface required for manager
|
// IFaceMapper defines subset methods of interface required for manager
|
||||||
type IFaceMapper interface {
|
type IFaceMapper interface {
|
||||||
|
Name() string
|
||||||
SetFilter(device.PacketFilter) error
|
SetFilter(device.PacketFilter) error
|
||||||
Address() wgaddr.Address
|
Address() wgaddr.Address
|
||||||
GetWGDevice() *wgdevice.Device
|
GetWGDevice() *wgdevice.Device
|
||||||
|
|||||||
@@ -31,12 +31,20 @@ var logger = log.NewFromLogrus(logrus.StandardLogger())
|
|||||||
var flowLogger = netflow.NewManager(nil, []byte{}, nil).GetLogger()
|
var flowLogger = netflow.NewManager(nil, []byte{}, nil).GetLogger()
|
||||||
|
|
||||||
type IFaceMock struct {
|
type IFaceMock struct {
|
||||||
|
NameFunc func() string
|
||||||
SetFilterFunc func(device.PacketFilter) error
|
SetFilterFunc func(device.PacketFilter) error
|
||||||
AddressFunc func() wgaddr.Address
|
AddressFunc func() wgaddr.Address
|
||||||
GetWGDeviceFunc func() *wgdevice.Device
|
GetWGDeviceFunc func() *wgdevice.Device
|
||||||
GetDeviceFunc func() *device.FilteredDevice
|
GetDeviceFunc func() *device.FilteredDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *IFaceMock) Name() string {
|
||||||
|
if i.NameFunc == nil {
|
||||||
|
return "wgtest"
|
||||||
|
}
|
||||||
|
return i.NameFunc()
|
||||||
|
}
|
||||||
|
|
||||||
func (i *IFaceMock) GetWGDevice() *wgdevice.Device {
|
func (i *IFaceMock) GetWGDevice() *wgdevice.Device {
|
||||||
if i.GetWGDeviceFunc == nil {
|
if i.GetWGDeviceFunc == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
defaultResolvConfPath = "/etc/resolv.conf"
|
defaultResolvConfPath = "/etc/resolv.conf"
|
||||||
|
nsswitchConfPath = "/etc/nsswitch.conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type resolvConf struct {
|
type resolvConf struct {
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ type restoreHostManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newHostManager(wgInterface string) (hostManager, error) {
|
func newHostManager(wgInterface string) (hostManager, error) {
|
||||||
osManager, err := getOSDNSManagerType()
|
osManager, reason, err := getOSDNSManagerType()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get os dns manager type: %w", err)
|
return nil, fmt.Errorf("get os dns manager type: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("System DNS manager discovered: %s", osManager)
|
log.Infof("System DNS manager discovered: %s (%s)", osManager, reason)
|
||||||
mgr, err := newHostManagerFromType(wgInterface, osManager)
|
mgr, err := newHostManagerFromType(wgInterface, osManager)
|
||||||
// need to explicitly return nil mgr on error to avoid returning a non-nil interface containing a nil value
|
// need to explicitly return nil mgr on error to avoid returning a non-nil interface containing a nil value
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,17 +74,49 @@ func newHostManagerFromType(wgInterface string, osManager osManagerType) (restor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOSDNSManagerType() (osManagerType, error) {
|
func getOSDNSManagerType() (osManagerType, string, error) {
|
||||||
|
resolved := isSystemdResolvedRunning()
|
||||||
|
nss := isLibnssResolveUsed()
|
||||||
|
stub := checkStub()
|
||||||
|
|
||||||
|
// Prefer systemd-resolved whenever it owns libc resolution, regardless of
|
||||||
|
// who wrote /etc/resolv.conf. File-mode rewrites do not affect lookups
|
||||||
|
// that go through nss-resolve, and in foreign mode they can loop back
|
||||||
|
// through resolved as an upstream.
|
||||||
|
if resolved && (nss || stub) {
|
||||||
|
return systemdManager, fmt.Sprintf("systemd-resolved active (nss-resolve=%t, stub=%t)", nss, stub), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, reason, rejected, err := scanResolvConfHeader()
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
if reason != "" {
|
||||||
|
return mgr, reason, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fallback := fmt.Sprintf("no manager matched (resolved=%t, nss-resolve=%t, stub=%t)", resolved, nss, stub)
|
||||||
|
if len(rejected) > 0 {
|
||||||
|
fallback += "; rejected: " + strings.Join(rejected, ", ")
|
||||||
|
}
|
||||||
|
return fileManager, fallback, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanResolvConfHeader walks /etc/resolv.conf header comments and returns the
|
||||||
|
// matching manager. If reason is empty the caller should pick file mode and
|
||||||
|
// use rejected for diagnostics.
|
||||||
|
func scanResolvConfHeader() (osManagerType, string, []string, error) {
|
||||||
file, err := os.Open(defaultResolvConfPath)
|
file, err := os.Open(defaultResolvConfPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("unable to open %s for checking owner, got error: %w", defaultResolvConfPath, err)
|
return 0, "", nil, fmt.Errorf("unable to open %s for checking owner, got error: %w", defaultResolvConfPath, err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := file.Close(); err != nil {
|
if cerr := file.Close(); cerr != nil {
|
||||||
log.Errorf("close file %s: %s", defaultResolvConfPath, err)
|
log.Errorf("close file %s: %s", defaultResolvConfPath, cerr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var rejected []string
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
@@ -92,41 +124,48 @@ func getOSDNSManagerType() (osManagerType, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if text[0] != '#' {
|
if text[0] != '#' {
|
||||||
return fileManager, nil
|
break
|
||||||
}
|
}
|
||||||
if strings.Contains(text, fileGeneratedResolvConfContentHeader) {
|
if mgr, reason, rej := matchResolvConfHeader(text); reason != "" {
|
||||||
return netbirdManager, nil
|
return mgr, reason, nil, nil
|
||||||
}
|
} else if rej != "" {
|
||||||
if strings.Contains(text, "NetworkManager") && isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
|
rejected = append(rejected, rej)
|
||||||
return networkManager, nil
|
|
||||||
}
|
|
||||||
if strings.Contains(text, "systemd-resolved") && isSystemdResolvedRunning() {
|
|
||||||
if checkStub() {
|
|
||||||
return systemdManager, nil
|
|
||||||
} else {
|
|
||||||
return fileManager, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(text, "resolvconf") {
|
|
||||||
if isSystemdResolveConfMode() {
|
|
||||||
return systemdManager, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvConfManager, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil && err != io.EOF {
|
if err := scanner.Err(); err != nil && err != io.EOF {
|
||||||
return 0, fmt.Errorf("scan: %w", err)
|
return 0, "", nil, fmt.Errorf("scan: %w", err)
|
||||||
|
}
|
||||||
|
return 0, "", rejected, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileManager, nil
|
// matchResolvConfHeader inspects a single comment line. Returns either a
|
||||||
|
// definitive (manager, reason) or a non-empty rejected diagnostic.
|
||||||
|
func matchResolvConfHeader(text string) (osManagerType, string, string) {
|
||||||
|
if strings.Contains(text, fileGeneratedResolvConfContentHeader) {
|
||||||
|
return netbirdManager, "netbird-managed resolv.conf header detected", ""
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "NetworkManager") {
|
||||||
|
if isDbusListenerRunning(networkManagerDest, networkManagerDbusObjectNode) && isNetworkManagerSupported() {
|
||||||
|
return networkManager, "NetworkManager header + supported version on dbus", ""
|
||||||
|
}
|
||||||
|
return 0, "", "NetworkManager header (no dbus or unsupported version)"
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "resolvconf") {
|
||||||
|
if isSystemdResolveConfMode() {
|
||||||
|
return systemdManager, "resolvconf header in systemd-resolved compatibility mode", ""
|
||||||
|
}
|
||||||
|
return resolvConfManager, "resolvconf header detected", ""
|
||||||
|
}
|
||||||
|
return 0, "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkStub checks if the stub resolver is disabled in systemd-resolved. If it is disabled, we fall back to file manager.
|
// checkStub reports whether systemd-resolved's stub (127.0.0.53) is listed
|
||||||
|
// in /etc/resolv.conf. On parse failure we assume it is, to avoid dropping
|
||||||
|
// into file mode while resolved is active.
|
||||||
func checkStub() bool {
|
func checkStub() bool {
|
||||||
rConf, err := parseDefaultResolvConf()
|
rConf, err := parseDefaultResolvConf()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to parse resolv conf: %s", err)
|
log.Warnf("failed to parse resolv conf, assuming stub is active: %s", err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,3 +178,36 @@ func checkStub() bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isLibnssResolveUsed reports whether nss-resolve is listed before dns on
|
||||||
|
// the hosts: line of /etc/nsswitch.conf. When it is, libc lookups are
|
||||||
|
// delegated to systemd-resolved regardless of /etc/resolv.conf.
|
||||||
|
func isLibnssResolveUsed() bool {
|
||||||
|
bs, err := os.ReadFile(nsswitchConfPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("read %s: %v", nsswitchConfPath, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return parseNsswitchResolveAhead(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNsswitchResolveAhead(data []byte) bool {
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
if i := strings.IndexByte(line, '#'); i >= 0 {
|
||||||
|
line = line[:i]
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 || fields[0] != "hosts:" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, module := range fields[1:] {
|
||||||
|
switch module {
|
||||||
|
case "dns":
|
||||||
|
return false
|
||||||
|
case "resolve":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
76
client/internal/dns/host_unix_test.go
Normal file
76
client/internal/dns/host_unix_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseNsswitchResolveAhead(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "resolve before dns with action token",
|
||||||
|
in: "hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns\n",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dns before resolve",
|
||||||
|
in: "hosts: files mdns4_minimal [NOTFOUND=return] dns resolve\n",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "debian default with only dns",
|
||||||
|
in: "hosts: files mdns4_minimal [NOTFOUND=return] dns mymachines\n",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neither resolve nor dns",
|
||||||
|
in: "hosts: files myhostname\n",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no hosts line",
|
||||||
|
in: "passwd: files systemd\ngroup: files systemd\n",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
in: "",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comments and blank lines ignored",
|
||||||
|
in: "# comment\n\n# another\nhosts: resolve dns\n",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing inline comment",
|
||||||
|
in: "hosts: resolve [!UNAVAIL=return] dns # fallback\n",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hosts token must be the first field",
|
||||||
|
in: " hosts: resolve dns\n",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other db line mentioning resolve is ignored",
|
||||||
|
in: "networks: resolve\nhosts: dns\n",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only resolve, no dns",
|
||||||
|
in: "hosts: files resolve\n",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := parseNsswitchResolveAhead([]byte(tt.in)); got != tt.want {
|
||||||
|
t.Errorf("parseNsswitchResolveAhead() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
"github.com/netbirdio/netbird/client/firewall"
|
"github.com/netbirdio/netbird/client/firewall"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/firewalld"
|
||||||
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/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
@@ -570,7 +571,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
e.connMgr.Start(e.ctx)
|
e.connMgr.Start(e.ctx)
|
||||||
|
|
||||||
e.srWatcher = guard.NewSRWatcher(e.signal, e.relayManager, e.mobileDep.IFaceDiscover, iceCfg)
|
e.srWatcher = guard.NewSRWatcher(e.signal, e.relayManager, e.mobileDep.IFaceDiscover, iceCfg)
|
||||||
e.srWatcher.Start()
|
e.srWatcher.Start(peer.IsForceRelayed())
|
||||||
|
|
||||||
e.receiveSignalEvents()
|
e.receiveSignalEvents()
|
||||||
e.receiveManagementEvents()
|
e.receiveManagementEvents()
|
||||||
@@ -604,6 +605,8 @@ func (e *Engine) createFirewall() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
firewalld.SetParentContext(e.ctx)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.flowManager.GetLogger(), e.config.DisableServerRoutes, e.config.MTU)
|
e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.flowManager.GetLogger(), e.config.DisableServerRoutes, e.config.MTU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -185,17 +185,20 @@ func (conn *Conn) Open(engineCtx context.Context) error {
|
|||||||
|
|
||||||
conn.workerRelay = NewWorkerRelay(conn.ctx, conn.Log, isController(conn.config), conn.config, conn, conn.relayManager)
|
conn.workerRelay = NewWorkerRelay(conn.ctx, conn.Log, isController(conn.config), conn.config, conn, conn.relayManager)
|
||||||
|
|
||||||
|
forceRelay := IsForceRelayed()
|
||||||
|
if !forceRelay {
|
||||||
relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
|
relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
|
||||||
workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally)
|
workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.workerICE = workerICE
|
conn.workerICE = workerICE
|
||||||
|
}
|
||||||
|
|
||||||
conn.handshaker = NewHandshaker(conn.Log, conn.config, conn.signaler, conn.workerICE, conn.workerRelay, conn.metricsStages)
|
conn.handshaker = NewHandshaker(conn.Log, conn.config, conn.signaler, conn.workerICE, conn.workerRelay, conn.metricsStages)
|
||||||
|
|
||||||
conn.handshaker.AddRelayListener(conn.workerRelay.OnNewOffer)
|
conn.handshaker.AddRelayListener(conn.workerRelay.OnNewOffer)
|
||||||
if !isForceRelayed() {
|
if !forceRelay {
|
||||||
conn.handshaker.AddICEListener(conn.workerICE.OnNewOffer)
|
conn.handshaker.AddICEListener(conn.workerICE.OnNewOffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +254,9 @@ func (conn *Conn) Close(signalToRemote bool) {
|
|||||||
conn.wgWatcherCancel()
|
conn.wgWatcherCancel()
|
||||||
}
|
}
|
||||||
conn.workerRelay.CloseConn()
|
conn.workerRelay.CloseConn()
|
||||||
|
if conn.workerICE != nil {
|
||||||
conn.workerICE.Close()
|
conn.workerICE.Close()
|
||||||
|
}
|
||||||
|
|
||||||
if conn.wgProxyRelay != nil {
|
if conn.wgProxyRelay != nil {
|
||||||
err := conn.wgProxyRelay.CloseConn()
|
err := conn.wgProxyRelay.CloseConn()
|
||||||
@@ -294,8 +299,10 @@ func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) {
|
|||||||
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
||||||
func (conn *Conn) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HAMap) {
|
func (conn *Conn) OnRemoteCandidate(candidate ice.Candidate, haRoutes route.HAMap) {
|
||||||
conn.dumpState.RemoteCandidate()
|
conn.dumpState.RemoteCandidate()
|
||||||
|
if conn.workerICE != nil {
|
||||||
conn.workerICE.OnRemoteCandidate(candidate, haRoutes)
|
conn.workerICE.OnRemoteCandidate(candidate, haRoutes)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetOnConnected sets a handler function to be triggered by Conn when a new connection to a remote peer established
|
// SetOnConnected sets a handler function to be triggered by Conn when a new connection to a remote peer established
|
||||||
func (conn *Conn) SetOnConnected(handler func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)) {
|
func (conn *Conn) SetOnConnected(handler func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)) {
|
||||||
@@ -712,33 +719,35 @@ func (conn *Conn) evalStatus() ConnStatus {
|
|||||||
return StatusConnecting
|
return StatusConnecting
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) isConnectedOnAllWay() (connected bool) {
|
// isConnectedOnAllWay evaluates the overall connection status based on ICE and Relay transports.
|
||||||
// would be better to protect this with a mutex, but it could cause deadlock with Close function
|
//
|
||||||
|
// The result is a tri-state:
|
||||||
|
// - ConnStatusConnected: all available transports are up
|
||||||
|
// - ConnStatusPartiallyConnected: relay is up but ICE is still pending/reconnecting
|
||||||
|
// - ConnStatusDisconnected: no working transport
|
||||||
|
func (conn *Conn) isConnectedOnAllWay() (status guard.ConnStatus) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if !connected {
|
if status == guard.ConnStatusDisconnected {
|
||||||
conn.logTraceConnState()
|
conn.logTraceConnState()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// For JS platform: only relay connection is supported
|
iceWorkerCreated := conn.workerICE != nil
|
||||||
if runtime.GOOS == "js" {
|
|
||||||
return conn.statusRelay.Get() == worker.StatusConnected
|
var iceInProgress bool
|
||||||
|
if iceWorkerCreated {
|
||||||
|
iceInProgress = conn.workerICE.InProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-JS platforms: check ICE connection status
|
return evalConnStatus(connStatusInputs{
|
||||||
if conn.statusICE.Get() == worker.StatusDisconnected && !conn.workerICE.InProgress() {
|
forceRelay: IsForceRelayed(),
|
||||||
return false
|
peerUsesRelay: conn.workerRelay.IsRelayConnectionSupportedWithPeer(),
|
||||||
}
|
relayConnected: conn.statusRelay.Get() == worker.StatusConnected,
|
||||||
|
remoteSupportsICE: conn.handshaker.RemoteICESupported(),
|
||||||
// If relay is supported with peer, it must also be connected
|
iceWorkerCreated: iceWorkerCreated,
|
||||||
if conn.workerRelay.IsRelayConnectionSupportedWithPeer() {
|
iceStatusConnecting: conn.statusICE.Get() != worker.StatusDisconnected,
|
||||||
if conn.statusRelay.Get() == worker.StatusDisconnected {
|
iceInProgress: iceInProgress,
|
||||||
return false
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) enableWgWatcherIfNeeded(enabledTime time.Time) {
|
func (conn *Conn) enableWgWatcherIfNeeded(enabledTime time.Time) {
|
||||||
@@ -926,3 +935,43 @@ func isController(config ConnConfig) bool {
|
|||||||
func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
|
func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
|
||||||
return remoteRosenpassPubKey != nil
|
return remoteRosenpassPubKey != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func evalConnStatus(in connStatusInputs) guard.ConnStatus {
|
||||||
|
// "Relay up and needed" — the peer uses relay and the transport is connected.
|
||||||
|
relayUsedAndUp := in.peerUsesRelay && in.relayConnected
|
||||||
|
|
||||||
|
// Force-relay mode: ICE never runs. Relay is the only transport and must be up.
|
||||||
|
if in.forceRelay {
|
||||||
|
return boolToConnStatus(relayUsedAndUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remote peer doesn't support ICE, or we haven't created the worker yet:
|
||||||
|
// relay is the only possible transport.
|
||||||
|
if !in.remoteSupportsICE || !in.iceWorkerCreated {
|
||||||
|
return boolToConnStatus(relayUsedAndUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICE counts as "up" when the status is anything other than Disconnected, OR
|
||||||
|
// when a negotiation is currently in progress (so we don't spam offers while one is in flight).
|
||||||
|
iceUp := in.iceStatusConnecting || in.iceInProgress
|
||||||
|
|
||||||
|
// Relay side is acceptable if the peer doesn't rely on relay, or relay is connected.
|
||||||
|
relayOK := !in.peerUsesRelay || in.relayConnected
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case iceUp && relayOK:
|
||||||
|
return guard.ConnStatusConnected
|
||||||
|
case relayUsedAndUp:
|
||||||
|
// Relay is up but ICE is down — partially connected.
|
||||||
|
return guard.ConnStatusPartiallyConnected
|
||||||
|
default:
|
||||||
|
return guard.ConnStatusDisconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToConnStatus(connected bool) guard.ConnStatus {
|
||||||
|
if connected {
|
||||||
|
return guard.ConnStatusConnected
|
||||||
|
}
|
||||||
|
return guard.ConnStatusDisconnected
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ const (
|
|||||||
StatusConnected
|
StatusConnected
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// connStatusInputs is the primitive-valued snapshot of the state that drives the
|
||||||
|
// tri-state connection classification. Extracted so the decision logic can be unit-tested
|
||||||
|
// without constructing full Worker/Handshaker objects.
|
||||||
|
type connStatusInputs struct {
|
||||||
|
forceRelay bool // NB_FORCE_RELAY or JS/WASM
|
||||||
|
peerUsesRelay bool // remote peer advertises relay support AND local has relay
|
||||||
|
relayConnected bool // statusRelay reports Connected (independent of whether peer uses relay)
|
||||||
|
remoteSupportsICE bool // remote peer sent ICE credentials
|
||||||
|
iceWorkerCreated bool // local WorkerICE exists (false in force-relay mode)
|
||||||
|
iceStatusConnecting bool // statusICE is anything other than Disconnected
|
||||||
|
iceInProgress bool // a negotiation is currently in flight
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ConnStatus describe the status of a peer's connection
|
// ConnStatus describe the status of a peer's connection
|
||||||
type ConnStatus int32
|
type ConnStatus int32
|
||||||
|
|
||||||
|
|||||||
201
client/internal/peer/conn_status_eval_test.go
Normal file
201
client/internal/peer/conn_status_eval_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEvalConnStatus_ForceRelay(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in connStatusInputs
|
||||||
|
want guard.ConnStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "force relay, peer uses relay, relay up",
|
||||||
|
in: connStatusInputs{
|
||||||
|
forceRelay: true,
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "force relay, peer uses relay, relay down",
|
||||||
|
in: connStatusInputs{
|
||||||
|
forceRelay: true,
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: false,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "force relay, peer does NOT use relay - disconnected forever",
|
||||||
|
in: connStatusInputs{
|
||||||
|
forceRelay: true,
|
||||||
|
peerUsesRelay: false,
|
||||||
|
relayConnected: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := evalConnStatus(tc.in); got != tc.want {
|
||||||
|
t.Fatalf("evalConnStatus = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalConnStatus_ICEUnavailable(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in connStatusInputs
|
||||||
|
want guard.ConnStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "remote does not support ICE, peer uses relay, relay up",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: true,
|
||||||
|
remoteSupportsICE: false,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remote does not support ICE, peer uses relay, relay down",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: false,
|
||||||
|
remoteSupportsICE: false,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE worker not yet created, relay up",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: true,
|
||||||
|
relayConnected: true,
|
||||||
|
remoteSupportsICE: true,
|
||||||
|
iceWorkerCreated: false,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remote does not support ICE, peer does not use relay",
|
||||||
|
in: connStatusInputs{
|
||||||
|
peerUsesRelay: false,
|
||||||
|
relayConnected: false,
|
||||||
|
remoteSupportsICE: false,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := evalConnStatus(tc.in); got != tc.want {
|
||||||
|
t.Fatalf("evalConnStatus = %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvalConnStatus_FullyAvailable(t *testing.T) {
|
||||||
|
base := connStatusInputs{
|
||||||
|
remoteSupportsICE: true,
|
||||||
|
iceWorkerCreated: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mutator func(*connStatusInputs)
|
||||||
|
want guard.ConnStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ICE connected, relay connected, peer uses relay",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = true
|
||||||
|
in.relayConnected = true
|
||||||
|
in.iceStatusConnecting = true
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE connected, peer does NOT use relay",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.relayConnected = false
|
||||||
|
in.iceStatusConnecting = true
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE InProgress only, peer does NOT use relay",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = true
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE down, relay up, peer uses relay -> partial",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = true
|
||||||
|
in.relayConnected = true
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = false
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusPartiallyConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE down, peer does NOT use relay -> disconnected",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.relayConnected = false
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = false
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE up, peer uses relay but relay down -> partial (relay required, ICE ignored)",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = true
|
||||||
|
in.relayConnected = false
|
||||||
|
in.iceStatusConnecting = true
|
||||||
|
},
|
||||||
|
// relayOK = false (peer uses relay but it's down), iceUp = true
|
||||||
|
// first switch arm fails (relayOK false), relayUsedAndUp = false (relay down),
|
||||||
|
// falls into default: Disconnected.
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICE down, relay up but peer does not use relay -> disconnected",
|
||||||
|
mutator: func(in *connStatusInputs) {
|
||||||
|
in.peerUsesRelay = false
|
||||||
|
in.relayConnected = true // not actually used since peer doesn't rely on it
|
||||||
|
in.iceStatusConnecting = false
|
||||||
|
in.iceInProgress = false
|
||||||
|
},
|
||||||
|
want: guard.ConnStatusDisconnected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
in := base
|
||||||
|
tc.mutator(&in)
|
||||||
|
if got := evalConnStatus(in); got != tc.want {
|
||||||
|
t.Fatalf("evalConnStatus = %v, want %v (inputs: %+v)", got, tc.want, in)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ const (
|
|||||||
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isForceRelayed() bool {
|
func IsForceRelayed() bool {
|
||||||
if runtime.GOOS == "js" {
|
if runtime.GOOS == "js" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,19 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type isConnectedFunc func() bool
|
// ConnStatus represents the connection state as seen by the guard.
|
||||||
|
type ConnStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ConnStatusDisconnected means neither ICE nor Relay is connected.
|
||||||
|
ConnStatusDisconnected ConnStatus = iota
|
||||||
|
// ConnStatusPartiallyConnected means Relay is connected but ICE is not.
|
||||||
|
ConnStatusPartiallyConnected
|
||||||
|
// ConnStatusConnected means all required connections are established.
|
||||||
|
ConnStatusConnected
|
||||||
|
)
|
||||||
|
|
||||||
|
type connStatusFunc func() ConnStatus
|
||||||
|
|
||||||
// Guard is responsible for the reconnection logic.
|
// Guard is responsible for the reconnection logic.
|
||||||
// It will trigger to send an offer to the peer then has connection issues.
|
// It will trigger to send an offer to the peer then has connection issues.
|
||||||
@@ -20,14 +32,14 @@ type isConnectedFunc func() bool
|
|||||||
// - ICE candidate changes
|
// - ICE candidate changes
|
||||||
type Guard struct {
|
type Guard struct {
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
isConnectedOnAllWay isConnectedFunc
|
isConnectedOnAllWay connStatusFunc
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
srWatcher *SRWatcher
|
srWatcher *SRWatcher
|
||||||
relayedConnDisconnected chan struct{}
|
relayedConnDisconnected chan struct{}
|
||||||
iCEConnDisconnected chan struct{}
|
iCEConnDisconnected chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGuard(log *log.Entry, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard {
|
func NewGuard(log *log.Entry, isConnectedFn connStatusFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard {
|
||||||
return &Guard{
|
return &Guard{
|
||||||
log: log,
|
log: log,
|
||||||
isConnectedOnAllWay: isConnectedFn,
|
isConnectedOnAllWay: isConnectedFn,
|
||||||
@@ -57,8 +69,17 @@ func (g *Guard) SetICEConnDisconnected() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconnectLoopWithRetry periodically check the connection status.
|
// reconnectLoopWithRetry periodically checks the connection status and sends offers to re-establish connectivity.
|
||||||
// Try to send offer while the P2P is not established or while the Relay is not connected if is it supported
|
//
|
||||||
|
// Behavior depends on the connection state reported by isConnectedOnAllWay:
|
||||||
|
// - Connected: no action, the peer is fully reachable.
|
||||||
|
// - Disconnected (neither ICE nor Relay): retries aggressively with exponential backoff (800ms doubling
|
||||||
|
// up to timeout), never gives up. This ensures rapid recovery when the peer has no connectivity at all.
|
||||||
|
// - PartiallyConnected (Relay up, ICE not): retries up to 3 times with exponential backoff, then switches
|
||||||
|
// to one attempt per hour. This limits signaling traffic when relay already provides connectivity.
|
||||||
|
//
|
||||||
|
// External events (relay/ICE disconnect, signal/relay reconnect, candidate changes) reset the retry
|
||||||
|
// counter and backoff ticker, giving ICE a fresh chance after network conditions change.
|
||||||
func (g *Guard) reconnectLoopWithRetry(ctx context.Context, callback func()) {
|
func (g *Guard) reconnectLoopWithRetry(ctx context.Context, callback func()) {
|
||||||
srReconnectedChan := g.srWatcher.NewListener()
|
srReconnectedChan := g.srWatcher.NewListener()
|
||||||
defer g.srWatcher.RemoveListener(srReconnectedChan)
|
defer g.srWatcher.RemoveListener(srReconnectedChan)
|
||||||
@@ -68,36 +89,47 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context, callback func()) {
|
|||||||
|
|
||||||
tickerChannel := ticker.C
|
tickerChannel := ticker.C
|
||||||
|
|
||||||
|
iceState := &iceRetryState{log: g.log}
|
||||||
|
defer iceState.reset()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case t := <-tickerChannel:
|
case <-tickerChannel:
|
||||||
if t.IsZero() {
|
switch g.isConnectedOnAllWay() {
|
||||||
g.log.Infof("retry timed out, stop periodic offer sending")
|
case ConnStatusConnected:
|
||||||
// after backoff timeout the ticker.C will be closed. We need to a dummy channel to avoid loop
|
// all good, nothing to do
|
||||||
tickerChannel = make(<-chan time.Time)
|
case ConnStatusDisconnected:
|
||||||
continue
|
callback()
|
||||||
|
case ConnStatusPartiallyConnected:
|
||||||
|
if iceState.shouldRetry() {
|
||||||
|
callback()
|
||||||
|
} else {
|
||||||
|
iceState.enterHourlyMode()
|
||||||
|
ticker.Stop()
|
||||||
|
tickerChannel = iceState.hourlyC()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !g.isConnectedOnAllWay() {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
case <-g.relayedConnDisconnected:
|
case <-g.relayedConnDisconnected:
|
||||||
g.log.Debugf("Relay connection changed, reset reconnection ticker")
|
g.log.Debugf("Relay connection changed, reset reconnection ticker")
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
ticker = g.prepareExponentTicker(ctx)
|
ticker = g.newReconnectTicker(ctx)
|
||||||
tickerChannel = ticker.C
|
tickerChannel = ticker.C
|
||||||
|
iceState.reset()
|
||||||
|
|
||||||
case <-g.iCEConnDisconnected:
|
case <-g.iCEConnDisconnected:
|
||||||
g.log.Debugf("ICE connection changed, reset reconnection ticker")
|
g.log.Debugf("ICE connection changed, reset reconnection ticker")
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
ticker = g.prepareExponentTicker(ctx)
|
ticker = g.newReconnectTicker(ctx)
|
||||||
tickerChannel = ticker.C
|
tickerChannel = ticker.C
|
||||||
|
iceState.reset()
|
||||||
|
|
||||||
case <-srReconnectedChan:
|
case <-srReconnectedChan:
|
||||||
g.log.Debugf("has network changes, reset reconnection ticker")
|
g.log.Debugf("has network changes, reset reconnection ticker")
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
ticker = g.prepareExponentTicker(ctx)
|
ticker = g.newReconnectTicker(ctx)
|
||||||
tickerChannel = ticker.C
|
tickerChannel = ticker.C
|
||||||
|
iceState.reset()
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
g.log.Debugf("context is done, stop reconnect loop")
|
g.log.Debugf("context is done, stop reconnect loop")
|
||||||
@@ -120,7 +152,7 @@ func (g *Guard) initialTicker(ctx context.Context) *backoff.Ticker {
|
|||||||
return backoff.NewTicker(bo)
|
return backoff.NewTicker(bo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Guard) prepareExponentTicker(ctx context.Context) *backoff.Ticker {
|
func (g *Guard) newReconnectTicker(ctx context.Context) *backoff.Ticker {
|
||||||
bo := backoff.WithContext(&backoff.ExponentialBackOff{
|
bo := backoff.WithContext(&backoff.ExponentialBackOff{
|
||||||
InitialInterval: 800 * time.Millisecond,
|
InitialInterval: 800 * time.Millisecond,
|
||||||
RandomizationFactor: 0.1,
|
RandomizationFactor: 0.1,
|
||||||
|
|||||||
61
client/internal/peer/guard/ice_retry_state.go
Normal file
61
client/internal/peer/guard/ice_retry_state.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package guard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxICERetries is the maximum number of ICE offer attempts when relay is connected
|
||||||
|
maxICERetries = 3
|
||||||
|
// iceRetryInterval is the periodic retry interval after ICE retries are exhausted
|
||||||
|
iceRetryInterval = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// iceRetryState tracks the limited ICE retry attempts when relay is already connected.
|
||||||
|
// After maxICERetries attempts it switches to a periodic hourly retry.
|
||||||
|
type iceRetryState struct {
|
||||||
|
log *log.Entry
|
||||||
|
retries int
|
||||||
|
hourly *time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *iceRetryState) reset() {
|
||||||
|
s.retries = 0
|
||||||
|
if s.hourly != nil {
|
||||||
|
s.hourly.Stop()
|
||||||
|
s.hourly = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldRetry reports whether the caller should send another ICE offer on this tick.
|
||||||
|
// Returns false when the per-cycle retry budget is exhausted and the caller must switch
|
||||||
|
// to the hourly ticker via enterHourlyMode + hourlyC.
|
||||||
|
func (s *iceRetryState) shouldRetry() bool {
|
||||||
|
if s.hourly != nil {
|
||||||
|
s.log.Debugf("hourly ICE retry attempt")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
s.retries++
|
||||||
|
if s.retries <= maxICERetries {
|
||||||
|
s.log.Debugf("ICE retry attempt %d/%d", s.retries, maxICERetries)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// enterHourlyMode starts the hourly retry ticker. Must be called after shouldRetry returns false.
|
||||||
|
func (s *iceRetryState) enterHourlyMode() {
|
||||||
|
s.log.Infof("ICE retries exhausted (%d/%d), switching to hourly retry", maxICERetries, maxICERetries)
|
||||||
|
s.hourly = time.NewTicker(iceRetryInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *iceRetryState) hourlyC() <-chan time.Time {
|
||||||
|
if s.hourly == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.hourly.C
|
||||||
|
}
|
||||||
103
client/internal/peer/guard/ice_retry_state_test.go
Normal file
103
client/internal/peer/guard/ice_retry_state_test.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package guard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestRetryState() *iceRetryState {
|
||||||
|
return &iceRetryState{log: log.NewEntry(log.StandardLogger())}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_AllowsInitialBudget(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
|
||||||
|
for i := 1; i <= maxICERetries; i++ {
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned false on attempt %d, want true (budget = %d)", i, maxICERetries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ExhaustsAfterBudget(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
|
||||||
|
for i := 0; i < maxICERetries; i++ {
|
||||||
|
_ = s.shouldRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned true after budget exhausted, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_HourlyCNilBeforeEnterHourlyMode(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
|
||||||
|
if s.hourlyC() != nil {
|
||||||
|
t.Fatalf("hourlyC returned non-nil channel before enterHourlyMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_EnterHourlyModeArmsTicker(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
for i := 0; i < maxICERetries+1; i++ {
|
||||||
|
_ = s.shouldRetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.enterHourlyMode()
|
||||||
|
defer s.reset()
|
||||||
|
|
||||||
|
if s.hourlyC() == nil {
|
||||||
|
t.Fatalf("hourlyC returned nil after enterHourlyMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ShouldRetryTrueInHourlyMode(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
s.enterHourlyMode()
|
||||||
|
defer s.reset()
|
||||||
|
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned false in hourly mode, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsequent calls also return true — we keep retrying on each hourly tick.
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("second shouldRetry returned false in hourly mode, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ResetRestoresBudget(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
for i := 0; i < maxICERetries+1; i++ {
|
||||||
|
_ = s.shouldRetry()
|
||||||
|
}
|
||||||
|
s.enterHourlyMode()
|
||||||
|
|
||||||
|
s.reset()
|
||||||
|
|
||||||
|
if s.hourlyC() != nil {
|
||||||
|
t.Fatalf("hourlyC returned non-nil channel after reset")
|
||||||
|
}
|
||||||
|
if s.retries != 0 {
|
||||||
|
t.Fatalf("retries = %d after reset, want 0", s.retries)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= maxICERetries; i++ {
|
||||||
|
if !s.shouldRetry() {
|
||||||
|
t.Fatalf("shouldRetry returned false on attempt %d after reset, want true", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestICERetryState_ResetIsIdempotent(t *testing.T) {
|
||||||
|
s := newTestRetryState()
|
||||||
|
s.reset()
|
||||||
|
s.reset() // second call must not panic or re-stop a nil ticker
|
||||||
|
|
||||||
|
if s.hourlyC() != nil {
|
||||||
|
t.Fatalf("hourlyC non-nil after double reset")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ func NewSRWatcher(signalClient chNotifier, relayManager chNotifier, iFaceDiscove
|
|||||||
return srw
|
return srw
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *SRWatcher) Start() {
|
func (w *SRWatcher) Start(disableICEMonitor bool) {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
@@ -50,8 +50,10 @@ func (w *SRWatcher) Start() {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
w.cancelIceMonitor = cancel
|
w.cancelIceMonitor = cancel
|
||||||
|
|
||||||
|
if !disableICEMonitor {
|
||||||
iceMonitor := NewICEMonitor(w.iFaceDiscover, w.iceConfig, GetICEMonitorPeriod())
|
iceMonitor := NewICEMonitor(w.iFaceDiscover, w.iceConfig, GetICEMonitorPeriod())
|
||||||
go iceMonitor.Start(ctx, w.onICEChanged)
|
go iceMonitor.Start(ctx, w.onICEChanged)
|
||||||
|
}
|
||||||
w.signalClient.SetOnReconnectedListener(w.onReconnected)
|
w.signalClient.SetOnReconnectedListener(w.onReconnected)
|
||||||
w.relayManager.SetOnReconnectedListener(w.onReconnected)
|
w.relayManager.SetOnReconnectedListener(w.onReconnected)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -43,6 +44,10 @@ type OfferAnswer struct {
|
|||||||
SessionID *ICESessionID
|
SessionID *ICESessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OfferAnswer) hasICECredentials() bool {
|
||||||
|
return o.IceCredentials.UFrag != "" && o.IceCredentials.Pwd != ""
|
||||||
|
}
|
||||||
|
|
||||||
type Handshaker struct {
|
type Handshaker struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
@@ -59,6 +64,10 @@ type Handshaker struct {
|
|||||||
relayListener *AsyncOfferListener
|
relayListener *AsyncOfferListener
|
||||||
iceListener func(remoteOfferAnswer *OfferAnswer)
|
iceListener func(remoteOfferAnswer *OfferAnswer)
|
||||||
|
|
||||||
|
// remoteICESupported tracks whether the remote peer includes ICE credentials in its offers/answers.
|
||||||
|
// When false, the local side skips ICE listener dispatch and suppresses ICE credentials in responses.
|
||||||
|
remoteICESupported atomic.Bool
|
||||||
|
|
||||||
// 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
|
||||||
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
|
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
|
||||||
@@ -66,7 +75,7 @@ type Handshaker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewHandshaker(log *log.Entry, config ConnConfig, signaler *Signaler, ice *WorkerICE, relay *WorkerRelay, metricsStages *MetricsStages) *Handshaker {
|
func NewHandshaker(log *log.Entry, config ConnConfig, signaler *Signaler, ice *WorkerICE, relay *WorkerRelay, metricsStages *MetricsStages) *Handshaker {
|
||||||
return &Handshaker{
|
h := &Handshaker{
|
||||||
log: log,
|
log: log,
|
||||||
config: config,
|
config: config,
|
||||||
signaler: signaler,
|
signaler: signaler,
|
||||||
@@ -76,6 +85,13 @@ func NewHandshaker(log *log.Entry, config ConnConfig, signaler *Signaler, ice *W
|
|||||||
remoteOffersCh: make(chan OfferAnswer),
|
remoteOffersCh: make(chan OfferAnswer),
|
||||||
remoteAnswerCh: make(chan OfferAnswer),
|
remoteAnswerCh: make(chan OfferAnswer),
|
||||||
}
|
}
|
||||||
|
// assume remote supports ICE until we learn otherwise from received offers
|
||||||
|
h.remoteICESupported.Store(ice != nil)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handshaker) RemoteICESupported() bool {
|
||||||
|
return h.remoteICESupported.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handshaker) AddRelayListener(offer func(remoteOfferAnswer *OfferAnswer)) {
|
func (h *Handshaker) AddRelayListener(offer func(remoteOfferAnswer *OfferAnswer)) {
|
||||||
@@ -90,18 +106,20 @@ func (h *Handshaker) Listen(ctx context.Context) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case remoteOfferAnswer := <-h.remoteOffersCh:
|
case remoteOfferAnswer := <-h.remoteOffersCh:
|
||||||
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, remote ICE supported: %t", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString(), remoteOfferAnswer.hasICECredentials())
|
||||||
|
|
||||||
// Record signaling received for reconnection attempts
|
// Record signaling received for reconnection attempts
|
||||||
if h.metricsStages != nil {
|
if h.metricsStages != nil {
|
||||||
h.metricsStages.RecordSignalingReceived()
|
h.metricsStages.RecordSignalingReceived()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.updateRemoteICEState(&remoteOfferAnswer)
|
||||||
|
|
||||||
if h.relayListener != nil {
|
if h.relayListener != nil {
|
||||||
h.relayListener.Notify(&remoteOfferAnswer)
|
h.relayListener.Notify(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.iceListener != nil {
|
if h.iceListener != nil && h.RemoteICESupported() {
|
||||||
h.iceListener(&remoteOfferAnswer)
|
h.iceListener(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,18 +128,20 @@ func (h *Handshaker) Listen(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
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, remote ICE supported: %t", remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort, remoteOfferAnswer.SessionIDString(), remoteOfferAnswer.hasICECredentials())
|
||||||
|
|
||||||
// Record signaling received for reconnection attempts
|
// Record signaling received for reconnection attempts
|
||||||
if h.metricsStages != nil {
|
if h.metricsStages != nil {
|
||||||
h.metricsStages.RecordSignalingReceived()
|
h.metricsStages.RecordSignalingReceived()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.updateRemoteICEState(&remoteOfferAnswer)
|
||||||
|
|
||||||
if h.relayListener != nil {
|
if h.relayListener != nil {
|
||||||
h.relayListener.Notify(&remoteOfferAnswer)
|
h.relayListener.Notify(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.iceListener != nil {
|
if h.iceListener != nil && h.RemoteICESupported() {
|
||||||
h.iceListener(&remoteOfferAnswer)
|
h.iceListener(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -183,15 +203,18 @@ func (h *Handshaker) sendAnswer() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handshaker) buildOfferAnswer() OfferAnswer {
|
func (h *Handshaker) buildOfferAnswer() OfferAnswer {
|
||||||
uFrag, pwd := h.ice.GetLocalUserCredentials()
|
|
||||||
sid := h.ice.SessionID()
|
|
||||||
answer := OfferAnswer{
|
answer := OfferAnswer{
|
||||||
IceCredentials: IceCredentials{uFrag, pwd},
|
|
||||||
WgListenPort: h.config.LocalWgPort,
|
WgListenPort: h.config.LocalWgPort,
|
||||||
Version: version.NetbirdVersion(),
|
Version: version.NetbirdVersion(),
|
||||||
RosenpassPubKey: h.config.RosenpassConfig.PubKey,
|
RosenpassPubKey: h.config.RosenpassConfig.PubKey,
|
||||||
RosenpassAddr: h.config.RosenpassConfig.Addr,
|
RosenpassAddr: h.config.RosenpassConfig.Addr,
|
||||||
SessionID: &sid,
|
}
|
||||||
|
|
||||||
|
if h.ice != nil && h.RemoteICESupported() {
|
||||||
|
uFrag, pwd := h.ice.GetLocalUserCredentials()
|
||||||
|
sid := h.ice.SessionID()
|
||||||
|
answer.IceCredentials = IceCredentials{uFrag, pwd}
|
||||||
|
answer.SessionID = &sid
|
||||||
}
|
}
|
||||||
|
|
||||||
if addr, err := h.relay.RelayInstanceAddress(); err == nil {
|
if addr, err := h.relay.RelayInstanceAddress(); err == nil {
|
||||||
@@ -200,3 +223,18 @@ func (h *Handshaker) buildOfferAnswer() OfferAnswer {
|
|||||||
|
|
||||||
return answer
|
return answer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handshaker) updateRemoteICEState(offer *OfferAnswer) {
|
||||||
|
hasICE := offer.hasICECredentials()
|
||||||
|
prev := h.remoteICESupported.Swap(hasICE)
|
||||||
|
if prev != hasICE {
|
||||||
|
if hasICE {
|
||||||
|
h.log.Infof("remote peer started sending ICE credentials")
|
||||||
|
} else {
|
||||||
|
h.log.Infof("remote peer stopped sending ICE credentials")
|
||||||
|
if h.ice != nil {
|
||||||
|
h.ice.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,10 +46,14 @@ func (s *Signaler) Ready() bool {
|
|||||||
|
|
||||||
// SignalOfferAnswer signals either an offer or an answer to remote peer
|
// SignalOfferAnswer signals either an offer or an answer to remote peer
|
||||||
func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string, bodyType sProto.Body_Type) error {
|
func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string, bodyType sProto.Body_Type) error {
|
||||||
sessionIDBytes, err := offerAnswer.SessionID.Bytes()
|
var sessionIDBytes []byte
|
||||||
|
if offerAnswer.SessionID != nil {
|
||||||
|
var err error
|
||||||
|
sessionIDBytes, err = offerAnswer.SessionID.Bytes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to get session ID bytes: %v", err)
|
log.Warnf("failed to get session ID bytes: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
msg, err := signal.MarshalCredential(
|
msg, err := signal.MarshalCredential(
|
||||||
s.wgPrivateKey,
|
s.wgPrivateKey,
|
||||||
offerAnswer.WgListenPort,
|
offerAnswer.WgListenPort,
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ server:
|
|||||||
|
|
||||||
# Reverse proxy settings (optional)
|
# Reverse proxy settings (optional)
|
||||||
# reverseProxy:
|
# reverseProxy:
|
||||||
# trustedHTTPProxies: []
|
# trustedHTTPProxies: [] # CIDRs of trusted reverse proxies (e.g. ["10.0.0.0/8"])
|
||||||
# trustedHTTPProxiesCount: 0
|
# trustedHTTPProxiesCount: 0 # Number of trusted proxies in front of the server (alternative to trustedHTTPProxies)
|
||||||
# trustedPeers: []
|
# trustedPeers: [] # CIDRs of trusted peer networks (e.g. ["100.64.0.0/10"])
|
||||||
|
# accessLogRetentionDays: 7 # Days to retain HTTP access logs. 0 (or unset) defaults to 7. Negative values disable cleanup (logs kept indefinitely).
|
||||||
|
# accessLogCleanupIntervalHours: 24 # How often (in hours) to run the access-log cleanup job. 0 (or unset) is treated as "not set" and defaults to 24 hours; cleanup remains enabled. To disable cleanup, set accessLogRetentionDays to a negative value.
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -323,3 +323,5 @@ replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184
|
|||||||
replace github.com/libp2p/go-netroute => github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944
|
replace github.com/libp2p/go-netroute => github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944
|
||||||
|
|
||||||
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.0
|
replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.0
|
||||||
|
|
||||||
|
replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -400,8 +400,6 @@ github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tA
|
|||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
|
||||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
@@ -449,6 +447,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/netbirdio/dex v0.244.0 h1:1GOvi8wnXYassnKGildzNqRHq0RbcfEUw7LKYpKIN7U=
|
github.com/netbirdio/dex v0.244.0 h1:1GOvi8wnXYassnKGildzNqRHq0RbcfEUw7LKYpKIN7U=
|
||||||
github.com/netbirdio/dex v0.244.0/go.mod h1:STGInJhPcAflrHmDO7vyit2kSq03PdL+8zQPoGALtcU=
|
github.com/netbirdio/dex v0.244.0/go.mod h1:STGInJhPcAflrHmDO7vyit2kSq03PdL+8zQPoGALtcU=
|
||||||
|
github.com/netbirdio/easyjson v0.9.0 h1:6Nw2lghSVuy8RSkAYDhDv1thBVEmfVbKZnV7T7Z6Aus=
|
||||||
|
github.com/netbirdio/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6Sf8uYFx/dMeqNOL90KUoRscdfpFZ3Im89uk=
|
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6Sf8uYFx/dMeqNOL90KUoRscdfpFZ3Im89uk=
|
||||||
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
|
github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
|
||||||
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 h1:Ov4qdafATOgGMB1wbSuh+0aAHcwz9hdvB6VZjh1mVMI=
|
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 h1:Ov4qdafATOgGMB1wbSuh+0aAHcwz9hdvB6VZjh1mVMI=
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ start_services_and_show_instructions() {
|
|||||||
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
|
if [[ "$ENABLE_CROWDSEC" == "true" ]]; then
|
||||||
echo "Registering CrowdSec bouncer..."
|
echo "Registering CrowdSec bouncer..."
|
||||||
local cs_retries=0
|
local cs_retries=0
|
||||||
while ! $DOCKER_COMPOSE_COMMAND exec -T crowdsec cscli capi status >/dev/null 2>&1; do
|
while ! $DOCKER_COMPOSE_COMMAND exec -T crowdsec cscli lapi status >/dev/null 2>&1; do
|
||||||
cs_retries=$((cs_retries + 1))
|
cs_retries=$((cs_retries + 1))
|
||||||
if [[ $cs_retries -ge 30 ]]; then
|
if [[ $cs_retries -ge 30 ]]; then
|
||||||
echo "WARNING: CrowdSec did not become ready. Skipping CrowdSec setup." > /dev/stderr
|
echo "WARNING: CrowdSec did not become ready. Skipping CrowdSec setup." > /dev/stderr
|
||||||
|
|||||||
Reference in New Issue
Block a user