diff --git a/go.mod b/go.mod index 061e828..0ee2cbb 100644 --- a/go.mod +++ b/go.mod @@ -51,4 +51,5 @@ require ( golang.org/x/sys v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 51efaf1..c10012f 100644 --- a/go.sum +++ b/go.sum @@ -161,6 +161,7 @@ google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= diff --git a/main.go b/main.go index 12849b1..30a30e9 100644 --- a/main.go +++ b/main.go @@ -74,6 +74,11 @@ type ExitNodePingResult struct { WasPreviouslyConnected bool `json:"wasPreviouslyConnected"` } +type BlueprintResult struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + // Custom flag type for multiple CA files type stringSlice []string @@ -115,6 +120,7 @@ var ( preferEndpoint string healthMonitor *healthcheck.Monitor enforceHealthcheckCert bool + blueprintFile string // New mTLS configuration variables tlsClientCert string @@ -172,6 +178,7 @@ func main() { if tlsPrivateKey == "" { tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT") } + blueprintFile = os.Getenv("BLUEPRINT_FILE") if endpoint == "" { flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server") @@ -271,6 +278,9 @@ func main() { if healthFile == "" { flag.StringVar(&healthFile, "health-file", "", "Path to health file (if unset, health file won't be written)") } + if blueprintFile == "" { + flag.StringVar(&blueprintFile, "blueprint-file", "", "Path to blueprint file (if unset, no blueprint will be applied)") + } // do a --version check version := flag.Bool("version", false, "Print the version") @@ -1193,6 +1203,29 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub } }) + // Register handler for getting health check status + client.RegisterHandler("newt/blueprint/results", func(msg websocket.WSMessage) { + logger.Debug("Received blueprint results message") + + var blueprintResult BlueprintResult + + jsonData, err := json.Marshal(msg.Data) + if err != nil { + logger.Info("Error marshaling data: %v", err) + return + } + if err := json.Unmarshal(jsonData, &blueprintResult); err != nil { + logger.Info("Error unmarshaling config results data: %v", err) + return + } + + if blueprintResult.Success { + logger.Info("Blueprint applied successfully!") + } else { + logger.Warn("Blueprint application failed: %s", blueprintResult.Message) + } + }) + client.OnConnect(func() error { publicKey = privateKey.PublicKey() logger.Debug("Public key: %s", publicKey) @@ -1216,6 +1249,8 @@ persistent_keepalive_interval=5`, fixKey(privateKey.String()), fixKey(wgData.Pub "backwardsCompatible": true, }) + sendBlueprint(client) + if err != nil { logger.Error("Failed to send registration message: %v", err) return err diff --git a/util.go b/util.go index 7d6da4f..72d2bda 100644 --- a/util.go +++ b/util.go @@ -21,6 +21,7 @@ import ( "golang.org/x/net/ipv4" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun/netstack" + "gopkg.in/yaml.v3" ) func fixKey(key string) string { @@ -558,3 +559,47 @@ func executeUpdownScript(action, proto, target string) (string, error) { return target, nil } + +func sendBlueprint(client *websocket.Client) error { + if blueprintFile == "" { + return nil + } + // try to read the blueprint file + blueprintData, err := os.ReadFile(blueprintFile) + if err != nil { + logger.Error("Failed to read blueprint file: %v", err) + } else { + // first we should convert the yaml to json and error if the yaml is bad + var yamlObj interface{} + var blueprintJsonData string + + err = yaml.Unmarshal(blueprintData, &yamlObj) + if err != nil { + logger.Error("Failed to parse blueprint YAML: %v", err) + } else { + // convert to json + jsonBytes, err := json.Marshal(yamlObj) + if err != nil { + logger.Error("Failed to convert blueprint to JSON: %v", err) + } else { + blueprintJsonData = string(jsonBytes) + logger.Debug("Converted blueprint to JSON: %s", blueprintJsonData) + } + } + + // if we have valid json data, we can send it to the server + if blueprintJsonData == "" { + logger.Error("No valid blueprint JSON data to send to server") + return nil + } + + logger.Info("Sending blueprint to server for application") + + // send the blueprint data to the server + err = client.SendMessage("newt/blueprint/apply", map[string]interface{}{ + "blueprint": blueprintJsonData, + }) + } + + return nil +}