Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NET-1980: Egress HA routing #3352

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
11 changes: 11 additions & 0 deletions cli/cmd/ext_client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ var extClientConfigCmd = &cobra.Command{
},
}

var extClientHAConfigCmd = &cobra.Command{
Use: "auto_config [NETWORK NAME]",
Args: cobra.ExactArgs(1),
Short: "Get an External Client Configuration",
Long: `Get an External Client Configuration`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(functions.GetExtClientHAConfig(args[0]))
},
}

func init() {
rootCmd.AddCommand(extClientConfigCmd)
rootCmd.AddCommand(extClientHAConfigCmd)
}
5 changes: 5 additions & 0 deletions cli/functions/ext_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func GetExtClientConfig(networkName, clientID string) string {
return get(fmt.Sprintf("/api/extclients/%s/%s/file", networkName, clientID))
}

// GetExtClientConfig - auto fetch a client config
func GetExtClientHAConfig(networkName string) string {
return get(fmt.Sprintf("/api/v1/client_conf/%s", networkName))
}

// CreateExtClient - create an external client
func CreateExtClient(networkName, nodeID string, extClient models.CustomExtClient) {
request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), extClient)
Expand Down
246 changes: 246 additions & 0 deletions controllers/ext_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func extClientHandlers(r *mux.Router) {
Methods(http.MethodDelete)
r.HandleFunc("/api/extclients/{network}/{nodeid}", logic.SecurityCheck(false, checkFreeTierLimits(limitChoiceMachines, http.HandlerFunc(createExtClient)))).
Methods(http.MethodPost)
r.HandleFunc("/api/v1/client_conf/{network}", logic.SecurityCheck(false, http.HandlerFunc(getExtClientHAConf))).Methods(http.MethodGet)
}

func checkIngressExists(nodeID string) bool {
Expand Down Expand Up @@ -387,6 +388,251 @@ Endpoint = %s
json.NewEncoder(w).Encode(client)
}

