Compare commits

...

2 Commits

Author SHA1 Message Date
mlsmaycon
bfeb60fbb5 Create a system proxy change after receiving a network map
This is experimental and needs more test.

the purpose of this change is to validate that a TLS connection stuck using old routes because of keepalive settings on the remote webserver are reset once netbird receives a network map
2026-02-01 10:23:25 +01:00
mlsmaycon
ea41cf2d2c Create a system proxy change after receiving a network map
This is experimental and needs more test.

the purpose of this change is to validate that a TLS connection stuck using old routes because of keepalive settings on the remote webserver are reset once netbird receives a network map
2026-02-01 10:21:51 +01:00
6 changed files with 566 additions and 0 deletions

View File

@@ -44,6 +44,7 @@ import (
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
"github.com/netbirdio/netbird/client/internal/peerstore"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/relay"
"github.com/netbirdio/netbird/client/internal/rosenpass"
"github.com/netbirdio/netbird/client/internal/routemanager"
@@ -140,6 +141,11 @@ type EngineConfig struct {
ProfileConfig *profilemanager.Config
LogPath string
// ProxyConfig contains system proxy settings for macOS
ProxyEnabled bool
ProxyHost string
ProxyPort int
}
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
@@ -223,6 +229,9 @@ type Engine struct {
jobExecutor *jobexec.Executor
jobExecutorWG sync.WaitGroup
// proxyManager manages system-wide browser proxy settings on macOS
proxyManager *proxy.Manager
}
// Peer is an instance of the Connection Peer
@@ -313,6 +322,12 @@ func (e *Engine) Stop() error {
e.updateManager.Stop()
}
if e.proxyManager != nil {
if err := e.proxyManager.DisableWebProxy(); err != nil {
log.Warnf("failed to disable system proxy: %v", err)
}
}
log.Info("cleaning up status recorder states")
e.statusRecorder.ReplaceOfflinePeers([]peer.State{})
e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{})
@@ -448,6 +463,10 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
}
e.stateManager.Start()
// Initialize proxy manager and register state for cleanup
proxy.RegisterState(e.stateManager)
e.proxyManager = proxy.NewManager(e.stateManager)
initialRoutes, dnsConfig, dnsFeatureFlag, err := e.readInitialSettings()
if err != nil {
e.close()
@@ -1312,6 +1331,9 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
// If no server of a server group responds this will disable the respective handler and retry later.
e.dnsServer.ProbeAvailability()
// Update system proxy state based on routes after network map is fully applied
e.updateSystemProxy(clientRoutes)
return nil
}
@@ -2303,6 +2325,26 @@ func createFile(path string) error {
return file.Close()
}
// updateSystemProxy triggers a proxy enable/disable cycle after the network map is updated.
func (e *Engine) updateSystemProxy(clientRoutes route.HAMap) {
if runtime.GOOS != "darwin" || e.proxyManager == nil {
log.Errorf("not updating proxy")
return
}
if err := e.proxyManager.EnableWebProxy(e.config.ProxyHost, e.config.ProxyPort); err != nil {
log.Errorf("enable system proxy: %v", err)
return
}
log.Error("system proxy enabled after network map update")
if err := e.proxyManager.DisableWebProxy(); err != nil {
log.Errorf("disable system proxy: %v", err)
return
}
log.Error("system proxy disabled after network map update")
}
func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) {
remoteCred, err := signal.UnMarshalCredential(msg)
if err != nil {

View File

@@ -0,0 +1,262 @@
//go:build darwin && !ios
package proxy
import (
"fmt"
"os/exec"
"strings"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
const networksetupPath = "/usr/sbin/networksetup"
// Manager handles system-wide proxy configuration on macOS.
type Manager struct {
mu sync.Mutex
stateManager *statemanager.Manager
modifiedServices []string
enabled bool
}
// NewManager creates a new proxy manager.
func NewManager(stateManager *statemanager.Manager) *Manager {
return &Manager{
stateManager: stateManager,
}
}
// GetActiveNetworkServices returns the list of active network services.
func GetActiveNetworkServices() ([]string, error) {
cmd := exec.Command(networksetupPath, "-listallnetworkservices")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("list network services: %w", err)
}
lines := strings.Split(string(out), "\n")
var services []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "*") || strings.Contains(line, "asterisk") {
continue
}
services = append(services, line)
}
return services, nil
}
// EnableWebProxy enables web proxy for all active network services.
func (m *Manager) EnableWebProxy(host string, port int) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.enabled {
log.Debug("web proxy already enabled")
return nil
}
services, err := GetActiveNetworkServices()
if err != nil {
return err
}
var modifiedServices []string
for _, service := range services {
if err := m.enableProxyForService(service, host, port); err != nil {
log.Warnf("enable proxy for %s: %v", service, err)
continue
}
modifiedServices = append(modifiedServices, service)
}
m.modifiedServices = modifiedServices
m.enabled = true
m.updateState()
log.Infof("enabled web proxy on %d services -> %s:%d", len(modifiedServices), host, port)
return nil
}
func (m *Manager) enableProxyForService(service, host string, port int) error {
portStr := fmt.Sprintf("%d", port)
// Set web proxy (HTTP)
cmd := exec.Command(networksetupPath, "-setwebproxy", service, host, portStr)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("set web proxy: %w, output: %s", err, out)
}
// Enable web proxy
cmd = exec.Command(networksetupPath, "-setwebproxystate", service, "on")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("enable web proxy state: %w, output: %s", err, out)
}
// Set secure web proxy (HTTPS)
cmd = exec.Command(networksetupPath, "-setsecurewebproxy", service, host, portStr)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("set secure web proxy: %w, output: %s", err, out)
}
// Enable secure web proxy
cmd = exec.Command(networksetupPath, "-setsecurewebproxystate", service, "on")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("enable secure web proxy state: %w, output: %s", err, out)
}
log.Debugf("enabled proxy for service %s", service)
return nil
}
// DisableWebProxy disables web proxy for all modified network services.
func (m *Manager) DisableWebProxy() error {
m.mu.Lock()
defer m.mu.Unlock()
if !m.enabled {
log.Debug("web proxy already disabled")
return nil
}
services := m.modifiedServices
if len(services) == 0 {
services, _ = GetActiveNetworkServices()
}
for _, service := range services {
if err := m.disableProxyForService(service); err != nil {
log.Warnf("disable proxy for %s: %v", service, err)
}
}
m.modifiedServices = nil
m.enabled = false
m.updateState()
log.Info("disabled web proxy")
return nil
}
func (m *Manager) disableProxyForService(service string) error {
// Disable web proxy (HTTP)
cmd := exec.Command(networksetupPath, "-setwebproxystate", service, "off")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("disable web proxy: %w, output: %s", err, out)
}
// Disable secure web proxy (HTTPS)
cmd = exec.Command(networksetupPath, "-setsecurewebproxystate", service, "off")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("disable secure web proxy: %w, output: %s", err, out)
}
log.Debugf("disabled proxy for service %s", service)
return nil
}
// SetAutoproxyURL sets the automatic proxy configuration URL (PAC file).
func (m *Manager) SetAutoproxyURL(pacURL string) error {
m.mu.Lock()
defer m.mu.Unlock()
services, err := GetActiveNetworkServices()
if err != nil {
return err
}
var modifiedServices []string
for _, service := range services {
cmd := exec.Command(networksetupPath, "-setautoproxyurl", service, pacURL)
if out, err := cmd.CombinedOutput(); err != nil {
log.Warnf("set autoproxy for %s: %v, output: %s", service, err, out)
continue
}
cmd = exec.Command(networksetupPath, "-setautoproxystate", service, "on")
if out, err := cmd.CombinedOutput(); err != nil {
log.Warnf("enable autoproxy for %s: %v, output: %s", service, err, out)
continue
}
modifiedServices = append(modifiedServices, service)
log.Debugf("set autoproxy URL for %s -> %s", service, pacURL)
}
m.modifiedServices = modifiedServices
m.enabled = true
m.updateState()
return nil
}
// DisableAutoproxy disables automatic proxy configuration.
func (m *Manager) DisableAutoproxy() error {
m.mu.Lock()
defer m.mu.Unlock()
services := m.modifiedServices
if len(services) == 0 {
services, _ = GetActiveNetworkServices()
}
for _, service := range services {
cmd := exec.Command(networksetupPath, "-setautoproxystate", service, "off")
if out, err := cmd.CombinedOutput(); err != nil {
log.Warnf("disable autoproxy for %s: %v, output: %s", service, err, out)
}
}
m.modifiedServices = nil
m.enabled = false
m.updateState()
return nil
}
// IsEnabled returns whether the proxy is currently enabled.
func (m *Manager) IsEnabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.enabled
}
// Restore restores proxy settings from a previous state.
func (m *Manager) Restore(services []string) error {
m.mu.Lock()
defer m.mu.Unlock()
for _, service := range services {
if err := m.disableProxyForService(service); err != nil {
log.Warnf("restore proxy for %s: %v", service, err)
}
}
m.modifiedServices = nil
m.enabled = false
return nil
}
func (m *Manager) updateState() {
if m.stateManager == nil {
return
}
if m.enabled && len(m.modifiedServices) > 0 {
state := &ShutdownState{
ModifiedServices: m.modifiedServices,
}
if err := m.stateManager.UpdateState(state); err != nil {
log.Errorf("update proxy state: %v", err)
}
} else {
if err := m.stateManager.DeleteState(&ShutdownState{}); err != nil {
log.Debugf("delete proxy state: %v", err)
}
}
}

