mirror of
https://github.com/netbirdio/netbird.git
synced 2026-06-29 19:29:56 +00:00
Compare commits
3 Commits
netmap_pro
...
client-jso
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aa1bad19e | ||
|
|
5bb3ab60a8 | ||
|
|
a2fd1bb0a8 |
@@ -5,6 +5,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -22,15 +23,21 @@ var serviceCmd = &cobra.Command{
|
|||||||
Short: "Manage the NetBird daemon service",
|
Short: "Manage the NetBird daemon service",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultJSONSocket = "unix:///var/run/netbird-http.sock"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
serviceName string
|
serviceName string
|
||||||
serviceEnvVars []string
|
serviceEnvVars []string
|
||||||
|
jsonSocket string
|
||||||
|
enableJSONSocket bool
|
||||||
)
|
)
|
||||||
|
|
||||||
type program struct {
|
type program struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
serv *grpc.Server
|
serv *grpc.Server
|
||||||
|
jsonServ *http.Server
|
||||||
|
jsonServMu sync.Mutex
|
||||||
serverInstance *server.Server
|
serverInstance *server.Server
|
||||||
serverInstanceMu sync.Mutex
|
serverInstanceMu sync.Mutex
|
||||||
}
|
}
|
||||||
@@ -46,6 +53,8 @@ func init() {
|
|||||||
serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings")
|
serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings")
|
||||||
serviceCmd.PersistentFlags().BoolVar(&captureEnabled, "enable-capture", false, "Enables packet capture via 'netbird debug capture'. To persist, use: netbird service install --enable-capture")
|
serviceCmd.PersistentFlags().BoolVar(&captureEnabled, "enable-capture", false, "Enables packet capture via 'netbird debug capture'. To persist, use: netbird service install --enable-capture")
|
||||||
serviceCmd.PersistentFlags().BoolVar(&networksDisabled, "disable-networks", false, "Disables network selection. If enabled, the client will not allow listing, selecting, or deselecting networks. To persist, use: netbird service install --disable-networks")
|
serviceCmd.PersistentFlags().BoolVar(&networksDisabled, "disable-networks", false, "Disables network selection. If enabled, the client will not allow listing, selecting, or deselecting networks. To persist, use: netbird service install --disable-networks")
|
||||||
|
serviceCmd.PersistentFlags().BoolVar(&enableJSONSocket, "enable-json-socket", false, "Enables the HTTP/JSON API socket served by grpc-gateway. To persist, use: netbird service install --enable-json-socket")
|
||||||
|
serviceCmd.PersistentFlags().StringVar(&jsonSocket, "json-socket", defaultJSONSocket, "HTTP/JSON API socket address [unix|tcp]://[path|host:port]. Requires --enable-json-socket to serve. To persist, use: netbird service install --enable-json-socket --json-socket")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||||
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
@@ -22,41 +19,56 @@ import (
|
|||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func validateJSONSocketFlags() error {
|
||||||
|
if serviceCmd.PersistentFlags().Changed("json-socket") && !enableJSONSocket {
|
||||||
|
return fmt.Errorf("--json-socket requires --enable-json-socket to configure the daemon JSON gateway")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *program) Start(svc service.Service) error {
|
func (p *program) Start(svc service.Service) error {
|
||||||
// Start should not block. Do the actual work async.
|
// Start should not block. Do the actual work async.
|
||||||
log.Info("starting NetBird service") //nolint
|
log.Info("starting NetBird service") //nolint
|
||||||
|
|
||||||
|
if err := validateJSONSocketFlags(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Collect static system and platform information
|
// Collect static system and platform information
|
||||||
system.UpdateStaticInfoAsync()
|
system.UpdateStaticInfoAsync()
|
||||||
|
|
||||||
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
||||||
p.serv = grpc.NewServer()
|
p.serv = grpc.NewServer()
|
||||||
|
|
||||||
split := strings.Split(daemonAddr, "://")
|
daemonListener, err := listenOnAddress(daemonAddr)
|
||||||
switch split[0] {
|
|
||||||
case "unix":
|
|
||||||
// cleanup failed close
|
|
||||||
stat, err := os.Stat(split[1])
|
|
||||||
if err == nil && !stat.IsDir() {
|
|
||||||
if err := os.Remove(split[1]); err != nil {
|
|
||||||
log.Debugf("remove socket file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "tcp":
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported daemon address protocol: %v", split[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
listen, err := net.Listen(split[0], split[1])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listen daemon interface: %w", err)
|
return fmt.Errorf("listen daemon interface: %w", err)
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
defer listen.Close()
|
|
||||||
|
|
||||||
if split[0] == "unix" {
|
var jsonListener *socketListener
|
||||||
if err := os.Chmod(split[1], 0666); err != nil {
|
if enableJSONSocket {
|
||||||
log.Errorf("failed setting daemon permissions: %v", split[1])
|
jsonListener, err = listenOnAddress(jsonSocket)
|
||||||
|
if err != nil {
|
||||||
|
_ = daemonListener.Close()
|
||||||
|
return fmt.Errorf("listen daemon JSON interface: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
removeStaleUnixSocketForAddress(jsonSocket)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer daemonListener.Close()
|
||||||
|
if jsonListener != nil {
|
||||||
|
defer jsonListener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := daemonListener.chmodUnixSocket("daemon"); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if jsonListener != nil {
|
||||||
|
if err := jsonListener.chmodUnixSocket("daemon JSON"); err != nil {
|
||||||
|
log.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,8 +83,16 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
p.serverInstance = serverInstance
|
p.serverInstance = serverInstance
|
||||||
p.serverInstanceMu.Unlock()
|
p.serverInstanceMu.Unlock()
|
||||||
|
|
||||||
log.Printf("started daemon server: %v", split[1])
|
if jsonListener != nil {
|
||||||
if err := p.serv.Serve(listen); err != nil {
|
if err := p.startJSONGateway(jsonListener, daemonAddr); err != nil {
|
||||||
|
log.Fatalf("failed to start daemon JSON server: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debug("daemon JSON socket disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("started daemon server: %v", daemonListener.address)
|
||||||
|
if err := p.serv.Serve(daemonListener.Listener); err != nil {
|
||||||
log.Errorf("failed to serve daemon requests: %v", err)
|
log.Errorf("failed to serve daemon requests: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -92,6 +112,20 @@ func (p *program) Stop(srv service.Service) error {
|
|||||||
|
|
||||||
p.cancel()
|
p.cancel()
|
||||||
|
|
||||||
|
p.jsonServMu.Lock()
|
||||||
|
jsonServ := p.jsonServ
|
||||||
|
p.jsonServMu.Unlock()
|
||||||
|
if jsonServ != nil {
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
if err := jsonServ.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Errorf("failed to stop daemon JSON server gracefully: %v", err)
|
||||||
|
if err := jsonServ.Close(); err != nil {
|
||||||
|
log.Errorf("failed to close daemon JSON server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shutdownCancel()
|
||||||
|
}
|
||||||
|
|
||||||
if p.serv != nil {
|
if p.serv != nil {
|
||||||
p.serv.Stop()
|
p.serv.Stop()
|
||||||
}
|
}
|
||||||
@@ -148,6 +182,9 @@ var runCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateJSONSocketFlags(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return s.Run()
|
return s.Run()
|
||||||
},
|
},
|
||||||
@@ -162,6 +199,9 @@ var startCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateJSONSocketFlags(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Start(); err != nil {
|
if err := s.Start(); err != nil {
|
||||||
return fmt.Errorf("start service: %w", err)
|
return fmt.Errorf("start service: %w", err)
|
||||||
@@ -198,6 +238,9 @@ var restartCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateJSONSocketFlags(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Restart(); err != nil {
|
if err := s.Restart(); err != nil {
|
||||||
return fmt.Errorf("restart service: %w", err)
|
return fmt.Errorf("restart service: %w", err)
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ func buildServiceArguments() []string {
|
|||||||
args = append(args, "--disable-networks")
|
args = append(args, "--disable-networks")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if enableJSONSocket {
|
||||||
|
args = append(args, "--enable-json-socket", "--json-socket", jsonSocket)
|
||||||
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +110,10 @@ func configurePlatformSpecificSettings(svcConfig *service.Config) error {
|
|||||||
|
|
||||||
// Create fully configured service config for install/reconfigure
|
// Create fully configured service config for install/reconfigure
|
||||||
func createServiceConfigForInstall() (*service.Config, error) {
|
func createServiceConfigForInstall() (*service.Config, error) {
|
||||||
|
if err := validateJSONSocketFlags(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
svcConfig, err := newSVCConfig()
|
svcConfig, err := newSVCConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create service config: %w", err)
|
return nil, fmt.Errorf("create service config: %w", err)
|
||||||
|
|||||||
52
client/cmd/service_json_gateway.go
Normal file
52
client/cmd/service_json_gateway.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//go:build !ios && !android
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func grpcGatewayEndpoint(addr string) string {
|
||||||
|
return strings.TrimPrefix(addr, "tcp://")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *program) startJSONGateway(jsonListener *socketListener, daemonEndpoint string) error {
|
||||||
|
mux := runtime.NewServeMux()
|
||||||
|
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||||
|
if err := proto.RegisterDaemonServiceHandlerFromEndpoint(p.ctx, mux, grpcGatewayEndpoint(daemonEndpoint), opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonServer := &http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
BaseContext: func(net.Listener) context.Context {
|
||||||
|
return p.ctx
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p.jsonServMu.Lock()
|
||||||
|
p.jsonServ = jsonServer
|
||||||
|
p.jsonServMu.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("started daemon JSON server: %v", jsonListener.address)
|
||||||
|
if err := jsonServer.Serve(jsonListener.Listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Errorf("failed to serve daemon JSON requests: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
176
client/cmd/service_json_socket_test.go
Normal file
176
client/cmd/service_json_socket_test.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
//go:build !ios && !android
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func preserveJSONSocketTestState(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
origJSONSocket := jsonSocket
|
||||||
|
origEnableJSONSocket := enableJSONSocket
|
||||||
|
origChanged := map[string]bool{}
|
||||||
|
serviceCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
|
||||||
|
origChanged[flag.Name] = flag.Changed
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
jsonSocket = origJSONSocket
|
||||||
|
enableJSONSocket = origEnableJSONSocket
|
||||||
|
serviceCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
|
||||||
|
flag.Changed = origChanged[flag.Name]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSocketFlagsArePositiveEnableOnly(t *testing.T) {
|
||||||
|
assert.NotNil(t, serviceCmd.PersistentFlags().Lookup("enable-json-socket"))
|
||||||
|
assert.NotNil(t, serviceCmd.PersistentFlags().Lookup("json-socket"))
|
||||||
|
assert.Nil(t, serviceCmd.PersistentFlags().Lookup("disable-json-socket"))
|
||||||
|
assert.Equal(t, "false", serviceCmd.PersistentFlags().Lookup("enable-json-socket").DefValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildServiceArgumentsDefaultDisablesJSONSocket(t *testing.T) {
|
||||||
|
preserveJSONSocketTestState(t)
|
||||||
|
|
||||||
|
enableJSONSocket = false
|
||||||
|
jsonSocket = "tcp://127.0.0.1:8080"
|
||||||
|
|
||||||
|
args := buildServiceArguments()
|
||||||
|
|
||||||
|
assert.NotContains(t, args, "--enable-json-socket")
|
||||||
|
assert.NotContains(t, args, "--json-socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildServiceArgumentsIncludesJSONSocketWhenEnabled(t *testing.T) {
|
||||||
|
preserveJSONSocketTestState(t)
|
||||||
|
|
||||||
|
enableJSONSocket = true
|
||||||
|
jsonSocket = "tcp://127.0.0.1:8080"
|
||||||
|
|
||||||
|
args := buildServiceArguments()
|
||||||
|
|
||||||
|
enableIndex := indexOfArg(args, "--enable-json-socket")
|
||||||
|
jsonIndex := indexOfArg(args, "--json-socket")
|
||||||
|
require.NotEqual(t, -1, enableIndex)
|
||||||
|
require.NotEqual(t, -1, jsonIndex)
|
||||||
|
require.Less(t, enableIndex, jsonIndex)
|
||||||
|
require.Less(t, jsonIndex+1, len(args))
|
||||||
|
assert.Equal(t, "tcp://127.0.0.1:8080", args[jsonIndex+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSocketWithoutEnableValidation(t *testing.T) {
|
||||||
|
preserveJSONSocketTestState(t)
|
||||||
|
|
||||||
|
enableJSONSocket = false
|
||||||
|
require.NoError(t, serviceCmd.PersistentFlags().Set("json-socket", "tcp://127.0.0.1:8080"))
|
||||||
|
|
||||||
|
err := validateJSONSocketFlags()
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "--enable-json-socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSocketWithEnableValidation(t *testing.T) {
|
||||||
|
preserveJSONSocketTestState(t)
|
||||||
|
|
||||||
|
require.NoError(t, serviceCmd.PersistentFlags().Set("enable-json-socket", "true"))
|
||||||
|
require.NoError(t, serviceCmd.PersistentFlags().Set("json-socket", "tcp://127.0.0.1:8080"))
|
||||||
|
|
||||||
|
assert.NoError(t, validateJSONSocketFlags())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONSocketServiceParamsPersistEnableAndAddress(t *testing.T) {
|
||||||
|
preserveJSONSocketTestState(t)
|
||||||
|
serviceCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
|
||||||
|
flag.Changed = false
|
||||||
|
})
|
||||||
|
|
||||||
|
enableJSONSocket = true
|
||||||
|
jsonSocket = "tcp://127.0.0.1:8080"
|
||||||
|
|
||||||
|
params := currentServiceParams()
|
||||||
|
require.True(t, params.EnableJSONSocket)
|
||||||
|
require.Equal(t, "tcp://127.0.0.1:8080", params.JSONSocket)
|
||||||
|
|
||||||
|
enableJSONSocket = false
|
||||||
|
jsonSocket = defaultJSONSocket
|
||||||
|
applyServiceParams(testServiceEnvCommand(), params)
|
||||||
|
|
||||||
|
assert.True(t, enableJSONSocket)
|
||||||
|
assert.Equal(t, "tcp://127.0.0.1:8080", jsonSocket)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveStaleUnixSocketDoesNotRemoveRegularFile(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "netbird-http.sock")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("not a socket"), 0600))
|
||||||
|
|
||||||
|
removeStaleUnixSocket(path)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("not a socket"), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveStaleUnixSocketRemovesSocket(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("unix sockets are not available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "netbird-http.sock")
|
||||||
|
addr := &net.UnixAddr{Name: path, Net: "unix"}
|
||||||
|
listener, err := net.ListenUnix("unix", addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
listener.SetUnlinkOnClose(false)
|
||||||
|
require.NoError(t, listener.Close())
|
||||||
|
|
||||||
|
_, err = os.Lstat(path)
|
||||||
|
require.NoError(t, err, "test setup must leave a stale Unix socket path")
|
||||||
|
|
||||||
|
removeStaleUnixSocket(path)
|
||||||
|
|
||||||
|
_, err = os.Lstat(path)
|
||||||
|
assert.True(t, os.IsNotExist(err), "expected stale Unix socket to be removed, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveStaleUnixSocketDoesNotRemoveLiveSocket(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("unix sockets are not available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "netbird-http.sock")
|
||||||
|
listener, err := net.Listen("unix", path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
removeStaleUnixSocket(path)
|
||||||
|
|
||||||
|
_, err = os.Lstat(path)
|
||||||
|
assert.NoError(t, err, "expected live Unix socket to be preserved")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testServiceEnvCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.Flags().StringSlice("service-env", nil, "")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOfArg(args []string, arg string) int {
|
||||||
|
for i, candidate := range args {
|
||||||
|
if candidate == arg {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ const serviceParamsFile = "service.json"
|
|||||||
type serviceParams struct {
|
type serviceParams struct {
|
||||||
LogLevel string `json:"log_level"`
|
LogLevel string `json:"log_level"`
|
||||||
DaemonAddr string `json:"daemon_addr"`
|
DaemonAddr string `json:"daemon_addr"`
|
||||||
|
JSONSocket string `json:"json_socket"`
|
||||||
ManagementURL string `json:"management_url,omitempty"`
|
ManagementURL string `json:"management_url,omitempty"`
|
||||||
ConfigPath string `json:"config_path,omitempty"`
|
ConfigPath string `json:"config_path,omitempty"`
|
||||||
LogFiles []string `json:"log_files,omitempty"`
|
LogFiles []string `json:"log_files,omitempty"`
|
||||||
@@ -30,6 +31,7 @@ type serviceParams struct {
|
|||||||
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
|
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
|
||||||
EnableCapture bool `json:"enable_capture,omitempty"`
|
EnableCapture bool `json:"enable_capture,omitempty"`
|
||||||
DisableNetworks bool `json:"disable_networks,omitempty"`
|
DisableNetworks bool `json:"disable_networks,omitempty"`
|
||||||
|
EnableJSONSocket bool `json:"enable_json_socket,omitempty"`
|
||||||
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ func currentServiceParams() *serviceParams {
|
|||||||
params := &serviceParams{
|
params := &serviceParams{
|
||||||
LogLevel: logLevel,
|
LogLevel: logLevel,
|
||||||
DaemonAddr: daemonAddr,
|
DaemonAddr: daemonAddr,
|
||||||
|
JSONSocket: jsonSocket,
|
||||||
ManagementURL: managementURL,
|
ManagementURL: managementURL,
|
||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
LogFiles: logFiles,
|
LogFiles: logFiles,
|
||||||
@@ -82,6 +85,7 @@ func currentServiceParams() *serviceParams {
|
|||||||
DisableUpdateSettings: updateSettingsDisabled,
|
DisableUpdateSettings: updateSettingsDisabled,
|
||||||
EnableCapture: captureEnabled,
|
EnableCapture: captureEnabled,
|
||||||
DisableNetworks: networksDisabled,
|
DisableNetworks: networksDisabled,
|
||||||
|
EnableJSONSocket: enableJSONSocket,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(serviceEnvVars) > 0 {
|
if len(serviceEnvVars) > 0 {
|
||||||
@@ -113,9 +117,8 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For fields with non-empty defaults (log-level, daemon-addr), keep the
|
// For fields with non-empty defaults, keep the != "" guard so that an older
|
||||||
// != "" guard so that an older service.json missing the field doesn't
|
// service.json missing the field doesn't clobber the default with an empty string.
|
||||||
// clobber the default with an empty string.
|
|
||||||
if !rootCmd.PersistentFlags().Changed("log-level") && params.LogLevel != "" {
|
if !rootCmd.PersistentFlags().Changed("log-level") && params.LogLevel != "" {
|
||||||
logLevel = params.LogLevel
|
logLevel = params.LogLevel
|
||||||
}
|
}
|
||||||
@@ -124,6 +127,14 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
|
|||||||
daemonAddr = params.DaemonAddr
|
daemonAddr = params.DaemonAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !serviceCmd.PersistentFlags().Changed("json-socket") && params.JSONSocket != "" {
|
||||||
|
jsonSocket = params.JSONSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
if !serviceCmd.PersistentFlags().Changed("enable-json-socket") {
|
||||||
|
enableJSONSocket = params.EnableJSONSocket
|
||||||
|
}
|
||||||
|
|
||||||
// For optional fields where empty means "use default", always apply so
|
// For optional fields where empty means "use default", always apply so
|
||||||
// that an explicit clear (--management-url "") persists across reinstalls.
|
// that an explicit clear (--management-url "") persists across reinstalls.
|
||||||
if !rootCmd.PersistentFlags().Changed("management-url") {
|
if !rootCmd.PersistentFlags().Changed("management-url") {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ func TestSaveAndLoadServiceParams(t *testing.T) {
|
|||||||
params := &serviceParams{
|
params := &serviceParams{
|
||||||
LogLevel: "debug",
|
LogLevel: "debug",
|
||||||
DaemonAddr: "unix:///var/run/netbird.sock",
|
DaemonAddr: "unix:///var/run/netbird.sock",
|
||||||
|
JSONSocket: "tcp://127.0.0.1:8080",
|
||||||
|
EnableJSONSocket: true,
|
||||||
ManagementURL: "https://my.server.com",
|
ManagementURL: "https://my.server.com",
|
||||||
ConfigPath: "/etc/netbird/config.json",
|
ConfigPath: "/etc/netbird/config.json",
|
||||||
LogFiles: []string{"/var/log/netbird/client.log", "console"},
|
LogFiles: []string{"/var/log/netbird/client.log", "console"},
|
||||||
@@ -63,6 +65,8 @@ func TestSaveAndLoadServiceParams(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, params.LogLevel, loaded.LogLevel)
|
assert.Equal(t, params.LogLevel, loaded.LogLevel)
|
||||||
assert.Equal(t, params.DaemonAddr, loaded.DaemonAddr)
|
assert.Equal(t, params.DaemonAddr, loaded.DaemonAddr)
|
||||||
|
assert.Equal(t, params.JSONSocket, loaded.JSONSocket)
|
||||||
|
assert.Equal(t, params.EnableJSONSocket, loaded.EnableJSONSocket)
|
||||||
assert.Equal(t, params.ManagementURL, loaded.ManagementURL)
|
assert.Equal(t, params.ManagementURL, loaded.ManagementURL)
|
||||||
assert.Equal(t, params.ConfigPath, loaded.ConfigPath)
|
assert.Equal(t, params.ConfigPath, loaded.ConfigPath)
|
||||||
assert.Equal(t, params.LogFiles, loaded.LogFiles)
|
assert.Equal(t, params.LogFiles, loaded.LogFiles)
|
||||||
@@ -101,6 +105,8 @@ func TestLoadServiceParams_InvalidJSON(t *testing.T) {
|
|||||||
func TestCurrentServiceParams(t *testing.T) {
|
func TestCurrentServiceParams(t *testing.T) {
|
||||||
origLogLevel := logLevel
|
origLogLevel := logLevel
|
||||||
origDaemonAddr := daemonAddr
|
origDaemonAddr := daemonAddr
|
||||||
|
origJSONSocket := jsonSocket
|
||||||
|
origEnableJSONSocket := enableJSONSocket
|
||||||
origManagementURL := managementURL
|
origManagementURL := managementURL
|
||||||
origConfigPath := configPath
|
origConfigPath := configPath
|
||||||
origLogFiles := logFiles
|
origLogFiles := logFiles
|
||||||
@@ -110,6 +116,8 @@ func TestCurrentServiceParams(t *testing.T) {
|
|||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
logLevel = origLogLevel
|
logLevel = origLogLevel
|
||||||
daemonAddr = origDaemonAddr
|
daemonAddr = origDaemonAddr
|
||||||
|
jsonSocket = origJSONSocket
|
||||||
|
enableJSONSocket = origEnableJSONSocket
|
||||||
managementURL = origManagementURL
|
managementURL = origManagementURL
|
||||||
configPath = origConfigPath
|
configPath = origConfigPath
|
||||||
logFiles = origLogFiles
|
logFiles = origLogFiles
|
||||||
@@ -120,6 +128,8 @@ func TestCurrentServiceParams(t *testing.T) {
|
|||||||
|
|
||||||
logLevel = "trace"
|
logLevel = "trace"
|
||||||
daemonAddr = "tcp://127.0.0.1:9999"
|
daemonAddr = "tcp://127.0.0.1:9999"
|
||||||
|
jsonSocket = "tcp://127.0.0.1:8080"
|
||||||
|
enableJSONSocket = true
|
||||||
managementURL = "https://mgmt.example.com"
|
managementURL = "https://mgmt.example.com"
|
||||||
configPath = "/tmp/test-config.json"
|
configPath = "/tmp/test-config.json"
|
||||||
logFiles = []string{"/tmp/test.log"}
|
logFiles = []string{"/tmp/test.log"}
|
||||||
@@ -131,6 +141,8 @@ func TestCurrentServiceParams(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "trace", params.LogLevel)
|
assert.Equal(t, "trace", params.LogLevel)
|
||||||
assert.Equal(t, "tcp://127.0.0.1:9999", params.DaemonAddr)
|
assert.Equal(t, "tcp://127.0.0.1:9999", params.DaemonAddr)
|
||||||
|
assert.Equal(t, "tcp://127.0.0.1:8080", params.JSONSocket)
|
||||||
|
assert.True(t, params.EnableJSONSocket)
|
||||||
assert.Equal(t, "https://mgmt.example.com", params.ManagementURL)
|
assert.Equal(t, "https://mgmt.example.com", params.ManagementURL)
|
||||||
assert.Equal(t, "/tmp/test-config.json", params.ConfigPath)
|
assert.Equal(t, "/tmp/test-config.json", params.ConfigPath)
|
||||||
assert.Equal(t, []string{"/tmp/test.log"}, params.LogFiles)
|
assert.Equal(t, []string{"/tmp/test.log"}, params.LogFiles)
|
||||||
@@ -142,6 +154,8 @@ func TestCurrentServiceParams(t *testing.T) {
|
|||||||
func TestApplyServiceParams_OnlyUnchangedFlags(t *testing.T) {
|
func TestApplyServiceParams_OnlyUnchangedFlags(t *testing.T) {
|
||||||
origLogLevel := logLevel
|
origLogLevel := logLevel
|
||||||
origDaemonAddr := daemonAddr
|
origDaemonAddr := daemonAddr
|
||||||
|
origJSONSocket := jsonSocket
|
||||||
|
origEnableJSONSocket := enableJSONSocket
|
||||||
origManagementURL := managementURL
|
origManagementURL := managementURL
|
||||||
origConfigPath := configPath
|
origConfigPath := configPath
|
||||||
origLogFiles := logFiles
|
origLogFiles := logFiles
|
||||||
@@ -151,6 +165,8 @@ func TestApplyServiceParams_OnlyUnchangedFlags(t *testing.T) {
|
|||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
logLevel = origLogLevel
|
logLevel = origLogLevel
|
||||||
daemonAddr = origDaemonAddr
|
daemonAddr = origDaemonAddr
|
||||||
|
jsonSocket = origJSONSocket
|
||||||
|
enableJSONSocket = origEnableJSONSocket
|
||||||
managementURL = origManagementURL
|
managementURL = origManagementURL
|
||||||
configPath = origConfigPath
|
configPath = origConfigPath
|
||||||
logFiles = origLogFiles
|
logFiles = origLogFiles
|
||||||
@@ -162,6 +178,8 @@ func TestApplyServiceParams_OnlyUnchangedFlags(t *testing.T) {
|
|||||||
// Reset all flags to defaults.
|
// Reset all flags to defaults.
|
||||||
logLevel = "info"
|
logLevel = "info"
|
||||||
daemonAddr = "unix:///var/run/netbird.sock"
|
daemonAddr = "unix:///var/run/netbird.sock"
|
||||||
|
jsonSocket = defaultJSONSocket
|
||||||
|
enableJSONSocket = false
|
||||||
managementURL = ""
|
managementURL = ""
|
||||||
configPath = "/etc/netbird/config.json"
|
configPath = "/etc/netbird/config.json"
|
||||||
logFiles = []string{"/var/log/netbird/client.log"}
|
logFiles = []string{"/var/log/netbird/client.log"}
|
||||||
@@ -184,6 +202,8 @@ func TestApplyServiceParams_OnlyUnchangedFlags(t *testing.T) {
|
|||||||
saved := &serviceParams{
|
saved := &serviceParams{
|
||||||
LogLevel: "debug",
|
LogLevel: "debug",
|
||||||
DaemonAddr: "tcp://127.0.0.1:5555",
|
DaemonAddr: "tcp://127.0.0.1:5555",
|
||||||
|
JSONSocket: "tcp://127.0.0.1:8080",
|
||||||
|
EnableJSONSocket: true,
|
||||||
ManagementURL: "https://saved.example.com",
|
ManagementURL: "https://saved.example.com",
|
||||||
ConfigPath: "/saved/config.json",
|
ConfigPath: "/saved/config.json",
|
||||||
LogFiles: []string{"/saved/client.log"},
|
LogFiles: []string{"/saved/client.log"},
|
||||||
@@ -201,6 +221,8 @@ func TestApplyServiceParams_OnlyUnchangedFlags(t *testing.T) {
|
|||||||
|
|
||||||
// All other fields were not Changed, so they should use saved values.
|
// All other fields were not Changed, so they should use saved values.
|
||||||
assert.Equal(t, "tcp://127.0.0.1:5555", daemonAddr)
|
assert.Equal(t, "tcp://127.0.0.1:5555", daemonAddr)
|
||||||
|
assert.Equal(t, "tcp://127.0.0.1:8080", jsonSocket)
|
||||||
|
assert.True(t, enableJSONSocket)
|
||||||
assert.Equal(t, "https://saved.example.com", managementURL)
|
assert.Equal(t, "https://saved.example.com", managementURL)
|
||||||
assert.Equal(t, "/saved/config.json", configPath)
|
assert.Equal(t, "/saved/config.json", configPath)
|
||||||
assert.Equal(t, []string{"/saved/client.log"}, logFiles)
|
assert.Equal(t, []string{"/saved/client.log"}, logFiles)
|
||||||
@@ -212,14 +234,17 @@ func TestApplyServiceParams_OnlyUnchangedFlags(t *testing.T) {
|
|||||||
func TestApplyServiceParams_BooleanRevertToFalse(t *testing.T) {
|
func TestApplyServiceParams_BooleanRevertToFalse(t *testing.T) {
|
||||||
origProfilesDisabled := profilesDisabled
|
origProfilesDisabled := profilesDisabled
|
||||||
origUpdateSettingsDisabled := updateSettingsDisabled
|
origUpdateSettingsDisabled := updateSettingsDisabled
|
||||||
|
origEnableJSONSocket := enableJSONSocket
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
profilesDisabled = origProfilesDisabled
|
profilesDisabled = origProfilesDisabled
|
||||||
updateSettingsDisabled = origUpdateSettingsDisabled
|
updateSettingsDisabled = origUpdateSettingsDisabled
|
||||||
|
enableJSONSocket = origEnableJSONSocket
|
||||||
})
|
})
|
||||||
|
|
||||||
// Simulate current state where booleans are true (e.g. set by previous install).
|
// Simulate current state where booleans are true (e.g. set by previous install).
|
||||||
profilesDisabled = true
|
profilesDisabled = true
|
||||||
updateSettingsDisabled = true
|
updateSettingsDisabled = true
|
||||||
|
enableJSONSocket = true
|
||||||
|
|
||||||
// Reset Changed state so flags appear unset.
|
// Reset Changed state so flags appear unset.
|
||||||
serviceCmd.PersistentFlags().VisitAll(func(f *pflag.Flag) {
|
serviceCmd.PersistentFlags().VisitAll(func(f *pflag.Flag) {
|
||||||
@@ -238,6 +263,7 @@ func TestApplyServiceParams_BooleanRevertToFalse(t *testing.T) {
|
|||||||
|
|
||||||
assert.False(t, profilesDisabled, "saved false should override current true")
|
assert.False(t, profilesDisabled, "saved false should override current true")
|
||||||
assert.False(t, updateSettingsDisabled, "saved false should override current true")
|
assert.False(t, updateSettingsDisabled, "saved false should override current true")
|
||||||
|
assert.False(t, enableJSONSocket, "saved false should override current true")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyServiceParams_ClearManagementURL(t *testing.T) {
|
func TestApplyServiceParams_ClearManagementURL(t *testing.T) {
|
||||||
@@ -530,6 +556,7 @@ func fieldToGlobalVar(field string) string {
|
|||||||
m := map[string]string{
|
m := map[string]string{
|
||||||
"LogLevel": "logLevel",
|
"LogLevel": "logLevel",
|
||||||
"DaemonAddr": "daemonAddr",
|
"DaemonAddr": "daemonAddr",
|
||||||
|
"JSONSocket": "jsonSocket",
|
||||||
"ManagementURL": "managementURL",
|
"ManagementURL": "managementURL",
|
||||||
"ConfigPath": "configPath",
|
"ConfigPath": "configPath",
|
||||||
"LogFiles": "logFiles",
|
"LogFiles": "logFiles",
|
||||||
@@ -537,6 +564,7 @@ func fieldToGlobalVar(field string) string {
|
|||||||
"DisableUpdateSettings": "updateSettingsDisabled",
|
"DisableUpdateSettings": "updateSettingsDisabled",
|
||||||
"EnableCapture": "captureEnabled",
|
"EnableCapture": "captureEnabled",
|
||||||
"DisableNetworks": "networksDisabled",
|
"DisableNetworks": "networksDisabled",
|
||||||
|
"EnableJSONSocket": "enableJSONSocket",
|
||||||
"ServiceEnvVars": "serviceEnvVars",
|
"ServiceEnvVars": "serviceEnvVars",
|
||||||
}
|
}
|
||||||
if v, ok := m[field]; ok {
|
if v, ok := m[field]; ok {
|
||||||
|
|||||||
111
client/cmd/service_socket.go
Normal file
111
client/cmd/service_socket.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
//go:build !ios && !android
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type socketListener struct {
|
||||||
|
net.Listener
|
||||||
|
network string
|
||||||
|
address string
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenOnAddress(addr string) (*socketListener, error) {
|
||||||
|
network, address, err := parseListenAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if network == "unix" {
|
||||||
|
removeStaleUnixSocket(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &socketListener{Listener: listener, network: network, address: address}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseListenAddress(addr string) (string, string, error) {
|
||||||
|
network, address, ok := strings.Cut(addr, "://")
|
||||||
|
if !ok || network == "" || address == "" {
|
||||||
|
return "", "", fmt.Errorf("address must be in [unix|tcp]://[path|host:port] format: %q", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch network {
|
||||||
|
case "unix", "tcp":
|
||||||
|
return network, address, nil
|
||||||
|
default:
|
||||||
|
return "", "", fmt.Errorf("unsupported daemon address protocol: %v", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeStaleUnixSocket(path string) {
|
||||||
|
stat, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
log.Debugf("stat socket file: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Mode()&os.ModeSocket == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isStaleUnixSocket(path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
log.Debugf("remove socket file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStaleUnixSocket(path string) bool {
|
||||||
|
conn, err := net.DialTimeout("unix", path, 100*time.Millisecond)
|
||||||
|
if err == nil {
|
||||||
|
if closeErr := conn.Close(); closeErr != nil {
|
||||||
|
log.Debugf("close unix socket probe: %v", closeErr)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.IsNotExist(err) || os.IsPermission(err) || os.IsTimeout(err) {
|
||||||
|
log.Debugf("not removing unix socket %s after probe error: %v", path, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Is(err, syscall.ECONNREFUSED)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeStaleUnixSocketForAddress(addr string) {
|
||||||
|
network, address, err := parseListenAddress(addr)
|
||||||
|
if err != nil || network != "unix" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removeStaleUnixSocket(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *socketListener) chmodUnixSocket(description string) error {
|
||||||
|
if l == nil || l.network != "unix" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chmod(l.address, 0666); err != nil {
|
||||||
|
return fmt.Errorf("failed setting %s permissions for %s: %w", description, l.address, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
2560
client/proto/daemon.pb.gw.go
Normal file
2560
client/proto/daemon.pb.gw.go
Normal file
File diff suppressed because it is too large
Load Diff
80
client/proto/daemon_gateway_test.go
Normal file
80
client/proto/daemon_gateway_test.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gatewayruntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/test/bufconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGatewayServerRoutesCoverDaemonRPCs(t *testing.T) {
|
||||||
|
mux := gatewayruntime.NewServeMux()
|
||||||
|
if err := RegisterDaemonServiceHandlerServer(context.Background(), mux, UnimplementedDaemonServiceServer{}); err != nil {
|
||||||
|
t.Fatalf("register daemon gateway server handlers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertAllDaemonGatewayRoutesRegistered(t, mux)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatewayClientRoutesCoverDaemonRPCs(t *testing.T) {
|
||||||
|
listener := bufconn.Listen(1024 * 1024)
|
||||||
|
server := grpc.NewServer()
|
||||||
|
RegisterDaemonServiceServer(server, UnimplementedDaemonServiceServer{})
|
||||||
|
go func() {
|
||||||
|
if err := server.Serve(listener); err != nil && err != grpc.ErrServerStopped {
|
||||||
|
t.Errorf("serve bufconn gRPC server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
server.Stop()
|
||||||
|
_ = listener.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mux := gatewayruntime.NewServeMux()
|
||||||
|
opts := []grpc.DialOption{
|
||||||
|
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||||
|
return listener.Dial()
|
||||||
|
}),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
}
|
||||||
|
if err := RegisterDaemonServiceHandlerFromEndpoint(ctx, mux, "passthrough:///bufnet", opts); err != nil {
|
||||||
|
t.Fatalf("register daemon gateway client handlers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertAllDaemonGatewayRoutesRegistered(t, mux)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAllDaemonGatewayRoutesRegistered(t *testing.T, mux http.Handler) {
|
||||||
|
t.Helper()
|
||||||
|
for _, method := range DaemonService_ServiceDesc.Methods {
|
||||||
|
assertGatewayRouteRegistered(t, mux, method.MethodName)
|
||||||
|
}
|
||||||
|
for _, stream := range DaemonService_ServiceDesc.Streams {
|
||||||
|
assertGatewayRouteRegistered(t, mux, stream.StreamName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertGatewayRouteRegistered(t *testing.T, mux http.Handler, methodName string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
path := "/daemon.DaemonService/" + methodName
|
||||||
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader("{}"))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mux.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
if res.Code == http.StatusNotFound {
|
||||||
|
t.Fatalf("gateway route for %s is not registered", methodName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,5 +12,11 @@ script_path=$(dirname "$(realpath "$0")")
|
|||||||
cd "$script_path"
|
cd "$script_path"
|
||||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.6
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.6
|
||||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.1
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.1
|
||||||
protoc -I ./ ./daemon.proto --go_out=../ --go-grpc_out=../ --experimental_allow_proto3_optional
|
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.26.3
|
||||||
|
protoc -I ./ ./daemon.proto \
|
||||||
|
--go_out=../ \
|
||||||
|
--go-grpc_out=../ \
|
||||||
|
--grpc-gateway_out=../ \
|
||||||
|
--grpc-gateway_opt=generate_unbound_methods=true \
|
||||||
|
--experimental_allow_proto3_optional
|
||||||
cd "$old_pwd"
|
cd "$old_pwd"
|
||||||
|
|||||||
223
client/test/json-socket-docker.sh
Executable file
223
client/test/json-socket-docker.sh
Executable file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eEuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: client/test/json-socket-docker.sh [tcp|unix|both]
|
||||||
|
|
||||||
|
Builds the NetBird client Docker image from the local source tree, starts
|
||||||
|
`netbird service run` in a container with --enable-json-socket, and verifies
|
||||||
|
that the HTTP/JSON daemon gateway responds to Status requests.
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
tcp Validate tcp://0.0.0.0:8080 via a published localhost port (default)
|
||||||
|
unix Validate unix:///sock/netbird-http.sock via a bind-mounted socket dir
|
||||||
|
both Run both validations
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
CONTAINER_RUNTIME docker or podman. Auto-detected if unset.
|
||||||
|
IMAGE Image tag to build. Default: netbird-json-socket-test:local
|
||||||
|
TARGETARCH Go/Docker target arch. Default: `go env GOARCH`
|
||||||
|
PLATFORM Docker platform. Default: linux/$TARGETARCH
|
||||||
|
WAIT_TIMEOUT Seconds to wait for the JSON socket. Default: 30
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
MODE="${1:-tcp}"
|
||||||
|
case "${MODE}" in
|
||||||
|
tcp|unix|both) ;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
echo "invalid mode: ${MODE}" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
RUNTIME="${CONTAINER_RUNTIME:-}"
|
||||||
|
if [[ -z "${RUNTIME}" ]]; then
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
RUNTIME=docker
|
||||||
|
elif command -v podman >/dev/null 2>&1; then
|
||||||
|
RUNTIME=podman
|
||||||
|
else
|
||||||
|
echo "docker or podman is required" >&2
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! command -v "${RUNTIME}" >/dev/null 2>&1; then
|
||||||
|
echo "container runtime not found: ${RUNTIME}" >&2
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
echo "curl is required" >&2
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
IMAGE="${IMAGE:-netbird-json-socket-test:local}"
|
||||||
|
TARGETARCH="${TARGETARCH:-$(go env GOARCH)}"
|
||||||
|
PLATFORM="${PLATFORM:-linux/${TARGETARCH}}"
|
||||||
|
WAIT_TIMEOUT="${WAIT_TIMEOUT:-30}"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
CONTAINERS=()
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local status=$?
|
||||||
|
for container in "${CONTAINERS[@]:-}"; do
|
||||||
|
"${RUNTIME}" rm -f "${container}" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
rm -rf "${TMP_DIR}"
|
||||||
|
exit "${status}"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
build_image() {
|
||||||
|
echo "==> Building Linux ${TARGETARCH} netbird binary"
|
||||||
|
mkdir -p "${TMP_DIR}/context/client"
|
||||||
|
cp "${ROOT_DIR}/client/Dockerfile" "${TMP_DIR}/context/Dockerfile"
|
||||||
|
cp "${ROOT_DIR}/client/netbird-entrypoint.sh" "${TMP_DIR}/context/client/netbird-entrypoint.sh"
|
||||||
|
|
||||||
|
(cd "${ROOT_DIR}" && CGO_ENABLED=0 GOOS=linux GOARCH="${TARGETARCH}" go build -o "${TMP_DIR}/context/netbird" ./client)
|
||||||
|
|
||||||
|
echo "==> Building ${IMAGE} for ${PLATFORM}"
|
||||||
|
"${RUNTIME}" build \
|
||||||
|
--platform "${PLATFORM}" \
|
||||||
|
--build-arg NETBIRD_BINARY=netbird \
|
||||||
|
-t "${IMAGE}" \
|
||||||
|
-f "${TMP_DIR}/context/Dockerfile" \
|
||||||
|
"${TMP_DIR}/context"
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_port() {
|
||||||
|
python3 - <<'PY'
|
||||||
|
import socket
|
||||||
|
sock = socket.socket()
|
||||||
|
sock.bind(("127.0.0.1", 0))
|
||||||
|
print(sock.getsockname()[1])
|
||||||
|
sock.close()
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_status_json() {
|
||||||
|
local response_file="$1"
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 - "${response_file}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
with open(sys.argv[1], encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
if not data.get("status"):
|
||||||
|
raise SystemExit("missing non-empty status field")
|
||||||
|
if "daemonVersion" not in data:
|
||||||
|
raise SystemExit("missing daemonVersion field")
|
||||||
|
print(f"status={data['status']} daemonVersion={data['daemonVersion']}")
|
||||||
|
PY
|
||||||
|
else
|
||||||
|
grep -q '"status"' "${response_file}"
|
||||||
|
grep -q '"daemonVersion"' "${response_file}"
|
||||||
|
cat "${response_file}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
container_logs() {
|
||||||
|
local container="$1"
|
||||||
|
echo "---- ${container} logs ----" >&2
|
||||||
|
"${RUNTIME}" logs "${container}" >&2 || true
|
||||||
|
echo "--------------------------" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_http_status() {
|
||||||
|
local container="$1"
|
||||||
|
local response="${TMP_DIR}/${container}.json"
|
||||||
|
local curl_err="${TMP_DIR}/${container}.curl.err"
|
||||||
|
shift
|
||||||
|
local deadline=$((SECONDS + WAIT_TIMEOUT))
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if curl -fsS "$@" \
|
||||||
|
-X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}' \
|
||||||
|
-o "${response}" \
|
||||||
|
2>"${curl_err}"; then
|
||||||
|
assert_status_json "${response}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "${RUNTIME}" ps --format '{{.Names}}' | grep -Fxq "${container}"; then
|
||||||
|
echo "container exited before JSON socket became ready" >&2
|
||||||
|
container_logs "${container}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "timed out waiting for JSON socket after ${WAIT_TIMEOUT}s" >&2
|
||||||
|
cat "${curl_err}" >&2 || true
|
||||||
|
container_logs "${container}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_netbird_container() {
|
||||||
|
local container="$1"
|
||||||
|
local json_socket="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
CONTAINERS+=("${container}")
|
||||||
|
"${RUNTIME}" run --rm -d \
|
||||||
|
--name "${container}" \
|
||||||
|
-e NB_STATE_DIR=/tmp/netbird-state \
|
||||||
|
--entrypoint /usr/local/bin/netbird \
|
||||||
|
"$@" \
|
||||||
|
"${IMAGE}" \
|
||||||
|
--log-file console \
|
||||||
|
--daemon-addr unix:///tmp/netbird.sock \
|
||||||
|
service run \
|
||||||
|
--enable-json-socket \
|
||||||
|
--json-socket "${json_socket}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
run_tcp_test() {
|
||||||
|
local port container
|
||||||
|
port="$(pick_port)"
|
||||||
|
container="nb-json-socket-tcp-$RANDOM-$RANDOM"
|
||||||
|
|
||||||
|
echo "==> Validating TCP JSON socket on 127.0.0.1:${port}"
|
||||||
|
run_netbird_container "${container}" "tcp://0.0.0.0:8080" -p "127.0.0.1:${port}:8080"
|
||||||
|
wait_for_http_status "${container}" "http://127.0.0.1:${port}/daemon.DaemonService/Status"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_unix_test() {
|
||||||
|
local sock_dir sock_path container
|
||||||
|
sock_dir="${TMP_DIR}/sock"
|
||||||
|
sock_path="${sock_dir}/netbird-http.sock"
|
||||||
|
container="nb-json-socket-unix-$RANDOM-$RANDOM"
|
||||||
|
mkdir -p "${sock_dir}"
|
||||||
|
|
||||||
|
echo "==> Validating Unix JSON socket at ${sock_path}"
|
||||||
|
run_netbird_container "${container}" "unix:///sock/netbird-http.sock" -v "${sock_dir}:/sock"
|
||||||
|
wait_for_http_status "${container}" --unix-socket "${sock_path}" "http://unix/daemon.DaemonService/Status"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_image
|
||||||
|
|
||||||
|
case "${MODE}" in
|
||||||
|
tcp)
|
||||||
|
run_tcp_test
|
||||||
|
;;
|
||||||
|
unix)
|
||||||
|
run_unix_test
|
||||||
|
;;
|
||||||
|
both)
|
||||||
|
run_tcp_test
|
||||||
|
run_unix_test
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "==> Docker JSON socket validation passed (${MODE})"
|
||||||
2
go.mod
2
go.mod
@@ -66,6 +66,7 @@ require (
|
|||||||
github.com/google/nftables v0.3.0
|
github.com/google/nftables v0.3.0
|
||||||
github.com/gopacket/gopacket v1.4.0
|
github.com/gopacket/gopacket v1.4.0
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
|
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
|
||||||
github.com/hashicorp/go-version v1.7.0
|
github.com/hashicorp/go-version v1.7.0
|
||||||
@@ -330,6 +331,7 @@ require (
|
|||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
golang.org/x/tools v0.43.0 // indirect
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
|
|||||||
Reference in New Issue
Block a user