// @Summary Get an individual remote access client
// @Router /api/extclients/{network}/{clientid}/{type} [get]
// @Tags Remote Access Client
// @Security oauth2
// @Success 200 {object} models.ExtClient
// @Failure 500 {object} models.ErrorResponse
// @Failure 403 {object} models.ErrorResponse
func getExtClientHAConf(w http.ResponseWriter, r *http.Request) {

var params = mux.Vars(r)
networkid := params["network"]
network, err := logic.GetParentNetwork(networkid)
if err != nil {
logger.Log(
1,
r.Header.Get("user"),
"Could not retrieve Ingress Gateway Network",
networkid,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
// fetch client based on availability
nodes, _ := logic.GetNetworkNodes(networkid)
defaultPolicy, _ := logic.GetDefaultPolicy(models.NetworkID(networkid), models.DevicePolicy)
var targetGwID string
var connectionCnt int = -1
for _, nodeI := range nodes {
if nodeI.IsGw {
// check health status
logic.GetNodeStatus(&nodeI, defaultPolicy.Enabled)
if nodeI.Status != models.OnlineSt {
continue
}
// Get Total connections on the gw
clients := logic.GetGwExtclients(nodeI.ID.String(), networkid)

if connectionCnt == -1 || len(clients) < connectionCnt {
connectionCnt = len(clients)
targetGwID = nodeI.ID.String()
}

}
}
gwnode, err := logic.GetNodeByID(targetGwID)
if err != nil {
logger.Log(
0,
r.Header.Get("user"),
fmt.Sprintf(
"failed to get ingress gateway node [%s] info: %v",
gwnode.ID,
err,
),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
host, err := logic.GetHost(gwnode.HostID.String())
if err != nil {
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to get ingress gateway host for node [%s] info: %v", gwnode.ID, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}

var userName string
if r.Header.Get("ismaster") == "yes" {
userName = logic.MasterUser
} else {
caller, err := logic.GetUser(r.Header.Get("user"))
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
userName = caller.UserName
}
// create client
var extclient models.ExtClient
extclient.OwnerID = userName
extclient.IngressGatewayID = targetGwID
extclient.Network = networkid
extclient.Tags = make(map[models.TagID]struct{})
// extclient.Tags[models.TagID(fmt.Sprintf("%s.%s", extclient.Network,
// models.RemoteAccessTagName))] = struct{}{}
// set extclient dns to ingressdns if extclient dns is not explicitly set
if (extclient.DNS == "") && (gwnode.IngressDNS != "") {
extclient.DNS = gwnode.IngressDNS
}

listenPort := logic.GetPeerListenPort(host)
extclient.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
extclient.Enabled = true

if err = logic.CreateExtClient(&extclient); err != nil {
slog.Error(
"failed to create extclient",
"user",
r.Header.Get("user"),
"network",
networkid,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
client, err := logic.GetExtClient(extclient.ClientID, networkid)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
addrString := client.Address
if addrString != "" {
addrString += "/32"
}
if client.Address6 != "" {
if addrString != "" {
addrString += ","
}
addrString += client.Address6 + "/128"
}

keepalive := ""
if network.DefaultKeepalive != 0 {
keepalive = "PersistentKeepalive = " + strconv.Itoa(int(network.DefaultKeepalive))
}
if gwnode.IngressPersistentKeepalive != 0 {
keepalive = "PersistentKeepalive = " + strconv.Itoa(int(gwnode.IngressPersistentKeepalive))
}
var newAllowedIPs string
if logic.IsInternetGw(gwnode) || gwnode.InternetGwID != "" {
egressrange := "0.0.0.0/0"
if gwnode.Address6.IP != nil && client.Address6 != "" {
egressrange += "," + "::/0"
}
newAllowedIPs = egressrange
} else {
newAllowedIPs = network.AddressRange
if newAllowedIPs != "" && network.AddressRange6 != "" {
newAllowedIPs += ","
}
if network.AddressRange6 != "" {
newAllowedIPs += network.AddressRange6
}
if egressGatewayRanges, err := logic.GetEgressRangesOnNetwork(&client); err == nil {
for _, egressGatewayRange := range egressGatewayRanges {
newAllowedIPs += "," + egressGatewayRange
}
}
}
gwendpoint := ""
if host.EndpointIP.To4() == nil {
gwendpoint = fmt.Sprintf("[%s]:%d", host.EndpointIPv6.String(), host.ListenPort)
} else {
gwendpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), host.ListenPort)
}
defaultDNS := ""
if client.DNS != "" {
defaultDNS = "DNS = " + client.DNS
} else if gwnode.IngressDNS != "" {
defaultDNS = "DNS = " + gwnode.IngressDNS
}

defaultMTU := 1420
if host.MTU != 0 {
defaultMTU = host.MTU
}
if gwnode.IngressMTU != 0 {
defaultMTU = int(gwnode.IngressMTU)
}

postUp := strings.Builder{}
if client.PostUp != "" && params["type"] != "qr" {
for _, loc := range strings.Split(client.PostUp, "\n") {
postUp.WriteString(fmt.Sprintf("PostUp = %s\n", loc))
}
}

postDown := strings.Builder{}
if client.PostDown != "" && params["type"] != "qr" {
for _, loc := range strings.Split(client.PostDown, "\n") {
postDown.WriteString(fmt.Sprintf("PostDown = %s\n", loc))
}
}

config := fmt.Sprintf(`[Interface]
Address = %s
PrivateKey = %s
MTU = %d
%s
%s
%s

[Peer]
PublicKey = %s
AllowedIPs = %s
Endpoint = %s
%s

`, addrString,
client.PrivateKey,
defaultMTU,
defaultDNS,
postUp.String(),
postDown.String(),
host.PublicKey,
newAllowedIPs,
gwendpoint,
keepalive,
)

go func() {
if err := logic.SetClientDefaultACLs(&extclient); err != nil {
slog.Error(
"failed to set default acls for extclient",
"user",
r.Header.Get("user"),
"network",
networkid,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if err := mq.PublishPeerUpdate(false); err != nil {
logger.Log(1, "error publishing peer update ", err.Error())
}
if servercfg.IsDNSMode() {
logic.SetDNS()
}
}()

name := client.ClientID + ".conf"
w.Header().Set("Content-Type", "application/config")
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
w.WriteHeader(http.StatusOK)
_, err = fmt.Fprint(w, config)
if err != nil {
logger.Log(1, r.Header.Get("user"), "response writer error (file) ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
}
}

// @Summary Create an individual remote access client
// @Router /api/extclients/{network}/{nodeid} [post]
// @Tags Remote Access Client
Expand Down
3 changes: 2 additions & 1 deletion controllers/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,14 +640,15 @@ func updateNetwork(w http.ResponseWriter, r *http.Request) {
return
}
netNew := netOld
netNew.NameServers = payload.NameServers
netNew.DefaultACL = payload.DefaultACL
_, _, _, err = logic.UpdateNetwork(&netOld, &netNew)
if err != nil {
slog.Info("failed to update network", "user", r.Header.Get("user"), "err", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}

go mq.PublishPeerUpdate(false)
slog.Info("updated network", "network", payload.NetID, "user", r.Header.Get("user"))
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(payload)
Expand Down
4 changes: 4 additions & 0 deletions controllers/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ var linuxHost models.Host
func TestCreateEgressGateway(t *testing.T) {
var gateway models.EgressGatewayRequest
gateway.Ranges = []string{"10.100.100.0/24"}
gateway.RangesWithMetric = append(gateway.RangesWithMetric, models.EgressRangeMetric{
Network: "10.100.100.0/24",
RouteMetric: 256,
})
gateway.NetID = "skynet"
deleteAllNetworks()
createNet()
Expand Down
33 changes: 33 additions & 0 deletions logic/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package logic
import (
"errors"
"fmt"
"slices"
"sort"
"time"

"github.com/gravitl/netmaker/database"
Expand Down Expand Up @@ -77,6 +79,14 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
if host.FirewallInUse == models.FIREWALL_NONE {
return models.Node{}, errors.New("please install iptables or nftables on the device")
}
if len(gateway.RangesWithMetric) == 0 && len(gateway.Ranges) > 0 {
for _, rangeI := range gateway.Ranges {
gateway.RangesWithMetric = append(gateway.RangesWithMetric, models.EgressRangeMetric{
Network: rangeI,
RouteMetric: 256,
})
}
}
for i := len(gateway.Ranges) - 1; i >= 0; i-- {
// check if internet gateway IPv4
if gateway.Ranges[i] == "0.0.0.0/0" || gateway.Ranges[i] == "::/0" {
Expand All @@ -91,6 +101,28 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
gateway.Ranges[i] = normalized

}
rangesWithMetric := []string{}
for i := len(gateway.RangesWithMetric) - 1; i >= 0; i-- {
if gateway.RangesWithMetric[i].Network == "0.0.0.0/0" || gateway.RangesWithMetric[i].Network == "::/0" {
// remove inet range
gateway.RangesWithMetric = append(gateway.RangesWithMetric[:i], gateway.RangesWithMetric[i+1:]...)
continue
}
normalized, err := NormalizeCIDR(gateway.RangesWithMetric[i].Network)
if err != nil {
return models.Node{}, err
}
gateway.RangesWithMetric[i].Network = normalized
rangesWithMetric = append(rangesWithMetric, gateway.RangesWithMetric[i].Network)
if gateway.RangesWithMetric[i].RouteMetric <= 0 || gateway.RangesWithMetric[i].RouteMetric > 999 {
gateway.RangesWithMetric[i].RouteMetric = 256
}
}
sort.Strings(gateway.Ranges)
sort.Strings(rangesWithMetric)
if !slices.Equal(gateway.Ranges, rangesWithMetric) {
return models.Node{}, errors.New("invalid ranges")
}
if gateway.NatEnabled == "" {
gateway.NatEnabled = "yes"
}
Expand All @@ -104,6 +136,7 @@ func CreateEgressGateway(gateway models.EgressGatewayRequest) (models.Node, erro
node.IsEgressGateway = true
node.EgressGatewayRanges = gateway.Ranges
node.EgressGatewayNatEnabled = models.ParseBool(gateway.NatEnabled)

node.EgressGatewayRequest = gateway // store entire request for use when preserving the egress gateway
node.SetLastModified()
if err = UpsertNode(&node); err != nil {
Expand Down
Loading