mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-16 03:39:07 +02:00
Merge remote-tracking branch 'origin/main' into refactor/permissions-manager
This commit is contained in:
@@ -31,6 +31,7 @@ import (
|
||||
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
networkmapdbfactory "github.com/netbirdio/netbird/management/internals/network_map_db/factory"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
|
||||
@@ -111,7 +112,8 @@ func (s *BaseServer) NetworkMapStore() *networkmapdb.NetworkMapDBStoreImpl {
|
||||
s.Config.StoreConfig.Engine,
|
||||
s.Config.Datadir,
|
||||
s.IntegratedValidator(),
|
||||
s.SettingsManager())
|
||||
s.SettingsManager(),
|
||||
)
|
||||
// networkmap db store supports postgres and sqlite backends only
|
||||
// for other backends a fallback is used, so NotSupportedStoreEngineError
|
||||
// is not a fatal error
|
||||
@@ -180,24 +182,7 @@ func (s *BaseServer) RateLimiter() *middleware.APIRateLimiter {
|
||||
|
||||
func (s *BaseServer) GRPCServer() *grpc.Server {
|
||||
return Create(s, func() *grpc.Server {
|
||||
trustedPeers := s.Config.ReverseProxy.TrustedPeers
|
||||
defaultTrustedPeers := []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}
|
||||
if len(trustedPeers) == 0 || slices.Equal[[]netip.Prefix](trustedPeers, defaultTrustedPeers) {
|
||||
log.WithContext(context.Background()).Warn("TrustedPeers are configured to default value '0.0.0.0/0', '::/0'. This allows connection IP spoofing.")
|
||||
trustedPeers = defaultTrustedPeers
|
||||
}
|
||||
trustedHTTPProxies := s.Config.ReverseProxy.TrustedHTTPProxies
|
||||
trustedProxiesCount := s.Config.ReverseProxy.TrustedHTTPProxiesCount
|
||||
if len(trustedHTTPProxies) > 0 && trustedProxiesCount > 0 {
|
||||
log.WithContext(context.Background()).Warn("TrustedHTTPProxies and TrustedHTTPProxiesCount both are configured. " +
|
||||
"This is not recommended way to extract X-Forwarded-For. Consider using one of these options.")
|
||||
}
|
||||
realipOpts := []realip.Option{
|
||||
realip.WithTrustedPeers(trustedPeers),
|
||||
realip.WithTrustedProxies(trustedHTTPProxies),
|
||||
realip.WithTrustedProxiesCount(trustedProxiesCount),
|
||||
realip.WithHeaders([]string{realip.XForwardedFor, realip.XRealIp}),
|
||||
}
|
||||
realipOpts := realIPOptions(s.Config.ReverseProxy)
|
||||
proxyUnary, proxyStream, proxyAuthClose := nbgrpc.NewProxyAuthInterceptors(s.Store())
|
||||
s.proxyAuthClose = proxyAuthClose
|
||||
gRPCOpts := []grpc.ServerOption{
|
||||
@@ -333,7 +318,7 @@ func (s *BaseServer) AccessLogsManager() accesslogs.Manager {
|
||||
})
|
||||
}
|
||||
|
||||
func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
|
||||
func loadTLSConfig(certFile, certKey string) (*tls.Config, error) {
|
||||
// Load server's certificate and private key
|
||||
serverCert, err := tls.LoadX509KeyPair(certFile, certKey)
|
||||
if err != nil {
|
||||
@@ -380,3 +365,34 @@ func streamInterceptor(
|
||||
wrapped.WrappedContext = context.WithValue(ctx, nbContext.RequestIDKey, reqID)
|
||||
return handler(srv, wrapped)
|
||||
}
|
||||
|
||||
// realIPOptions builds the real-IP middleware options from the reverse proxy config.
|
||||
//
|
||||
// TrustedPeers controls which transport peers are allowed to supply forwarded-IP
|
||||
// headers. If empty, forwarded headers are ignored and the transport peer address
|
||||
// is used directly. Operators terminating connections at a reverse proxy should
|
||||
// configure TrustedPeers with that proxy's address or network.
|
||||
//
|
||||
// Only X-Forwarded-For is trusted. X-Real-IP contains a single client-supplied
|
||||
// address with no proxy chain to validate, and none of the reverse proxies we ship
|
||||
// use it on the gRPC path.
|
||||
func realIPOptions(cfg nbconfig.ReverseProxy) []realip.Option {
|
||||
if idx := slices.IndexFunc(cfg.TrustedPeers, func(p netip.Prefix) bool { return p.Bits() == 0 }); idx >= 0 {
|
||||
log.WithContext(context.Background()).Warnf("TrustedPeers contains the default route %s, which trusts "+
|
||||
"X-Forwarded-For from every client and allows connection IP spoofing. Set TrustedPeers to the address "+
|
||||
"of your reverse proxy, or leave it empty to use the connection's source address.", cfg.TrustedPeers[idx])
|
||||
}
|
||||
if cfg.TrustedHTTPProxiesCount > 0 {
|
||||
log.WithContext(context.Background()).Warn(
|
||||
"TrustedHTTPProxiesCount skips X-Forwarded-For entries by position before TrustedHTTPProxies filters by address. " +
|
||||
"An incorrect count may skip the real client IP and produce an incorrect source address.",
|
||||
)
|
||||
}
|
||||
|
||||
return []realip.Option{
|
||||
realip.WithTrustedPeers(cfg.TrustedPeers),
|
||||
realip.WithTrustedProxies(cfg.TrustedHTTPProxies),
|
||||
realip.WithTrustedProxiesCount(cfg.TrustedHTTPProxiesCount),
|
||||
realip.WithHeaders([]string{realip.XForwardedFor}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
)
|
||||
|
||||
const (
|
||||
realIPProbeMethod = "/netbird.test.RealIPProbe/Probe"
|
||||
realIPProbeStreamMethod = "/netbird.test.RealIPProbe/ProbeStream"
|
||||
)
|
||||
|
||||
// realIPProbe records the real IP the middleware derived for each call.
|
||||
type realIPProbe struct {
|
||||
got chan string
|
||||
}
|
||||
|
||||
func (p *realIPProbe) record(ctx context.Context) {
|
||||
addr, _ := realip.FromContext(ctx)
|
||||
p.got <- addr.String()
|
||||
}
|
||||
|
||||
func (p *realIPProbe) wait(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case got := <-p.got:
|
||||
return got
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for probe")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func startProbeServer(t *testing.T, cfg nbconfig.ReverseProxy) (*grpc.ClientConn, *realIPProbe) {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
probe := &realIPProbe{got: make(chan string, 1)}
|
||||
opts := realIPOptions(cfg)
|
||||
srv := grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(realip.UnaryServerInterceptorOpts(opts...)),
|
||||
grpc.ChainStreamInterceptor(realip.StreamServerInterceptorOpts(opts...)),
|
||||
)
|
||||
srv.RegisterService(&grpc.ServiceDesc{
|
||||
ServiceName: "netbird.test.RealIPProbe",
|
||||
HandlerType: (*any)(nil),
|
||||
Methods: []grpc.MethodDesc{{
|
||||
MethodName: "Probe",
|
||||
Handler: func(_ any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||
req := new(emptypb.Empty)
|
||||
if err := dec(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handler := func(ctx context.Context, _ any) (any, error) {
|
||||
probe.record(ctx)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
if interceptor == nil {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
return interceptor(ctx, req, &grpc.UnaryServerInfo{FullMethod: realIPProbeMethod}, handler)
|
||||
},
|
||||
}},
|
||||
Streams: []grpc.StreamDesc{{
|
||||
StreamName: "ProbeStream",
|
||||
ServerStreams: true,
|
||||
Handler: func(_ any, stream grpc.ServerStream) error {
|
||||
probe.record(stream.Context())
|
||||
return nil
|
||||
},
|
||||
}},
|
||||
}, probe)
|
||||
|
||||
go func() { _ = srv.Serve(listener) }()
|
||||
t.Cleanup(srv.Stop)
|
||||
|
||||
conn, err := grpc.NewClient(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
|
||||
return conn, probe
|
||||
}
|
||||
|
||||
func callUnary(t *testing.T, conn *grpc.ClientConn, probe *realIPProbe, kv ...string) string {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, kv...)
|
||||
require.NoError(t, conn.Invoke(ctx, realIPProbeMethod, &emptypb.Empty{}, &emptypb.Empty{}))
|
||||
|
||||
return probe.wait(t)
|
||||
}
|
||||
|
||||
func callStream(t *testing.T, conn *grpc.ClientConn, probe *realIPProbe, kv ...string) string {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, kv...)
|
||||
desc := &grpc.StreamDesc{StreamName: "ProbeStream", ServerStreams: true}
|
||||
stream, err := conn.NewStream(ctx, desc, realIPProbeStreamMethod)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, stream.CloseSend())
|
||||
require.ErrorIs(t, stream.RecvMsg(&emptypb.Empty{}), io.EOF)
|
||||
|
||||
return probe.wait(t)
|
||||
}
|
||||
|
||||
func assertRealIP(t *testing.T, cfg nbconfig.ReverseProxy, want string, kv ...string) {
|
||||
t.Helper()
|
||||
|
||||
conn, probe := startProbeServer(t, cfg)
|
||||
t.Run("unary", func(t *testing.T) {
|
||||
assert.Equal(t, want, callUnary(t, conn, probe, kv...))
|
||||
})
|
||||
t.Run("stream", func(t *testing.T) {
|
||||
assert.Equal(t, want, callStream(t, conn, probe, kv...))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRealIPDefaultIgnoresClientForwardedHeaders(t *testing.T) {
|
||||
assertRealIP(t, nbconfig.ReverseProxy{}, "127.0.0.1",
|
||||
realip.XForwardedFor, "203.0.113.44",
|
||||
realip.XRealIp, "203.0.113.44",
|
||||
)
|
||||
}
|
||||
|
||||
func TestRealIPUntrustedPeerIgnoresForwardedHeaders(t *testing.T) {
|
||||
cfg := nbconfig.ReverseProxy{TrustedPeers: []netip.Prefix{netip.MustParsePrefix("10.9.8.7/32")}}
|
||||
|
||||
assertRealIP(t, cfg, "127.0.0.1",
|
||||
realip.XForwardedFor, "203.0.113.44",
|
||||
realip.XRealIp, "203.0.113.44",
|
||||
)
|
||||
}
|
||||
|
||||
func TestRealIPTrustedPeerHonoursForwardedHeaders(t *testing.T) {
|
||||
cfg := nbconfig.ReverseProxy{TrustedPeers: []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}}
|
||||
|
||||
assertRealIP(t, cfg, "203.0.113.44",
|
||||
realip.XForwardedFor, "203.0.113.44",
|
||||
realip.XRealIp, "203.0.113.44",
|
||||
)
|
||||
}
|
||||
|
||||
func TestRealIPIgnoresXRealIPWhenProxyCountIsSet(t *testing.T) {
|
||||
cfg := nbconfig.ReverseProxy{
|
||||
TrustedPeers: []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")},
|
||||
TrustedHTTPProxiesCount: 1,
|
||||
}
|
||||
|
||||
assertRealIP(t, cfg, "127.0.0.1", realip.XRealIp, "203.0.113.44")
|
||||
}
|
||||
@@ -66,7 +66,8 @@ type BaseServer struct {
|
||||
disableLegacyManagementPort bool
|
||||
autoResolveDomains bool
|
||||
|
||||
proxyAuthClose func()
|
||||
proxyAuthClose func()
|
||||
domainCleanupStop func()
|
||||
|
||||
// grpcExtensions holds additional gRPC services, interceptors, and shutdown
|
||||
// hooks registered by external modules via RegisterGRPCExtension. Populated
|
||||
@@ -74,6 +75,7 @@ type BaseServer struct {
|
||||
grpcExtensions []GRPCExtension
|
||||
|
||||
listener net.Listener
|
||||
tlsConfig *tls.Config
|
||||
certManager *autocert.Manager
|
||||
update *version.Update
|
||||
|
||||
@@ -94,6 +96,7 @@ type Config struct {
|
||||
DisableGeoliteUpdate bool
|
||||
UserDeleteFromIDPEnabled bool
|
||||
AutoResolveDomains bool
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// NewServer initializes and configures a new Server instance
|
||||
@@ -110,6 +113,7 @@ func NewServer(cfg *Config) *BaseServer {
|
||||
disableLegacyManagementPort: cfg.DisableLegacyManagementPort,
|
||||
mgmtMetricsPort: cfg.MgmtMetricsPort,
|
||||
autoResolveDomains: cfg.AutoResolveDomains,
|
||||
tlsConfig: cfg.TLSConfig,
|
||||
}
|
||||
s.container[ContainerKeyBaseServer] = s
|
||||
|
||||
@@ -139,21 +143,9 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
||||
}
|
||||
s.EphemeralManager().LoadInitialPeers(srvCtx)
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
tlsEnabled := false
|
||||
if s.Config.HttpConfig.LetsEncryptDomain != "" {
|
||||
s.certManager, err = encryption.CreateCertManager(s.Config.Datadir, s.Config.HttpConfig.LetsEncryptDomain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating LetsEncrypt cert manager: %v", err)
|
||||
}
|
||||
tlsEnabled = true
|
||||
} else if s.Config.HttpConfig.CertFile != "" && s.Config.HttpConfig.CertKey != "" {
|
||||
tlsConfig, err = loadTLSConfig(s.Config.HttpConfig.CertFile, s.Config.HttpConfig.CertKey)
|
||||
if err != nil {
|
||||
log.WithContext(srvCtx).Errorf("cannot load TLS credentials: %v", err)
|
||||
return err
|
||||
}
|
||||
tlsEnabled = true
|
||||
tlsEnabled, err := s.setupTLS(srvCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installationID, err := getInstallationID(srvCtx, s.Store())
|
||||
@@ -215,8 +207,8 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
||||
log.WithContext(ctx).Infof("running HTTP server (LetsEncrypt challenge handler): %s", cml.Addr().String())
|
||||
s.serveHTTP(ctx, cml, s.certManager.HTTPHandler(nil))
|
||||
}
|
||||
case tlsConfig != nil:
|
||||
s.listener, err = tls.Listen("tcp", fmt.Sprintf(":%d", s.mgmtPort), tlsConfig)
|
||||
case s.tlsConfig != nil:
|
||||
s.listener, err = tls.Listen("tcp", fmt.Sprintf(":%d", s.mgmtPort), s.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating TLS listener on port %d: %v", s.mgmtPort, err)
|
||||
}
|
||||
@@ -236,14 +228,59 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
||||
s.update.SetOnUpdateListener(func() {
|
||||
log.WithContext(ctx).Infof("your management version, \"%s\", is outdated, a new management version is available. Learn more here: https://github.com/netbirdio/netbird/releases", version.NetbirdVersion())
|
||||
})
|
||||
s.startDomainCleanup(srvCtx)
|
||||
|
||||
return nil
|
||||
}
|
||||
func (s *BaseServer) startDomainCleanup(ctx context.Context) {
|
||||
if s.domainCleanupStop != nil {
|
||||
return
|
||||
}
|
||||
mgr := s.ReverseProxyDomainManager()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
done := make(chan struct{})
|
||||
s.domainCleanupStop = func() {
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
go func() {
|
||||
defer close(done)
|
||||
mgr.RunValidationCleanup(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
// setupTLS resolves the listener's TLS source: an injected config wins over the HttpConfig certificate settings
|
||||
func (s *BaseServer) setupTLS(ctx context.Context) (bool, error) {
|
||||
switch {
|
||||
case s.tlsConfig != nil:
|
||||
return true, nil
|
||||
case s.Config.HttpConfig.LetsEncryptDomain != "":
|
||||
certManager, err := encryption.CreateCertManager(s.Config.Datadir, s.Config.HttpConfig.LetsEncryptDomain)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed creating LetsEncrypt cert manager: %v", err)
|
||||
}
|
||||
s.certManager = certManager
|
||||
return true, nil
|
||||
case s.Config.HttpConfig.CertFile != "" && s.Config.HttpConfig.CertKey != "":
|
||||
tlsConfig, err := loadTLSConfig(s.Config.HttpConfig.CertFile, s.Config.HttpConfig.CertKey)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("cannot load TLS credentials: %v", err)
|
||||
return false, err
|
||||
}
|
||||
s.tlsConfig = tlsConfig
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Stop attempts a graceful shutdown, waiting up to 5 seconds for active connections to finish
|
||||
func (s *BaseServer) Stop() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if s.domainCleanupStop != nil {
|
||||
s.domainCleanupStop()
|
||||
}
|
||||
|
||||
s.IntegratedValidator().Stop(ctx)
|
||||
if s.GeoLocationManager() != nil {
|
||||
|
||||
Reference in New Issue
Block a user