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.
This commit is contained in:
Zoltan Papp 2023-03-24 08:40:39 +01:00 committed by GitHub
parent a27fe4326c
commit d1703479ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 285 additions and 24 deletions

View File

@ -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
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

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}