Skip to content

Commit

Permalink
Feature/exit nodes - Windows and macOS support (#1726)
Browse files Browse the repository at this point in the history
  • Loading branch information
lixmal committed Apr 3, 2024
1 parent 9af532f commit 7938295
Show file tree
Hide file tree
Showing 41 changed files with 2,253 additions and 964 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/golang-test-darwin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
restore-keys: |
macos-go-
- name: Install libpcap
run: brew install libpcap

- name: Install modules
run: go mod tidy

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/golang-test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=C:\Users\runneradmin\AppData\Local\go-build

- name: test
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 5m -p 1 ./... > test-out.txt 2>&1"
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 10m -p 1 ./... > test-out.txt 2>&1"
- name: test output
if: ${{ always() }}
run: Get-Content test-out.txt
26 changes: 20 additions & 6 deletions client/internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ type Engine struct {
mgmClient mgm.Client
// peerConns is a map that holds all the peers that are known to this peer
peerConns map[string]*peer.Conn

beforePeerHook peer.BeforeAddPeerHookFunc
afterPeerHook peer.AfterRemovePeerHookFunc

// rpManager is a Rosenpass manager
rpManager *rosenpass.Manager

Expand Down Expand Up @@ -260,9 +264,14 @@ func (e *Engine) Start() error {
e.dnsServer = dnsServer

e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, initialRoutes)
if err := e.routeManager.Init(); err != nil {
beforePeerHook, afterPeerHook, err := e.routeManager.Init()
if err != nil {
log.Errorf("Failed to initialize route manager: %s", err)
} else {
e.beforePeerHook = beforePeerHook
e.afterPeerHook = afterPeerHook
}

e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener)

err = e.wgInterfaceCreate()
Expand Down Expand Up @@ -808,10 +817,15 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
if _, ok := e.peerConns[peerKey]; !ok {
conn, err := e.createPeerConn(peerKey, strings.Join(peerIPs, ","))
if err != nil {
return err
return fmt.Errorf("create peer connection: %w", err)
}
e.peerConns[peerKey] = conn

if e.beforePeerHook != nil && e.afterPeerHook != nil {
conn.AddBeforeAddPeerHook(e.beforePeerHook)
conn.AddAfterRemovePeerHook(e.afterPeerHook)
}

err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn)
if err != nil {
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
Expand Down Expand Up @@ -1105,6 +1119,10 @@ func (e *Engine) close() {
e.dnsServer.Stop()
}

if e.routeManager != nil {
e.routeManager.Stop()
}

log.Debugf("removing Netbird interface %s", e.config.WgIfaceName)
if e.wgInterface != nil {
if err := e.wgInterface.Close(); err != nil {
Expand All @@ -1119,10 +1137,6 @@ func (e *Engine) close() {
}
}

if e.routeManager != nil {
e.routeManager.Stop()
}

if e.firewall != nil {
err := e.firewall.Reset()
if err != nil {
Expand Down
33 changes: 33 additions & 0 deletions client/internal/peer/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/netbirdio/netbird/iface/bind"
signal "github.com/netbirdio/netbird/signal/client"
sProto "github.com/netbirdio/netbird/signal/proto"
nbnet "github.com/netbirdio/netbird/util/net"
"github.com/netbirdio/netbird/version"
)

Expand Down Expand Up @@ -100,6 +101,9 @@ type IceCredentials struct {
Pwd string
}

type BeforeAddPeerHookFunc func(connID nbnet.ConnectionID, IP net.IP) error
type AfterRemovePeerHookFunc func(connID nbnet.ConnectionID) error

type Conn struct {
config ConnConfig
mu sync.Mutex
Expand Down Expand Up @@ -138,6 +142,10 @@ type Conn struct {

remoteEndpoint *net.UDPAddr
remoteConn *ice.Conn

connID nbnet.ConnectionID
beforeAddPeerHooks []BeforeAddPeerHookFunc
afterRemovePeerHooks []AfterRemovePeerHookFunc
}

// meta holds meta information about a connection
Expand Down Expand Up @@ -393,6 +401,14 @@ func isRelayCandidate(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeRelay
}

func (conn *Conn) AddBeforeAddPeerHook(hook BeforeAddPeerHookFunc) {
conn.beforeAddPeerHooks = append(conn.beforeAddPeerHooks, hook)
}

func (conn *Conn) AddAfterRemovePeerHook(hook AfterRemovePeerHookFunc) {
conn.afterRemovePeerHooks = append(conn.afterRemovePeerHooks, hook)
}

// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int, remoteRosenpassPubKey []byte, remoteRosenpassAddr string) (net.Addr, error) {
conn.mu.Lock()
Expand All @@ -419,6 +435,14 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int, rem

endpointUdpAddr, _ := net.ResolveUDPAddr(endpoint.Network(), endpoint.String())
conn.remoteEndpoint = endpointUdpAddr
log.Debugf("Conn resolved IP for %s: %s", endpoint, endpointUdpAddr.IP)

conn.connID = nbnet.GenerateConnID()
for _, hook := range conn.beforeAddPeerHooks {
if err := hook(conn.connID, endpointUdpAddr.IP); err != nil {
log.Errorf("Before add peer hook failed: %v", err)
}
}

err = conn.config.WgConfig.WgInterface.UpdatePeer(conn.config.WgConfig.RemoteKey, conn.config.WgConfig.AllowedIps, defaultWgKeepAlive, endpointUdpAddr, conn.config.WgConfig.PreSharedKey)
if err != nil {
Expand Down Expand Up @@ -510,6 +534,15 @@ func (conn *Conn) cleanup() error {
// todo: is it problem if we try to remove a peer what is never existed?
err3 = conn.config.WgConfig.WgInterface.RemovePeer(conn.config.WgConfig.RemoteKey)

if conn.connID != "" {
for _, hook := range conn.afterRemovePeerHooks {
if err := hook(conn.connID); err != nil {
log.Errorf("After remove peer hook failed: %v", err)
}
}
}
conn.connID = ""

if conn.notifyDisconnected != nil {
conn.notifyDisconnected()
conn.notifyDisconnected = nil
Expand Down
4 changes: 2 additions & 2 deletions client/internal/routemanager/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {

func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
if c.chosenRoute != nil {
if err := removeFromRouteTableIfNonSystem(c.network, c.wgInterface.Address().IP.String(), c.wgInterface.Name()); err != nil {
if err := removeVPNRoute(c.network, c.wgInterface.Name()); err != nil {
return fmt.Errorf("remove route %s from system, err: %v", c.network, err)
}

Expand Down Expand Up @@ -234,7 +234,7 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
}
} else {
// otherwise add the route to the system
if err := addToRouteTableIfNoExists(c.network, c.wgInterface.Address().IP.String(), c.wgInterface.Name()); err != nil {
if err := addVPNRoute(c.network, c.wgInterface.Name()); err != nil {
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
c.network.String(), c.wgInterface.Address().IP.String(), err)
}
Expand Down
41 changes: 34 additions & 7 deletions client/internal/routemanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package routemanager
import (
"context"
"fmt"
"net"
"net/netip"
"net/url"
"runtime"
"sync"

Expand All @@ -24,7 +26,7 @@ var defaultv6 = netip.PrefixFrom(netip.IPv6Unspecified(), 0)

// Manager is a route manager interface
type Manager interface {
Init() error
Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error)
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error
SetRouteChangeListener(listener listener.NetworkChangeListener)
InitialRouteRange() []string
Expand Down Expand Up @@ -65,16 +67,21 @@ func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface,
}

// Init sets up the routing
func (m *DefaultManager) Init() error {
func (m *DefaultManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) {
if err := cleanupRouting(); err != nil {
log.Warnf("Failed cleaning up routing: %v", err)
}

if err := setupRouting(); err != nil {
return fmt.Errorf("setup routing: %w", err)
mgmtAddress := m.statusRecorder.GetManagementState().URL
signalAddress := m.statusRecorder.GetSignalState().URL
ips := resolveURLsToIPs([]string{mgmtAddress, signalAddress})

beforePeerHook, afterPeerHook, err := setupRouting(ips, m.wgInterface)
if err != nil {
return nil, nil, fmt.Errorf("setup routing: %w", err)
}
log.Info("Routing setup complete")
return nil
return beforePeerHook, afterPeerHook, nil
}

func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error {
Expand Down Expand Up @@ -203,16 +210,36 @@ func (m *DefaultManager) clientRoutes(initialRoutes []*route.Route) []*route.Rou
}

func isPrefixSupported(prefix netip.Prefix) bool {
if runtime.GOOS == "linux" {
switch runtime.GOOS {
case "linux", "windows", "darwin":
return true
}

// If prefix is too small, lets assume it is a possible default prefix which is not yet supported
// we skip this prefix management
if prefix.Bits() < minRangeBits {
if prefix.Bits() <= minRangeBits {
log.Warnf("This agent version: %s, doesn't support default routes, received %s, skipping this prefix",
version.NetbirdVersion(), prefix)
return false
}
return true
}

// resolveURLsToIPs takes a slice of URLs, resolves them to IP addresses and returns a slice of IPs.
func resolveURLsToIPs(urls []string) []net.IP {
var ips []net.IP
for _, rawurl := range urls {
u, err := url.Parse(rawurl)
if err != nil {
log.Errorf("Failed to parse url %s: %v", rawurl, err)
continue
}
ipAddrs, err := net.LookupIP(u.Hostname())
if err != nil {
log.Errorf("Failed to resolve host %s: %v", u.Hostname(), err)
continue
}
ips = append(ips, ipAddrs...)
}
return ips
}
30 changes: 16 additions & 14 deletions client/internal/routemanager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ const remotePeerKey2 = "remote1"

func TestManagerUpdateRoutes(t *testing.T) {
testCases := []struct {
name string
inputInitRoutes []*route.Route
inputRoutes []*route.Route
inputSerial uint64
removeSrvRouter bool
serverRoutesExpected int
clientNetworkWatchersExpected int
clientNetworkWatchersExpectedLinux int
name string
inputInitRoutes []*route.Route
inputRoutes []*route.Route
inputSerial uint64
removeSrvRouter bool
serverRoutesExpected int
clientNetworkWatchersExpected int
clientNetworkWatchersExpectedAllowed int
}{
{
name: "Should create 2 client networks",
Expand Down Expand Up @@ -201,9 +201,9 @@ func TestManagerUpdateRoutes(t *testing.T) {
Enabled: true,
},
},
inputSerial: 1,
clientNetworkWatchersExpected: 0,
clientNetworkWatchersExpectedLinux: 1,
inputSerial: 1,
clientNetworkWatchersExpected: 0,
clientNetworkWatchersExpectedAllowed: 1,
},
{
name: "Remove 1 Client Route",
Expand Down Expand Up @@ -417,7 +417,9 @@ func TestManagerUpdateRoutes(t *testing.T) {
statusRecorder := peer.NewRecorder("https://mgm")
ctx := context.TODO()
routeManager := NewManager(ctx, localPeerKey, wgInterface, statusRecorder, nil)
err = routeManager.Init()

_, _, err = routeManager.Init()

require.NoError(t, err, "should init route manager")
defer routeManager.Stop()

Expand All @@ -434,8 +436,8 @@ func TestManagerUpdateRoutes(t *testing.T) {
require.NoError(t, err, "should update routes")

expectedWatchers := testCase.clientNetworkWatchersExpected
if runtime.GOOS == "linux" && testCase.clientNetworkWatchersExpectedLinux != 0 {
expectedWatchers = testCase.clientNetworkWatchersExpectedLinux
if (runtime.GOOS == "linux" || runtime.GOOS == "windows" || runtime.GOOS == "darwin") && testCase.clientNetworkWatchersExpectedAllowed != 0 {
expectedWatchers = testCase.clientNetworkWatchersExpectedAllowed
}
require.Len(t, routeManager.clientNetworks, expectedWatchers, "client networks size should match")

Expand Down
5 changes: 3 additions & 2 deletions client/internal/routemanager/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
)
Expand All @@ -16,8 +17,8 @@ type MockManager struct {
StopFunc func()
}

func (m *MockManager) Init() error {
return nil
func (m *MockManager) Init() (peer.BeforeAddPeerHookFunc, peer.AfterRemovePeerHookFunc, error) {
return nil, nil, nil
}

// InitialRouteRange mock implementation of InitialRouteRange from Manager interface
Expand Down

0 comments on commit 7938295

Please sign in to comment.