Skip to content

Commit b5daec3

Browse files
authored
[client,signal,management] Add browser client support (#4415)
1 parent 5e1a40c commit b5daec3

File tree

107 files changed

+3591
-284
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+3591
-284
lines changed

.github/workflows/golangci-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: codespell
2020
uses: codespell-project/actions-codespell@v2
2121
with:
22-
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe
22+
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros
2323
skip: go.mod,go.sum
2424
golangci:
2525
strategy:
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Wasm
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
js_lint:
15+
name: "JS / Lint"
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
- name: Install Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version: "1.23.x"
24+
- name: Install dependencies
25+
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
26+
- name: Install golangci-lint
27+
uses: golangci/golangci-lint-action@d6238b002a20823d52840fda27e2d4891c5952dc
28+
with:
29+
version: latest
30+
install-mode: binary
31+
skip-cache: true
32+
skip-pkg-cache: true
33+
skip-build-cache: true
34+
- name: Run golangci-lint for WASM
35+
run: |
36+
GOOS=js GOARCH=wasm golangci-lint run --timeout=12m --out-format colored-line-number ./client/...
37+
continue-on-error: true
38+
39+
js_build:
40+
name: "JS / Build"
41+
runs-on: ubuntu-latest
42+
steps:
43+
- name: Checkout repository
44+
uses: actions/checkout@v4
45+
- name: Install Go
46+
uses: actions/setup-go@v5
47+
with:
48+
go-version: "1.23.x"
49+
- name: Build Wasm client
50+
run: GOOS=js GOARCH=wasm go build -o netbird.wasm ./client/wasm/cmd
51+
env:
52+
CGO_ENABLED: 0
53+
- name: Check Wasm build size
54+
run: |
55+
echo "Wasm build size:"
56+
ls -lh netbird.wasm
57+
58+
SIZE=$(stat -c%s netbird.wasm)
59+
SIZE_MB=$((SIZE / 1024 / 1024))
60+
61+
echo "Size: ${SIZE} bytes (${SIZE_MB} MB)"
62+
63+
if [ ${SIZE} -gt 52428800 ]; then
64+
echo "Wasm binary size (${SIZE_MB}MB) exceeds 50MB limit!"
65+
exit 1
66+
fi
67+

.gitmodules

Whitespace-only changes.

.goreleaser.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ version: 2
22

33
project_name: netbird
44
builds:
5+
- id: netbird-wasm
6+
dir: client/wasm/cmd
7+
binary: netbird
8+
env: [GOOS=js, GOARCH=wasm, CGO_ENABLED=0]
9+
goos:
10+
- js
11+
goarch:
12+
- wasm
13+
ldflags:
14+
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
15+
mod_timestamp: "{{ .CommitTimestamp }}"
16+
517
- id: netbird
618
dir: client
719
binary: netbird
@@ -115,6 +127,11 @@ archives:
115127
- builds:
116128
- netbird
117129
- netbird-static
130+
- id: netbird-wasm
131+
builds:
132+
- netbird-wasm
133+
name_template: "{{ .ProjectName }}_{{ .Version }}"
134+
format: binary
118135

119136
nfpms:
120137
- maintainer: Netbird <[email protected]>

client/cmd/debug_js.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package cmd
2+
3+
import "context"
4+
5+
// SetupDebugHandler is a no-op for WASM
6+
func SetupDebugHandler(context.Context, interface{}, interface{}, interface{}, string) {
7+
// Debug handler not needed for WASM
8+
}

client/cmd/testutil_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"google.golang.org/grpc"
1313

1414
"github.com/netbirdio/management-integrations/integrations"
15+
1516
clientProto "github.com/netbirdio/netbird/client/proto"
1617
client "github.com/netbirdio/netbird/client/server"
1718
"github.com/netbirdio/netbird/management/internals/server/config"
@@ -20,6 +21,7 @@ import (
2021
"github.com/netbirdio/netbird/management/server/groups"
2122
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
2223
"github.com/netbirdio/netbird/management/server/peers"
24+
"github.com/netbirdio/netbird/management/server/peers/ephemeral/manager"
2325
"github.com/netbirdio/netbird/management/server/permissions"
2426
"github.com/netbirdio/netbird/management/server/settings"
2527
"github.com/netbirdio/netbird/management/server/store"
@@ -114,7 +116,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
114116
}
115117

116118
secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
117-
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil, &mgmt.MockIntegratedValidator{})
119+
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, &manager.EphemeralManager{}, nil, &mgmt.MockIntegratedValidator{})
118120
if err != nil {
119121
t.Fatal(err)
120122
}

