From d1703479ff236d56d17fe8daf66199c99590497a Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 24 Mar 2023 08:40:39 +0100 Subject: [PATCH] Add custom ice stdnet implementation (#754) On Android, because of the hard SELinux policies can not list the interfaces of the ICE package. Without it can not generate a host type candidate. In this pull request, the list of interfaces comes via the Java interface. --- .github/workflows/golang-test-linux.yml | 4 +- client/android/client.go | 12 +- client/cmd/up.go | 2 +- client/internal/connect.go | 8 +- client/internal/engine.go | 19 +++- client/internal/engine_stdnet.go | 11 ++ client/internal/engine_stdnet_android.go | 7 ++ client/internal/peer/conn.go | 15 ++- client/internal/peer/conn_test.go | 12 +- client/internal/peer/stdnet.go | 11 ++ client/internal/peer/stdnet_android.go | 7 ++ client/internal/stdnet/iface_discover.go | 8 ++ client/internal/stdnet/stdnet.go | 137 +++++++++++++++++++++++ client/internal/stdnet/stdnet_test.go | 52 +++++++++ client/server/server.go | 4 +- 15 files changed, 285 insertions(+), 24 deletions(-) create mode 100644 client/internal/engine_stdnet.go create mode 100644 client/internal/engine_stdnet_android.go create mode 100644 client/internal/peer/stdnet.go create mode 100644 client/internal/peer/stdnet_android.go create mode 100644 client/internal/stdnet/iface_discover.go create mode 100644 client/internal/stdnet/stdnet.go create mode 100644 client/internal/stdnet/stdnet_test.go diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 4186baf38..d600575e6 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -72,7 +72,7 @@ jobs: run: go test -c -o routemanager-testing.bin ./client/internal/routemanager/... - name: Generate Engine Test bin - run: go test -c -o engine-testing.bin ./client/internal/*.go + run: go test -c -o engine-testing.bin ./client/internal - name: Generate Peer Test bin run: go test -c -o peer-testing.bin ./client/internal/peer/... @@ -89,4 +89,4 @@ jobs: run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1 - name: Run Peer tests in docker - run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1 \ No newline at end of file + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1 diff --git a/client/android/client.go b/client/android/client.go index 778c3d15a..ac16316ed 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -8,6 +8,7 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/formatter" "github.com/netbirdio/netbird/iface" @@ -23,6 +24,11 @@ type TunAdapter interface { iface.TunAdapter } +// IFaceDiscover export internal IFaceDiscover for mobile +type IFaceDiscover interface { + stdnet.IFaceDiscover +} + func init() { formatter.SetLogcatFormatter(log.StandardLogger()) } @@ -31,6 +37,7 @@ func init() { type Client struct { cfgFile string tunAdapter iface.TunAdapter + iFaceDiscover IFaceDiscover recorder *peer.Status ctxCancel context.CancelFunc ctxCancelLock *sync.Mutex @@ -38,7 +45,7 @@ type Client struct { } // NewClient instantiate a new Client -func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter) *Client { +func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover) *Client { lvl, _ := log.ParseLevel("trace") log.SetLevel(lvl) @@ -46,6 +53,7 @@ func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter) *Client { cfgFile: cfgFile, deviceName: deviceName, tunAdapter: tunAdapter, + iFaceDiscover: iFaceDiscover, recorder: peer.NewRecorder(""), ctxCancelLock: &sync.Mutex{}, } @@ -77,7 +85,7 @@ func (c *Client) Run(urlOpener URLOpener) error { // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - return internal.RunClient(ctx, cfg, c.recorder, c.tunAdapter) + return internal.RunClient(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover) } // Stop the internal client and free the resources diff --git a/client/cmd/up.go b/client/cmd/up.go index 5bbdab690..fc576e8d4 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -94,7 +94,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { var cancel context.CancelFunc ctx, cancel = context.WithCancel(ctx) SetupCloseHandler(ctx, cancel) - return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()), nil) + return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()), nil, nil) } func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { diff --git a/client/internal/connect.go b/client/internal/connect.go index eeb0e640e..3aca0bab9 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -13,6 +13,7 @@ import ( gstatus "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/internal/peer" + "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/iface" @@ -22,7 +23,7 @@ import ( ) // RunClient with main logic. -func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter) error { +func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.IFaceDiscover) error { backOff := &backoff.ExponentialBackOff{ InitialInterval: time.Second, RandomizationFactor: 1, @@ -146,7 +147,7 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, peerConfig := loginResp.GetPeerConfig() - engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig, tunAdapter) + engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig, tunAdapter, iFaceDiscover) if err != nil { log.Error(err) return wrapErr(err) @@ -193,12 +194,13 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, } // createEngineConfig converts configuration received from Management Service to EngineConfig -func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig, tunAdapter iface.TunAdapter) (*EngineConfig, error) { +func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.IFaceDiscover) (*EngineConfig, error) { engineConf := &EngineConfig{ WgIfaceName: config.WgIface, WgAddr: peerConfig.Address, TunAdapter: tunAdapter, + IFaceDiscover: iFaceDiscover, IFaceBlackList: config.IFaceBlackList, DisableIPv6Discovery: config.DisableIPv6Discovery, WgPrivateKey: key, diff --git a/client/internal/engine.go b/client/internal/engine.go index 10d74d931..a7fa82c11 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -20,6 +20,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/proxy" "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/stdnet" nbssh "github.com/netbirdio/netbird/client/ssh" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/iface" @@ -49,6 +50,8 @@ type EngineConfig struct { // TunAdapter is option. It is necessary for mobile version. TunAdapter iface.TunAdapter + IFaceDiscover stdnet.IFaceDiscover + // WgAddr is a Wireguard local address (Netbird Network IP) WgAddr string @@ -186,12 +189,22 @@ func (e *Engine) Start() error { networkName = "udp4" } + transportNet, err := e.newStdNet() + if err != nil { + log.Warnf("failed to create pion's stdnet: %s", err) + } + e.udpMuxConn, err = net.ListenUDP(networkName, &net.UDPAddr{Port: e.config.UDPMuxPort}) if err != nil { log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxPort, err.Error()) e.close() return err } + udpMuxParams := ice.UDPMuxParams{ + UDPConn: e.udpMuxConn, + Net: transportNet, + } + e.udpMux = ice.NewUDPMuxDefault(udpMuxParams) e.udpMuxConnSrflx, err = net.ListenUDP(networkName, &net.UDPAddr{Port: e.config.UDPMuxSrflxPort}) if err != nil { @@ -199,9 +212,7 @@ func (e *Engine) Start() error { e.close() return err } - - e.udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: e.udpMuxConn}) - e.udpMuxSrflx = ice.NewUniversalUDPMuxDefault(ice.UniversalUDPMuxParams{UDPConn: e.udpMuxConnSrflx}) + e.udpMuxSrflx = ice.NewUniversalUDPMuxDefault(ice.UniversalUDPMuxParams{UDPConn: e.udpMuxConnSrflx, Net: transportNet}) err = e.wgInterface.Create() if err != nil { @@ -813,7 +824,7 @@ func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, er NATExternalIPs: e.parseNATExternalIPMappings(), } - peerConn, err := peer.NewConn(config, e.statusRecorder) + peerConn, err := peer.NewConn(config, e.statusRecorder, e.config.TunAdapter, e.config.IFaceDiscover) if err != nil { return nil, err } diff --git a/client/internal/engine_stdnet.go b/client/internal/engine_stdnet.go new file mode 100644 index 000000000..b4e05768c --- /dev/null +++ b/client/internal/engine_stdnet.go @@ -0,0 +1,11 @@ +//go:build !android + +package internal + +import ( + "github.com/pion/transport/v2/stdnet" +) + +func (e *Engine) newStdNet() (*stdnet.Net, error) { + return stdnet.NewNet() +} diff --git a/client/internal/engine_stdnet_android.go b/client/internal/engine_stdnet_android.go new file mode 100644 index 000000000..976ffd656 --- /dev/null +++ b/client/internal/engine_stdnet_android.go @@ -0,0 +1,7 @@ +package internal + +import "github.com/netbirdio/netbird/client/internal/stdnet" + +func (e *Engine) newStdNet() (*stdnet.Net, error) { + return stdnet.NewNet(e.config.IFaceDiscover) +} diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index e42e6305d..ee45a6ba0 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -9,15 +9,15 @@ import ( "time" "github.com/pion/ice/v2" - "github.com/pion/transport/v2/stdnet" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl" "github.com/netbirdio/netbird/client/internal/proxy" + "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/iface" - "github.com/netbirdio/netbird/version" signal "github.com/netbirdio/netbird/signal/client" sProto "github.com/netbirdio/netbird/signal/proto" + "github.com/netbirdio/netbird/version" ) // ConnConfig is a peer Connection configuration @@ -93,6 +93,9 @@ type Conn struct { proxy proxy.Proxy remoteModeCh chan ModeMessage meta meta + + adapter iface.TunAdapter + iFaceDiscover stdnet.IFaceDiscover } // meta holds meta information about a connection @@ -118,7 +121,7 @@ func (conn *Conn) UpdateConf(conf ConnConfig) { // NewConn creates a new not opened Conn to the remote peer. // To establish a connection run Conn.Open -func NewConn(config ConnConfig, statusRecorder *Status) (*Conn, error) { +func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter, iFaceDiscover stdnet.IFaceDiscover) (*Conn, error) { return &Conn{ config: config, mu: sync.Mutex{}, @@ -128,6 +131,8 @@ func NewConn(config ConnConfig, statusRecorder *Status) (*Conn, error) { remoteAnswerCh: make(chan OfferAnswer), statusRecorder: statusRecorder, remoteModeCh: make(chan ModeMessage, 1), + adapter: adapter, + iFaceDiscover: iFaceDiscover, }, nil } @@ -162,7 +167,9 @@ func (conn *Conn) reCreateAgent() error { defer conn.mu.Unlock() failedTimeout := 6 * time.Second - transportNet, err := stdnet.NewNet() + + var err error + transportNet, err := conn.newStdNet() if err != nil { log.Warnf("failed to create pion's stdnet: %s", err) } diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index 7f9b263e4..ddee91800 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -37,7 +37,7 @@ func TestNewConn_interfaceFilter(t *testing.T) { } func TestConn_GetKey(t *testing.T) { - conn, err := NewConn(connConf, nil) + conn, err := NewConn(connConf, nil, nil, nil) if err != nil { return } @@ -49,7 +49,7 @@ func TestConn_GetKey(t *testing.T) { func TestConn_OnRemoteOffer(t *testing.T) { - conn, err := NewConn(connConf, NewRecorder("https://mgm")) + conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) if err != nil { return } @@ -83,7 +83,7 @@ func TestConn_OnRemoteOffer(t *testing.T) { func TestConn_OnRemoteAnswer(t *testing.T) { - conn, err := NewConn(connConf, NewRecorder("https://mgm")) + conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) if err != nil { return } @@ -116,7 +116,7 @@ func TestConn_OnRemoteAnswer(t *testing.T) { } func TestConn_Status(t *testing.T) { - conn, err := NewConn(connConf, NewRecorder("https://mgm")) + conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) if err != nil { return } @@ -143,7 +143,7 @@ func TestConn_Status(t *testing.T) { func TestConn_Close(t *testing.T) { - conn, err := NewConn(connConf, NewRecorder("https://mgm")) + conn, err := NewConn(connConf, NewRecorder("https://mgm"), nil, nil) if err != nil { return } @@ -411,7 +411,7 @@ func TestGetProxyWithMessageExchange(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { g := errgroup.Group{} - conn, err := NewConn(connConf, nil) + conn, err := NewConn(connConf, nil, nil, nil) if err != nil { t.Fatal(err) } diff --git a/client/internal/peer/stdnet.go b/client/internal/peer/stdnet.go new file mode 100644 index 000000000..588aaa929 --- /dev/null +++ b/client/internal/peer/stdnet.go @@ -0,0 +1,11 @@ +//go:build !android + +package peer + +import ( + "github.com/pion/transport/v2/stdnet" +) + +func (conn *Conn) newStdNet() (*stdnet.Net, error) { + return stdnet.NewNet() +} diff --git a/client/internal/peer/stdnet_android.go b/client/internal/peer/stdnet_android.go new file mode 100644 index 000000000..71a962c21 --- /dev/null +++ b/client/internal/peer/stdnet_android.go @@ -0,0 +1,7 @@ +package peer + +import "github.com/netbirdio/netbird/client/internal/stdnet" + +func (conn *Conn) newStdNet() (*stdnet.Net, error) { + return stdnet.NewNet(conn.iFaceDiscover) +} diff --git a/client/internal/stdnet/iface_discover.go b/client/internal/stdnet/iface_discover.go new file mode 100644 index 000000000..cbe306e2e --- /dev/null +++ b/client/internal/stdnet/iface_discover.go @@ -0,0 +1,8 @@ +package stdnet + +// IFaceDiscover provide an option for external services (mobile) +// to collect network interface information +type IFaceDiscover interface { + // IFaces return with the description of the interfaces + IFaces() (string, error) +} diff --git a/client/internal/stdnet/stdnet.go b/client/internal/stdnet/stdnet.go new file mode 100644 index 000000000..311306535 --- /dev/null +++ b/client/internal/stdnet/stdnet.go @@ -0,0 +1,137 @@ +// Package stdnet is an extension of the pion's stdnet. +// With it the list of the interface can come from external source. +// More info: https://github.com/golang/go/issues/40569 +package stdnet + +import ( + "fmt" + "net" + "strings" + + "github.com/pion/transport/v2" + "github.com/pion/transport/v2/stdnet" + log "github.com/sirupsen/logrus" +) + +// Net is an implementation of the net.Net interface +// based on functions of the standard net package. +type Net struct { + stdnet.Net + interfaces []*transport.Interface +} + +// NewNet creates a new StdNet instance. +func NewNet(iFaceDiscover IFaceDiscover) (*Net, error) { + n := &Net{} + + return n, n.UpdateInterfaces(iFaceDiscover) +} + +// UpdateInterfaces updates the internal list of network interfaces +// and associated addresses. +func (n *Net) UpdateInterfaces(iFaceDiscover IFaceDiscover) error { + ifacesString, err := iFaceDiscover.IFaces() + if err != nil { + return err + } + n.interfaces = parseInterfacesString(ifacesString) + return err +} + +// Interfaces returns a slice of interfaces which are available on the +// system +func (n *Net) Interfaces() ([]*transport.Interface, error) { + return n.interfaces, nil +} + +// InterfaceByIndex returns the interface specified by index. +// +// On Solaris, it returns one of the logical network interfaces +// sharing the logical data link; for more precision use +// InterfaceByName. +func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) { + for _, ifc := range n.interfaces { + if ifc.Index == index { + return ifc, nil + } + } + + return nil, fmt.Errorf("%w: index=%d", transport.ErrInterfaceNotFound, index) +} + +// InterfaceByName returns the interface specified by name. +func (n *Net) InterfaceByName(name string) (*transport.Interface, error) { + for _, ifc := range n.interfaces { + if ifc.Name == name { + return ifc, nil + } + } + + return nil, fmt.Errorf("%w: %s", transport.ErrInterfaceNotFound, name) +} + +func parseInterfacesString(interfaces string) []*transport.Interface { + ifs := []*transport.Interface{} + + for _, iface := range strings.Split(interfaces, "\n") { + if strings.TrimSpace(iface) == "" { + continue + } + + fields := strings.Split(iface, "|") + if len(fields) != 2 { + log.Warnf("parseInterfacesString: unable to split %q", iface) + continue + } + + var name string + var index, mtu int + var up, broadcast, loopback, pointToPoint, multicast bool + _, err := fmt.Sscanf(fields[0], "%s %d %d %t %t %t %t %t", + &name, &index, &mtu, &up, &broadcast, &loopback, &pointToPoint, &multicast) + if err != nil { + log.Warnf("parseInterfacesString: unable to parse %q: %v", iface, err) + continue + } + + newIf := net.Interface{ + Name: name, + Index: index, + MTU: mtu, + } + if up { + newIf.Flags |= net.FlagUp + } + if broadcast { + newIf.Flags |= net.FlagBroadcast + } + if loopback { + newIf.Flags |= net.FlagLoopback + } + if pointToPoint { + newIf.Flags |= net.FlagPointToPoint + } + if multicast { + newIf.Flags |= net.FlagMulticast + } + + ifc := transport.NewInterface(newIf) + + addrs := strings.Trim(fields[1], " \n") + foundAddress := false + for _, addr := range strings.Split(addrs, " ") { + ip, ipNet, err := net.ParseCIDR(addr) + if err != nil { + log.Warnf("%s", err) + continue + } + ipNet.IP = ip + ifc.AddAddress(ipNet) + foundAddress = true + } + if foundAddress { + ifs = append(ifs, ifc) + } + } + return ifs +} diff --git a/client/internal/stdnet/stdnet_test.go b/client/internal/stdnet/stdnet_test.go new file mode 100644 index 000000000..f3c09c61e --- /dev/null +++ b/client/internal/stdnet/stdnet_test.go @@ -0,0 +1,52 @@ +package stdnet + +import ( + "fmt" + "testing" +) + +func Test_parseInterfacesString(t *testing.T) { + testData := []struct { + name string + index int + mtu int + up bool + broadcast bool + loopBack bool + pointToPoint bool + multicast bool + addr string + }{ + {"wlan0", 30, 1500, true, true, false, false, true, "10.1.10.131/24"}, + {"rmnet0", 30, 1500, true, true, false, false, true, "192.168.0.56/24"}, + } + + var exampleString string + for _, d := range testData { + exampleString = fmt.Sprintf("%s\n%s %d %d %t %t %t %t %t | %s", exampleString, + d.name, + d.index, + d.mtu, + d.up, + d.broadcast, + d.loopBack, + d.pointToPoint, + d.multicast, + d.addr) + } + nets := parseInterfacesString(exampleString) + if len(nets) == 0 { + t.Fatalf("failed to parse interfaces") + } + + for i, net := range nets { + if net.MTU != testData[i].mtu { + t.Errorf("invalid mtu: %d, expected: %d", net.MTU, testData[0].mtu) + + } + + if net.Interface.Name != testData[i].name { + t.Errorf("invalid interface name: %s, expected: %s", net.Interface.Name, testData[i].name) + } + } +} diff --git a/client/server/server.go b/client/server/server.go index 238b15acc..44502b148 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -102,7 +102,7 @@ func (s *Server) Start() error { } go func() { - if err := internal.RunClient(ctx, config, s.statusRecorder, nil); err != nil { + if err := internal.RunClient(ctx, config, s.statusRecorder, nil, nil); err != nil { log.Errorf("init connections: %v", err) } }() @@ -394,7 +394,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes } go func() { - if err := internal.RunClient(ctx, s.config, s.statusRecorder, nil); err != nil { + if err := internal.RunClient(ctx, s.config, s.statusRecorder, nil, nil); err != nil { log.Errorf("run client connection: %v", err) return }