mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-30 02:29:08 +02:00
Merge branch 'main' into loopback-wg-proxy
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package daemonaddr
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvMaxRecvMsgSize overrides the default gRPC max receive message size for
|
||||
// connections to the daemon. Value is in bytes.
|
||||
EnvMaxRecvMsgSize = "NB_DAEMON_GRPC_MAX_MSG_SIZE"
|
||||
|
||||
// defaultMaxRecvMsgSize is the max gRPC receive message size used for daemon
|
||||
// connections when EnvMaxRecvMsgSize is unset or invalid. It overrides the
|
||||
// gRPC library default of 4 MB, which a detailed status already exceeds on a
|
||||
// network of a few thousand peers.
|
||||
defaultMaxRecvMsgSize = 1024 * 1024 * 16
|
||||
)
|
||||
|
||||
// MaxRecvMsgSize returns the max gRPC receive message size for daemon connections
|
||||
// from the environment, or defaultMaxRecvMsgSize (16 MB) if unset or invalid.
|
||||
func MaxRecvMsgSize() int {
|
||||
val := os.Getenv(EnvMaxRecvMsgSize)
|
||||
if val == "" {
|
||||
return defaultMaxRecvMsgSize
|
||||
}
|
||||
|
||||
size, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
log.Warnf("invalid %s value %q, using default: %v", EnvMaxRecvMsgSize, val, err)
|
||||
return defaultMaxRecvMsgSize
|
||||
}
|
||||
|
||||
if size <= 0 {
|
||||
log.Warnf("invalid %s value %d, must be positive, using default", EnvMaxRecvMsgSize, size)
|
||||
return defaultMaxRecvMsgSize
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package daemonaddr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
func TestMaxRecvMsgSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envValue string
|
||||
expected int
|
||||
}{
|
||||
{name: "unset returns default", envValue: "", expected: defaultMaxRecvMsgSize},
|
||||
{name: "non-numeric returns default", envValue: "abc", expected: defaultMaxRecvMsgSize},
|
||||
{name: "negative returns default", envValue: "-1", expected: defaultMaxRecvMsgSize},
|
||||
{name: "zero returns default", envValue: "0", expected: defaultMaxRecvMsgSize},
|
||||
{name: "valid value is used", envValue: "33554432", expected: 33554432},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Set first so the previous value is restored on cleanup, then unset to
|
||||
// exercise the absent case.
|
||||
t.Setenv(EnvMaxRecvMsgSize, tc.envValue)
|
||||
if tc.envValue == "" {
|
||||
require.NoError(t, os.Unsetenv(EnvMaxRecvMsgSize), "unset the override")
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expected, MaxRecvMsgSize(), "max receive message size")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// bigStatusServer answers Status with a response larger than gRPC's 4 MB default
|
||||
// receive limit, which is what a detailed status on a large network looks like.
|
||||
type bigStatusServer struct {
|
||||
proto.UnimplementedDaemonServiceServer
|
||||
payload string
|
||||
}
|
||||
|
||||
func (s *bigStatusServer) Status(context.Context, *proto.StatusRequest) (*proto.StatusResponse, error) {
|
||||
return &proto.StatusResponse{Status: s.payload}, nil
|
||||
}
|
||||
|
||||
func startBigStatusServer(t *testing.T, payload string) string {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err, "listen on loopback")
|
||||
|
||||
srv := grpc.NewServer()
|
||||
proto.RegisterDaemonServiceServer(srv, &bigStatusServer{payload: payload})
|
||||
go func() {
|
||||
_ = srv.Serve(listener)
|
||||
}()
|
||||
t.Cleanup(srv.Stop)
|
||||
|
||||
return "tcp://" + listener.Addr().String()
|
||||
}
|
||||
|
||||
func TestDialTargetAcceptsAStatusOverTheGrpcDefault(t *testing.T) {
|
||||
payload := strings.Repeat("x", 5*1024*1024)
|
||||
addr := startBigStatusServer(t, payload)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
target, opts := DialTarget(addr)
|
||||
conn, err := grpc.NewClient(target, opts...)
|
||||
require.NoError(t, err, "dial the daemon")
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
|
||||
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{})
|
||||
require.NoError(t, err, "a detailed status must not be rejected for its size")
|
||||
assert.Len(t, resp.GetStatus(), len(payload), "the whole response must arrive")
|
||||
}
|
||||
|
||||
// TestDialTargetRaisesTheDefaultLimit is the negative control: the same response
|
||||
// over a connection carrying gRPC's own defaults is refused, which is the failure
|
||||
// reported by `netbird status -d` on a large deployment.
|
||||
func TestDialTargetRaisesTheDefaultLimit(t *testing.T) {
|
||||
payload := strings.Repeat("x", 5*1024*1024)
|
||||
addr := startBigStatusServer(t, payload)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.NewClient(
|
||||
strings.TrimPrefix(addr, "tcp://"),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
require.NoError(t, err, "dial with the library defaults")
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
|
||||
_, err = proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{})
|
||||
require.Error(t, err, "the library default must reject this response")
|
||||
assert.Equal(t, codes.ResourceExhausted, status.Code(err), "gRPC rejects an oversized message")
|
||||
}
|
||||
@@ -36,7 +36,10 @@ const (
|
||||
// address. The npipe scheme needs a context dialer because gRPC has no
|
||||
// named-pipe resolver; unix and tcp are handled by gRPC itself.
|
||||
func DialTarget(addr string) (string, []grpc.DialOption) {
|
||||
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(MaxRecvMsgSize())),
|
||||
}
|
||||
|
||||
if name, ok := strings.CutPrefix(addr, pipeScheme); ok {
|
||||
paths := PipePaths(name)
|
||||
|
||||
@@ -6,6 +6,17 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// CheckOnlyOwnerWritable reports an error unless path, and every directory
|
||||
// leading to it, is owned by an account that can already act with the privileges
|
||||
// the caller holds, and is writable by nobody else.
|
||||
//
|
||||
// Exported for callers outside elevation that read a file while privileged and
|
||||
// then act on what it says: the same question this package asks of an
|
||||
// executable, asked of a configuration file.
|
||||
func CheckOnlyOwnerWritable(path string) error {
|
||||
return checkOnlyOwnerWritable(path)
|
||||
}
|
||||
|
||||
// trustedSelf returns the path of this executable, provided it is one we are
|
||||
// willing to have run as root.
|
||||
//
|
||||
|
||||
@@ -1040,7 +1040,11 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||
// back to empty if the FQDN doesn't have the expected shape.
|
||||
dnsName = extractDNSDomainFromFQDN(pc.GetFqdn())
|
||||
}
|
||||
result, err := nbnetworkmap.EnvelopeToNetworkMap(e.ctx, envelope, localKey, dnsName)
|
||||
// With the firewall disabled there is no ACL manager to program, so
|
||||
// RoutesFirewallRules would be built and then dropped. On a peer that
|
||||
// routes many network resources that is the single most expensive
|
||||
// step of the sync.
|
||||
result, err := nbnetworkmap.EnvelopeToNetworkMap(e.ctx, envelope, localKey, dnsName, e.config.DisableFirewall)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode network map envelope: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package profilemanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Regression test: a concurrent Get and Set of the ActiveProfileState will
|
||||
// fail on Windows since the write is a temp file renamed over an open file.
|
||||
// Windows will refuse to replace a file another handle holds open by default.
|
||||
func TestActiveProfileState_ReadsDoNotBreakAConcurrentWrite(t *testing.T) {
|
||||
withTempConfigDir(t, func(configDir string) {
|
||||
withPatchedGlobals(t, configDir, func() {
|
||||
sm := &ServiceManager{}
|
||||
require.NoError(t, sm.CreateDefaultProfile())
|
||||
require.NoError(t, sm.SetActiveProfileStateToDefault())
|
||||
|
||||
const switched = ID("0123456789abcdef0123456789abcdef")
|
||||
const rounds = 50
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, 128)
|
||||
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for r := 0; r < rounds; r++ {
|
||||
state, err := sm.GetActiveProfileState()
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("read: %w", err)
|
||||
return
|
||||
}
|
||||
if state.ID != defaultProfileName && state.ID != switched {
|
||||
errs <- fmt.Errorf("read: active profile is %q, which no writer wrote", state.ID)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for r := 0; r < rounds; r++ {
|
||||
id := switched
|
||||
if r%2 == 0 {
|
||||
id = defaultProfileName
|
||||
}
|
||||
if err := sm.SetActiveProfileState(&ActiveProfileState{ID: id, Username: "testuser"}); err != nil {
|
||||
errs <- fmt.Errorf("switch: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for err := range errs {
|
||||
assert.NoError(t, err, "a switch and a read of the active profile state must not collide")
|
||||
}
|
||||
|
||||
state, err := sm.GetActiveProfileState()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, []ID{defaultProfileName, switched}, state.ID,
|
||||
"the file holds whichever switch landed last, not a mix of the two")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Package wincmd locates the Windows utilities the client shells out to.
|
||||
package wincmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// defaultSystem32Dir is where the system directory is on every supported
|
||||
// install, used only when the API that reports it fails.
|
||||
const defaultSystem32Dir = `C:\Windows\System32`
|
||||
|
||||
// System32 returns the full path of a Windows utility under the system
|
||||
// directory.
|
||||
//
|
||||
// PATH is deliberately not consulted. The daemon runs as LocalSystem with an
|
||||
// environment of its own, so whoever can place an entry in that PATH chooses
|
||||
// which binary runs with those privileges. The system directory is read from
|
||||
// the API rather than from %SystemRoot% for the same reason.
|
||||
func System32(command string) string {
|
||||
sysDir, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to locate the Windows system directory, falling back to %s: %v", defaultSystem32Dir, err)
|
||||
sysDir = defaultSystem32Dir
|
||||
}
|
||||
|
||||
return filepath.Join(sysDir, command+".exe")
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package wincmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSystem32IgnoresPATH(t *testing.T) {
|
||||
// A directory holding something that would win a PATH lookup, in front of
|
||||
// everything else: the daemon runs as LocalSystem, so a PATH entry must not
|
||||
// be able to decide what it executes.
|
||||
planted := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(planted, "netsh.exe"), []byte("not really netsh"), 0o600))
|
||||
t.Setenv("PATH", planted+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
got := System32("netsh")
|
||||
|
||||
assert.True(t, filepath.IsAbs(got), "the path must be absolute, got %q", got)
|
||||
assert.NotContains(t, got, planted, "a PATH entry must not be consulted")
|
||||
assert.True(t, strings.EqualFold(filepath.Base(got), "netsh.exe"), "unexpected file name in %q", got)
|
||||
|
||||
// The system directory is what Windows reports it to be, not %SystemRoot%,
|
||||
// which the same caller could have set alongside PATH.
|
||||
t.Setenv("SystemRoot", planted)
|
||||
assert.Equal(t, got, System32("netsh"), "%SystemRoot% must not move the lookup")
|
||||
}
|
||||
Reference in New Issue
Block a user