mirror of
https://github.com/netbirdio/netbird.git
synced 2025-02-14 17:29:39 +01:00
538 lines
17 KiB
Go
538 lines
17 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/netip"
|
|
"slices"
|
|
"unicode/utf8"
|
|
|
|
"github.com/rs/xid"
|
|
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
|
|
"github.com/netbirdio/netbird/management/domain"
|
|
"github.com/netbirdio/netbird/management/proto"
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
|
"github.com/netbirdio/netbird/management/server/status"
|
|
"github.com/netbirdio/netbird/route"
|
|
)
|
|
|
|
// GetRoute gets a route object from account and route IDs
|
|
func (am *DefaultAccountManager) GetRoute(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error) {
|
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user.AccountID != accountID {
|
|
return nil, status.NewUserNotPartOfAccountError()
|
|
}
|
|
|
|
if user.IsRegularUser() {
|
|
return nil, status.NewAdminPermissionError()
|
|
}
|
|
|
|
return am.Store.GetRouteByID(ctx, store.LockingStrengthShare, accountID, string(routeID))
|
|
}
|
|
|
|
// GetRoutesByPrefixOrDomains return list of routes by account and route prefix
|
|
func getRoutesByPrefixOrDomains(ctx context.Context, transaction store.Store, accountID string, prefix netip.Prefix, domains domain.List) ([]*route.Route, error) {
|
|
accountRoutes, err := transaction.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
routes := make([]*route.Route, 0)
|
|
for _, r := range accountRoutes {
|
|
dynamic := r.IsDynamic()
|
|
if dynamic && r.Domains.PunycodeString() == domains.PunycodeString() ||
|
|
!dynamic && r.Network.String() == prefix.String() {
|
|
routes = append(routes, r)
|
|
}
|
|
}
|
|
|
|
return routes, nil
|
|
}
|
|
|
|
// checkRoutePrefixOrDomainsExistForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups.
|
|
func checkRoutePrefixOrDomainsExistForPeers(ctx context.Context, transaction store.Store, accountID string, checkRoute *route.Route, groupsMap map[string]*types.Group) error {
|
|
// routes can have both peer and peer_groups
|
|
prefix := checkRoute.Network
|
|
domains := checkRoute.Domains
|
|
|
|
routesWithPrefix, err := getRoutesByPrefixOrDomains(ctx, transaction, accountID, prefix, domains)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// lets remember all the peers and the peer groups from routesWithPrefix
|
|
seenPeers := make(map[string]bool)
|
|
seenPeerGroups := make(map[string]bool)
|
|
|
|
for _, prefixRoute := range routesWithPrefix {
|
|
// we skip route(s) with the same network ID as we want to allow updating of the existing route
|
|
// when creating a new route routeID is newly generated so nothing will be skipped
|
|
if checkRoute.ID == prefixRoute.ID {
|
|
continue
|
|
}
|
|
|
|
if prefixRoute.Peer != "" {
|
|
seenPeers[string(prefixRoute.ID)] = true
|
|
}
|
|
|
|
peerGroupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, prefixRoute.PeerGroups)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, groupID := range prefixRoute.PeerGroups {
|
|
seenPeerGroups[groupID] = true
|
|
|
|
group, ok := peerGroupsMap[groupID]
|
|
if !ok || group == nil {
|
|
return status.Errorf(
|
|
status.InvalidArgument, "failed to add route with %s - peer group %s doesn't exist",
|
|
getRouteDescriptor(prefix, domains), groupID,
|
|
)
|
|
}
|
|
|
|
for _, pID := range group.Peers {
|
|
seenPeers[pID] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if peerID := checkRoute.Peer; peerID != "" {
|
|
// check that peerID exists and is not in any route as single peer or part of the group
|
|
_, err = transaction.GetPeerByID(context.Background(), store.LockingStrengthShare, accountID, peerID)
|
|
if err != nil {
|
|
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
|
}
|
|
|
|
if _, ok := seenPeers[peerID]; ok {
|
|
return status.Errorf(status.AlreadyExists,
|
|
"failed to add route with %s - peer %s already has this route", getRouteDescriptor(prefix, domains), peerID)
|
|
}
|
|
}
|
|
|
|
// check that peerGroupIDs are not in any route peerGroups list
|
|
for _, groupID := range checkRoute.PeerGroups {
|
|
group := groupsMap[groupID] // we validated the group existence before entering this function, no need to check again.
|
|
if _, ok := seenPeerGroups[groupID]; ok {
|
|
return status.Errorf(
|
|
status.AlreadyExists, "failed to add route with %s - peer group %s already has this route",
|
|
getRouteDescriptor(prefix, domains), group.Name)
|
|
}
|
|
|
|
// check that the peers from peerGroupIDs groups are not the same peers we saw in routesWithPrefix
|
|
peersMap, err := transaction.GetPeersByIDs(ctx, store.LockingStrengthShare, accountID, group.Peers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, id := range group.Peers {
|
|
if _, ok := seenPeers[id]; ok {
|
|
peer, ok := peersMap[id]
|
|
if !ok || peer == nil {
|
|
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", id)
|
|
}
|
|
|
|
return status.Errorf(status.AlreadyExists,
|
|
"failed to add route with %s - peer %s from the group %s already has this route",
|
|
getRouteDescriptor(prefix, domains), peer.Name, group.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getRouteDescriptor(prefix netip.Prefix, domains domain.List) string {
|
|
if len(domains) > 0 {
|
|
return fmt.Sprintf("domains [%s]", domains.SafeString())
|
|
}
|
|
return fmt.Sprintf("prefix %s", prefix.String())
|
|
}
|
|
|
|
// CreateRoute creates and saves a new route
|
|
func (am *DefaultAccountManager) CreateRoute(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool) (*route.Route, error) {
|
|
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
|
|
defer unlock()
|
|
|
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user.AccountID != accountID {
|
|
return nil, status.NewUserNotPartOfAccountError()
|
|
}
|
|
|
|
var newRoute *route.Route
|
|
var updateAccountPeers bool
|
|
|
|
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
|
newRoute = &route.Route{
|
|
ID: route.ID(xid.New().String()),
|
|
AccountID: accountID,
|
|
Network: prefix,
|
|
Domains: domains,
|
|
KeepRoute: keepRoute,
|
|
NetID: netID,
|
|
Description: description,
|
|
Peer: peerID,
|
|
PeerGroups: peerGroupIDs,
|
|
NetworkType: networkType,
|
|
Masquerade: masquerade,
|
|
Metric: metric,
|
|
Enabled: enabled,
|
|
Groups: groups,
|
|
AccessControlGroups: accessControlGroupIDs,
|
|
}
|
|
|
|
if err = validateRoute(ctx, transaction, accountID, newRoute); err != nil {
|
|
return err
|
|
}
|
|
|
|
updateAccountPeers, err = areRouteChangesAffectPeers(ctx, transaction, newRoute)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return transaction.SaveRoute(ctx, store.LockingStrengthUpdate, newRoute)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
am.StoreEvent(ctx, userID, string(newRoute.ID), accountID, activity.RouteCreated, newRoute.EventMeta())
|
|
|
|
if updateAccountPeers {
|
|
am.UpdateAccountPeers(ctx, accountID)
|
|
}
|
|
|
|
return newRoute, nil
|
|
}
|
|
|
|
// SaveRoute saves route
|
|
func (am *DefaultAccountManager) SaveRoute(ctx context.Context, accountID, userID string, routeToSave *route.Route) error {
|
|
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
|
|
defer unlock()
|
|
|
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if user.AccountID != accountID {
|
|
return status.NewUserNotPartOfAccountError()
|
|
}
|
|
|
|
var oldRoute *route.Route
|
|
var oldRouteAffectsPeers bool
|
|
var newRouteAffectsPeers bool
|
|
|
|
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
|
if err = validateRoute(ctx, transaction, accountID, routeToSave); err != nil {
|
|
return err
|
|
}
|
|
|
|
oldRoute, err = transaction.GetRouteByID(ctx, store.LockingStrengthUpdate, accountID, string(routeToSave.ID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldRouteAffectsPeers, err = areRouteChangesAffectPeers(ctx, transaction, oldRoute)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newRouteAffectsPeers, err = areRouteChangesAffectPeers(ctx, transaction, routeToSave)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
routeToSave.AccountID = accountID
|
|
|
|
if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return transaction.SaveRoute(ctx, store.LockingStrengthUpdate, routeToSave)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
am.StoreEvent(ctx, userID, string(routeToSave.ID), accountID, activity.RouteUpdated, routeToSave.EventMeta())
|
|
|
|
if oldRouteAffectsPeers || newRouteAffectsPeers {
|
|
am.UpdateAccountPeers(ctx, accountID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteRoute deletes route with routeID
|
|
func (am *DefaultAccountManager) DeleteRoute(ctx context.Context, accountID string, routeID route.ID, userID string) error {
|
|
unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
|
|
defer unlock()
|
|
|
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if user.AccountID != accountID {
|
|
return status.NewUserNotPartOfAccountError()
|
|
}
|
|
|
|
var route *route.Route
|
|
var updateAccountPeers bool
|
|
|
|
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
|
route, err = transaction.GetRouteByID(ctx, store.LockingStrengthUpdate, accountID, string(routeID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updateAccountPeers, err = areRouteChangesAffectPeers(ctx, transaction, route)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = transaction.IncrementNetworkSerial(ctx, store.LockingStrengthUpdate, accountID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return transaction.DeleteRoute(ctx, store.LockingStrengthUpdate, accountID, string(routeID))
|
|
})
|
|
|
|
am.StoreEvent(ctx, userID, string(route.ID), accountID, activity.RouteRemoved, route.EventMeta())
|
|
|
|
if updateAccountPeers {
|
|
am.UpdateAccountPeers(ctx, accountID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListRoutes returns a list of routes from account
|
|
func (am *DefaultAccountManager) ListRoutes(ctx context.Context, accountID, userID string) ([]*route.Route, error) {
|
|
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user.AccountID != accountID {
|
|
return nil, status.NewUserNotPartOfAccountError()
|
|
}
|
|
|
|
if user.IsRegularUser() {
|
|
return nil, status.NewAdminPermissionError()
|
|
}
|
|
|
|
return am.Store.GetAccountRoutes(ctx, store.LockingStrengthShare, accountID)
|
|
}
|
|
|
|
func validateRoute(ctx context.Context, transaction store.Store, accountID string, routeToSave *route.Route) error {
|
|
if routeToSave == nil {
|
|
return status.Errorf(status.InvalidArgument, "route provided is nil")
|
|
}
|
|
|
|
if err := validateRouteProperties(routeToSave); err != nil {
|
|
return err
|
|
}
|
|
|
|
if routeToSave.Peer != "" {
|
|
peer, err := transaction.GetPeerByID(ctx, store.LockingStrengthShare, accountID, routeToSave.Peer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if peer.Meta.GoOS != "linux" {
|
|
return status.Errorf(status.InvalidArgument, "non-linux peers are not supported as network routes")
|
|
}
|
|
}
|
|
|
|
groupsMap, err := validateRouteGroups(ctx, transaction, accountID, routeToSave)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkRoutePrefixOrDomainsExistForPeers(ctx, transaction, accountID, routeToSave, groupsMap)
|
|
}
|
|
|
|
// Helper to validate route properties.
|
|
func validateRouteProperties(routeToSave *route.Route) error {
|
|
if routeToSave.Metric < route.MinMetric || routeToSave.Metric > route.MaxMetric {
|
|
return status.Errorf(status.InvalidArgument, "metric should be between %d and %d", route.MinMetric, route.MaxMetric)
|
|
}
|
|
|
|
if utf8.RuneCountInString(string(routeToSave.NetID)) > route.MaxNetIDChar || routeToSave.NetID == "" {
|
|
return status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar)
|
|
}
|
|
|
|
if len(routeToSave.Domains) > 0 && routeToSave.Network.IsValid() {
|
|
return status.Errorf(status.InvalidArgument, "domains and network should not be provided at the same time")
|
|
}
|
|
|
|
if len(routeToSave.Domains) == 0 && !routeToSave.Network.IsValid() {
|
|
return status.Errorf(status.InvalidArgument, "invalid Prefix")
|
|
}
|
|
|
|
if len(routeToSave.Domains) > 0 {
|
|
routeToSave.Network = getPlaceholderIP()
|
|
}
|
|
|
|
if routeToSave.Peer != "" && len(routeToSave.PeerGroups) != 0 {
|
|
return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateRouteGroups validates the route groups and returns the validated groups map.
|
|
func validateRouteGroups(ctx context.Context, transaction store.Store, accountID string, routeToSave *route.Route) (map[string]*types.Group, error) {
|
|
groupsToValidate := slices.Concat(routeToSave.Groups, routeToSave.PeerGroups, routeToSave.AccessControlGroups)
|
|
groupsMap, err := transaction.GetGroupsByIDs(ctx, store.LockingStrengthShare, accountID, groupsToValidate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(routeToSave.PeerGroups) > 0 {
|
|
if err = validateGroups(routeToSave.PeerGroups, groupsMap); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(routeToSave.AccessControlGroups) > 0 {
|
|
if err = validateGroups(routeToSave.AccessControlGroups, groupsMap); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err = validateGroups(routeToSave.Groups, groupsMap); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return groupsMap, nil
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// getPlaceholderIP returns a placeholder IP address for the route if domains are used
|
|
func getPlaceholderIP() netip.Prefix {
|
|
// Using an IP from the documentation range to minimize impact in case older clients try to set a route
|
|
return netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32)
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// areRouteChangesAffectPeers checks if a given route affects peers by determining
|
|
// if it has a routing peer, distribution, or peer groups that include peers.
|
|
func areRouteChangesAffectPeers(ctx context.Context, transaction store.Store, route *route.Route) (bool, error) {
|
|
if route.Peer != "" {
|
|
return true, nil
|
|
}
|
|
|
|
hasPeers, err := anyGroupHasPeersOrResources(ctx, transaction, route.AccountID, route.Groups)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if hasPeers {
|
|
return true, nil
|
|
}
|
|
|
|
return anyGroupHasPeersOrResources(ctx, transaction, route.AccountID, route.PeerGroups)
|
|
}
|