View File

@@ -0,0 +1,45 @@
//go:build !darwin || ios
package proxy
import (
"github.com/netbirdio/netbird/client/internal/statemanager"
)
// Manager is a no-op proxy manager for non-macOS platforms.
type Manager struct{}
// NewManager creates a new proxy manager (no-op on non-macOS).
func NewManager(_ *statemanager.Manager) *Manager {
return &Manager{}
}
// EnableWebProxy is a no-op on non-macOS platforms.
func (m *Manager) EnableWebProxy(host string, port int) error {
return nil
}
// DisableWebProxy is a no-op on non-macOS platforms.
func (m *Manager) DisableWebProxy() error {
return nil
}
// SetAutoproxyURL is a no-op on non-macOS platforms.
func (m *Manager) SetAutoproxyURL(pacURL string) error {
return nil
}
// DisableAutoproxy is a no-op on non-macOS platforms.
func (m *Manager) DisableAutoproxy() error {
return nil
}
// IsEnabled always returns false on non-macOS platforms.
func (m *Manager) IsEnabled() bool {
return false
}
// Restore is a no-op on non-macOS platforms.
func (m *Manager) Restore(services []string) error {
return nil
}

View File

@@ -0,0 +1,88 @@
//go:build darwin && !ios
package proxy
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetActiveNetworkServices(t *testing.T) {
services, err := GetActiveNetworkServices()
assert.NoError(t, err)
assert.NotEmpty(t, services, "should have at least one network service")
// Check that services don't contain invalid entries
for _, service := range services {
assert.NotEmpty(t, service)
assert.NotContains(t, service, "*")
}
}
func TestManager_EnableDisableWebProxy(t *testing.T) {
// Skip this test in CI as it requires admin privileges
if testing.Short() {
t.Skip("skipping proxy test in short mode")
}
m := NewManager(nil)
assert.NotNil(t, m)
assert.False(t, m.IsEnabled())
// This test would require admin privileges to actually enable the proxy
// So we just test the basic state management
}
func TestShutdownState_Name(t *testing.T) {
state := &ShutdownState{}
assert.Equal(t, "proxy_state", state.Name())
}
func TestShutdownState_Cleanup_EmptyServices(t *testing.T) {
state := &ShutdownState{
ModifiedServices: []string{},
}
err := state.Cleanup()
assert.NoError(t, err)
}
func TestContains(t *testing.T) {
tests := []struct {
s string
substr string
want bool
}{
{"Enabled: Yes", "Enabled: Yes", true},
{"Enabled: No", "Enabled: Yes", false},
{"Server: 127.0.0.1\nEnabled: Yes\nPort: 8080", "Enabled: Yes", true},
{"", "Enabled: Yes", false},
{"Enabled: Yes", "", true},
}
for _, tt := range tests {
t.Run(tt.s+"_"+tt.substr, func(t *testing.T) {
got := contains(tt.s, tt.substr)
assert.Equal(t, tt.want, got)
})
}
}
func TestIsProxyEnabled(t *testing.T) {
tests := []struct {
output string
want bool
}{
{"Enabled: Yes\nServer: 127.0.0.1\nPort: 8080", true},
{"Enabled: No\nServer: \nPort: 0", false},
{"Server: 127.0.0.1\nEnabled: Yes\nPort: 8080", true},
{"", false},
}
for _, tt := range tests {
t.Run(tt.output, func(t *testing.T) {
got := isProxyEnabled(tt.output)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,105 @@
//go:build darwin && !ios
package proxy
import (
"fmt"
"os/exec"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
// ShutdownState stores proxy state for cleanup on unclean shutdown.
type ShutdownState struct {
ModifiedServices []string `json:"modified_services"`
}
// Name returns the state name for persistence.
func (s *ShutdownState) Name() string {
return "proxy_state"
}
// Cleanup restores proxy settings after an unclean shutdown.
func (s *ShutdownState) Cleanup() error {
if len(s.ModifiedServices) == 0 {
return nil
}
log.Infof("cleaning up proxy state for %d services", len(s.ModifiedServices))
for _, service := range s.ModifiedServices {
// Disable web proxy (HTTP)
cmd := exec.Command(networksetupPath, "-setwebproxystate", service, "off")
if out, err := cmd.CombinedOutput(); err != nil {
log.Warnf("cleanup web proxy for %s: %v, output: %s", service, err, out)
}
// Disable secure web proxy (HTTPS)
cmd = exec.Command(networksetupPath, "-setsecurewebproxystate", service, "off")
if out, err := cmd.CombinedOutput(); err != nil {
log.Warnf("cleanup secure web proxy for %s: %v, output: %s", service, err, out)
}
// Disable autoproxy
cmd = exec.Command(networksetupPath, "-setautoproxystate", service, "off")
if out, err := cmd.CombinedOutput(); err != nil {
log.Warnf("cleanup autoproxy for %s: %v, output: %s", service, err, out)
}
log.Debugf("cleaned up proxy for service %s", service)
}
return nil
}
// RegisterState registers the proxy state with the state manager.
func RegisterState(stateManager *statemanager.Manager) {
if stateManager == nil {
return
}
stateManager.RegisterState(&ShutdownState{})
}
// GetProxyState returns the current proxy state from the command line.
func GetProxyState(service string) (webProxy, secureProxy, autoProxy bool, err error) {
// Check web proxy state
cmd := exec.Command(networksetupPath, "-getwebproxy", service)
out, err := cmd.Output()
if err != nil {
return false, false, false, fmt.Errorf("get web proxy: %w", err)
}
webProxy = isProxyEnabled(string(out))
// Check secure web proxy state
cmd = exec.Command(networksetupPath, "-getsecurewebproxy", service)
out, err = cmd.Output()
if err != nil {
return false, false, false, fmt.Errorf("get secure web proxy: %w", err)
}
secureProxy = isProxyEnabled(string(out))
// Check autoproxy state
cmd = exec.Command(networksetupPath, "-getautoproxyurl", service)
out, err = cmd.Output()
if err != nil {
return false, false, false, fmt.Errorf("get autoproxy: %w", err)
}
autoProxy = isProxyEnabled(string(out))
return webProxy, secureProxy, autoProxy, nil
}
func isProxyEnabled(output string) bool {
return !contains(output, "Enabled: No") && contains(output, "Enabled: Yes")
}
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@@ -0,0 +1,24 @@
//go:build !darwin || ios
package proxy
import (
"github.com/netbirdio/netbird/client/internal/statemanager"
)
// ShutdownState is a no-op state for non-macOS platforms.
type ShutdownState struct{}
// Name returns the state name.
func (s *ShutdownState) Name() string {
return "proxy_state"
}
// Cleanup is a no-op on non-macOS platforms.
func (s *ShutdownState) Cleanup() error {
return nil
}
// RegisterState is a no-op on non-macOS platforms.
func RegisterState(stateManager *statemanager.Manager) {
}