netmaker/logic/egress.go
Abhishek K 307a3d1e4b
NET-1932: Merge egress and internet gateways (#3436)
* feat: api access tokens

* revoke all user tokens

* redefine access token api routes, add auto egress option to enrollment keys

* add server settings apis, add db table for settigs

* handle server settings updates

* switch to using settings from DB

* fix sever settings migration

* revet force migration for settings

* fix server settings database write

* egress model

* fix revoked tokens to be unauthorized

* update egress model

* remove unused functions

* convert access token to sql schema

* switch access token to sql schema

* fix merge conflicts

* fix server settings types

* bypass basic auth setting for super admin

* add TODO comment

* setup api handlers for egress revamp

* use single DB, fix update nat boolean field

* extend validaiton checks for egress ranges

* add migration to convert to new egress model

* fix panic interface conversion

* publish peer update on settings update

* revoke token generated by an user

* add user token creation restriction by user role

* add forbidden check for access token creation

* revoke user token when group or role is changed

* add default group to admin users on update

* chore(go): import style changes from migration branch;

1. Singular file names for table schema.
2. No table name method.
3. Use .Model instead of .Table.
4. No unnecessary tagging.

* remove nat check on egress gateway request

* Revert "remove nat check on egress gateway request"

This reverts commit 0aff12a189.

* remove nat check on egress gateway request

* feat(go): add db middleware;

* feat(go): restore method;

* feat(go): add user access token schema;

* add inet gw status to egress model

* fetch node ids in the tag, add inet gw info clients

* add inet gw info to node from egress list

* add migration logic internet gws

* create default acl policies

* add egress info

* add egress TODO

* add egress TODO

* fix user auth api:

* add reference id to acl policy

* add egress response from DB

* publish peer update on egress changes

* re initalise oauth and email config

* set verbosity

* normalise cidr on egress req

* add egress id to acl group

* change acls to use egress id

* resolve merge conflicts

* fix egress reference errors

* move egress model to schema

* add api context to DB

* sync auto update settings with hosts

* sync auto update settings with hosts

* check acl for egress node

* check for egress policy in the acl dst groups

* fix acl rules for egress policies with new models

* add status to egress model

* fix inet node func

* mask secret and convert jwt duration to minutes

* enable egress policies on creation

* convert jwt duration to minutes

* add relevant ranges to inet egress

* skip non active egress routes

* resolve merge conflicts

* fix static check

* update gorm tag for primary key on egress model

* create user policies for egress resources

* resolve merge conflicts

* get egress info on failover apis, add egress src validation for inet gws

* add additional validation checks on egress req

* add additional validation checks on egress req

* skip all resources for inet policy

* delete associated egress acl policies

* fix failover of inetclient

* avoid setting inet client asd inet gw

* fix all resource egress policy

* fix inet gw egress rule

* check for node egress on relay req

* fix egress acl rules comms

* add new field for egress info on node

* check acl policy in failover ctx

* avoid default host to be set as inet client

* fix relayed egress node

* add valid error messaging for egress validate func

* return if inet default host

* jump port detection to 51821

* check host ports on pull

* check user access gws via acls

* add validation check for default host and failover for inet clients

* add error messaging for acl policy check

* fix inet gw status

* ignore failover req for peer using inet gw

* check for allowed egress ranges for a peer

* add egress routes to static nodes by access

* avoid setting failvoer as inet client

* fix egress error messaging

* fix extclients egress comms

* fix inet gw acting as inet client

* return formatted error on update acl validation

* add default route for static nodes on inetclient

* check relay node acting as inetclient

* move inet node info to separate field, fix all resouces policy

* remove debug logs

---------

Co-authored-by: Vishal Dalwadi <dalwadivishal26@gmail.com>
2025-05-21 12:50:21 +05:30

366 lines
9 KiB
Go

package logic
import (
"context"
"encoding/json"
"errors"
"maps"
"net"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/schema"
)
func ValidateEgressReq(e *schema.Egress) error {
if e.Network == "" {
return errors.New("network id is empty")
}
_, err := GetNetwork(e.Network)
if err != nil {
return errors.New("failed to get network " + err.Error())
}
if !e.IsInetGw {
if e.Range == "" {
return errors.New("egress range is empty")
}
_, _, err = net.ParseCIDR(e.Range)
if err != nil {
return errors.New("invalid egress range " + err.Error())
}
err = ValidateEgressRange(e.Network, []string{e.Range})
if err != nil {
return errors.New("invalid egress range " + err.Error())
}
} else {
if len(e.Nodes) > 1 {
return errors.New("can only set one internet routing node")
}
req := models.InetNodeReq{}
for k := range e.Nodes {
inetNode, err := GetNodeByID(k)
if err != nil {
return errors.New("invalid routing node " + err.Error())
}
// check if node is acting as egress gw already
GetNodeEgressInfo(&inetNode)
if err := ValidateInetGwReq(inetNode, req, false); err != nil {
return err
}
}
}
if len(e.Nodes) != 0 {
for k := range e.Nodes {
_, err := GetNodeByID(k)
if err != nil {
return errors.New("invalid routing node " + err.Error())
}
}
}
return nil
}
func GetInetClientsFromAclPolicies(eID string) (inetClientIDs []string) {
e := schema.Egress{ID: eID}
err := e.Get(db.WithContext(context.TODO()))
if err != nil || !e.Status {
return
}
acls, _ := ListAclsByNetwork(models.NetworkID(e.Network))
for _, acl := range acls {
for _, dstI := range acl.Dst {
if dstI.ID == models.EgressID {
if dstI.Value != eID {
continue
}
for _, srcI := range acl.Src {
if srcI.Value == "*" {
continue
}
if srcI.ID == models.NodeID {
inetClientIDs = append(inetClientIDs, srcI.Value)
}
if srcI.ID == models.NodeTagID {
inetClientIDs = append(inetClientIDs, GetNodeIDsWithTag(models.TagID(srcI.Value))...)
}
}
}
}
}
return
}
func isNodeUsingInternetGw(node *models.Node) {
host, err := GetHost(node.HostID.String())
if err != nil {
return
}
if host.IsDefault || node.IsFailOver {
return
}
nodeTags := maps.Clone(node.Tags)
nodeTags[models.TagID(node.ID.String())] = struct{}{}
acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
var isUsing bool
for _, acl := range acls {
if !acl.Enabled {
continue
}
srcVal := convAclTagToValueMap(acl.Src)
for _, dstI := range acl.Dst {
if dstI.ID == models.EgressID {
e := schema.Egress{ID: dstI.Value}
err := e.Get(db.WithContext(context.TODO()))
if err != nil || !e.Status {
continue
}
if e.IsInetGw {
if _, ok := srcVal[node.ID.String()]; ok {
for nodeID := range e.Nodes {
if nodeID == node.ID.String() {
continue
}
node.EgressDetails.InternetGwID = nodeID
isUsing = true
return
}
}
for tagID := range nodeTags {
if _, ok := srcVal[tagID.String()]; ok {
for nodeID := range e.Nodes {
if nodeID == node.ID.String() {
continue
}
node.EgressDetails.InternetGwID = nodeID
isUsing = true
return
}
}
}
}
}
}
}
if !isUsing {
node.EgressDetails.InternetGwID = ""
}
}
func DoesNodeHaveAccessToEgress(node *models.Node, e *schema.Egress) bool {
nodeTags := maps.Clone(node.Tags)
nodeTags[models.TagID(node.ID.String())] = struct{}{}
if !e.IsInetGw {
nodeTags[models.TagID("*")] = struct{}{}
}
acls, _ := ListAclsByNetwork(models.NetworkID(node.Network))
if !e.IsInetGw {
defaultDevicePolicy, _ := GetDefaultPolicy(models.NetworkID(node.Network), models.DevicePolicy)
if defaultDevicePolicy.Enabled {
return true
}
}
for _, acl := range acls {
if !acl.Enabled {
continue
}
srcVal := convAclTagToValueMap(acl.Src)
if !e.IsInetGw && acl.AllowedDirection == models.TrafficDirectionBi {
if _, ok := srcVal["*"]; ok {
return true
}
}
for _, dstI := range acl.Dst {
if !e.IsInetGw && dstI.ID == models.NodeTagID && dstI.Value == "*" {
return true
}
if dstI.ID == models.EgressID && dstI.Value == e.ID {
e := schema.Egress{ID: dstI.Value}
err := e.Get(db.WithContext(context.TODO()))
if err != nil {
continue
}
if node.IsStatic {
if _, ok := srcVal[node.StaticNode.ClientID]; ok {
return true
}
} else {
if _, ok := srcVal[node.ID.String()]; ok {
return true
}
}
for tagID := range nodeTags {
if _, ok := srcVal[tagID.String()]; ok {
return true
}
}
}
}
}
return false
}
func AddEgressInfoToPeerByAccess(node, targetNode *models.Node) {
eli, _ := (&schema.Egress{Network: targetNode.Network}).ListByNetwork(db.WithContext(context.TODO()))
req := models.EgressGatewayRequest{
NodeID: targetNode.ID.String(),
NetID: targetNode.Network,
}
defer func() {
if targetNode.Mutex != nil {
targetNode.Mutex.Lock()
}
isNodeUsingInternetGw(targetNode)
if targetNode.Mutex != nil {
targetNode.Mutex.Unlock()
}
}()
for _, e := range eli {
if !e.Status || e.Network != targetNode.Network {
continue
}
if !DoesNodeHaveAccessToEgress(node, &e) {
if node.IsRelayed && node.RelayedBy == targetNode.ID.String() {
if !DoesNodeHaveAccessToEgress(targetNode, &e) {
continue
}
} else {
continue
}
}
if metric, ok := e.Nodes[targetNode.ID.String()]; ok {
if e.IsInetGw {
targetNode.EgressDetails.IsInternetGateway = true
targetNode.EgressDetails.InetNodeReq = models.InetNodeReq{
InetNodeClientIDs: GetInetClientsFromAclPolicies(e.ID),
}
req.Ranges = append(req.Ranges, "0.0.0.0/0")
req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
Network: "0.0.0.0/0",
Nat: true,
RouteMetric: 256,
})
req.Ranges = append(req.Ranges, "::/0")
req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
Network: "::/0",
Nat: true,
RouteMetric: 256,
})
} else {
m64, err := metric.(json.Number).Int64()
if err != nil {
m64 = 256
}
m := uint32(m64)
req.Ranges = append(req.Ranges, e.Range)
req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
Network: e.Range,
Nat: e.Nat,
RouteMetric: m,
})
}
}
}
if targetNode.Mutex != nil {
targetNode.Mutex.Lock()
}
if len(req.Ranges) > 0 {
targetNode.EgressDetails.IsEgressGateway = true
targetNode.EgressDetails.EgressGatewayRanges = req.Ranges
targetNode.EgressDetails.EgressGatewayRequest = req
} else {
targetNode.EgressDetails = models.EgressDetails{}
}
if targetNode.Mutex != nil {
targetNode.Mutex.Unlock()
}
}
func GetNodeEgressInfo(targetNode *models.Node) {
eli, _ := (&schema.Egress{Network: targetNode.Network}).ListByNetwork(db.WithContext(context.TODO()))
req := models.EgressGatewayRequest{
NodeID: targetNode.ID.String(),
NetID: targetNode.Network,
}
defer func() {
if targetNode.Mutex != nil {
targetNode.Mutex.Lock()
}
isNodeUsingInternetGw(targetNode)
if targetNode.Mutex != nil {
targetNode.Mutex.Unlock()
}
}()
for _, e := range eli {
if !e.Status || e.Network != targetNode.Network {
continue
}
if metric, ok := e.Nodes[targetNode.ID.String()]; ok {
if e.IsInetGw {
targetNode.EgressDetails.IsInternetGateway = true
targetNode.EgressDetails.InetNodeReq = models.InetNodeReq{
InetNodeClientIDs: GetInetClientsFromAclPolicies(e.ID),
}
req.Ranges = append(req.Ranges, "0.0.0.0/0")
req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
Network: "0.0.0.0/0",
Nat: true,
RouteMetric: 256,
})
req.Ranges = append(req.Ranges, "::/0")
req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
Network: "::/0",
Nat: true,
RouteMetric: 256,
})
} else {
m64, err := metric.(json.Number).Int64()
if err != nil {
m64 = 256
}
m := uint32(m64)
req.Ranges = append(req.Ranges, e.Range)
req.RangesWithMetric = append(req.RangesWithMetric, models.EgressRangeMetric{
Network: e.Range,
Nat: e.Nat,
RouteMetric: m,
})
}
}
}
if targetNode.Mutex != nil {
targetNode.Mutex.Lock()
}
if len(req.Ranges) > 0 {
targetNode.EgressDetails.IsEgressGateway = true
targetNode.EgressDetails.EgressGatewayRanges = req.Ranges
targetNode.EgressDetails.EgressGatewayRequest = req
} else {
targetNode.EgressDetails = models.EgressDetails{}
}
if targetNode.Mutex != nil {
targetNode.Mutex.Unlock()
}
}
func RemoveNodeFromEgress(node models.Node) {
egs, _ := (&schema.Egress{}).ListByNetwork(db.WithContext(context.TODO()))
for _, egI := range egs {
if _, ok := egI.Nodes[node.ID.String()]; ok {
delete(egI.Nodes, node.ID.String())
egI.Update(db.WithContext(context.TODO()))
}
}
}