diff --git a/compose/docker-compose.ee.yml b/compose/docker-compose.ee.yml index a9e06375..d2410e5e 100644 --- a/compose/docker-compose.ee.yml +++ b/compose/docker-compose.ee.yml @@ -8,9 +8,9 @@ services: volumes: - dnsconfig:/root/config/dnsconfig - sqldata:/root/data - - mosquitto_data:/etc/netmaker environment: SERVER_NAME: "broker.NETMAKER_BASE_DOMAIN" + STUN_DOMAIN: "stun.NETMAKER_BASE_DOMAIN" SERVER_HOST: "SERVER_PUBLIC_IP" SERVER_API_CONN_STRING: "api.NETMAKER_BASE_DOMAIN:443" COREDNS_ADDR: "SERVER_PUBLIC_IP" @@ -24,14 +24,17 @@ services: NODE_ID: "netmaker-server-1" MQ_HOST: "mq" MQ_PORT: "443" + STUN_PORT: "3478" MQ_SERVER_PORT: "1883" VERBOSITY: "1" METRICS_EXPORTER: "on" LICENSE_KEY: "YOUR_LICENSE_KEY" NETMAKER_ACCOUNT_ID: "YOUR_ACCOUNT_ID" - MQ_ADMIN_PASSWORD: "REPLACE_MQ_ADMIN_PASSWORD" + MQ_PASSWORD: "REPLACE_MQ_PASSWORD" + MQ_USERNAME: "REPLACE_MQ_USERNAME" ports: - "51821-51830:51821-51830/udp" + - "3478:3478/udp" netmaker-ui: container_name: netmaker-ui image: gravitl/netmaker-ui:v0.18.0 @@ -70,11 +73,11 @@ services: restart: unless-stopped command: ["/mosquitto/config/wait.sh"] environment: - NETMAKER_SERVER_HOST: "https://api.NETMAKER_BASE_DOMAIN" + MQ_PASSWORD: "REPLACE_MQ_PASSWORD" + MQ_USERNAME: "REPLACE_MQ_USERNAME" volumes: - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf - /root/wait.sh:/mosquitto/config/wait.sh - - mosquitto_data:/mosquitto/data - mosquitto_logs:/mosquitto/log ports: - "1883:1883" @@ -125,7 +128,6 @@ volumes: caddy_conf: {} sqldata: {} dnsconfig: {} - mosquitto_data: {} mosquitto_logs: {} prometheus_data: {} grafana_data: {} diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index 07aadd45..821c2385 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -8,7 +8,6 @@ services: volumes: - dnsconfig:/root/config/dnsconfig - sqldata:/root/data - - mosquitto_data:/etc/netmaker environment: BROKER_NAME: "broker.NETMAKER_BASE_DOMAIN" SERVER_NAME: "NETMAKER_BASE_DOMAIN" @@ -28,7 +27,8 @@ services: MQ_PORT: "443" MQ_SERVER_PORT: "1883" VERBOSITY: "1" - MQ_ADMIN_PASSWORD: "REPLACE_MQ_ADMIN_PASSWORD" + MQ_PASSWORD: "REPLACE_MQ_PASSWORD" + MQ_USERNAME: "REPLACE_MQ_USERNAME" STUN_PORT: "3478" PROXY: "on" ports: @@ -72,16 +72,15 @@ services: restart: unless-stopped command: ["/mosquitto/config/wait.sh"] environment: - NETMAKER_SERVER_HOST: "https://api.NETMAKER_BASE_DOMAIN" + MQ_PASSWORD: "REPLACE_MQ_PASSWORD" + MQ_USERNAME: "REPLACE_MQ_USERNAME" volumes: - /root/mosquitto.conf:/mosquitto/config/mosquitto.conf - /root/wait.sh:/mosquitto/config/wait.sh - - mosquitto_data:/mosquitto/data - mosquitto_logs:/mosquitto/log volumes: caddy_data: {} caddy_conf: {} sqldata: {} dnsconfig: {} - mosquitto_data: {} mosquitto_logs: {} diff --git a/config/config.go b/config/config.go index 833687af..ec80aba9 100644 --- a/config/config.go +++ b/config/config.go @@ -67,7 +67,8 @@ type ServerConfig struct { Server string `yaml:"server"` Broker string `yam:"broker"` PublicIPService string `yaml:"publicipservice"` - MQAdminPassword string `yaml:"mqadminpassword"` + MQPassword string `yaml:"mqpassword"` + MQUserName string `yaml:"mqusername"` MetricsExporter string `yaml:"metrics_exporter"` BasicAuth string `yaml:"basic_auth"` LicenseValue string `yaml:"license_value"` diff --git a/controllers/hosts.go b/controllers/hosts.go index bcd5c5de..0aefeb56 100644 --- a/controllers/hosts.go +++ b/controllers/hosts.go @@ -97,7 +97,6 @@ func updateHost(w http.ResponseWriter, r *http.Request) { if updateRelay { logic.UpdateHostRelay(currHost.ID.String(), currHost.RelayedHosts, newHost.RelayedHosts) } - // publish host update through MQ if err := mq.HostUpdate(&models.HostUpdate{ Action: models.UpdateHost, @@ -177,10 +176,6 @@ func deleteHost(w http.ResponseWriter, r *http.Request) { logger.Log(0, r.Header.Get("user"), "failed to send delete host update: ", currHost.ID.String(), err.Error()) } - if err = mq.DeleteMqClient(currHost.ID.String()); err != nil { - logger.Log(0, "error removing DynSec credentials for host:", currHost.Name, err.Error()) - } - apiHostData := currHost.ConvertNMHostToAPI() logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name) w.WriteHeader(http.StatusOK) diff --git a/controllers/node.go b/controllers/node.go index 5c046b67..c3229928 100644 --- a/controllers/node.go +++ b/controllers/node.go @@ -577,8 +577,6 @@ func createNode(w http.ResponseWriter, r *http.Request) { } server := servercfg.GetServerInfo() server.TrafficKey = key - // consume password before hashing for mq client creation - hostPassword := data.Host.HostPass data.Node.Server = servercfg.GetServer() if !logic.HostExists(&data.Host) { logic.CheckHostPorts(&data.Host) @@ -606,18 +604,6 @@ func createNode(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) return } - } else { - // Create client for this host in Mq - if err := mq.CreateMqClient(&mq.MqClient{ - ID: data.Host.ID.String(), - Text: data.Host.Name, - Password: hostPassword, - Networks: []string{networkName}, - }); err != nil { - logger.Log(0, fmt.Sprintf("failed to create DynSec client: %v", err.Error())) - logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) - return - } } err = logic.AssociateNodeToHost(&data.Node, &data.Host) diff --git a/docker/mosquitto.conf b/docker/mosquitto.conf index 19597b80..eac096c4 100644 --- a/docker/mosquitto.conf +++ b/docker/mosquitto.conf @@ -7,5 +7,4 @@ listener 1883 protocol websockets allow_anonymous false -plugin /usr/lib/mosquitto_dynamic_security.so -plugin_opt_config_file /mosquitto/data/dynamic-security.json +password_file /mosquitto/password.txt diff --git a/docker/wait.sh b/docker/wait.sh index caf9d29d..bf98768f 100755 --- a/docker/wait.sh +++ b/docker/wait.sh @@ -1,18 +1,13 @@ #!/bin/ash -wait_for_netmaker() { - echo "SERVER: ${NETMAKER_SERVER_HOST}" - until curl --output /dev/null --silent --fail --head \ - --location "${NETMAKER_SERVER_HOST}/api/server/health"; do - echo "Waiting for netmaker server to startup" - sleep 1 - done +encrypt_password() { + echo "${MQ_USERNAME}:${MQ_PASSWORD}" > /mosquitto/password.txt + mosquitto_passwd -U /mosquitto/password.txt } main(){ - # wait for netmaker to startup - apk add curl - wait_for_netmaker + + encrypt_password echo "Starting MQ..." # Run the main container command. /docker-entrypoint.sh diff --git a/main.go b/main.go index 311856c8..215028f4 100644 --- a/main.go +++ b/main.go @@ -118,11 +118,6 @@ func startControllers() { logger.Log(0, "error occurred initializing DNS: ", err.Error()) } } - if servercfg.IsMessageQueueBackend() { - if err := mq.Configure(); err != nil { - logger.FatalLog("failed to configure MQ: ", err.Error()) - } - } //Run Rest Server if servercfg.IsRestBackend() { @@ -172,7 +167,6 @@ func runMessageQueue(wg *sync.WaitGroup) { defer wg.Done() brokerHost, secure := servercfg.GetMessageQueueEndpoint() logger.Log(0, "connecting to mq broker at", brokerHost, "with TLS?", fmt.Sprintf("%v", secure)) - mq.SetUpAdminClient() mq.SetupMQTT() ctx, cancel := context.WithCancel(context.Background()) go mq.Keepalive(ctx) diff --git a/models/structs.go b/models/structs.go index 0a73bdf1..d59c716c 100644 --- a/models/structs.go +++ b/models/structs.go @@ -229,6 +229,8 @@ type ServerConfig struct { DNSMode string `yaml:"dnsmode"` Version string `yaml:"version"` MQPort string `yaml:"mqport"` + MQUserName string `yaml:"mq_username"` + MQPassword string `yaml:"mq_password"` Server string `yaml:"server"` Broker string `yaml:"broker"` Is_EE bool `yaml:"isee"` diff --git a/mq/dynsec.go b/mq/dynsec.go deleted file mode 100644 index db36c210..00000000 --- a/mq/dynsec.go +++ /dev/null @@ -1,214 +0,0 @@ -package mq - -import ( - "crypto/sha512" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "os" - "time" - - mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/gravitl/netmaker/functions" - "github.com/gravitl/netmaker/logger" - "github.com/gravitl/netmaker/logic" - "github.com/gravitl/netmaker/netclient/ncutils" - "github.com/gravitl/netmaker/servercfg" - "golang.org/x/crypto/pbkdf2" -) - -// mq client for admin -var mqAdminClient mqtt.Client - -const ( - // constant for client command - CreateClientCmd = "createClient" - // constant for disable command - DisableClientCmd = "disableClient" - // constant for delete client command - DeleteClientCmd = "deleteClient" - // constant for modify client command - ModifyClientCmd = "modifyClient" - - // constant for create role command - CreateRoleCmd = "createRole" - // constant for delete role command - DeleteRoleCmd = "deleteRole" - - // constant for admin user name - mqAdminUserName = "Netmaker-Admin" - // constant for server user name - mqNetmakerServerUserName = "Netmaker-Server" - // constant for exporter user name - mqExporterUserName = "Netmaker-Exporter" - - // DynamicSecSubTopic - constant for dynamic security subscription topic - dynamicSecSubTopic = "$CONTROL/dynamic-security/#" - // DynamicSecPubTopic - constant for dynamic security subscription topic - dynamicSecPubTopic = "$CONTROL/dynamic-security/v1" -) - -// struct for dynamic security file -type dynJSON struct { - Clients []client `json:"clients"` - Roles []role `json:"roles"` - DefaultAcl defaultAccessAcl `json:"defaultACLAccess"` -} - -// struct for client role -type clientRole struct { - Rolename string `json:"rolename"` -} - -// struct for MQ client -type client struct { - Username string `json:"username"` - TextName string `json:"textName"` - Password string `json:"password"` - Salt string `json:"salt"` - Iterations int `json:"iterations"` - Roles []clientRole `json:"roles"` -} - -// struct for MQ role -type role struct { - Rolename string `json:"rolename"` - Acls []Acl `json:"acls"` -} - -// struct for default acls -type defaultAccessAcl struct { - PublishClientSend bool `json:"publishClientSend"` - PublishClientReceive bool `json:"publishClientReceive"` - Subscribe bool `json:"subscribe"` - Unsubscribe bool `json:"unsubscribe"` -} - -// MqDynSecGroup - struct for MQ client group -type MqDynSecGroup struct { - Groupname string `json:"groupname"` - Priority int `json:"priority"` -} - -// MqDynSecRole - struct for MQ client role -type MqDynSecRole struct { - Rolename string `json:"rolename"` - Priority int `json:"priority"` -} - -// Acl - struct for MQ acls -type Acl struct { - AclType string `json:"acltype"` - Topic string `json:"topic"` - Priority int `json:"priority,omitempty"` - Allow bool `json:"allow"` -} - -// MqDynSecCmd - struct for MQ dynamic security command -type MqDynSecCmd struct { - Command string `json:"command"` - Username string `json:"username"` - Password string `json:"password"` - RoleName string `json:"rolename,omitempty"` - Acls []Acl `json:"acls,omitempty"` - Clientid string `json:"clientid"` - Textname string `json:"textname"` - Textdescription string `json:"textdescription"` - Groups []MqDynSecGroup `json:"groups"` - Roles []MqDynSecRole `json:"roles"` -} - -// MqDynsecPayload - struct for dynamic security command payload -type MqDynsecPayload struct { - Commands []MqDynSecCmd `json:"commands"` -} - -// encodePasswordToPBKDF2 - encodes the given password with PBKDF2 hashing for MQ -func encodePasswordToPBKDF2(password string, salt string, iterations int, keyLength int) string { - binaryEncoded := pbkdf2.Key([]byte(password), []byte(salt), iterations, keyLength, sha512.New) - return base64.StdEncoding.EncodeToString(binaryEncoded) -} - -// Configure - configures the dynamic initial configuration for MQ -func Configure() error { - - logger.Log(0, "Configuring MQ...") - dynConfig := dynConfigInI - path := functions.GetNetmakerPath() + ncutils.GetSeparator() + dynamicSecurityFile - - password := servercfg.GetMqAdminPassword() - if password == "" { - return errors.New("MQ admin password not provided") - } - if logic.CheckIfFileExists(path) { - data, err := os.ReadFile(path) - if err == nil { - var cfg dynJSON - err = json.Unmarshal(data, &cfg) - if err == nil { - logger.Log(0, "MQ config exists already, So Updating Existing Config...") - dynConfig = cfg - } - } - } - exporter := false - for i, cI := range dynConfig.Clients { - if cI.Username == mqAdminUserName || cI.Username == mqNetmakerServerUserName { - salt := logic.RandomString(12) - hashed := encodePasswordToPBKDF2(password, salt, 101, 64) - cI.Password = hashed - cI.Iterations = 101 - cI.Salt = base64.StdEncoding.EncodeToString([]byte(salt)) - dynConfig.Clients[i] = cI - } else if servercfg.Is_EE && cI.Username == mqExporterUserName { - exporter = true - exporterPassword := servercfg.GetLicenseKey() - salt := logic.RandomString(12) - hashed := encodePasswordToPBKDF2(exporterPassword, salt, 101, 64) - cI.Password = hashed - cI.Iterations = 101 - cI.Salt = base64.StdEncoding.EncodeToString([]byte(salt)) - dynConfig.Clients[i] = cI - } - } - if servercfg.Is_EE && !exporter { - exporterPassword := servercfg.GetLicenseKey() - salt := logic.RandomString(12) - hashed := encodePasswordToPBKDF2(exporterPassword, salt, 101, 64) - exporterMQClient.Password = hashed - exporterMQClient.Iterations = 101 - exporterMQClient.Salt = base64.StdEncoding.EncodeToString([]byte(salt)) - dynConfig.Clients = append(dynConfig.Clients, exporterMQClient) - } - data, err := json.MarshalIndent(dynConfig, "", " ") - if err != nil { - return err - } - return os.WriteFile(path, data, 0755) -} - -// publishes the message to dynamic security topic -func publishEventToDynSecTopic(payload MqDynsecPayload) error { - - d, err := json.Marshal(payload) - if err != nil { - return err - } - var connecterr error - if token := mqAdminClient.Publish(dynamicSecPubTopic, 2, false, d); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil { - if token.Error() == nil { - connecterr = errors.New("connect timeout") - } else { - connecterr = token.Error() - } - } - return connecterr -} - -// watchDynSecTopic - message handler for dynamic security responses -func watchDynSecTopic(client mqtt.Client, msg mqtt.Message) { - - logger.Log(1, fmt.Sprintf("----->WatchDynSecTopic Message: %+v", string(msg.Payload()))) - -} diff --git a/mq/dynsec_clients.go b/mq/dynsec_clients.go deleted file mode 100644 index 7ae71fb5..00000000 --- a/mq/dynsec_clients.go +++ /dev/null @@ -1,47 +0,0 @@ -package mq - -// MqClient - type for taking in an MQ client's data -type MqClient struct { - ID string - Text string - Password string - Networks []string -} - -// DeleteMqClient - removes a client from the DynSec system -func DeleteMqClient(hostID string) error { - - event := MqDynsecPayload{ - Commands: []MqDynSecCmd{ - { - Command: DeleteClientCmd, - Username: hostID, - }, - }, - } - return publishEventToDynSecTopic(event) -} - -// CreateMqClient - creates an MQ DynSec client -func CreateMqClient(client *MqClient) error { - - event := MqDynsecPayload{ - Commands: []MqDynSecCmd{ - { - Command: CreateClientCmd, - Username: client.ID, - Password: client.Password, - Textname: client.Text, - Roles: []MqDynSecRole{ - { - Rolename: genericRole, - Priority: -1, - }, - }, - Groups: make([]MqDynSecGroup, 0), - }, - }, - } - - return publishEventToDynSecTopic(event) -} diff --git a/mq/dynsec_helper.go b/mq/dynsec_helper.go deleted file mode 100644 index 426e3993..00000000 --- a/mq/dynsec_helper.go +++ /dev/null @@ -1,187 +0,0 @@ -package mq - -import ( - "errors" - "time" - - mqtt "github.com/eclipse/paho.mqtt.golang" - "github.com/gravitl/netmaker/servercfg" -) - -const ( - // constant for admin role - adminRole = "admin" - // constant for generic role - genericRole = "generic" - - // const for dynamic security file - dynamicSecurityFile = "dynamic-security.json" -) - -var ( - // default configuration of dynamic security - dynConfigInI = dynJSON{ - Clients: []client{ - { - Username: mqAdminUserName, - TextName: "netmaker admin user", - Password: "", - Salt: "", - Iterations: 0, - Roles: []clientRole{ - { - Rolename: adminRole, - }, - }, - }, - { - Username: mqNetmakerServerUserName, - TextName: "netmaker server user", - Password: "", - Salt: "", - Iterations: 0, - Roles: []clientRole{ - { - Rolename: genericRole, - }, - }, - }, - exporterMQClient, - }, - Roles: []role{ - { - Rolename: adminRole, - Acls: fetchAdminAcls(), - }, - { - Rolename: genericRole, - Acls: fetchGenericAcls(), - }, - }, - DefaultAcl: defaultAccessAcl{ - PublishClientSend: false, - PublishClientReceive: true, - Subscribe: false, - Unsubscribe: true, - }, - } - - exporterMQClient = client{ - Username: mqExporterUserName, - TextName: "netmaker metrics exporter", - Password: "", - Salt: "", - Iterations: 101, - Roles: []clientRole{ - { - Rolename: genericRole, - }, - }, - } -) - -// GetAdminClient - fetches admin client of the MQ -func GetAdminClient() (mqtt.Client, error) { - opts := mqtt.NewClientOptions() - setMqOptions(mqAdminUserName, servercfg.GetMqAdminPassword(), opts) - mqclient := mqtt.NewClient(opts) - var connecterr error - if token := mqclient.Connect(); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil { - if token.Error() == nil { - connecterr = errors.New("connect timeout") - } else { - connecterr = token.Error() - } - } - return mqclient, connecterr -} - -// genericAcls - fetches generice role related acls -func fetchGenericAcls() []Acl { - return []Acl{ - { - AclType: "publishClientSend", - Topic: "#", - Priority: -1, - Allow: true, - }, - { - AclType: "publishClientReceive", - Topic: "#", - Priority: -1, - Allow: true, - }, - { - AclType: "subscribePattern", - Topic: "#", - Priority: -1, - Allow: true, - }, - { - AclType: "unsubscribePattern", - Topic: "#", - Priority: -1, - Allow: true, - }, - } -} - -// fetchAdminAcls - fetches admin role related acls -func fetchAdminAcls() []Acl { - return []Acl{ - { - AclType: "publishClientSend", - Topic: "$CONTROL/dynamic-security/#", - Priority: -1, - Allow: true, - }, - { - AclType: "publishClientReceive", - Topic: "$CONTROL/dynamic-security/#", - Priority: -1, - Allow: true, - }, - { - AclType: "subscribePattern", - Topic: "$CONTROL/dynamic-security/#", - Priority: -1, - Allow: true, - }, - { - AclType: "publishClientReceive", - Topic: "$SYS/#", - Priority: -1, - Allow: true, - }, - { - AclType: "subscribePattern", - Topic: "$SYS/#", - Priority: -1, - Allow: true, - }, - { - AclType: "publishClientReceive", - Topic: "#", - Priority: -1, - Allow: true, - }, - { - AclType: "subscribePattern", - Topic: "#", - Priority: -1, - Allow: true, - }, - { - AclType: "unsubscribePattern", - Topic: "#", - Priority: -1, - Allow: true, - }, - { - AclType: "publishClientSend", - Topic: "#", - Priority: -1, - Allow: true, - }, - } -} diff --git a/mq/mq.go b/mq/mq.go index e24811ae..056dc925 100644 --- a/mq/mq.go +++ b/mq/mq.go @@ -2,7 +2,6 @@ package mq import ( "context" - "fmt" "time" mqtt "github.com/eclipse/paho.mqtt.golang" @@ -23,39 +22,6 @@ var peer_force_send = 0 var mqclient mqtt.Client -// SetUpAdminClient - sets up admin client for the MQ -func SetUpAdminClient() { - opts := mqtt.NewClientOptions() - setMqOptions(mqAdminUserName, servercfg.GetMqAdminPassword(), opts) - mqAdminClient = mqtt.NewClient(opts) - opts.SetOnConnectHandler(func(client mqtt.Client) { - if token := client.Subscribe(dynamicSecSubTopic, 2, mqtt.MessageHandler(watchDynSecTopic)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil { - client.Disconnect(240) - logger.Log(0, fmt.Sprintf("Dynamic security client subscription failed: %v ", token.Error())) - } - - opts.SetOrderMatters(true) - opts.SetResumeSubs(true) - }) - tperiod := time.Now().Add(10 * time.Second) - for { - if token := mqAdminClient.Connect(); !token.WaitTimeout(MQ_TIMEOUT*time.Second) || token.Error() != nil { - logger.Log(2, "Admin: unable to connect to broker, retrying ...") - if time.Now().After(tperiod) { - if token.Error() == nil { - logger.FatalLog("Admin: could not connect to broker, token timeout, exiting ...") - } else { - logger.FatalLog("Admin: could not connect to broker, exiting ...", token.Error().Error()) - } - } - } else { - break - } - time.Sleep(2 * time.Second) - } - -} - func setMqOptions(user, password string, opts *mqtt.ClientOptions) { broker, _ := servercfg.GetMessageQueueEndpoint() opts.AddBroker(broker) @@ -73,7 +39,7 @@ func setMqOptions(user, password string, opts *mqtt.ClientOptions) { // SetupMQTT creates a connection to broker and return client func SetupMQTT() { opts := mqtt.NewClientOptions() - setMqOptions(mqNetmakerServerUserName, servercfg.GetMqAdminPassword(), opts) + setMqOptions(servercfg.GetMqUserName(), servercfg.GetMqPassword(), opts) opts.SetOnConnectHandler(func(client mqtt.Client) { if token := client.Subscribe("ping/#", 2, mqtt.MessageHandler(Ping)); token.WaitTimeout(MQ_TIMEOUT*time.Second) && token.Error() != nil { client.Disconnect(240) diff --git a/scripts/nm-quick-interactive.sh b/scripts/nm-quick-interactive.sh index 180e6a09..baa9e875 100644 --- a/scripts/nm-quick-interactive.sh +++ b/scripts/nm-quick-interactive.sh @@ -188,9 +188,7 @@ NETMAKER_BASE_DOMAIN=nm.$(curl -s ifconfig.me | tr . -).nip.io COREDNS_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p') SERVER_PUBLIC_IP=$(curl -s ifconfig.me) MASTER_KEY=$(tr -dc A-Za-z0-9