client/embed/embed.go

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,29 @@ import (
2323

2424
var ErrClientAlreadyStarted = errors.New("client already started")
2525
var ErrClientNotStarted = errors.New("client not started")
26+
var ErrConfigNotInitialized = errors.New("config not initialized")
2627

27-
// Client manages a netbird embedded client instance
28+
// Client manages a netbird embedded client instance.
2829
type Client struct {
2930
deviceName string
3031
config *profilemanager.Config
3132
mu sync.Mutex
3233
cancel context.CancelFunc
3334
setupKey string
35+
jwtToken string
3436
connect *internal.ConnectClient
3537
}
3638

37-
// Options configures a new Client
39+
// Options configures a new Client.
3840
type Options struct {
3941
// DeviceName is this peer's name in the network
4042
DeviceName string
4143
// SetupKey is used for authentication
4244
SetupKey string
45+
// JWTToken is used for JWT-based authentication
46+
JWTToken string
47+
// PrivateKey is used for direct private key authentication
48+
PrivateKey string
4349
// ManagementURL overrides the default management server URL
4450
ManagementURL string
4551
// PreSharedKey is the pre-shared key for the WireGuard interface
@@ -58,8 +64,35 @@ type Options struct {
5864
DisableClientRoutes bool
5965
}
6066

61-
// New creates a new netbird embedded client
67+
// validateCredentials checks that exactly one credential type is provided
68+
func (opts *Options) validateCredentials() error {
69+
credentialsProvided := 0
70+
if opts.SetupKey != "" {
71+
credentialsProvided++
72+
}
73+
if opts.JWTToken != "" {
74+
credentialsProvided++
75+
}
76+
if opts.PrivateKey != "" {
77+
credentialsProvided++
78+
}
79+
80+
if credentialsProvided == 0 {
81+
return fmt.Errorf("one of SetupKey, JWTToken, or PrivateKey must be provided")
82+
}
83+
if credentialsProvided > 1 {
84+
return fmt.Errorf("only one of SetupKey, JWTToken, or PrivateKey can be specified")
85+
}
86+
87+
return nil
88+
}
89+
90+
// New creates a new netbird embedded client.
6291
func New(opts Options) (*Client, error) {
92+
if err := opts.validateCredentials(); err != nil {
93+
return nil, err
94+
}
95+
6396
if opts.LogOutput != nil {
6497
logrus.SetOutput(opts.LogOutput)
6598
}
@@ -107,9 +140,14 @@ func New(opts Options) (*Client, error) {
107140
return nil, fmt.Errorf("create config: %w", err)
108141
}
109142

143+
if opts.PrivateKey != "" {
144+
config.PrivateKey = opts.PrivateKey
145+
}
146+
110147
return &Client{
111148
deviceName: opts.DeviceName,
112149
setupKey: opts.SetupKey,
150+
jwtToken: opts.JWTToken,
113151
config: config,
114152
}, nil
115153
}
@@ -126,7 +164,7 @@ func (c *Client) Start(startCtx context.Context) error {
126164
ctx := internal.CtxInitState(context.Background())
127165
// nolint:staticcheck
128166
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
129-
if err := internal.Login(ctx, c.config, c.setupKey, ""); err != nil {
167+
if err := internal.Login(ctx, c.config, c.setupKey, c.jwtToken); err != nil {
130168
return fmt.Errorf("login: %w", err)
131169
}
132170

@@ -187,6 +225,16 @@ func (c *Client) Stop(ctx context.Context) error {
187225
}
188226
}
189227

228+
// GetConfig returns a copy of the internal client config.
229+
func (c *Client) GetConfig() (profilemanager.Config, error) {
230+
c.mu.Lock()
231+
defer c.mu.Unlock()
232+
if c.config == nil {
233+
return profilemanager.Config{}, ErrConfigNotInitialized
234+
}
235+
return *c.config, nil
236+
}
237+
190238
// Dial dials a network address in the netbird network.
191239
// Not applicable if the userspace networking mode is disabled.
192240
func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
@@ -211,7 +259,7 @@ func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, e
211259
return nsnet.DialContext(ctx, network, address)
212260
}
213261

214-
// ListenTCP listens on the given address in the netbird network
262+
// ListenTCP listens on the given address in the netbird network.
215263
// Not applicable if the userspace networking mode is disabled.
216264
func (c *Client) ListenTCP(address string) (net.Listener, error) {
217265
nsnet, addr, err := c.getNet()
@@ -232,7 +280,7 @@ func (c *Client) ListenTCP(address string) (net.Listener, error) {
232280
return nsnet.ListenTCP(tcpAddr)
233281
}
234282

235-
// ListenUDP listens on the given address in the netbird network
283+
// ListenUDP listens on the given address in the netbird network.
236284
// Not applicable if the userspace networking mode is disabled.
237285
func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
238286
nsnet, addr, err := c.getNet()

client/grpc/dialer.go

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,28 @@ import (
44
"context"
55
"crypto/tls"
66
"crypto/x509"
7-
"fmt"
8-
"net"
9-
"os/user"
107
"runtime"
118
"time"
129

13-
"google.golang.org/grpc/codes"
14-
"google.golang.org/grpc/status"
15-
1610
"github.com/cenkalti/backoff/v4"
1711
log "github.com/sirupsen/logrus"
1812
"google.golang.org/grpc"
1913
"google.golang.org/grpc/credentials"
2014
"google.golang.org/grpc/credentials/insecure"
2115
"google.golang.org/grpc/keepalive"
2216

23-
nbnet "github.com/netbirdio/netbird/client/net"
24-
2517
"github.com/netbirdio/netbird/util/embeddedroots"
2618
)
2719

28-
func WithCustomDialer() grpc.DialOption {
29-
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
30-
if runtime.GOOS == "linux" {
31-
currentUser, err := user.Current()
32-
if err != nil {
33-
return nil, status.Errorf(codes.FailedPrecondition, "failed to get current user: %v", err)
34-
}
35-
36-
// the custom dialer requires root permissions which are not required for use cases run as non-root
37-
if currentUser.Uid != "0" {
38-
log.Debug("Not running as root, using standard dialer")
39-
dialer := &net.Dialer{}
40-
return dialer.DialContext(ctx, "tcp", addr)
41-
}
42-
}
43-
44-
conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
45-
if err != nil {
46-
log.Errorf("Failed to dial: %s", err)
47-
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
48-
}
49-
return conn, nil
50-
})
51-
}
52-
53-
// grpcDialBackoff is the backoff mechanism for the grpc calls
20+
// Backoff returns a backoff configuration for gRPC calls
5421
func Backoff(ctx context.Context) backoff.BackOff {
5522
b := backoff.NewExponentialBackOff()
5623
b.MaxElapsedTime = 10 * time.Second
5724
b.Clock = backoff.SystemClock
5825
return backoff.WithContext(b, ctx)
5926
}
6027

28+
// CreateConnection creates a gRPC client connection with the appropriate transport options
6129
func CreateConnection(ctx context.Context, addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
6230
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
6331
if tlsEnabled {
@@ -68,7 +36,9 @@ func CreateConnection(ctx context.Context, addr string, tlsEnabled bool) (*grpc.
6836
}
6937

7038
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
71-
RootCAs: certPool,
39+
// for js, outer websocket layer takes care of tls verification via WithCustomDialer
40+
InsecureSkipVerify: runtime.GOOS == "js",
41+
RootCAs: certPool,
7242
}))
7343
}
7444

