Files
netmaker/controllers/ext_client.go
2025-09-16 13:09:38 +05:30

1075 lines
32 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gorilla/mux"
"github.com/gravitl/netmaker/database"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/schema"
"github.com/gravitl/netmaker/servercfg"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/mq"
"github.com/skip2/go-qrcode"
"golang.org/x/exp/slices"
"golang.org/x/exp/slog"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func extClientHandlers(r *mux.Router) {
r.HandleFunc("/api/extclients", logic.SecurityCheck(true, http.HandlerFunc(getAllExtClients))).
Methods(http.MethodGet)
r.HandleFunc("/api/extclients/{network}", logic.SecurityCheck(true, http.HandlerFunc(getNetworkExtClients))).
Methods(http.MethodGet)
r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(getExtClient))).
Methods(http.MethodGet)
r.HandleFunc("/api/extclients/{network}/{clientid}/{type}", logic.SecurityCheck(false, http.HandlerFunc(getExtClientConf))).
Methods(http.MethodGet)
r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(updateExtClient))).
Methods(http.MethodPut)
r.HandleFunc("/api/extclients/{network}/{clientid}", logic.SecurityCheck(false, http.HandlerFunc(deleteExtClient))).
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 {
node, err := logic.GetNodeByID(nodeID)
if err != nil {
return false
}
return node.IsIngressGateway
}
// @Summary Get all remote access client associated with network
// @Router /api/extclients/{network} [get]
// @Tags Remote Access Client
// @Security oauth2
// @Success 200 {object} models.ExtClient
// @Failure 500 {object} models.ErrorResponse
func getNetworkExtClients(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var extclients []models.ExtClient
var params = mux.Vars(r)
network := params["network"]
extclients, err := logic.GetNetworkExtClients(network)
if err != nil {
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to get ext clients for network [%s]: %v", network, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
//Returns all the extclients in JSON format
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(extclients)
}
// @Summary Fetches All Remote Access Clients across all networks
// @Router /api/extclients [get]
// @Tags Remote Access Client
// @Security oauth2
// @Success 200 {object} models.ExtClient
// @Failure 500 {object} models.ErrorResponse
// Not quite sure if this is necessary. Probably necessary based on front end but may
// want to review after iteration 1 if it's being used or not
func getAllExtClients(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
clients, err := logic.GetAllExtClients()
if err != nil && !database.IsEmptyRecord(err) {
logger.Log(0, "failed to get all extclients: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
//Return all the extclients in JSON format
logic.SortExtClient(clients[:])
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(clients)
}
// @Summary Get an individual remote access client
// @Router /api/extclients/{network}/{clientid} [get]
// @Tags Remote Access Client
// @Security oauth2
// @Success 200 {object} models.ExtClient
// @Failure 500 {object} models.ErrorResponse
// @Failure 403 {object} models.ErrorResponse
func getExtClient(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
clientid := params["clientid"]
network := params["network"]
client, err := logic.GetExtClient(clientid, network)
if err != nil {
logger.Log(
0,
r.Header.Get("user"),
fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
clientid, network, err),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
gwNode, err := logic.GetNodeByID(client.IngressGatewayID)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.SetDNSOnWgConfig(&gwNode, &client)
w.WriteHeader(http.StatusOK)
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 getExtClientConf(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
clientid := params["clientid"]
networkid := params["network"]
client, err := logic.GetExtClient(clientid, networkid)
if err != nil {
logger.Log(
0,
r.Header.Get("user"),
fmt.Sprintf("failed to get extclient for [%s] on network [%s]: %v",
clientid, networkid, err),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
gwnode, err := logic.GetNodeByID(client.IngressGatewayID)
if err != nil {
logger.Log(
0,
r.Header.Get("user"),
fmt.Sprintf(
"failed to get ingress gateway node [%s] info: %v",
client.IngressGatewayID,
err,
),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
eli, _ := (&schema.Egress{Network: gwnode.Network}).ListByNetwork(db.WithContext(context.TODO()))
acls, _ := logic.ListAclsByNetwork(models.NetworkID(client.Network))
logic.GetNodeEgressInfo(&gwnode, eli, acls)
host, err := logic.GetHost(gwnode.HostID.String())
if err != nil {
logger.Log(
0,
r.Header.Get("user"),
fmt.Sprintf(
"failed to get host for ingress gateway node [%s] info: %v",
client.IngressGatewayID,
err,
),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
network, err := logic.GetParentNetwork(client.Network)
if err != nil {
logger.Log(
1,
r.Header.Get("user"),
"Could not retrieve Ingress Gateway Network",
client.Network,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
preferredIp := strings.TrimSpace(r.URL.Query().Get("preferredip"))
if preferredIp != "" {
allowedPreferredIps := []string{}
for i := range gwnode.AdditionalRagIps {
allowedPreferredIps = append(allowedPreferredIps, gwnode.AdditionalRagIps[i].String())
}
allowedPreferredIps = append(allowedPreferredIps, host.EndpointIP.String())
allowedPreferredIps = append(allowedPreferredIps, host.EndpointIPv6.String())
if !slices.Contains(allowedPreferredIps, preferredIp) {
slog.Warn(
"preferred endpoint ip is not associated with the RAG. proceeding with preferred ip",
"preferred ip",
preferredIp,
)
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
errors.New("preferred endpoint ip is not associated with the RAG"),
"badrequest",
),
)
return
}
if net.ParseIP(preferredIp).To4() == nil {
preferredIp = fmt.Sprintf("[%s]", preferredIp)
}
}
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))
}
gwendpoint := ""
if preferredIp == "" {
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)
}
} else {
gwendpoint = fmt.Sprintf("%s:%d", preferredIp, host.ListenPort)
}
var newAllowedIPs string
if logic.IsInternetGw(gwnode) {
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
}
}
}
logic.SetDNSOnWgConfig(&gwnode, &client)
defaultDNS := ""
if client.DNS != "" {
defaultDNS = "DNS = " + client.DNS
}
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,
)
if params["type"] == "qr" {
bytes, err := qrcode.Encode(config, qrcode.Medium, 220)
if err != nil {
logger.Log(1, r.Header.Get("user"), "failed to encode qr code: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
_, err = w.Write(bytes)
if err != nil {
logger.Log(1, r.Header.Get("user"), "response writer error (qr) ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
return
}
if params["type"] == "file" {
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"))
}
return
}
logger.Log(2, r.Header.Get("user"), "retrieved ext client config")
w.WriteHeader(http.StatusOK)
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{})
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
}
logic.SetDNSOnWgConfig(&gwnode, &client)
defaultDNS := ""
if client.DNS != "" {
defaultDNS = "DNS = " + client.DNS
}
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)
}
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("Client-ID", client.ClientID)
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
// @Security oauth2
// @Success 200 {string} string "OK"
// @Failure 500 {object} models.ErrorResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 403 {object} models.ErrorResponse
func createExtClient(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
nodeid := params["nodeid"]
ingressExists := checkIngressExists(nodeid)
if !ingressExists {
err := errors.New("ingress does not exist")
slog.Error("failed to create extclient", "user", r.Header.Get("user"), "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
var customExtClient models.CustomExtClient
if err := json.NewDecoder(r.Body).Decode(&customExtClient); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
if err := validateCustomExtClient(&customExtClient, true); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
var gateway models.EgressGatewayRequest
gateway.NetID = params["network"]
gateway.Ranges = customExtClient.ExtraAllowedIPs
err := logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
if err != nil {
logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
node, err := logic.GetNodeByID(nodeid)
if err != nil {
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to get ingress gateway node [%s] info: %v", nodeid, 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
// check if user has a config already for remote access client
extclients, err := logic.GetNetworkExtClients(node.Network)
if err != nil {
slog.Error("failed to get extclients", "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
// if device id is sent, we don't want to create another extclient for the same user
// and gw, with the same device id.
if customExtClient.DeviceID != "" {
// let's first confirm that none of the user's extclients for this gw have device id.
for _, extclient := range extclients {
if extclient.DeviceID == customExtClient.DeviceID &&
extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
err = errors.New("remote client config already exists on the gateway")
slog.Error("failed to create extclient", "user", userName, "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
}
for _, extclient := range extclients {
if extclient.RemoteAccessClientID != "" &&
extclient.RemoteAccessClientID == customExtClient.RemoteAccessClientID &&
extclient.OwnerID == caller.UserName && nodeid == extclient.IngressGatewayID {
if customExtClient.DeviceID != "" && extclient.DeviceID == "" {
// This extclient doesnt include a device ID (and neither do the others).
// We patch it by assigning the device ID from the incoming request.
// When clients see that the config already exists, they will fetch
// the one with their device ID. And we will return this one.
extclient.DeviceID = customExtClient.DeviceID
_ = logic.SaveExtClient(&extclient)
}
err = errors.New("remote client config already exists on the gateway")
slog.Error("failed to create extclient", "user", userName, "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
}
extclient := logic.UpdateExtClient(&models.ExtClient{}, &customExtClient)
extclient.OwnerID = userName
extclient.RemoteAccessClientID = customExtClient.RemoteAccessClientID
extclient.IngressGatewayID = nodeid
extclient.Network = node.Network
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
gwDNS := logic.GetGwDNS(&node)
if (extclient.DNS == "") && (gwDNS != "") {
dns := gwDNS
extclient.DNS = dns
}
host, err := logic.GetHost(node.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", nodeid, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
listenPort := logic.GetPeerListenPort(host)
extclient.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
extclient.Enabled = true
parentNetwork, err := logic.GetNetwork(node.Network)
if err == nil { // check if parent network default ACL is enabled (yes) or not (no)
extclient.Enabled = parentNetwork.DefaultACL == "yes"
}
extclient.Os = customExtClient.Os
extclient.DeviceID = customExtClient.DeviceID
extclient.DeviceName = customExtClient.DeviceName
if customExtClient.IsAlreadyConnectedToInetGw {
slog.Warn("RAC/Client is already connected to internet gateway. this may mask their real IP address", "client IP", customExtClient.PublicEndpoint)
}
extclient.PublicEndpoint = customExtClient.PublicEndpoint
extclient.Country = customExtClient.Country
if customExtClient.RemoteAccessClientID != "" && customExtClient.Location == "" {
extclient.Location = logic.GetHostLocInfo(logic.GetClientIP(r), os.Getenv("IP_INFO_TOKEN"))
}
extclient.Location = customExtClient.Location
if err = logic.CreateExtClient(&extclient); err != nil {
slog.Error(
"failed to create extclient",
"user",
r.Header.Get("user"),
"network",
node.Network,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
slog.Info(
"created extclient",
"user",
r.Header.Get("user"),
"network",
node.Network,
"clientid",
extclient.ClientID,
)
if extclient.RemoteAccessClientID != "" {
// if created by user from client app, log event
logic.LogEvent(&models.Event{
Action: models.Connect,
Source: models.Subject{
ID: userName,
Name: userName,
Type: models.UserSub,
},
TriggeredBy: userName,
Target: models.Subject{
ID: extclient.Network,
Name: extclient.Network,
Type: models.NetworkSub,
Info: extclient,
},
NetworkID: models.NetworkID(extclient.Network),
Origin: models.ClientApp,
})
}
w.WriteHeader(http.StatusOK)
go func() {
if err := logic.SetClientDefaultACLs(&extclient); err != nil {
slog.Error(
"failed to set default acls for extclient",
"user",
r.Header.Get("user"),
"network",
node.Network,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if err := mq.PublishPeerUpdate(false); err != nil {
logger.Log(1, "error setting ext peers on "+nodeid+": "+err.Error())
}
if servercfg.IsDNSMode() {
logic.SetDNS()
}
}()
}
// @Summary Update an individual remote access client
// @Router /api/extclients/{network}/{clientid} [put]
// @Tags Remote Access Client
// @Security oauth2
// @Success 200 {object} models.ExtClient
// @Failure 500 {object} models.ErrorResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 403 {object} models.ErrorResponse
func updateExtClient(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var params = mux.Vars(r)
var update models.CustomExtClient
//var oldExtClient models.ExtClient
var replacePeers bool
err := json.NewDecoder(r.Body).Decode(&update)
if err != nil {
logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
clientid := params["clientid"]
oldExtClient, err := logic.GetExtClientByName(clientid)
if err != nil {
slog.Error(
"failed to retrieve extclient",
"user",
r.Header.Get("user"),
"id",
clientid,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if oldExtClient.ClientID == update.ClientID {
if err := validateCustomExtClient(&update, false); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
} else {
if err := validateCustomExtClient(&update, true); err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
var gateway models.EgressGatewayRequest
gateway.NetID = params["network"]
gateway.Ranges = update.ExtraAllowedIPs
err = logic.ValidateEgressRange(gateway.NetID, gateway.Ranges)
if err != nil {
logger.Log(0, r.Header.Get("user"), "error validating egress range: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
var changedID = update.ClientID != oldExtClient.ClientID
if !reflect.DeepEqual(update.DeniedACLs, oldExtClient.DeniedACLs) {
logic.SetClientACLs(&oldExtClient, update.DeniedACLs)
}
if update.PublicKey != oldExtClient.PublicKey {
//remove old peer entry
replacePeers = true
}
if update.RemoteAccessClientID != "" && update.Location == "" {
update.Location = logic.GetHostLocInfo(logic.GetClientIP(r), os.Getenv("IP_INFO_TOKEN"))
}
newclient := logic.UpdateExtClient(&oldExtClient, &update)
if err := logic.DeleteExtClient(oldExtClient.Network, oldExtClient.ClientID, true); err != nil {
slog.Error(
"failed to delete ext client",
"user",
r.Header.Get("user"),
"id",
oldExtClient.ClientID,
"network",
oldExtClient.Network,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if err := logic.SaveExtClient(&newclient); err != nil {
slog.Error(
"failed to save ext client",
"user",
r.Header.Get("user"),
"id",
newclient.ClientID,
"network",
newclient.Network,
"error",
err,
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logger.Log(0, r.Header.Get("user"), "updated ext client", update.ClientID)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(newclient)
go func() {
if changedID && servercfg.IsDNSMode() {
logic.SetDNS()
}
if replacePeers || !update.Enabled {
if err := mq.PublishDeletedClientPeerUpdate(&oldExtClient); err != nil {
slog.Error("error deleting old ext peers", "error", err.Error())
}
}
mq.PublishPeerUpdate(false)
}()
}
// @Summary Delete an individual remote access client
// @Router /api/extclients/{network}/{clientid} [delete]
// @Tags Remote Access Client
// @Security oauth2
// @Success 200
// @Failure 500 {object} models.ErrorResponse
// @Failure 403 {object} models.ErrorResponse
func deleteExtClient(w http.ResponseWriter, r *http.Request) {
// Set header
w.Header().Set("Content-Type", "application/json")
// get params
var params = mux.Vars(r)
clientid := params["clientid"]
network := params["network"]
extclient, err := logic.GetExtClient(clientid, network)
if err != nil {
err = errors.New("Could not delete extclient " + params["clientid"])
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to get extclient [%s],network [%s]: %v", clientid, network, err))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
ingressnode, err := logic.GetNodeByID(extclient.IngressGatewayID)
if err != nil {
logger.Log(
0,
r.Header.Get("user"),
fmt.Sprintf(
"failed to get ingress gateway node [%s] info: %v",
extclient.IngressGatewayID,
err,
),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
err = logic.DeleteExtClientAndCleanup(extclient)
if err != nil {
slog.Error("deleteExtClient: ", "Error", err.Error())
err = errors.New("Could not delete extclient " + params["clientid"])
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
go func() {
if err := mq.PublishDeletedClientPeerUpdate(&extclient); err != nil {
slog.Error("error setting ext peers on " + ingressnode.ID.String() + ": " + err.Error())
}
if servercfg.IsDNSMode() {
logic.SetDNS()
}
}()
logger.Log(0, r.Header.Get("user"),
"Deleted extclient client", params["clientid"], "from network", params["network"])
logic.ReturnSuccessResponse(w, r, params["clientid"]+" deleted.")
}
// validateCustomExtClient Validates the extclient object
func validateCustomExtClient(customExtClient *models.CustomExtClient, checkID bool) error {
v := validator.New()
err := v.Struct(customExtClient)
if err != nil {
return err
}
//validate clientid
if customExtClient.ClientID != "" {
if err := isValid(customExtClient.ClientID, checkID); err != nil {
return fmt.Errorf("client validation: %v", err)
}
}
//extclient.ClientID = customExtClient.ClientID
if len(customExtClient.PublicKey) > 0 {
if _, err := wgtypes.ParseKey(customExtClient.PublicKey); err != nil {
return errInvalidExtClientPubKey
}
//extclient.PublicKey = customExtClient.PublicKey
}
//validate extra ips
if len(customExtClient.ExtraAllowedIPs) > 0 {
for _, ip := range customExtClient.ExtraAllowedIPs {
if _, _, err := net.ParseCIDR(ip); err != nil {
return errInvalidExtClientExtraIP
}
}
//extclient.ExtraAllowedIPs = customExtClient.ExtraAllowedIPs
}
//validate DNS
if customExtClient.DNS != "" {
ips := strings.Split(customExtClient.DNS, ",")
for _, ip := range ips {
trimmedIp := strings.TrimSpace(ip)
if ip := net.ParseIP(trimmedIp); ip == nil {
return errInvalidExtClientDNS
}
}
}
return nil
}
// isValid Checks if the clientid is valid
func isValid(clientid string, checkID bool) error {
if !validName(clientid) {
return errInvalidExtClientID
}
if checkID {
extclients, err := logic.GetAllExtClients()
if err != nil {
return fmt.Errorf("extclients isValid: %v", err)
}
for _, extclient := range extclients {
if clientid == extclient.ClientID {
return errDuplicateExtClientName
}
}
}
return nil
}