mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-29 01:59:07 +02:00
Add wasm client
This commit is contained in:
+3
-35
@@ -4,15 +4,8 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
@@ -21,35 +14,9 @@ import (
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
"github.com/netbirdio/netbird/util/embeddedroots"
|
||||
nbnet "github.com/netbirdio/netbird/util/net"
|
||||
)
|
||||
|
||||
func WithCustomDialer() grpc.DialOption {
|
||||
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
if runtime.GOOS == "linux" {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "failed to get current user: %v", err)
|
||||
}
|
||||
|
||||
// the custom dialer requires root permissions which are not required for use cases run as non-root
|
||||
if currentUser.Uid != "0" {
|
||||
log.Debug("Not running as root, using standard dialer")
|
||||
dialer := &net.Dialer{}
|
||||
return dialer.DialContext(ctx, "tcp", addr)
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to dial: %s", err)
|
||||
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
|
||||
}
|
||||
return conn, nil
|
||||
})
|
||||
}
|
||||
|
||||
// grpcDialBackoff is the backoff mechanism for the grpc calls
|
||||
// Backoff returns a backoff configuration for gRPC calls
|
||||
func Backoff(ctx context.Context) backoff.BackOff {
|
||||
b := backoff.NewExponentialBackOff()
|
||||
b.MaxElapsedTime = 10 * time.Second
|
||||
@@ -57,6 +24,7 @@ func Backoff(ctx context.Context) backoff.BackOff {
|
||||
return backoff.WithContext(b, ctx)
|
||||
}
|
||||
|
||||
// CreateConnection creates a gRPC client connection with the appropriate transport options
|
||||
func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
|
||||
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||
if tlsEnabled {
|
||||
@@ -78,7 +46,7 @@ func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
|
||||
connCtx,
|
||||
addr,
|
||||
transportOption,
|
||||
WithCustomDialer(),
|
||||
WithCustomDialer(tlsEnabled),
|
||||
grpc.WithBlock(),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 30 * time.Second,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build !js
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/user"
|
||||
"runtime"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
nbnet "github.com/netbirdio/netbird/util/net"
|
||||
)
|
||||
|
||||
func WithCustomDialer(tlsEnabled bool) grpc.DialOption {
|
||||
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
if runtime.GOOS == "linux" {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "failed to get current user: %v", err)
|
||||
}
|
||||
|
||||
// the custom dialer requires root permissions which are not required for use cases run as non-root
|
||||
if currentUser.Uid != "0" {
|
||||
log.Debug("Not running as root, using standard dialer")
|
||||
dialer := &net.Dialer{}
|
||||
return dialer.DialContext(ctx, "tcp", addr)
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to dial: %s", err)
|
||||
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
|
||||
}
|
||||
return conn, nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/netbirdio/netbird/util/wsproxy/client"
|
||||
)
|
||||
|
||||
// WithCustomDialer returns a gRPC dial option that uses WebSocket transport for WASM/JS environments.
|
||||
func WithCustomDialer(tlsEnabled bool) grpc.DialOption {
|
||||
return client.WithWebSocketDialer(tlsEnabled)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build js
|
||||
|
||||
package util
|
||||
|
||||
// IsAdmin returns false for WASM as there's no admin concept in browser
|
||||
func IsAdmin() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/netbirdio/netbird/util/wsproxy"
|
||||
)
|
||||
|
||||
const dialTimeout = 30 * time.Second
|
||||
|
||||
// websocketConn wraps a JavaScript WebSocket to implement net.Conn
|
||||
type websocketConn struct {
|
||||
ws js.Value
|
||||
remoteAddr string
|
||||
messages chan []byte
|
||||
readBuf []byte
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *websocketConn) Read(b []byte) (int, error) {
|
||||
c.mu.Lock()
|
||||
if len(c.readBuf) > 0 {
|
||||
n := copy(b, c.readBuf)
|
||||
c.readBuf = c.readBuf[n:]
|
||||
c.mu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case data := <-c.messages:
|
||||
n := copy(b, data)
|
||||
if n < len(data) {
|
||||
c.mu.Lock()
|
||||
c.readBuf = data[n:]
|
||||
c.mu.Unlock()
|
||||
}
|
||||
return n, nil
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *websocketConn) Write(b []byte) (int, error) {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
uint8Array := js.Global().Get("Uint8Array").New(len(b))
|
||||
js.CopyBytesToJS(uint8Array, b)
|
||||
c.ws.Call("send", uint8Array)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *websocketConn) Close() error {
|
||||
c.cancel()
|
||||
c.ws.Call("close")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *websocketConn) LocalAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *websocketConn) RemoteAddr() net.Addr {
|
||||
return stringAddr(c.remoteAddr)
|
||||
}
|
||||
func (c *websocketConn) SetDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *websocketConn) SetReadDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *websocketConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// stringAddr is a simple net.Addr that returns a string
|
||||
type stringAddr string
|
||||
|
||||
func (s stringAddr) Network() string { return "tcp" }
|
||||
func (s stringAddr) String() string { return string(s) }
|
||||
|
||||
// WithWebSocketDialer returns a gRPC dial option that uses WebSocket transport for JS/WASM environments.
|
||||
func WithWebSocketDialer(tlsEnabled bool) grpc.DialOption {
|
||||
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
scheme := "wss"
|
||||
if !tlsEnabled {
|
||||
scheme = "ws"
|
||||
}
|
||||
wsURL := fmt.Sprintf("%s://%s%s", scheme, addr, wsproxy.ProxyPath)
|
||||
|
||||
ws := js.Global().Get("WebSocket").New(wsURL)
|
||||
|
||||
connCtx, connCancel := context.WithCancel(context.Background())
|
||||
conn := &websocketConn{
|
||||
ws: ws,
|
||||
remoteAddr: addr,
|
||||
messages: make(chan []byte, 100),
|
||||
ctx: connCtx,
|
||||
cancel: connCancel,
|
||||
}
|
||||
|
||||
ws.Set("binaryType", "arraybuffer")
|
||||
|
||||
openCh := make(chan struct{})
|
||||
errorCh := make(chan error, 1)
|
||||
|
||||
ws.Set("onopen", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
close(openCh)
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onerror", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
select {
|
||||
case errorCh <- wsproxy.ErrConnectionFailed:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onmessage", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
event := args[0]
|
||||
data := event.Get("data")
|
||||
|
||||
uint8Array := js.Global().Get("Uint8Array").New(data)
|
||||
length := uint8Array.Get("length").Int()
|
||||
bytes := make([]byte, length)
|
||||
js.CopyBytesToGo(bytes, uint8Array)
|
||||
|
||||
select {
|
||||
case conn.messages <- bytes:
|
||||
default:
|
||||
log.Warnf("gRPC WebSocket message dropped for %s - buffer full", addr)
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
ws.Set("onclose", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
conn.cancel()
|
||||
return nil
|
||||
}))
|
||||
|
||||
select {
|
||||
case <-openCh:
|
||||
return conn, nil
|
||||
case err := <-errorCh:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
ws.Call("close")
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(dialTimeout):
|
||||
ws.Call("close")
|
||||
return nil, wsproxy.ErrConnectionTimeout
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package wsproxy
|
||||
|
||||
import "errors"
|
||||
|
||||
// ProxyPath is the standard path where the WebSocket proxy is mounted on servers.
|
||||
const ProxyPath = "/ws-proxy"
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrConnectionTimeout = errors.New("WebSocket connection timeout")
|
||||
ErrConnectionFailed = errors.New("WebSocket connection failed")
|
||||
ErrBackendUnavailable = errors.New("backend unavailable")
|
||||
)
|
||||
Reference in New Issue
Block a user