Merge pull request #3657 from gravitl/release-v1.1.0

Release v1.1.0
This commit is contained in:
Abhishek K
2025-09-18 22:53:09 +05:30
committed by GitHub
12 changed files with 402 additions and 84 deletions

View File

@@ -466,23 +466,6 @@ func getExtClientHAConf(w http.ResponseWriter, r *http.Request) {
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 != "") {
network, _ := logic.GetNetwork(gwnode.Network)
dns := gwnode.IngressDNS
if len(network.NameServers) > 0 {
if dns == "" {
dns = strings.Join(network.NameServers, ",")
} else {
dns += "," + strings.Join(network.NameServers, ",")
}
}
extclient.DNS = dns
}
listenPort := logic.GetPeerListenPort(host)
extclient.IngressGatewayEndpoint = fmt.Sprintf("%s:%d", host.EndpointIP.String(), listenPort)
@@ -506,6 +489,11 @@ func getExtClientHAConf(w http.ResponseWriter, r *http.Request) {
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"
@@ -551,13 +539,6 @@ func getExtClientHAConf(w http.ResponseWriter, r *http.Request) {
} 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
@@ -630,6 +611,7 @@ Endpoint = %s
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)

View File

@@ -71,7 +71,7 @@ func userMiddleWare(handler http.Handler) http.Handler {
if strings.Contains(route, "tags") {
r.Header.Set("TARGET_RSRC", models.TagRsrc.String())
}
if strings.Contains(route, "extclients") {
if strings.Contains(route, "extclients") || strings.Contains(route, "client_conf") {
r.Header.Set("TARGET_RSRC", models.ExtClientsRsrc.String())
}
if strings.Contains(route, "enrollment-keys") {

View File

@@ -575,21 +575,40 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
// validate address ranges: must be private
if network.AddressRange != "" {
_, _, err := net.ParseCIDR(network.AddressRange)
_, cidr, err := net.ParseCIDR(network.AddressRange)
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
} else {
ones, bits := cidr.Mask.Size()
if bits-ones <= 1 {
err = fmt.Errorf("cannot create network with /31 or /32 cidr")
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
}
if network.AddressRange6 != "" {
_, _, err := net.ParseCIDR(network.AddressRange6)
_, cidr, err := net.ParseCIDR(network.AddressRange6)
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
} else {
ones, bits := cidr.Mask.Size()
if bits-ones <= 1 {
err = fmt.Errorf("cannot create network with /127 or /128 cidr")
logger.Log(0, r.Header.Get("user"), "failed to create network: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
}

View File

@@ -50,16 +50,31 @@ func GetFwRulesOnIngressGateway(node models.Node) (rules []models.FwRule) {
if defaultDevicePolicy.Enabled {
return
}
defer func() {
if len(rules) == 0 && IsNodeAllowedToCommunicateWithAllRsrcs(node) {
if node.NetworkRange.IP != nil {
rules = append(rules, models.FwRule{
SrcIP: node.NetworkRange,
Allow: true,
})
}
if node.NetworkRange6.IP != nil {
rules = append(rules, models.FwRule{
SrcIP: node.NetworkRange6,
Allow: true,
})
}
return
}
}()
for _, nodeI := range nodes {
if !nodeI.IsStatic || nodeI.IsUserNode {
continue
}
if !node.StaticNode.Enabled {
if !nodeI.StaticNode.Enabled {
continue
}
// if nodeI.StaticNode.IngressGatewayID != node.ID.String() {
// continue
// }
if IsNodeAllowedToCommunicateWithAllRsrcs(nodeI) {
if nodeI.Address.IP != nil {
rules = append(rules, models.FwRule{
@@ -525,7 +540,18 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
continue
}
if _, ok := eI.Nodes[targetnode.ID.String()]; ok {
if eI.Range != "" {
if servercfg.IsPro && eI.Domain != "" && len(eI.DomainAns) > 0 {
for _, domainAnsI := range eI.DomainAns {
ip, cidr, err := net.ParseCIDR(domainAnsI)
if err == nil {
if ip.To4() != nil {
egressRanges4 = append(egressRanges4, *cidr)
} else {
egressRanges6 = append(egressRanges6, *cidr)
}
}
}
} else if eI.Range != "" {
_, cidr, err := net.ParseCIDR(eI.Range)
if err == nil {
if cidr.IP.To4() != nil {
@@ -535,6 +561,7 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
}
}
}
dstTags[targetnode.ID.String()] = struct{}{}
}
}
break
@@ -544,7 +571,18 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
err := e.Get(db.WithContext(context.TODO()))
if err == nil && e.Status && len(e.Nodes) > 0 {
if _, ok := e.Nodes[targetnode.ID.String()]; ok {
if e.Range != "" {
if servercfg.IsPro && e.Domain != "" && len(e.DomainAns) > 0 {
for _, domainAnsI := range e.DomainAns {
ip, cidr, err := net.ParseCIDR(domainAnsI)
if err == nil {
if ip.To4() != nil {
egressRanges4 = append(egressRanges4, *cidr)
} else {
egressRanges6 = append(egressRanges6, *cidr)
}
}
}
} else if e.Range != "" {
_, cidr, err := net.ParseCIDR(e.Range)
if err == nil {
if cidr.IP.To4() != nil {
@@ -554,6 +592,7 @@ func GetAclRulesForNode(targetnodeI *models.Node) (rules map[string]models.AclRu
}
}
}
dstTags[targetnode.ID.String()] = struct{}{}
}
}
@@ -800,10 +839,10 @@ func GetEgressRulesForNode(targetnode models.Node) (rules map[string]models.AclR
if node.ID == targetnode.ID {
continue
}
if node.Address.IP != nil {
if !node.IsStatic && node.Address.IP != nil {
aclRule.IPList = append(aclRule.IPList, node.AddressIPNet4())
}
if node.Address6.IP != nil {
if !node.IsStatic && node.Address6.IP != nil {
aclRule.IP6List = append(aclRule.IP6List, node.AddressIPNet6())
}
if node.IsStatic && node.StaticNode.Address != "" {

View File

@@ -434,6 +434,25 @@ func validateNameserverReq(ns schema.Nameserver) error {
if len(ns.Servers) == 0 {
return errors.New("atleast one nameserver should be specified")
}
network, err := GetNetwork(ns.NetworkID)
if err != nil {
return errors.New("invalid network id")
}
_, cidr, err4 := net.ParseCIDR(network.AddressRange)
_, cidr6, err6 := net.ParseCIDR(network.AddressRange6)
for _, nsIPStr := range ns.Servers {
nsIP := net.ParseIP(nsIPStr)
if nsIP == nil {
return errors.New("invalid nameserver " + nsIPStr)
}
if err4 == nil && nsIP.To4() != nil {
if cidr.Contains(nsIP) {
return errors.New("cannot use netmaker IP as nameserver")
}
} else if err6 == nil && cidr6.Contains(nsIP) {
return errors.New("cannot use netmaker IP as nameserver")
}
}
if !ns.MatchAll && len(ns.MatchDomains) == 0 {
return errors.New("atleast one match domain is required")
}

View File

@@ -70,23 +70,12 @@ func storeExtClientInCache(key string, extclient models.ExtClient) {
func GetEgressRangesOnNetwork(client *models.ExtClient) ([]string, error) {
var result []string
networkNodes, err := GetNetworkNodes(client.Network)
if err != nil {
return []string{}, err
}
eli, _ := (&schema.Egress{Network: client.Network}).ListByNetwork(db.WithContext(context.TODO()))
acls, _ := ListAclsByNetwork(models.NetworkID(client.Network))
// clientNode := client.ConvertToStaticNode()
for _, currentNode := range networkNodes {
if currentNode.Network != client.Network {
for _, eI := range eli {
if !eI.Status || eI.Range == "" {
continue
}
GetNodeEgressInfo(&currentNode, eli, acls)
if currentNode.EgressDetails.IsEgressGateway { // add the egress gateway range(s) to the result
if len(currentNode.EgressDetails.EgressGatewayRanges) > 0 {
result = append(result, currentNode.EgressDetails.EgressGatewayRanges...)
}
}
result = append(result, eI.Range)
}
extclients, _ := GetNetworkExtClients(client.Network)
for _, extclient := range extclients {

View File

@@ -149,10 +149,11 @@ func GetPeerUpdateForHost(network string, host *models.Host, allNodes []models.N
}
defer func() {
if !hostPeerUpdate.FwUpdate.AllowAll {
hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"] = models.EgressInfo{
EgressID: "allowed-network-rules",
EgressFwRules: make(map[string]models.AclRule),
if len(hostPeerUpdate.FwUpdate.AllowedNetworks) > 0 {
hostPeerUpdate.FwUpdate.EgressInfo["allowed-network-rules"] = models.EgressInfo{
EgressID: "allowed-network-rules",
EgressFwRules: make(map[string]models.AclRule),
}
}
for _, aclRule := range hostPeerUpdate.FwUpdate.AllowedNetworks {
hostPeerUpdate.FwUpdate.AclRules[aclRule.ID] = aclRule

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"net"
"os"
"time"
@@ -63,6 +64,10 @@ func migrateNameservers() {
}
for _, netI := range nets {
_, cidr, err := net.ParseCIDR(netI.AddressRange)
if err != nil {
continue
}
if len(netI.NameServers) > 0 {
ns := schema.Nameserver{
ID: uuid.NewString(),
@@ -78,8 +83,14 @@ func migrateNameservers() {
Status: true,
CreatedBy: user.UserName,
}
for _, ip := range netI.NameServers {
ns.Servers = append(ns.Servers, ip)
for _, nsIP := range netI.NameServers {
if net.ParseIP(nsIP) == nil {
continue
}
if !cidr.Contains(net.ParseIP(nsIP)) {
ns.Servers = append(ns.Servers, nsIP)
}
}
ns.Create(db.WithContext(context.TODO()))
netI.NameServers = []string{}

View File

@@ -1498,6 +1498,10 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
continue
}
if extClient.RemoteAccessClientID == "" {
continue
}
_, ok := userExtClients[extClient.IngressGatewayID]
if !ok {
userExtClients[extClient.IngressGatewayID] = []models.ExtClient{}
@@ -1526,13 +1530,21 @@ func getUserRemoteAccessGwsV1(w http.ResponseWriter, r *http.Request) {
}
}
if !found {
// TODO: prevent ip clashes.
if len(extClients) > 0 {
gwClient = extClients[0]
if !found && req.RemoteAccessClientID != "" {
for _, extClient := range extClients {
if extClient.RemoteAccessClientID == req.RemoteAccessClientID {
gwClient = extClient
found = true
break
}
}
}
if !found && len(extClients) > 0 {
// TODO: prevent ip clashes.
gwClient = extClients[0]
}
host, err := logic.GetHost(node.HostID.String())
if err != nil {
continue

View File

@@ -17,6 +17,7 @@ func NewOktaClient(oktaOrgURL, oktaAPIToken string) (*Client, error) {
config, err := okta.NewConfiguration(
okta.WithOrgUrl(oktaOrgURL),
okta.WithToken(oktaAPIToken),
okta.WithRateLimitPrevent(true),
)
if err != nil {
return nil, err
@@ -45,18 +46,17 @@ func (o *Client) Verify() error {
func (o *Client) GetUsers(filters []string) ([]idp.User, error) {
var retval []idp.User
var allUsersFetched bool
for !allUsersFetched {
users, resp, err := o.client.UserAPI.ListUsers(context.TODO()).
Search(buildPrefixFilter("profile.login", filters)).
Execute()
if err != nil {
return nil, err
}
users, resp, err := o.client.UserAPI.ListUsers(context.TODO()).
Search(buildPrefixFilter("profile.login", filters)).
Execute()
if err != nil {
return nil, err
}
allUsersFetched = !resp.HasNextPage()
usersProcessingPending := len(users) > 0 || resp.HasNextPage()
for usersProcessingPending {
for _, user := range users {
id := *user.Id
username := *user.Profile.Login
@@ -79,6 +79,19 @@ func (o *Client) GetUsers(filters []string) ([]idp.User, error) {
AccountArchived: false,
})
}
if resp.HasNextPage() {
users = make([]okta.User, 0)
resp, err = resp.Next(&users)
if err != nil {
return nil, err
}
usersProcessingPending = len(users) > 0 || resp.HasNextPage()
} else {
usersProcessingPending = false
}
}
return retval, nil
@@ -86,35 +99,46 @@ func (o *Client) GetUsers(filters []string) ([]idp.User, error) {
func (o *Client) GetGroups(filters []string) ([]idp.Group, error) {
var retval []idp.Group
var allGroupsFetched bool
for !allGroupsFetched {
groups, resp, err := o.client.GroupAPI.ListGroups(context.TODO()).
Search(buildPrefixFilter("profile.name", filters)).
Execute()
if err != nil {
return nil, err
}
groups, resp, err := o.client.GroupAPI.ListGroups(context.TODO()).
Search(buildPrefixFilter("profile.name", filters)).
Execute()
if err != nil {
return nil, err
}
allGroupsFetched = !resp.HasNextPage()
groupsProcessingPending := len(groups) > 0 || resp.HasNextPage()
for groupsProcessingPending {
for _, group := range groups {
var allMembersFetched bool
id := *group.Id
name := *group.Profile.Name
var members []string
for !allMembersFetched {
groupUsers, resp, err := o.client.GroupAPI.ListGroupUsers(context.TODO(), id).Execute()
if err != nil {
return nil, err
}
groupUsers, groupUsersResp, err := o.client.GroupAPI.ListGroupUsers(context.TODO(), id).Execute()
if err != nil {
return nil, err
}
allMembersFetched = !resp.HasNextPage()
groupUsersProcessingPending := len(groupUsers) > 0 || groupUsersResp.HasNextPage()
for groupUsersProcessingPending {
for _, groupUser := range groupUsers {
members = append(members, *groupUser.Id)
}
if groupUsersResp.HasNextPage() {
groupUsers = make([]okta.GroupMember, 0)
groupUsersResp, err = groupUsersResp.Next(&groupUsers)
if err != nil {
return nil, err
}
groupUsersProcessingPending = len(groupUsers) > 0 || groupUsersResp.HasNextPage()
} else {
groupUsersProcessingPending = false
}
}
retval = append(retval, idp.Group{
@@ -123,6 +147,19 @@ func (o *Client) GetGroups(filters []string) ([]idp.Group, error) {
Members: members,
})
}
if resp.HasNextPage() {
groups = make([]okta.Group, 0)
resp, err = resp.Next(&groups)
if err != nil {
return nil, err
}
groupsProcessingPending = len(groups) > 0 || resp.HasNextPage()
} else {
groupsProcessingPending = false
}
}
return retval, nil

View File

@@ -3,6 +3,7 @@ package logic
import (
"context"
"errors"
"net"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/logic"
@@ -20,6 +21,25 @@ func ValidateNameserverReq(ns schema.Nameserver) error {
if len(ns.Servers) == 0 {
return errors.New("atleast one nameserver should be specified")
}
network, err := logic.GetNetwork(ns.NetworkID)
if err != nil {
return errors.New("invalid network id")
}
_, cidr, err4 := net.ParseCIDR(network.AddressRange)
_, cidr6, err6 := net.ParseCIDR(network.AddressRange6)
for _, nsIPStr := range ns.Servers {
nsIP := net.ParseIP(nsIPStr)
if nsIP == nil {
return errors.New("invalid nameserver " + nsIPStr)
}
if err4 == nil && nsIP.To4() != nil {
if cidr.Contains(nsIP) {
return errors.New("cannot use netmaker IP as nameserver")
}
} else if err6 == nil && cidr6.Contains(nsIP) {
return errors.New("cannot use netmaker IP as nameserver")
}
}
if !ns.MatchAll && len(ns.MatchDomains) == 0 {
return errors.New("atleast one match domain is required")
}

View File

@@ -0,0 +1,189 @@
#!/usr/bin/env bash
# Netmaker CI helper: bring WireGuard up/down and manage ephemeral client lifecycle.
# Subcommands:
# up - fetch config, capture Client-ID, bring interface up, save state
# down - bring interface down, delete local conf, delete client via API
#
# Env vars (can be overridden by flags):
# NETMAKER_BASE_URL (required) e.g. https://nm.example.com or pass --base-url
# NETMAKER_NETWORK (required) e.g. corpnet or pass --network
# NETMAKER_API_JWT (required) Bearer token or pass --jwt
# WG_IFACE (default netmaker) or pass --iface
# WG_CONF_DIR (default /etc/wireguard) or pass --confdir
# NETMAKER_STATE_FILE (default RUNNER_TEMP or /tmp)
# You may also pass --client-id on `down` to avoid relying on the state file.
set -euo pipefail
# ---------- defaults ----------
WG_IFACE="${WG_IFACE:-netmaker}"
WG_CONF_DIR="${WG_CONF_DIR:-/etc/wireguard}"
SUBCMD=""
CLIENT_ID_OVERRIDE=""
usage() {
cat <<USAGE
Usage:
$0 up [--iface IFACE] [--confdir DIR] [--base-url URL] [--network NET] [--jwt TOKEN]
$0 down [--iface IFACE] [--confdir DIR] [--base-url URL] [--network NET] [--jwt TOKEN] [--client-id ID]
Flags override env vars. Env vars documented at top of the script.
Examples:
NETMAKER_BASE_URL=https://nm.example.com NETMAKER_NETWORK=corpnet NETMAKER_API_JWT=... $0 up
$0 down --base-url https://nm.example.com --network corpnet --jwt ... --client-id icy-water
USAGE
}
# ---------- arg parse ----------
if [[ $# -lt 1 ]]; then usage; exit 2; fi
SUBCMD="$1"; shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--iface) WG_IFACE="$2"; shift 2;;
--confdir) WG_CONF_DIR="$2"; shift 2;;
--base-url) NETMAKER_BASE_URL="$2"; shift 2;;
--network) NETMAKER_NETWORK="$2"; shift 2;;
--jwt) NETMAKER_API_JWT="$2"; shift 2;;
--client-id) CLIENT_ID_OVERRIDE="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) echo "Unknown arg: $1" >&2; usage; exit 2;;
esac
done
STATE_FILE="${NETMAKER_STATE_FILE:-${RUNNER_TEMP:-/tmp}/netmaker_ci_${WG_IFACE}.env}"
require_env() {
: "${NETMAKER_BASE_URL:?ERROR: NETMAKER_BASE_URL not set}"
: "${NETMAKER_NETWORK:?ERROR: NETMAKER_NETWORK not set}"
: "${NETMAKER_API_JWT:?ERROR: NETMAKER_API_JWT not set}"
}
install_deps() {
echo "[*] Checking dependencies ..."
local need=(curl jq wg-quick ip)
local miss=()
for b in "${need[@]}"; do command -v "$b" >/dev/null 2>&1 || miss+=("$b"); done
if [[ ${#miss[@]} -eq 0 ]]; then
echo "[*] All dependencies present."
return
fi
echo "[*] Installing missing deps: ${miss[*]}"
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo apt-get install -y wireguard-tools jq curl iproute2 resolvconf
elif command -v yum >/dev/null 2>&1; then
sudo yum install -y wireguard-tools jq curl iproute iproute-tc
elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -y wireguard-tools jq curl iproute
else
echo "ERROR: no supported package manager found; install: curl jq wireguard-tools iproute" >&2
exit 1
fi
}
do_up() {
require_env
install_deps
local ep="${NETMAKER_BASE_URL}/api/v1/client_conf/${NETMAKER_NETWORK}"
local tmp_conf="/tmp/${WG_IFACE}.conf"
local tmp_hdr="/tmp/${WG_IFACE}.headers"
echo "[*] Requesting client config: ${ep}"
# Optional headers
declare -a hdrs
hdrs=(-H "Authorization: Bearer ${NETMAKER_API_JWT}")
[[ -n "${NM_CLIENT_LABEL:-}" ]] && hdrs+=(-H "X-NM-Client-Label: ${NM_CLIENT_LABEL}")
[[ -n "${NM_REQUESTED_NAME:-}" ]] && hdrs+=(-H "X-NM-Requested-Name: ${NM_REQUESTED_NAME}")
local code
code="$(curl -sS -L --dump-header "${tmp_hdr}" -w '%{http_code}' -o "${tmp_conf}" "${hdrs[@]}" "${ep}")"
if [[ "${code}" != "200" ]]; then
echo "ERROR: client_conf HTTP ${code}" >&2
curl -sS -L "${hdrs[@]}" "${ep}" | head -c 400 >&2 || true
exit 1
fi
grep -q "^\[Interface\]" "${tmp_conf}" || { echo "ERROR: not a WireGuard conf"; head -n 20 "${tmp_conf}"; exit 1; }
# --- Extract Client-ID (one-liner, trim spaces/quotes) ---
local client_id
client_id="$(grep -i '^Client-ID:' "${tmp_hdr}" | head -n1 | cut -d: -f2- | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//; s/"$//' -e "s/^'//; s/'$//")"
if [[ -z "${client_id}" ]]; then
echo "ERROR: Client-ID header missing in response; cannot manage lifecycle." >&2
exit 1
fi
echo "[*] Client-ID: ${client_id}"
# Optional marker
if ! grep -q "^#interface-name=" "${tmp_conf}"; then
echo "#interface-name=${WG_IFACE}" | cat - "${tmp_conf}" > "${tmp_conf}.tmp" && mv "${tmp_conf}.tmp" "${tmp_conf}"
fi
# Install & bring up
sudo mkdir -p "${WG_CONF_DIR}"
sudo mv "${tmp_conf}" "${WG_CONF_DIR}/${WG_IFACE}.conf"
sudo chmod 600 "${WG_CONF_DIR}/${WG_IFACE}.conf"
echo "[*] Bringing up ${WG_IFACE} ..."
sudo wg-quick up "${WG_IFACE}"
echo "==== ${WG_IFACE} is up ===="
ip addr show "${WG_IFACE}" || true
wg show "${WG_IFACE}" || true
# Persist state
cat > "${STATE_FILE}" <<EOF
NETMAKER_BASE_URL='${NETMAKER_BASE_URL}'
NETMAKER_NETWORK='${NETMAKER_NETWORK}'
NETMAKER_API_JWT='${NETMAKER_API_JWT}'
WG_IFACE='${WG_IFACE}'
WG_CONF_DIR='${WG_CONF_DIR}'
CLIENT_ID='${client_id}'
EOF
chmod 600 "${STATE_FILE}"
echo "[*] Saved state: ${STATE_FILE}"
}
do_down() {
# Load state if present; flags/env can still override
if [[ -f "${STATE_FILE}" ]]; then
# shellcheck disable=SC1090
source "${STATE_FILE}"
fi
require_env
local client_id="${CLIENT_ID_OVERRIDE:-${CLIENT_ID:-}}"
echo "[*] Bringing down ${WG_IFACE} ..."
sudo wg-quick down "${WG_IFACE}" || echo "WARN: wg-quick down failed (already down?)."
# Remove local conf
if [[ -f "${WG_CONF_DIR}/${WG_IFACE}.conf" ]]; then
sudo shred -u "${WG_CONF_DIR}/${WG_IFACE}.conf" 2>/dev/null || sudo rm -f "${WG_CONF_DIR}/${WG_IFACE}.conf"
fi
# Delete ephemeral client on server (if we know its ID)
if [[ -n "${client_id}" ]]; then
local del_ep="${NETMAKER_BASE_URL}/api/extclients/${NETMAKER_NETWORK}/${client_id}"
echo "[*] Deleting client: DELETE ${del_ep}"
local http
http="$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE -H "Authorization: Bearer ${NETMAKER_API_JWT}" "${del_ep}")"
if [[ "${http}" =~ ^20[0-9]$ ]]; then
echo "[*] Client deleted (HTTP ${http})."
else
echo "WARN: deletion returned HTTP ${http}; verify server state."
fi
else
echo "WARN: client id not known (missing --client-id and state file); skipping server delete."
fi
rm -f "${STATE_FILE}" || true
echo "[*] Teardown finished."
}
case "${SUBCMD}" in
up) do_up ;;
down) do_down ;;
*) usage; exit 2 ;;
esac