@@ -79,7 +49,7 @@ func CreateConnection(ctx context.Context, addr string, tlsEnabled bool) (*grpc.
7949
connCtx,
8050
addr,
8151
transportOption,
82-
WithCustomDialer(),
52+
WithCustomDialer(tlsEnabled),
8353
grpc.WithBlock(),
8454
grpc.WithKeepaliveParams(keepalive.ClientParameters{
8555
Time: 30 * time.Second,

client/grpc/dialer_generic.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//go:build !js
2+
3+
package grpc
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"net"
9+
"os/user"
10+
"runtime"
11+
12+
"google.golang.org/grpc/codes"
13+
"google.golang.org/grpc/status"
14+
15+
log "github.com/sirupsen/logrus"
16+
"google.golang.org/grpc"
17+
18+
nbnet "github.com/netbirdio/netbird/client/net"
19+
)
20+
21+
func WithCustomDialer(tlsEnabled bool) grpc.DialOption {
22+
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
23+
if runtime.GOOS == "linux" {
24+
currentUser, err := user.Current()
25+
if err != nil {
26+
return nil, status.Errorf(codes.FailedPrecondition, "failed to get current user: %v", err)
27+
}
28+
29+
// the custom dialer requires root permissions which are not required for use cases run as non-root
30+
if currentUser.Uid != "0" {
31+
log.Debug("Not running as root, using standard dialer")
32+
dialer := &net.Dialer{}
33+
return dialer.DialContext(ctx, "tcp", addr)
34+
}
35+
}
36+
37+
conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
38+
if err != nil {
39+
log.Errorf("Failed to dial: %s", err)
40+
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
41+
}
42+
return conn, nil
43+
})
44+
}

0 commit comments

Comments
 (0)