mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-29 20:19:56 +00:00
324 lines
12 KiB
Go
324 lines
12 KiB
Go
// Package networkmap contains the shared NetworkMap helpers that both the
|
|
// management server and the client agent need.
|
|
//
|
|
// The proto-conversion helpers (types.NetworkMap → proto.NetworkMap) live
|
|
// here so the client can run the same conversion locally after deriving its
|
|
// NetworkMap from a NetworkMapEnvelope, without taking a dependency on the
|
|
// server-side conversion package (which pulls in cloud integrations and is
|
|
// otherwise an unwanted internal import on the client).
|
|
//
|
|
// The helpers are pure functions over inputs — no caches, no IO, no logging
|
|
// beyond a context-aware error log when an individual user-id hash fails.
|
|
package networkmap
|
|
|
|
import (
|
|
"context"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
goproto "google.golang.org/protobuf/proto"
|
|
|
|
nbdns "github.com/netbirdio/netbird/dns"
|
|
"net/netip"
|
|
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
nbroute "github.com/netbirdio/netbird/route"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
"github.com/netbirdio/netbird/shared/netiputil"
|
|
"github.com/netbirdio/netbird/shared/sshauth"
|
|
)
|
|
|
|
// ToProtocolRoutes converts a slice of typed routes to their proto form.
|
|
func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
|
protoRoutes := make([]*proto.Route, 0, len(routes))
|
|
for _, r := range routes {
|
|
protoRoutes = append(protoRoutes, ToProtocolRoute(r))
|
|
}
|
|
return protoRoutes
|
|
}
|
|
|
|
// ToProtocolRoute converts one typed route to its proto form.
|
|
func ToProtocolRoute(route *nbroute.Route) *proto.Route {
|
|
return &proto.Route{
|
|
ID: string(route.ID),
|
|
NetID: string(route.NetID),
|
|
Network: route.Network.String(),
|
|
Domains: route.Domains.ToPunycodeList(),
|
|
NetworkType: int64(route.NetworkType),
|
|
Peer: route.Peer,
|
|
Metric: int64(route.Metric),
|
|
Masquerade: route.Masquerade,
|
|
KeepRoute: route.KeepRoute,
|
|
SkipAutoApply: route.SkipAutoApply,
|
|
}
|
|
}
|
|
|
|
// ToProtocolFirewallRules converts the firewall rules to the protocol form.
|
|
// When useSourcePrefixes is true, the compact SourcePrefixes field is
|
|
// populated alongside the deprecated PeerIP for forward compatibility.
|
|
// Wildcard rules ("0.0.0.0") are expanded into separate v4/v6 SourcePrefixes
|
|
// when includeIPv6 is true.
|
|
func ToProtocolFirewallRules(rules []*types.FirewallRule, includeIPv6, useSourcePrefixes bool) []*proto.FirewallRule {
|
|
result := make([]*proto.FirewallRule, 0, len(rules))
|
|
for i := range rules {
|
|
rule := rules[i]
|
|
|
|
fwRule := &proto.FirewallRule{
|
|
PolicyID: []byte(rule.PolicyID),
|
|
PeerIP: rule.PeerIP, //nolint:staticcheck // populated for backward compatibility
|
|
Direction: GetProtoDirection(rule.Direction),
|
|
Action: GetProtoAction(rule.Action),
|
|
Protocol: GetProtoProtocol(rule.Protocol),
|
|
Port: rule.Port,
|
|
}
|
|
|
|
if useSourcePrefixes && rule.PeerIP != "" {
|
|
result = append(result, populateSourcePrefixes(fwRule, rule, includeIPv6)...)
|
|
}
|
|
|
|
if ShouldUsePortRange(fwRule) {
|
|
fwRule.PortInfo = rule.PortRange.ToProto()
|
|
}
|
|
|
|
result = append(result, fwRule)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// populateSourcePrefixes sets SourcePrefixes on fwRule and returns any
|
|
// additional rules needed (e.g. a v6 wildcard clone when the peer IP is
|
|
// unspecified).
|
|
func populateSourcePrefixes(fwRule *proto.FirewallRule, rule *types.FirewallRule, includeIPv6 bool) []*proto.FirewallRule {
|
|
addr, err := netip.ParseAddr(rule.PeerIP)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
if !addr.IsUnspecified() {
|
|
fwRule.SourcePrefixes = [][]byte{netiputil.EncodeAddr(addr.Unmap())}
|
|
return nil
|
|
}
|
|
|
|
v4Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0))
|
|
fwRule.SourcePrefixes = [][]byte{v4Wildcard}
|
|
|
|
if !includeIPv6 {
|
|
return nil
|
|
}
|
|
|
|
v6Rule := goproto.Clone(fwRule).(*proto.FirewallRule)
|
|
v6Rule.PeerIP = "::" //nolint:staticcheck // populated for backward compatibility
|
|
v6Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0))
|
|
v6Rule.SourcePrefixes = [][]byte{v6Wildcard}
|
|
if ShouldUsePortRange(v6Rule) {
|
|
v6Rule.PortInfo = rule.PortRange.ToProto()
|
|
}
|
|
return []*proto.FirewallRule{v6Rule}
|
|
}
|
|
|
|
// GetProtoDirection converts the direction to proto.RuleDirection.
|
|
func GetProtoDirection(direction int) proto.RuleDirection {
|
|
if direction == types.FirewallRuleDirectionOUT {
|
|
return proto.RuleDirection_OUT
|
|
}
|
|
return proto.RuleDirection_IN
|
|
}
|
|
|
|
// GetProtoAction converts the action to proto.RuleAction.
|
|
func GetProtoAction(action string) proto.RuleAction {
|
|
if action == string(types.PolicyTrafficActionDrop) {
|
|
return proto.RuleAction_DROP
|
|
}
|
|
return proto.RuleAction_ACCEPT
|
|
}
|
|
|
|
// GetProtoProtocol converts the protocol to proto.RuleProtocol.
|
|
func GetProtoProtocol(protocol string) proto.RuleProtocol {
|
|
switch types.PolicyRuleProtocolType(protocol) {
|
|
case types.PolicyRuleProtocolALL:
|
|
return proto.RuleProtocol_ALL
|
|
case types.PolicyRuleProtocolTCP:
|
|
return proto.RuleProtocol_TCP
|
|
case types.PolicyRuleProtocolUDP:
|
|
return proto.RuleProtocol_UDP
|
|
case types.PolicyRuleProtocolICMP:
|
|
return proto.RuleProtocol_ICMP
|
|
case types.PolicyRuleProtocolNetbirdSSH:
|
|
return proto.RuleProtocol_NETBIRD_SSH
|
|
default:
|
|
return proto.RuleProtocol_UNKNOWN
|
|
}
|
|
}
|
|
|
|
// GetProtoPortInfo converts route-firewall-rule port info to proto.PortInfo.
|
|
func GetProtoPortInfo(rule *types.RouteFirewallRule) *proto.PortInfo {
|
|
var portInfo proto.PortInfo
|
|
if rule.Port != 0 {
|
|
portInfo.PortSelection = &proto.PortInfo_Port{Port: uint32(rule.Port)}
|
|
} else if portRange := rule.PortRange; portRange.Start != 0 && portRange.End != 0 {
|
|
portInfo.PortSelection = &proto.PortInfo_Range_{
|
|
Range: &proto.PortInfo_Range{
|
|
Start: uint32(portRange.Start),
|
|
End: uint32(portRange.End),
|
|
},
|
|
}
|
|
}
|
|
return &portInfo
|
|
}
|
|
|
|
// ShouldUsePortRange reports whether the firewall rule should use a port
|
|
// range rather than a single port (TCP/UDP without a single port).
|
|
func ShouldUsePortRange(rule *proto.FirewallRule) bool {
|
|
return rule.Port == "" && (rule.Protocol == proto.RuleProtocol_UDP || rule.Protocol == proto.RuleProtocol_TCP)
|
|
}
|
|
|
|
// ToProtocolRoutesFirewallRules converts a slice of typed route-firewall
|
|
// rules to proto.
|
|
func ToProtocolRoutesFirewallRules(rules []*types.RouteFirewallRule) []*proto.RouteFirewallRule {
|
|
result := make([]*proto.RouteFirewallRule, len(rules))
|
|
for i := range rules {
|
|
rule := rules[i]
|
|
result[i] = &proto.RouteFirewallRule{
|
|
SourceRanges: rule.SourceRanges,
|
|
Action: GetProtoAction(rule.Action),
|
|
Destination: rule.Destination,
|
|
Protocol: GetProtoProtocol(rule.Protocol),
|
|
PortInfo: GetProtoPortInfo(rule),
|
|
IsDynamic: rule.IsDynamic,
|
|
Domains: rule.Domains.ToPunycodeList(),
|
|
PolicyID: []byte(rule.PolicyID),
|
|
RouteID: string(rule.RouteID),
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ConvertToProtoCustomZone converts an nbdns.CustomZone to its proto form.
|
|
func ConvertToProtoCustomZone(zone nbdns.CustomZone) *proto.CustomZone {
|
|
protoZone := &proto.CustomZone{
|
|
Domain: zone.Domain,
|
|
Records: make([]*proto.SimpleRecord, 0, len(zone.Records)),
|
|
SearchDomainDisabled: zone.SearchDomainDisabled,
|
|
NonAuthoritative: zone.NonAuthoritative,
|
|
}
|
|
for _, record := range zone.Records {
|
|
protoZone.Records = append(protoZone.Records, &proto.SimpleRecord{
|
|
Name: record.Name,
|
|
Type: int64(record.Type),
|
|
Class: record.Class,
|
|
TTL: int64(record.TTL),
|
|
RData: record.RData,
|
|
})
|
|
}
|
|
return protoZone
|
|
}
|
|
|
|
// ConvertToProtoNameServerGroup converts a NameServerGroup to its proto form.
|
|
func ConvertToProtoNameServerGroup(nsGroup *nbdns.NameServerGroup) *proto.NameServerGroup {
|
|
protoGroup := &proto.NameServerGroup{
|
|
Primary: nsGroup.Primary,
|
|
Domains: nsGroup.Domains,
|
|
SearchDomainsEnabled: nsGroup.SearchDomainsEnabled,
|
|
NameServers: make([]*proto.NameServer, 0, len(nsGroup.NameServers)),
|
|
}
|
|
for _, ns := range nsGroup.NameServers {
|
|
protoGroup.NameServers = append(protoGroup.NameServers, &proto.NameServer{
|
|
IP: ns.IP.String(),
|
|
Port: int64(ns.Port),
|
|
NSType: int64(ns.NSType),
|
|
})
|
|
}
|
|
return protoGroup
|
|
}
|
|
|
|
// DNSConfigCache is the cache contract for amortising NameServerGroup
|
|
// proto-conversion across peers in the same account. Server uses a concrete
|
|
// implementation; client passes nil (no cross-peer caching needed when
|
|
// rebuilding a single NetworkMap from an envelope).
|
|
type DNSConfigCache interface {
|
|
GetNameServerGroup(key string) (*proto.NameServerGroup, bool)
|
|
SetNameServerGroup(key string, value *proto.NameServerGroup)
|
|
}
|
|
|
|
// ToProtocolDNSConfig converts nbdns.Config to proto.DNSConfig. If cache is
|
|
// non-nil, NameServerGroup proto values are cached by NSG.ID across calls —
|
|
// the server amortises this across peers, the client passes nil.
|
|
func ToProtocolDNSConfig(update nbdns.Config, cache DNSConfigCache, forwardPort int64) *proto.DNSConfig {
|
|
protoUpdate := &proto.DNSConfig{
|
|
ServiceEnable: update.ServiceEnable,
|
|
CustomZones: make([]*proto.CustomZone, 0, len(update.CustomZones)),
|
|
NameServerGroups: make([]*proto.NameServerGroup, 0, len(update.NameServerGroups)),
|
|
ForwarderPort: forwardPort,
|
|
}
|
|
|
|
for _, zone := range update.CustomZones {
|
|
protoUpdate.CustomZones = append(protoUpdate.CustomZones, ConvertToProtoCustomZone(zone))
|
|
}
|
|
|
|
for _, nsGroup := range update.NameServerGroups {
|
|
if cache != nil {
|
|
if cachedGroup, exists := cache.GetNameServerGroup(nsGroup.ID); exists {
|
|
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, cachedGroup)
|
|
continue
|
|
}
|
|
}
|
|
protoGroup := ConvertToProtoNameServerGroup(nsGroup)
|
|
if cache != nil {
|
|
cache.SetNameServerGroup(nsGroup.ID, protoGroup)
|
|
}
|
|
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, protoGroup)
|
|
}
|
|
|
|
return protoUpdate
|
|
}
|
|
|
|
// AppendRemotePeerConfig appends typed peers as proto.RemotePeerConfig
|
|
// entries to dst and returns the result.
|
|
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig {
|
|
for _, rPeer := range peers {
|
|
allowedIPs := []string{rPeer.IP.String() + "/32"}
|
|
if includeIPv6 && rPeer.IPv6.IsValid() {
|
|
allowedIPs = append(allowedIPs, rPeer.IPv6.String()+"/128")
|
|
}
|
|
dst = append(dst, &proto.RemotePeerConfig{
|
|
WgPubKey: rPeer.Key,
|
|
AllowedIps: allowedIPs,
|
|
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
|
Fqdn: rPeer.FQDN(dnsName),
|
|
AgentVersion: rPeer.Meta.WtVersion,
|
|
})
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// BuildAuthorizedUsersProto deduplicates user-IDs into a hashed list and
|
|
// builds per-machine-user index maps. Returns (hashedUsers, machineUsers).
|
|
// Errors from individual hash failures are logged via the provided context;
|
|
// they leave the offending user out of the result but don't abort the build.
|
|
func BuildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]map[string]struct{}) ([][]byte, map[string]*proto.MachineUserIndexes) {
|
|
userIDToIndex := make(map[string]uint32)
|
|
var hashedUsers [][]byte
|
|
machineUsers := make(map[string]*proto.MachineUserIndexes, len(authorizedUsers))
|
|
|
|
for machineUser, users := range authorizedUsers {
|
|
indexes := make([]uint32, 0, len(users))
|
|
for userID := range users {
|
|
idx, exists := userIDToIndex[userID]
|
|
if !exists {
|
|
hash, err := sshauth.HashUserID(userID)
|
|
if err != nil {
|
|
log.WithContext(ctx).WithError(err).Error("failed to hash user id")
|
|
continue
|
|
}
|
|
idx = uint32(len(hashedUsers))
|
|
userIDToIndex[userID] = idx
|
|
hashedUsers = append(hashedUsers, hash[:])
|
|
}
|
|
indexes = append(indexes, idx)
|
|
}
|
|
machineUsers[machineUser] = &proto.MachineUserIndexes{Indexes: indexes}
|
|
}
|
|
|
|
return hashedUsers, machineUsers
|
|
}
|