mirror of
https://github.com/gravitl/netmaker.git
synced 2025-09-26 21:01:32 +08:00
@@ -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)
|
||||
|
@@ -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") {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 != "" {
|
||||
|
19
logic/dns.go
19
logic/dns.go
@@ -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")
|
||||
}
|
||||
|
@@ -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(¤tNode, 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 {
|
||||
|
@@ -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
|
||||
|
@@ -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{}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
}
|
||||
|
189
scripts/netmaker-ci-runner.sh
Normal file
189
scripts/netmaker-ci-runner.sh
Normal 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
|
||||
|
Reference in New Issue
Block a user