mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
Auto-update logic moved out of the UI into a dedicated updatemanager.Manager service that runs in the connection layer. The UI no longer polls or checks for updates independently. The update manager supports three modes driven by the management server's auto-update policy: No policy set by mgm: checks GitHub for the latest version and notifies the user (previous behavior, now centralized) mgm enforces update: the "About" menu triggers installation directly instead of just downloading the file — user still initiates the action mgm forces update: installation proceeds automatically without user interaction updateManager lifecycle is now owned by daemon, giving the daemon server direct control via a new TriggerUpdate RPC Introduces EngineServices struct to group external service dependencies passed to NewEngine, reducing its argument count from 11 to 4
465 lines
15 KiB
Go
465 lines
15 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
|
"github.com/netbirdio/netbird/client/ssh/auth"
|
|
|
|
nbdns "github.com/netbirdio/netbird/dns"
|
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/management/server/posture"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/route"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
"github.com/netbirdio/netbird/shared/sshauth"
|
|
)
|
|
|
|
func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings) *proto.NetbirdConfig {
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
|
|
var stuns []*proto.HostConfig
|
|
for _, stun := range config.Stuns {
|
|
stuns = append(stuns, &proto.HostConfig{
|
|
Uri: stun.URI,
|
|
Protocol: ToResponseProto(stun.Proto),
|
|
})
|
|
}
|
|
|
|
var turns []*proto.ProtectedHostConfig
|
|
if config.TURNConfig != nil {
|
|
for _, turn := range config.TURNConfig.Turns {
|
|
var username string
|
|
var password string
|
|
if turnCredentials != nil {
|
|
username = turnCredentials.Payload
|
|
password = turnCredentials.Signature
|
|
} else {
|
|
username = turn.Username
|
|
password = turn.Password
|
|
}
|
|
turns = append(turns, &proto.ProtectedHostConfig{
|
|
HostConfig: &proto.HostConfig{
|
|
Uri: turn.URI,
|
|
Protocol: ToResponseProto(turn.Proto),
|
|
},
|
|
User: username,
|
|
Password: password,
|
|
})
|
|
}
|
|
}
|
|
|
|
var relayCfg *proto.RelayConfig
|
|
if config.Relay != nil && len(config.Relay.Addresses) > 0 {
|
|
relayCfg = &proto.RelayConfig{
|
|
Urls: config.Relay.Addresses,
|
|
}
|
|
|
|
if relayToken != nil {
|
|
relayCfg.TokenPayload = relayToken.Payload
|
|
relayCfg.TokenSignature = relayToken.Signature
|
|
}
|
|
}
|
|
|
|
var signalCfg *proto.HostConfig
|
|
if config.Signal != nil {
|
|
signalCfg = &proto.HostConfig{
|
|
Uri: config.Signal.URI,
|
|
Protocol: ToResponseProto(config.Signal.Proto),
|
|
}
|
|
}
|
|
|
|
nbConfig := &proto.NetbirdConfig{
|
|
Stuns: stuns,
|
|
Turns: turns,
|
|
Signal: signalCfg,
|
|
Relay: relayCfg,
|
|
}
|
|
|
|
return nbConfig
|
|
}
|
|
|
|
func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool) *proto.PeerConfig {
|
|
netmask, _ := network.Net.Mask.Size()
|
|
fqdn := peer.FQDN(dnsName)
|
|
|
|
sshConfig := &proto.SSHConfig{
|
|
SshEnabled: peer.SSHEnabled || enableSSH,
|
|
}
|
|
|
|
if sshConfig.SshEnabled {
|
|
sshConfig.JwtConfig = buildJWTConfig(httpConfig, deviceFlowConfig)
|
|
}
|
|
|
|
return &proto.PeerConfig{
|
|
Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask),
|
|
SshConfig: sshConfig,
|
|
Fqdn: fqdn,
|
|
RoutingPeerDnsResolutionEnabled: settings.RoutingPeerDNSResolutionEnabled,
|
|
LazyConnectionEnabled: settings.LazyConnectionEnabled,
|
|
AutoUpdate: &proto.AutoUpdateSettings{
|
|
Version: settings.AutoUpdateVersion,
|
|
AlwaysUpdate: settings.AutoUpdateAlways,
|
|
},
|
|
}
|
|
}
|
|
|
|
func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse {
|
|
response := &proto.SyncResponse{
|
|
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
|
NetworkMap: &proto.NetworkMap{
|
|
Serial: networkMap.Network.CurrentSerial(),
|
|
Routes: toProtocolRoutes(networkMap.Routes),
|
|
DNSConfig: toProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort),
|
|
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
|
},
|
|
Checks: toProtocolChecks(ctx, checks),
|
|
}
|
|
|
|
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings)
|
|
extendedConfig := integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings)
|
|
response.NetbirdConfig = extendedConfig
|
|
|
|
response.NetworkMap.PeerConfig = response.PeerConfig
|
|
|
|
remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers))
|
|
remotePeers = appendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName)
|
|
response.RemotePeers = remotePeers
|
|
response.NetworkMap.RemotePeers = remotePeers
|
|
response.RemotePeersIsEmpty = len(remotePeers) == 0
|
|
response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty
|
|
|
|
response.NetworkMap.OfflinePeers = appendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName)
|
|
|
|
firewallRules := toProtocolFirewallRules(networkMap.FirewallRules)
|
|
response.NetworkMap.FirewallRules = firewallRules
|
|
response.NetworkMap.FirewallRulesIsEmpty = len(firewallRules) == 0
|
|
|
|
routesFirewallRules := toProtocolRoutesFirewallRules(networkMap.RoutesFirewallRules)
|
|
response.NetworkMap.RoutesFirewallRules = routesFirewallRules
|
|
response.NetworkMap.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
|
|
|
if networkMap.ForwardingRules != nil {
|
|
forwardingRules := make([]*proto.ForwardingRule, 0, len(networkMap.ForwardingRules))
|
|
for _, rule := range networkMap.ForwardingRules {
|
|
forwardingRules = append(forwardingRules, rule.ToProto())
|
|
}
|
|
response.NetworkMap.ForwardingRules = forwardingRules
|
|
}
|
|
|
|
if networkMap.AuthorizedUsers != nil {
|
|
hashedUsers, machineUsers := buildAuthorizedUsersProto(ctx, networkMap.AuthorizedUsers)
|
|
userIDClaim := auth.DefaultUserIDClaim
|
|
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
|
userIDClaim = httpConfig.AuthUserIDClaim
|
|
}
|
|
response.NetworkMap.SshAuth = &proto.SSHAuth{AuthorizedUsers: hashedUsers, MachineUsers: machineUsers, UserIDClaim: userIDClaim}
|
|
}
|
|
|
|
return response
|
|
}
|
|
|
|
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).Errorf("failed to hash user id %s: %v", userID, err)
|
|
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
|
|
}
|
|
|
|
func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string) []*proto.RemotePeerConfig {
|
|
for _, rPeer := range peers {
|
|
dst = append(dst, &proto.RemotePeerConfig{
|
|
WgPubKey: rPeer.Key,
|
|
AllowedIps: []string{rPeer.IP.String() + "/32"},
|
|
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
|
Fqdn: rPeer.FQDN(dnsName),
|
|
AgentVersion: rPeer.Meta.WtVersion,
|
|
})
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// toProtocolDNSConfig converts nbdns.Config to proto.DNSConfig using the cache
|
|
func toProtocolDNSConfig(update nbdns.Config, cache *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 {
|
|
protoZone := convertToProtoCustomZone(zone)
|
|
protoUpdate.CustomZones = append(protoUpdate.CustomZones, protoZone)
|
|
}
|
|
|
|
for _, nsGroup := range update.NameServerGroups {
|
|
cacheKey := nsGroup.ID
|
|
if cachedGroup, exists := cache.GetNameServerGroup(cacheKey); exists {
|
|
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, cachedGroup)
|
|
} else {
|
|
protoGroup := convertToProtoNameServerGroup(nsGroup)
|
|
cache.SetNameServerGroup(cacheKey, protoGroup)
|
|
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, protoGroup)
|
|
}
|
|
}
|
|
|
|
return protoUpdate
|
|
}
|
|
|
|
func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
|
switch configProto {
|
|
case nbconfig.UDP:
|
|
return proto.HostConfig_UDP
|
|
case nbconfig.DTLS:
|
|
return proto.HostConfig_DTLS
|
|
case nbconfig.HTTP:
|
|
return proto.HostConfig_HTTP
|
|
case nbconfig.HTTPS:
|
|
return proto.HostConfig_HTTPS
|
|
case nbconfig.TCP:
|
|
return proto.HostConfig_TCP
|
|
default:
|
|
panic(fmt.Errorf("unexpected config protocol type %v", configProto))
|
|
}
|
|
}
|
|
|
|
func toProtocolRoutes(routes []*route.Route) []*proto.Route {
|
|
protoRoutes := make([]*proto.Route, 0, len(routes))
|
|
for _, r := range routes {
|
|
protoRoutes = append(protoRoutes, toProtocolRoute(r))
|
|
}
|
|
return protoRoutes
|
|
}
|
|
|
|
func toProtocolRoute(route *route.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 firewall rules.
|
|
func toProtocolFirewallRules(rules []*types.FirewallRule) []*proto.FirewallRule {
|
|
result := make([]*proto.FirewallRule, len(rules))
|
|
for i := range rules {
|
|
rule := rules[i]
|
|
|
|
fwRule := &proto.FirewallRule{
|
|
PolicyID: []byte(rule.PolicyID),
|
|
PeerIP: rule.PeerIP,
|
|
Direction: getProtoDirection(rule.Direction),
|
|
Action: getProtoAction(rule.Action),
|
|
Protocol: getProtoProtocol(rule.Protocol),
|
|
Port: rule.Port,
|
|
}
|
|
|
|
if shouldUsePortRange(fwRule) {
|
|
fwRule.PortInfo = rule.PortRange.ToProto()
|
|
}
|
|
|
|
result[i] = fwRule
|
|
}
|
|
return result
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
default:
|
|
return proto.RuleProtocol_UNKNOWN
|
|
}
|
|
}
|
|
|
|
// getProtoPortInfo converts the 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
|
|
}
|
|
|
|
func shouldUsePortRange(rule *proto.FirewallRule) bool {
|
|
return rule.Port == "" && (rule.Protocol == proto.RuleProtocol_UDP || rule.Protocol == proto.RuleProtocol_TCP)
|
|
}
|
|
|
|
// Helper function to convert nbdns.CustomZone to proto.CustomZone
|
|
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
|
|
}
|
|
|
|
// Helper function to convert nbdns.NameServerGroup to proto.NameServerGroup
|
|
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
|
|
}
|
|
|
|
// buildJWTConfig constructs JWT configuration for SSH servers from management server config
|
|
func buildJWTConfig(config *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow) *proto.JWTConfig {
|
|
if config == nil || config.AuthAudience == "" {
|
|
return nil
|
|
}
|
|
|
|
issuer := strings.TrimSpace(config.AuthIssuer)
|
|
if issuer == "" && deviceFlowConfig != nil {
|
|
if d := deriveIssuerFromTokenEndpoint(deviceFlowConfig.ProviderConfig.TokenEndpoint); d != "" {
|
|
issuer = d
|
|
}
|
|
}
|
|
if issuer == "" {
|
|
return nil
|
|
}
|
|
|
|
keysLocation := strings.TrimSpace(config.AuthKeysLocation)
|
|
if keysLocation == "" {
|
|
keysLocation = strings.TrimSuffix(issuer, "/") + "/.well-known/jwks.json"
|
|
}
|
|
|
|
audience := config.AuthAudience
|
|
if config.CLIAuthAudience != "" {
|
|
audience = config.CLIAuthAudience
|
|
}
|
|
|
|
audiences := []string{config.AuthAudience}
|
|
if config.CLIAuthAudience != "" && config.CLIAuthAudience != config.AuthAudience {
|
|
audiences = append(audiences, config.CLIAuthAudience)
|
|
}
|
|
|
|
return &proto.JWTConfig{
|
|
Issuer: issuer,
|
|
Audience: audience,
|
|
Audiences: audiences,
|
|
KeysLocation: keysLocation,
|
|
}
|
|
}
|
|
|
|
// deriveIssuerFromTokenEndpoint extracts the issuer URL from a token endpoint
|
|
func deriveIssuerFromTokenEndpoint(tokenEndpoint string) string {
|
|
if tokenEndpoint == "" {
|
|
return ""
|
|
}
|
|
|
|
u, err := url.Parse(tokenEndpoint)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return fmt.Sprintf("%s://%s/", u.Scheme, u.Host)
|
|
}
|