Merge remote-tracking branch 'origin/main' into embedded-vnc

# Conflicts:
#	client/cmd/up.go
#	client/internal/engine.go
#	client/internal/profilemanager/config.go
#	client/proto/daemon.pb.go
#	client/server/mdm.go
#	client/server/server.go
#	client/status/status.go
#	client/ui/main.go
#	client/ui/services/windowmanager.go
#	management/internals/shared/grpc/conversion.go
#	management/server/store/sql_store.go
#	management/server/store/sql_store_test.go
#	shared/management/networkmap/envelope.go
#	shared/management/proto/management.pb.go
This commit is contained in:
Viktor Liu
2026-10-19 19:49:54 +09:00
804 changed files with 72029 additions and 25943 deletions
@@ -198,6 +198,7 @@ func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
KernelVersion: p.Meta.KernelVersion,
NetworkAddresses: networkAddresses,
Files: files,
Certificates: p.Meta.Certificates,
Capabilities: p.Meta.Capabilities,
SyncMessageVersion: p.Meta.SyncMessageVersion,
Flags: nmdata.Flags{
@@ -213,8 +214,7 @@ func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
}
}
// TwinPeer converts a real peer to its slim nmdata twin. Exported for the
// port-forwarding integration, which builds proxy NetworkMaps holding twins.
// TwinPeer converts a real peer to its slim nmdata twin.
func TwinPeer(p *nbpeer.Peer) *nmdata.Peer {
return twinPeer(p)
}
@@ -455,6 +455,9 @@ func TwinPostureChecks(pc *posture.Checks) *nmdata.PostureChecks {
}
out.Checks.ProcessCheck = &nmdata.ProcessCheck{Processes: procs}
}
if def.CertificateCheck != nil {
out.Checks.CertificateCheck = &nmdata.CertificateCheck{CACertificates: def.CertificateCheck.CACertificates}
}
return out
}
-1
View File
@@ -15,7 +15,6 @@ import (
type FirewallRule = sharedtypes.FirewallRule
type NetworkMap = sharedtypes.NetworkMap
type ForwardingRule = sharedtypes.ForwardingRule
type PolicyTrafficActionType = sharedtypes.PolicyTrafficActionType
type PolicyRuleProtocolType = sharedtypes.PolicyRuleProtocolType
@@ -10,7 +10,6 @@ type (
DNSSettings = types.DNSSettings
FirewallRule = sharedtypes.FirewallRule
ForwardingRule = sharedtypes.ForwardingRule
Group = types.Group
Network = types.Network
Policy = types.Policy
@@ -19,7 +19,6 @@ type NetworkMap struct {
OfflinePeers []*ComponentPeer
FirewallRules []*FirewallRule
RoutesFirewallRules []*RouteFirewallRule
ForwardingRules []*ForwardingRule
AuthorizedUsers map[string]map[string]struct{}
EnableSSH bool
// ForceRoutingPeerDNSResolution forces the peer to run/use routing-peer DNS
@@ -320,7 +320,6 @@ func canonicalize(nm *proto.NetworkMap) {
slices.SortFunc(nm.Routes, cmpRoute)
slices.SortFunc(nm.FirewallRules, cmpFirewallRule)
slices.SortFunc(nm.RoutesFirewallRules, cmpRouteFirewallRule)
slices.SortFunc(nm.ForwardingRules, cmpForwardingRule)
for _, r := range nm.FirewallRules {
slices.SortFunc(r.SourcePrefixes, bytes.Compare)
@@ -550,16 +549,6 @@ func cmpRouteFirewallRule(a, b *proto.RouteFirewallRule) int {
return boolCmp(a.IsDynamic, b.IsDynamic)
}
func cmpForwardingRule(a, b *proto.ForwardingRule) int {
if a == nil || b == nil {
return boolCmp(a == nil, b == nil)
}
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
return c
}
return bytes.Compare(a.TranslatedAddress, b.TranslatedAddress)
}
func portInfoKey(pi *proto.PortInfo) string {
if pi == nil {
return ""
@@ -591,7 +580,6 @@ func describeDivergence(legacy, updated *proto.NetworkMap, accountID, peerID str
{"Routes", len(legacy.Routes), len(updated.Routes), func() string { return diffLists(legacy.Routes, updated.Routes) }},
{"FirewallRules", len(legacy.FirewallRules), len(updated.FirewallRules), func() string { return diffLists(legacy.FirewallRules, updated.FirewallRules) }},
{"RoutesFirewallRules", len(legacy.RoutesFirewallRules), len(updated.RoutesFirewallRules), func() string { return diffLists(legacy.RoutesFirewallRules, updated.RoutesFirewallRules) }},
{"ForwardingRules", len(legacy.ForwardingRules), len(updated.ForwardingRules), func() string { return diffLists(legacy.ForwardingRules, updated.ForwardingRules) }},
}
for _, l := range lens {
if l.a != l.b {
@@ -186,14 +186,6 @@ func ToProtoNetworkMap(
pm.RoutesFirewallRules = routesFirewallRules
pm.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
if nm.ForwardingRules != nil {
forwardingRules := make([]*proto.ForwardingRule, 0, len(nm.ForwardingRules))
for _, rule := range nm.ForwardingRules {
forwardingRules = append(forwardingRules, rule.ToProto())
}
pm.ForwardingRules = forwardingRules
}
if nm.AuthorizedUsers != nil {
hashedUsers, machineUsers := networkmap.BuildAuthorizedUsersProto(ctx, nm.AuthorizedUsers)
userIDClaim := auth.DefaultUserIDClaim
+35 -18
View File
@@ -118,14 +118,9 @@ func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, err
return netip.Addr{}, err
}
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
baseIP, firstOffset, lastOffset := ipv4PeerRange(prefix)
taken := make(map[uint32]struct{}, len(takenIps)+1)
taken[baseIP] = struct{}{} // reserve network IP
taken[baseIP+totalIPs-1] = struct{}{} // reserve broadcast IP
taken := make(map[uint32]struct{}, len(takenIps))
for _, ip := range takenIps {
if !ip.Is4() {
@@ -135,17 +130,16 @@ func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, err
taken[binary.BigEndian.Uint32(ab[:])] = struct{}{}
}
maxAttempts := (int(totalIPs) - len(taken)) / 100
maxAttempts := (int(lastOffset-firstOffset+1) - len(taken)) / 100
for i := 0; i < maxAttempts; i++ {
offset := uint32(util.RandIntn(int(totalIPs-2))) + 1
candidate := baseIP + offset
candidate := baseIP + randomOffset(firstOffset, lastOffset)
if _, exists := taken[candidate]; !exists {
return uint32ToIP(candidate), nil
}
}
for offset := uint32(1); offset < totalIPs-1; offset++ {
for offset := firstOffset; offset <= lastOffset; offset++ {
candidate := baseIP + offset
if _, exists := taken[candidate]; !exists {
return uint32ToIP(candidate), nil
@@ -161,15 +155,38 @@ func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
return netip.Addr{}, err
}
baseIP, firstOffset, lastOffset := ipv4PeerRange(prefix)
return uint32ToIP(baseIP + randomOffset(firstOffset, lastOffset)), nil
}
// IsReservedPeerIP reports whether ip is an address of the IPv4 prefix that is never assigned to a peer:
// the network address, the broadcast address or the address before it, which clients use for their DNS resolver.
// It returns false if prefix is not a valid IPv4 peer network or ip is not within it.
func IsReservedPeerIP(prefix netip.Prefix, ip netip.Addr) bool {
ip = ip.Unmap()
if validateIPv4Prefix(prefix) != nil || !ip.Is4() || !prefix.Contains(ip) {
return false
}
baseIP, firstOffset, lastOffset := ipv4PeerRange(prefix)
b := ip.As4()
offset := binary.BigEndian.Uint32(b[:]) - baseIP
return offset < firstOffset || offset > lastOffset
}
// ipv4PeerRange returns the network address of the IPv4 prefix as uint32 and the first and last offset
// from it that can be assigned to a peer. Reserved are the network address, the broadcast address and the
// address before it, which clients in userspace and netstack mode use for their DNS resolver.
func ipv4PeerRange(prefix netip.Prefix) (baseIP, firstOffset, lastOffset uint32) {
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
broadcastOffset := uint32(1)<<(32-prefix.Bits()) - 1
dnsResolverOffset := broadcastOffset - 1
return binary.BigEndian.Uint32(b[:]), 1, dnsResolverOffset - 1
}
offset := uint32(util.RandIntn(int(totalIPs-2))) + 1
candidate := baseIP + offset
return uint32ToIP(candidate), nil
// randomOffset returns a random offset in [firstOffset, lastOffset].
func randomOffset(firstOffset, lastOffset uint32) uint32 {
return firstOffset + uint32(util.RandIntn(int(lastOffset-firstOffset+1)))
}
// AllocateRandomPeerIPv6 picks a random host address within the given IPv6 prefix.
+75 -11
View File
@@ -42,12 +42,12 @@ func TestAllocatePeerIP(t *testing.T) {
}
func TestAllocatePeerIPSmallSubnet(t *testing.T) {
// Test /27 network (10.0.0.0/27) - should only have 30 usable IPs (10.0.0.1 to 10.0.0.30)
// Test /27 network (10.0.0.0/27) - should only have 29 usable IPs (10.0.0.1 to 10.0.0.29)
prefix := netip.MustParsePrefix("10.0.0.0/27")
var ips []netip.Addr
// Allocate all available IPs in the /27 network
for i := 0; i < 30; i++ {
for i := 0; i < 29; i++ {
ip, err := AllocatePeerIP(prefix, ips)
if err != nil {
t.Fatal(err)
@@ -61,7 +61,7 @@ func TestAllocatePeerIPSmallSubnet(t *testing.T) {
ips = append(ips, ip)
}
assert.Len(t, ips, 30)
assert.Len(t, ips, 29)
// Verify all IPs are unique
uniq := make(map[string]struct{})
@@ -86,13 +86,13 @@ func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
cidr string
expectedUsable int
}{
{"/30 network", "192.168.1.0/30", 2}, // 4 total - 2 reserved = 2 usable
{"/29 network", "192.168.1.0/29", 6}, // 8 total - 2 reserved = 6 usable
{"/28 network", "192.168.1.0/28", 14}, // 16 total - 2 reserved = 14 usable
{"/27 network", "192.168.1.0/27", 30}, // 32 total - 2 reserved = 30 usable
{"/26 network", "192.168.1.0/26", 62}, // 64 total - 2 reserved = 62 usable
{"/25 network", "192.168.1.0/25", 126}, // 128 total - 2 reserved = 126 usable
{"/16 network", "10.0.0.0/16", 65534}, // 65536 total - 2 reserved = 65534 usable
{"/30 network", "192.168.1.0/30", 1}, // 4 total - 3 reserved = 1 usable
{"/29 network", "192.168.1.0/29", 5}, // 8 total - 3 reserved = 5 usable
{"/28 network", "192.168.1.0/28", 13}, // 16 total - 3 reserved = 13 usable
{"/27 network", "192.168.1.0/27", 29}, // 32 total - 3 reserved = 29 usable
{"/26 network", "192.168.1.0/26", 61}, // 64 total - 3 reserved = 61 usable
{"/25 network", "192.168.1.0/25", 125}, // 128 total - 3 reserved = 125 usable
{"/16 network", "10.0.0.0/16", 65533}, // 65536 total - 3 reserved = 65533 usable
}
for _, tc := range testCases {
@@ -117,14 +117,16 @@ func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
// Verify IP is within the correct range
assert.True(t, prefix.Contains(ip), "allocated IP %s is not within network %s", ip.String(), prefix.String())
// Verify IP is not network or broadcast address
// Verify IP is not network, DNS resolver or broadcast address
networkAddr := prefix.Masked().Addr()
hostBits := 32 - prefix.Bits()
b := networkAddr.As4()
baseIP := binary.BigEndian.Uint32(b[:])
dnsIP := uint32ToIP(baseIP + (1 << hostBits) - 2)
broadcastIP := uint32ToIP(baseIP + (1 << hostBits) - 1)
assert.NotEqual(t, networkAddr, ip, "allocated network address %s", ip.String())
assert.NotEqual(t, dnsIP, ip, "allocated DNS resolver address %s", ip.String())
assert.NotEqual(t, broadcastIP, ip, "allocated broadcast address %s", ip.String())
ips = append(ips, ip)
@@ -143,6 +145,68 @@ func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
}
}
func TestAllocatePeerIPSkipsDNSResolverIP(t *testing.T) {
prefix := netip.MustParsePrefix("192.168.1.0/29")
ips := []netip.Addr{
netip.MustParseAddr("192.168.1.1"),
netip.MustParseAddr("192.168.1.2"),
netip.MustParseAddr("192.168.1.3"),
netip.MustParseAddr("192.168.1.4"),
}
ip, err := AllocatePeerIP(prefix, ips)
require.NoError(t, err)
assert.Equal(t, netip.MustParseAddr("192.168.1.5"), ip)
_, err = AllocatePeerIP(prefix, append(ips, ip))
assert.Error(t, err, "192.168.1.6 is the DNS resolver address and must not be allocated")
}
func TestAllocateRandomPeerIPSkipsReservedIPs(t *testing.T) {
for _, cidr := range []string{"192.168.1.0/30", "192.168.1.0/29"} {
t.Run(cidr, func(t *testing.T) {
prefix := netip.MustParsePrefix(cidr)
for i := 0; i < 1000; i++ {
ip, err := AllocateRandomPeerIP(prefix)
require.NoError(t, err)
require.True(t, prefix.Contains(ip), "allocated IP %s is not within network %s", ip, prefix)
require.False(t, IsReservedPeerIP(prefix, ip), "allocated reserved IP %s", ip)
}
})
}
ip, err := AllocateRandomPeerIP(netip.MustParsePrefix("192.168.1.0/30"))
require.NoError(t, err)
assert.Equal(t, netip.MustParseAddr("192.168.1.1"), ip)
}
func TestIsReservedPeerIP(t *testing.T) {
prefix := netip.MustParsePrefix("100.64.0.0/24")
testCases := []struct {
ip string
reserved bool
}{
{"100.64.0.0", true},
{"100.64.0.1", false},
{"100.64.0.253", false},
{"100.64.0.254", true},
{"100.64.0.255", true},
{"::ffff:100.64.0.254", true},
{"100.64.1.254", false},
{"fd12:3456:7890:abcd::1", false},
}
for _, tc := range testCases {
t.Run(tc.ip, func(t *testing.T) {
assert.Equal(t, tc.reserved, IsReservedPeerIP(prefix, netip.MustParseAddr(tc.ip)))
})
}
smallest := netip.MustParsePrefix("192.168.1.0/30")
assert.False(t, IsReservedPeerIP(smallest, netip.MustParseAddr("192.168.1.1")))
assert.True(t, IsReservedPeerIP(smallest, netip.MustParseAddr("192.168.1.2")))
}
func TestAllocateIPv4InvalidPrefixes(t *testing.T) {
prefixes := []netip.Prefix{
{},
+5
View File
@@ -76,6 +76,10 @@ type Settings struct {
// MetricsPushEnabled globally enables or disables client metrics push for the account
MetricsPushEnabled bool `gorm:"default:false"`
// PeerHostnameCollisionRejected rejects registering or renaming a peer to a hostname
// already used by another peer of the account instead of suffixing it
PeerHostnameCollisionRejected bool `gorm:"default:false"`
// AgentNetworkOnly limits the dashboard to the Agent Network surface for this account.
// Set for accounts created via netbird.ai signups; users can disable it later.
AgentNetworkOnly bool `gorm:"default:false"`
@@ -123,6 +127,7 @@ func (s *Settings) Copy() *Settings {
AutoUpdateAlways: s.AutoUpdateAlways,
IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups),
MetricsPushEnabled: s.MetricsPushEnabled,
PeerHostnameCollisionRejected: s.PeerHostnameCollisionRejected,
AgentNetworkOnly: s.AgentNetworkOnly,
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
LocalAuthDisabled: s.LocalAuthDisabled,
+6 -4
View File
@@ -1,10 +1,12 @@
package types
type Engine string
import "github.com/netbirdio/netbird/management/internals/shared/db"
type Engine = db.Engine
const (
PostgresStoreEngine Engine = "postgres"
PostgresStoreEngine = db.PostgresStoreEngine
FileStoreEngine Engine = "jsonfile"
SqliteStoreEngine Engine = "sqlite"
MysqlStoreEngine Engine = "mysql"
SqliteStoreEngine = db.SqliteStoreEngine
MysqlStoreEngine = db.MysqlStoreEngine
)
+3
View File
@@ -34,4 +34,7 @@ const (
UpdateOperationCreate UpdateOperation = "create"
UpdateOperationUpdate UpdateOperation = "update"
UpdateOperationDelete UpdateOperation = "delete"
// UpdateOperationRefresh is a periodic push that carries no change of its own, so
// it stays out of the counters that track what an administrator actually edited.
UpdateOperationRefresh UpdateOperation = "refresh"
)