├── .gitignore ├── CHANGELOG.adoc ├── LICENSE ├── Makefile ├── README.adoc ├── cmd ├── neutron │ ├── genconfig.go │ ├── join.go │ ├── main.go │ ├── template.go │ ├── template.yml │ └── update.go └── quasar │ ├── address.go │ ├── config.go │ ├── db.go │ ├── firewall.go │ ├── main.go │ ├── networks.go │ ├── neutron.go │ ├── nodes.go │ └── serve.go ├── docs ├── conclusion.adoc ├── hubble.adoc ├── images │ ├── hubblenetwork.png │ └── hubblenodes.png ├── introduction.adoc ├── neutron.adoc ├── quasar.adoc └── report.adoc ├── examples ├── install-neutron.sh ├── nebula@.service └── quasar.yml ├── go.mod ├── go.sum ├── hubble ├── README.adoc ├── package-lock.json ├── package.json ├── public │ ├── favicon.png │ ├── global.css │ └── index.html ├── rollup.config.js ├── scripts │ └── setupTypeScript.js ├── src │ ├── App.svelte │ ├── api.js │ ├── components │ │ ├── firewall.svelte │ │ ├── nodecard.svelte │ │ ├── nodesettings.svelte │ │ └── settings.svelte │ ├── main.js │ ├── pages │ │ ├── Home.svelte │ │ ├── login.svelte │ │ ├── network.svelte │ │ └── newnet.svelte │ ├── server.js │ ├── sidebar.svelte │ └── store.js └── tailwind.config.js ├── nebutils ├── config.go ├── keyconvert.go └── keygen.go ├── report.pdf ├── wormhole ├── schemas.go └── tokens.go └── xpaseto ├── curve25519.go ├── curve25519_test.go ├── signer.go ├── signer_test.go ├── utils.go ├── v2.go └── v2_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | hubble/node_modules/ 2 | hubble/public/build/ 3 | *.db 4 | .vim/ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | = Changelog 2 | 3 | The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog], 4 | and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Versioning]. 5 | 6 | 7 | [discrete] 8 | == 0.3.0 (2021-05-23) 9 | 10 | [discrete] 11 | === Added 12 | 13 | * Quasar signs config using XPASETO tokens (additional security layer to HTTPS) 14 | * Network certificate fingerprints are shown in Hubble frontend 15 | * Node public key fingerprints are shown in Hubble frontend 16 | * JWT Middleware has been added to require username+password auth to manage networks. 17 | 18 | [discrete] 19 | == 0.2.0 (2021-05-22) 20 | 21 | [discrete] 22 | === Added 23 | 24 | * Added ability to modify firewall rules through Quasar 25 | * Added firewall update forms to Hubble 26 | * Time is saved when a node fetches the latest config 27 | * Latest config fetch time is shown in Hubble 28 | 29 | [discrete] 30 | == 0.1.0 (2021-05-22) 31 | 32 | [discrete] 33 | === 2021-05-22 34 | 35 | ==== Added 36 | 37 | * Listen port can now be changed through frontend 38 | 39 | ==== Fixed 40 | 41 | * Formatting of static host map in nebula yaml config 42 | 43 | [discrete] 44 | === 2021-05-21 45 | 46 | ==== Added 47 | 48 | * Quasar config endpoint fully working 49 | * Neutron get config and write to yaml file working 50 | 51 | [discrete] 52 | === 2021-05-20 53 | 54 | ==== Added 55 | 56 | * Update node endpoint working in Quasar 57 | * Update node functionality working in hubble 58 | 59 | [discrete] 60 | === 2021-05-19 61 | 62 | ==== Changed 63 | 64 | * Using negroni golang library for logging and future authentication middleware. 65 | 66 | ==== Added 67 | 68 | * Added cipher type to network information in db and API responses. 69 | * Completed update network API endpoint. 70 | * Added node info endpoints. 71 | * Added groups array to networks in db and made them updatable. 72 | 73 | [discrete] 74 | === 2021-05-18 75 | 76 | ==== Added 77 | 78 | * Started working on Hubble frontend using Svelte compiler 79 | * Created frontend app structure and integrated with quasar 80 | * Modified how CORS requests work with quasar to work with client 81 | 82 | [discrete] 83 | === 2021-05-17 84 | 85 | ==== Added 86 | 87 | * Finished neutron join network capability 88 | * Started adding neutron update capability 89 | 90 | [discrete] 91 | === 2021-05-16 92 | 93 | ==== Added 94 | 95 | * Node Endpoints for Quasar 96 | * Finished key network endpoints for Quasar 97 | * Added some neutron endpoints 98 | 99 | [discrete] 100 | === 2021-05-14 101 | 102 | ==== Changed 103 | 104 | * Removed Python and switched back to golang due to crypto dependencies 105 | 106 | [discrete] 107 | === 2021-05-10 108 | 109 | ==== Changed 110 | 111 | * Using Python for Quasar instead of golang 112 | 113 | [discrete] 114 | === Initial Stages - 2021-04-08 115 | 116 | ==== Added 117 | 118 | * Project Structure 119 | ** `cmd` directory for neutron and quasar main outputs 120 | ** `examples` for example config 121 | ** `nebutils` as library for neutron and quasar to share code 122 | ** general repo files e.g. README, LICENSE, CHANGELOG 123 | * Started work on `quasar` 124 | ** Added functionality to add a new network 125 | ** Added basic http server functionality 126 | ** added ability to interact with boltdb as a database interface 127 | * Started work on `neutron` 128 | ** Added main function to parse user flags 129 | ** Started on `init.go` which gets ca cert, 130 | generates keys and requests signing 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Billy Bromell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | default: release 4 | 5 | neutron: cmd/neutron/*.go 6 | go build -o neutron cmd/neutron/*.go 7 | 8 | quasar: cmd/quasar/*.go 9 | go build -o quasar cmd/quasar/*.go 10 | 11 | release: neutron quasar examples/nebula@.service 12 | mkdir -p release/linux-x86_64 13 | cp neutron release/linux-x86_64 14 | cp quasar release/linux-x86_64 15 | cp examples/nebula@.service release/linux-x86_64 16 | tar -cvf release/linux-x86_64.tar.gz release/linux-x86_64 17 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Starship 🚀 2 | 3 | == Overview 4 | 5 | CAUTION: THIS PROJECT IS NOT PRODUCTION READY. 6 | 7 | The goal of this project is to provide a config and certificate 8 | management system for link:https://github.com/slackhq/nebula[nebula]. 9 | This project was done in a short amount of time and it is my first 10 | project using golang. 11 | I would not recommend using it without auditing it first. 12 | 13 | == Quasar Server 14 | 15 | === Overview 16 | 17 | Quasar is a Central Management System (CMS) for managing Starship networks. 18 | It provides APIs for two types of clients: 19 | 20 | * Neutron Nodes 21 | ** These authenticate by signing requests using their nebula private key 22 | * Frontend clients / management tools 23 | ** These authenticate using JSON Web Tokens 24 | 25 | Quasar can be configured using a yaml config file. 26 | 27 | The API for neutron nodes provides the following endpoints: 28 | 29 | The API for management clients provides endpoints for: 30 | 31 | * listing networks 32 | * getting CA cert for a network 33 | * listing nodes in a network 34 | * updating network settings 35 | * updating node settings 36 | * approving / enabling / disabling nodes 37 | 38 | === Installation Instructions 39 | 40 | [source,shell] 41 | ---- 42 | make quasar 43 | ---- 44 | 45 | === Operating Instructions 46 | 47 | [source,shell] 48 | ---- 49 | # set JWT signing secret 50 | export QUASAR_AUTHSECRET=$(uuid) 51 | 52 | # set admin account password 53 | export QUASAR_ADMINPASS="password" 54 | 55 | # start server 56 | ./quasar serve -config examples/quasar.yml 57 | ---- 58 | 59 | == Neutron 60 | 61 | === Overview 62 | 63 | Neutron is a client which Starship nodes use to request to join networks, 64 | and update their configuration and certificates. 65 | 66 | When joining a new network, Neutron will create a new Nebula keypair. 67 | It will then send a request to Quasar to join a specific network. 68 | This request includes the node name, the network it wants to join, 69 | its hostname and its Nebula public key. 70 | This information is sent as a JSON payload, signed using the Nebula 71 | private key. 72 | This is encoded similarly to a PASETO token. 73 | PASETO tokens are similar to JSON Web Tokens (JWTs), 74 | however do not suffer the same vulnerabilities JWTs suffer due to the vague 75 | protocol specification. 76 | 77 | When updating, Neutron will send requests to Quasar to obtain 78 | an updated certificate and configuration file. 79 | For Quasar to send these, Neutron must include a signed token 80 | which includes it's nodename and the network name it is trying to 81 | update, and the node must be approved and active on the Quasar server. 82 | The signature on the token is verified against the public key stored 83 | for the node on the Quasar server. 84 | 85 | The update script can be run at frequent intervals to keep the node updated 86 | with the most recent configuration changes. 87 | 88 | === Installation Instructions 89 | 90 | [source,shell] 91 | ---- 92 | # build 93 | cd starship 94 | 95 | # equivalent of `go build -o neutron cmd/neutron/*.go` 96 | make neutron 97 | ---- 98 | 99 | === Operating Instructions 100 | 101 | ==== Manual install 102 | 103 | [source,shell] 104 | ---- 105 | # request to join network 106 | ./neutron join -quasar http://127.0.0.1:6947 -network NETWORK -name NAME 107 | 108 | # approve node from frontend then fetch latest config from Quasar 109 | ./neutron update -network NETWORK 110 | # send SIGHUP to nebula to force config reload 111 | pgrep nebula | xargs sudo kill -1 112 | ---- 113 | 114 | ==== Using Install Script 115 | 116 | [source, shell] 117 | ---- 118 | # quick install from release 119 | wget https://github.com/b177y/starship/releases/download/v0.3.0/install-neutron.sh -O /tmp/install-neutron.sh 120 | 121 | # check content 122 | less /tmp/install-neutron.sh 123 | bash /tmp/install-neutron.sh 124 | 125 | # approve node from frontend then fetch latest config from Quasar 126 | neutron update -network NETWORK 127 | 128 | # start nebula with systemd 129 | sudo systemctl start nebula@NETWORK 130 | 131 | # send SIGHUP to nebula to force config reload 132 | pgrep nebula | xargs sudo kill -1 133 | ---- 134 | 135 | == Hubble 136 | 137 | Hubble is the frontend for managing Starship networks. 138 | See link:hubble/README.adoc[] for setup instructions. 139 | -------------------------------------------------------------------------------- /cmd/neutron/genconfig.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/template" 7 | ) 8 | 9 | type StaticHost struct { 10 | NebulaAddress string 11 | Endpoint []string 12 | } 13 | 14 | type FirewallRule struct { 15 | Port string `json:"port"` 16 | Proto string `json:"proto"` 17 | Groups []string `json:"groups"` 18 | Any bool `json:"any"` 19 | } 20 | 21 | type NodeConfigSchema struct { 22 | Address string `json:"address"` 23 | Lighthouses []string `json:"lighthouses"` 24 | AmLighthouse bool `json:"am_lighthouse"` 25 | StaticHosts []StaticHost `json:"static_hosts"` 26 | ListenPort int `json:"listen_port"` 27 | FirewallInbound []FirewallRule `json:"firewall_inbound"` 28 | FirewallOutbound []FirewallRule `json:"firewall_outbound"` 29 | Cipher string `json:"cipher"` 30 | Cert string `json:"cert"` 31 | Netname string 32 | } 33 | 34 | func genConfig(config NodeConfigSchema) (err error) { 35 | templateData, err := Asset("cmd/neutron/template.yml") 36 | if err != nil { 37 | return err 38 | } 39 | path := fmt.Sprintf("/etc/nebula/%s/nebula.yml", config.Netname) 40 | f, err := os.Create(path) 41 | if err != nil { 42 | return err 43 | } 44 | err = f.Truncate(0) 45 | if err != nil { 46 | return err 47 | } 48 | tmpl, err := template.New("NebulaConfig").Parse(string(templateData)) 49 | if err != nil { 50 | return err 51 | } 52 | err = tmpl.Execute(f, config) 53 | if err != nil { 54 | return err 55 | } 56 | return f.Close() 57 | } 58 | -------------------------------------------------------------------------------- /cmd/neutron/join.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | 13 | "github.com/b177y/starship/nebutils" 14 | "github.com/b177y/starship/wormhole" 15 | "github.com/b177y/starship/xpaseto" 16 | log "github.com/sirupsen/logrus" 17 | "github.com/slackhq/nebula/cert" 18 | "github.com/teris-io/shortid" 19 | "gopkg.in/yaml.v2" 20 | ) 21 | 22 | // config to save the address of a quasar server and the node name 23 | type NeutronConfig struct { 24 | Quasar string `yaml:"quasar"` 25 | Nodename string `yaml:"nodename"` 26 | } 27 | 28 | // save the neutron config to a yaml file 29 | func saveNeutronConfig(netname, quasar, nodename string) error { 30 | nconf := NeutronConfig{ 31 | Quasar: quasar, 32 | Nodename: nodename, 33 | } 34 | path := fmt.Sprintf("/etc/nebula/%s/neutron.yml", netname) 35 | f, err := os.Create(path) 36 | if err != nil { 37 | return err 38 | } 39 | err = f.Truncate(0) 40 | if err != nil { 41 | return err 42 | } 43 | e := yaml.NewEncoder(f) 44 | e.Encode(nconf) 45 | return f.Close() 46 | } 47 | 48 | // request to join a Starship network 49 | func signReq(netname, hostname, nodename, qAddr string, 50 | privkey, pubkey []byte) { 51 | // create request payload 52 | t := wormhole.RequestJoinSchema{ 53 | Netname: netname, 54 | Nodename: nodename, 55 | Hostname: hostname, 56 | PubKey: string(cert.MarshalX25519PublicKey(pubkey)), 57 | } 58 | 59 | // turn payload into token 60 | jsonToken, err := wormhole.NewToken(t) 61 | if err != nil { 62 | log.Fatal("Error creating paseto." + err.Error()) 63 | } 64 | // sign token using nebula private key 65 | signer := xpaseto.NewSigner(privkey, pubkey) 66 | token, err := signer.SelfSignPaseto(jsonToken) 67 | if err != nil { 68 | log.Fatal("Error signing paseto." + err.Error()) 69 | } 70 | 71 | body := bytes.NewBuffer([]byte(token)) 72 | resp, err := http.Post(qAddr+"/api/neutron/join", "text/plain", body) 73 | if err != nil { 74 | log.Fatal("Error contacting quasar." + err.Error()) 75 | } 76 | defer resp.Body.Close() 77 | 78 | respBody, err := ioutil.ReadAll(resp.Body) 79 | if err != nil { 80 | log.Fatal("Error reading response body from quasar." + err.Error()) 81 | } 82 | if resp.StatusCode != 200 { 83 | log.Fatal("Error status from quasar: " + string(respBody)) 84 | } 85 | pubFingerprint := base64.StdEncoding.EncodeToString(pubkey) 86 | log.Println("Node Fingerprint: ", pubFingerprint) 87 | 88 | } 89 | 90 | // get the certificate of the CA of a network 91 | func getCaCert(qAddr string, netName string) { 92 | path := "/etc/nebula/" 93 | os.MkdirAll(path, 0775) 94 | path = path + netName 95 | err := os.Mkdir(path, 0770) 96 | if err != nil { 97 | if os.IsExist(err) { 98 | log.Warning("Network already exists locally. Existing config may be overwritten!") 99 | } else if os.IsPermission(err) { 100 | log.Fatal("Permission denied. Are you running as root?") 101 | } else { 102 | log.Fatal("Error: " + err.Error()) 103 | } 104 | } 105 | ca_url := qAddr + "/api/networks/" + netName + "/cert" 106 | resp, err := http.Get(ca_url) 107 | if err != nil { 108 | log.Fatal("Could not get ca from Quasar server. " + err.Error()) 109 | } 110 | defer resp.Body.Close() 111 | if resp.StatusCode != http.StatusOK { 112 | if resp.StatusCode == 404 { 113 | log.Fatalf("Network %s does not exist.\n", netName) 114 | } 115 | log.Fatalf("Bad status code from Quasar: %d", resp.StatusCode) 116 | } 117 | body, err := ioutil.ReadAll(resp.Body) 118 | if err != nil { 119 | log.Fatal("Could not read HTTP body") 120 | } 121 | cacert, _, err := cert.UnmarshalNebulaCertificateFromPEM(body) 122 | if err != nil { 123 | log.Fatal("Cert is not a valid Nebula Certificate.") 124 | } 125 | certfp, err := cacert.Sha256Sum() 126 | if err != nil { 127 | log.Fatal("Cert is not a valid Nebula Certificate. Could not calculate fingerprint.") 128 | } 129 | fmt.Printf("Certificate Fingerprint: %s\n", certfp) 130 | reader := bufio.NewReader(os.Stdin) 131 | fmt.Print("Trust fingerprint? (Y/n) ") 132 | inp, _ := reader.ReadString('\n') 133 | if inp == "n\n" || inp == "N\n" { 134 | log.Fatal("Certificate is not trusted. Exitting.") 135 | } 136 | log.Println("Saving certificate") 137 | dst := path + "/ca.crt" 138 | f, err := os.Create(dst) 139 | if err != nil { 140 | log.Fatalf("Could not create file %s", dst) 141 | } 142 | r := bytes.NewReader(body) 143 | _, err = io.Copy(f, r) 144 | } 145 | 146 | func initialise(qAddr string, 147 | nodeName string, 148 | netName string, 149 | ) { 150 | hostname, err := os.Hostname() 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | // validate commandline arguments 155 | if qAddr == "" { 156 | log.Fatal("Quasar address must be given with -quasar") 157 | } 158 | if netName == "" { 159 | log.Fatal("Network name must be given with -network") 160 | } 161 | if nodeName == "" { 162 | // generate node name based on hostname if none provided 163 | hostid, err := shortid.Generate() 164 | if err != nil { 165 | log.Fatal("Could not generate id for nodename" + err.Error()) 166 | } 167 | nodeName = fmt.Sprintf("%s-%s", hostname, hostid) 168 | } 169 | getCaCert(qAddr, netName) 170 | // create nebula keypair 171 | pubkey, privkey := nebutils.X25519KeyPair() 172 | err = nebutils.SaveKey("/etc/nebula/"+netName, 173 | "neutron.key", 174 | privkey, 175 | ) 176 | if err != nil { 177 | log.Fatal("Error saving key" + err.Error()) 178 | } 179 | // request server to sign pubkey 180 | signReq(netName, hostname, nodeName, qAddr, 181 | privkey, pubkey) 182 | // save config 183 | saveNeutronConfig(netName, qAddr, nodeName) 184 | } 185 | -------------------------------------------------------------------------------- /cmd/neutron/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func main() { 12 | // Set log level 13 | log.SetLevel(log.DebugLevel) 14 | 15 | // define -help command flag 16 | printUsage := flag.Bool("help", false, "Print command line usage") 17 | 18 | // define subcommands 19 | joinCommand := flag.NewFlagSet("join", flag.ExitOnError) 20 | updateCommand := flag.NewFlagSet("update", flag.ExitOnError) 21 | 22 | // define flags for join subcommand 23 | joinQaddrFlag := joinCommand.String("quasar", "", "Quasar address") 24 | joinNameFlag := joinCommand.String("name", "", "Node name") 25 | joinNetnameFlag := joinCommand.String("network", "", "Name of network to join") 26 | 27 | // define flags for update subcommand 28 | updateNetnameFlag := updateCommand.String("network", "", "Name of network to update") 29 | 30 | // parse basic flags (for help option) 31 | flag.Parse() 32 | if *printUsage { 33 | flag.Usage() 34 | os.Exit(0) 35 | } 36 | 37 | if len(os.Args) < 2 { 38 | fmt.Println("join or update subcommand is required.") 39 | os.Exit(1) 40 | } 41 | 42 | switch os.Args[1] { 43 | case "join": 44 | joinCommand.Parse(os.Args[2:]) 45 | case "update": 46 | updateCommand.Parse(os.Args[2:]) 47 | default: 48 | flag.PrintDefaults() 49 | os.Exit(1) 50 | } 51 | 52 | if joinCommand.Parsed() { 53 | // run initialise (join) function 54 | initialise(*joinQaddrFlag, 55 | *joinNameFlag, 56 | *joinNetnameFlag, 57 | ) 58 | } else if updateCommand.Parsed() { 59 | // run update function 60 | update(*updateNetnameFlag) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cmd/neutron/template.go: -------------------------------------------------------------------------------- 1 | // Code generated for package main by go-bindata DO NOT EDIT. (@generated) 2 | // sources: 3 | // cmd/neutron/template.yml 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | func bindataRead(data []byte, name string) ([]byte, error) { 19 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 20 | if err != nil { 21 | return nil, fmt.Errorf("Read %q: %v", name, err) 22 | } 23 | 24 | var buf bytes.Buffer 25 | _, err = io.Copy(&buf, gz) 26 | clErr := gz.Close() 27 | 28 | if err != nil { 29 | return nil, fmt.Errorf("Read %q: %v", name, err) 30 | } 31 | if clErr != nil { 32 | return nil, err 33 | } 34 | 35 | return buf.Bytes(), nil 36 | } 37 | 38 | type asset struct { 39 | bytes []byte 40 | info os.FileInfo 41 | } 42 | 43 | type bindataFileInfo struct { 44 | name string 45 | size int64 46 | mode os.FileMode 47 | modTime time.Time 48 | } 49 | 50 | // Name return file name 51 | func (fi bindataFileInfo) Name() string { 52 | return fi.name 53 | } 54 | 55 | // Size return file size 56 | func (fi bindataFileInfo) Size() int64 { 57 | return fi.size 58 | } 59 | 60 | // Mode return file mode 61 | func (fi bindataFileInfo) Mode() os.FileMode { 62 | return fi.mode 63 | } 64 | 65 | // Mode return file modify time 66 | func (fi bindataFileInfo) ModTime() time.Time { 67 | return fi.modTime 68 | } 69 | 70 | // IsDir return file whether a directory 71 | func (fi bindataFileInfo) IsDir() bool { 72 | return fi.mode&os.ModeDir != 0 73 | } 74 | 75 | // Sys return file is sys mode 76 | func (fi bindataFileInfo) Sys() interface{} { 77 | return nil 78 | } 79 | 80 | var _cmdNeutronTemplateYml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x52\xcb\x8e\xdb\x30\x0c\xbc\xeb\x2b\xf8\x03\x75\xbc\x5d\xb4\x07\xdd\x82\xa2\x2f\x60\xb1\x2d\xd0\x0f\x30\x14\x89\x76\x84\xc8\x92\x2b\x51\x69\x02\xc3\xff\x5e\x50\x76\x63\xa7\x87\x45\x81\x5e\x37\xb9\x88\x33\xa3\xe1\x98\xd4\x70\xb2\x52\x00\x68\x25\x61\x87\xa4\x77\x1e\x0f\xd9\xa9\xdd\x38\x56\xcf\x48\x5e\xf5\x38\x4d\x3b\xad\x2a\x1d\x49\x00\x9c\xf0\xfa\x82\xcc\x63\xa6\x18\x7c\x75\xc2\x2b\x3b\x62\xa4\x7f\x10\xb3\xb1\x48\xa4\xc8\xea\xe6\x18\x12\x35\xbd\x1a\x38\xd0\x38\x46\xe5\x3b\x84\xea\x47\xe1\xbe\x84\x44\x69\x9a\x0a\x01\xd5\x73\x31\xdc\x1b\x13\x31\x25\x98\x26\x09\xe3\x58\x7d\xf4\x66\x08\xd6\xd3\xa2\x42\x6f\xa6\x49\x08\x67\xbb\x23\x1d\x43\x4e\xc8\xae\xaa\x6f\x36\x00\xdf\xda\xf7\x4f\x37\xa0\xdc\xb4\x9e\x30\x9e\x95\x93\xf0\xbe\x16\x00\x9c\x29\xdd\x05\x5a\xf5\x73\x20\x80\x37\x25\x14\xdc\x37\xd6\x76\x38\x62\x2c\x3d\x3e\x94\x23\x83\x43\xf6\xfa\x78\x65\xbb\x72\x92\x40\x31\x23\x87\x4c\x84\x5e\x2e\xed\x24\xd4\x55\xf9\xb3\x2c\xf0\x14\xd9\xfe\xa9\x68\xbe\x87\x48\xdc\x48\x50\x2e\x7a\x63\x93\x3a\x38\x34\x12\x5a\xe5\x12\x32\x82\xe7\xd2\xf4\x36\x6a\xb6\x31\x31\x0c\x8d\x0b\x5a\xb9\xe6\x10\x83\x32\x5a\x71\x9b\xdb\x15\x66\xfb\xec\xc8\xde\xe3\x74\x69\x7e\x66\xcc\x28\xe1\x5d\xcd\x2e\x3d\x65\x09\x0f\x8f\x75\x2d\x84\x0b\x5d\x67\x7d\xc7\x11\x1c\x9e\xd1\x49\xb0\xbe\x0d\x02\xa0\x0d\xb1\x57\x24\x81\xf0\x42\x42\xb4\x36\xe2\x2f\xe5\x5c\x79\x62\xc1\x7b\x8a\x4a\x9f\x64\x19\x1a\xe9\xa1\x21\xdb\x63\xc8\x24\xe1\xe1\x6d\x5f\xc0\x6c\x36\xe0\xe3\x8c\x19\x6c\x55\x76\xb4\x11\xd7\x33\xd1\xab\x4b\xc3\xa6\xa8\xc9\x06\x9f\x98\xe0\x9f\x00\x08\x99\x0e\x21\x7b\x33\x77\x1a\x47\x58\x56\xf7\x69\x89\xf3\x6d\xe1\x61\x59\x60\xd1\xd8\x16\xaa\xbd\xbf\xc2\x6d\xa9\xeb\xec\xff\x4c\x7d\xd6\x0e\x31\x50\x58\x08\x3e\xae\xcc\xbc\x3d\xe5\xaf\xab\x2b\xba\x84\xff\x63\xd9\xc5\x90\x87\x24\x97\x6a\xfb\x31\x9f\x0b\xb3\x2a\xef\xdf\xe1\x1a\xe0\xaf\xcf\xdc\xd4\xdb\xca\xfa\x17\x27\xf6\xd5\xbf\x0e\x6c\x5b\xfd\x0e\x00\x00\xff\xff\x25\x0b\x88\xc0\x37\x05\x00\x00") 81 | 82 | func cmdNeutronTemplateYmlBytes() ([]byte, error) { 83 | return bindataRead( 84 | _cmdNeutronTemplateYml, 85 | "cmd/neutron/template.yml", 86 | ) 87 | } 88 | 89 | func cmdNeutronTemplateYml() (*asset, error) { 90 | bytes, err := cmdNeutronTemplateYmlBytes() 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | info := bindataFileInfo{name: "cmd/neutron/template.yml", size: 1335, mode: os.FileMode(420), modTime: time.Unix(1621717310, 0)} 96 | a := &asset{bytes: bytes, info: info} 97 | return a, nil 98 | } 99 | 100 | // Asset loads and returns the asset for the given name. 101 | // It returns an error if the asset could not be found or 102 | // could not be loaded. 103 | func Asset(name string) ([]byte, error) { 104 | cannonicalName := strings.Replace(name, "\\", "/", -1) 105 | if f, ok := _bindata[cannonicalName]; ok { 106 | a, err := f() 107 | if err != nil { 108 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 109 | } 110 | return a.bytes, nil 111 | } 112 | return nil, fmt.Errorf("Asset %s not found", name) 113 | } 114 | 115 | // MustAsset is like Asset but panics when Asset would return an error. 116 | // It simplifies safe initialization of global variables. 117 | func MustAsset(name string) []byte { 118 | a, err := Asset(name) 119 | if err != nil { 120 | panic("asset: Asset(" + name + "): " + err.Error()) 121 | } 122 | 123 | return a 124 | } 125 | 126 | // AssetInfo loads and returns the asset info for the given name. 127 | // It returns an error if the asset could not be found or 128 | // could not be loaded. 129 | func AssetInfo(name string) (os.FileInfo, error) { 130 | cannonicalName := strings.Replace(name, "\\", "/", -1) 131 | if f, ok := _bindata[cannonicalName]; ok { 132 | a, err := f() 133 | if err != nil { 134 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 135 | } 136 | return a.info, nil 137 | } 138 | return nil, fmt.Errorf("AssetInfo %s not found", name) 139 | } 140 | 141 | // AssetNames returns the names of the assets. 142 | func AssetNames() []string { 143 | names := make([]string, 0, len(_bindata)) 144 | for name := range _bindata { 145 | names = append(names, name) 146 | } 147 | return names 148 | } 149 | 150 | // _bindata is a table, holding each asset generator, mapped to its name. 151 | var _bindata = map[string]func() (*asset, error){ 152 | "cmd/neutron/template.yml": cmdNeutronTemplateYml, 153 | } 154 | 155 | // AssetDir returns the file names below a certain 156 | // directory embedded in the file by go-bindata. 157 | // For example if you run go-bindata on data/... and data contains the 158 | // following hierarchy: 159 | // data/ 160 | // foo.txt 161 | // img/ 162 | // a.png 163 | // b.png 164 | // then AssetDir("data") would return []string{"foo.txt", "img"} 165 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 166 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 167 | // AssetDir("") will return []string{"data"}. 168 | func AssetDir(name string) ([]string, error) { 169 | node := _bintree 170 | if len(name) != 0 { 171 | cannonicalName := strings.Replace(name, "\\", "/", -1) 172 | pathList := strings.Split(cannonicalName, "/") 173 | for _, p := range pathList { 174 | node = node.Children[p] 175 | if node == nil { 176 | return nil, fmt.Errorf("Asset %s not found", name) 177 | } 178 | } 179 | } 180 | if node.Func != nil { 181 | return nil, fmt.Errorf("Asset %s not found", name) 182 | } 183 | rv := make([]string, 0, len(node.Children)) 184 | for childName := range node.Children { 185 | rv = append(rv, childName) 186 | } 187 | return rv, nil 188 | } 189 | 190 | type bintree struct { 191 | Func func() (*asset, error) 192 | Children map[string]*bintree 193 | } 194 | 195 | var _bintree = &bintree{nil, map[string]*bintree{ 196 | "cmd": &bintree{nil, map[string]*bintree{ 197 | "neutron": &bintree{nil, map[string]*bintree{ 198 | "template.yml": &bintree{cmdNeutronTemplateYml, map[string]*bintree{}}, 199 | }}, 200 | }}, 201 | }} 202 | 203 | // RestoreAsset restores an asset under the given directory 204 | func RestoreAsset(dir, name string) error { 205 | data, err := Asset(name) 206 | if err != nil { 207 | return err 208 | } 209 | info, err := AssetInfo(name) 210 | if err != nil { 211 | return err 212 | } 213 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 214 | if err != nil { 215 | return err 216 | } 217 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 218 | if err != nil { 219 | return err 220 | } 221 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 222 | if err != nil { 223 | return err 224 | } 225 | return nil 226 | } 227 | 228 | // RestoreAssets restores an asset under the given directory recursively 229 | func RestoreAssets(dir, name string) error { 230 | children, err := AssetDir(name) 231 | // File 232 | if err != nil { 233 | return RestoreAsset(dir, name) 234 | } 235 | // Dir 236 | for _, child := range children { 237 | err = RestoreAssets(dir, filepath.Join(name, child)) 238 | if err != nil { 239 | return err 240 | } 241 | } 242 | return nil 243 | } 244 | 245 | func _filePath(dir, name string) string { 246 | cannonicalName := strings.Replace(name, "\\", "/", -1) 247 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 248 | } 249 | -------------------------------------------------------------------------------- /cmd/neutron/template.yml: -------------------------------------------------------------------------------- 1 | pki: 2 | ca: /etc/nebula/{{.Netname}}/ca.crt 3 | key: /etc/nebula/{{.Netname}}/neutron.key 4 | cert: /etc/nebula/{{.Netname}}/neutron.crt 5 | 6 | static_host_map: 7 | {{range .StaticHosts}} 8 | {{ .NebulaAddress }}: {{.Endpoint}} 9 | {{end}} 10 | 11 | lighthouse: 12 | am_lighthouse: {{.AmLighthouse}} 13 | interval: 60 14 | hosts: 15 | {{range .Lighthouses}} 16 | - {{ . }} 17 | {{end}} 18 | 19 | cipher: {{.Cipher}} 20 | 21 | punchy: 22 | punch: true 23 | 24 | listen: 25 | host: 0.0.0.0 26 | port: {{ .ListenPort }} 27 | 28 | tun: 29 | disabled: false 30 | dev: {{.Netname}}0 31 | drop_local_broadcast: false 32 | drop_multicast: false 33 | tx_queue: 500 34 | mtu: 1300 35 | 36 | logging: 37 | level: info 38 | format: text 39 | 40 | firewall: 41 | conntrack: 42 | tcp_timeout: 12m 43 | udp_timeout: 3m 44 | default_timeout: 10m 45 | max_connections: 100000 46 | outbound: 47 | {{ range .FirewallOutbound }} 48 | {{ if .Any }} 49 | - port: {{ .Port }} 50 | proto: {{ .Proto }} 51 | host: any 52 | {{ else }} 53 | - port: {{ .Port }} 54 | proto: {{ .Proto }} 55 | groups: 56 | {{ range .Groups }} 57 | - {{ . }} 58 | {{ end }} 59 | {{ end }} 60 | {{ end }} 61 | inbound: 62 | {{ range .FirewallInbound }} 63 | {{ if .Any }} 64 | - port: {{ .Port }} 65 | proto: {{ .Proto }} 66 | host: any 67 | {{ else }} 68 | - port: {{ .Port }} 69 | proto: {{ .Proto }} 70 | groups: 71 | {{ range .Groups }} 72 | - {{ . }} 73 | {{ end }} 74 | {{ end }} 75 | {{ end }} 76 | -------------------------------------------------------------------------------- /cmd/neutron/update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/b177y/starship/nebutils" 11 | "github.com/b177y/starship/wormhole" 12 | "github.com/b177y/starship/xpaseto" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/slackhq/nebula/cert" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | // create signed XPASETO token to authenticate to Quasar server 19 | func getIdentityToken(netname, nodename string, 20 | privkey []byte) (token string, 21 | err error) { 22 | t := wormhole.NodeIdentitySchema{ 23 | Netname: netname, 24 | Nodename: nodename, 25 | } 26 | jsonToken, err := wormhole.NewToken(t) 27 | if err != nil { 28 | log.Fatal("Error creating paseto." + err.Error()) 29 | } 30 | signer := xpaseto.NewSigner(privkey, []byte{}) 31 | token, err = signer.SelfSignPaseto(jsonToken) 32 | if err != nil { 33 | log.Fatal("Error signing paseto." + err.Error()) 34 | } 35 | return token, nil 36 | } 37 | 38 | // open private key to use for signing requests 39 | func getKey(netname string) (key []byte, err error) { 40 | privpem, err := ioutil.ReadFile(fmt.Sprintf("/etc/nebula/%s/neutron.key", 41 | netname)) 42 | if err != nil { 43 | log.Error("Error reading private key from /etc/nebula") 44 | return nil, err 45 | } 46 | key, _, err = cert.UnmarshalX25519PrivateKey(privpem) 47 | if err != nil { 48 | log.Error("Error decoding key from /etc/nebula") 49 | return nil, err 50 | } 51 | return key, nil 52 | } 53 | 54 | // Save certificate from fetched config to neutron network directory 55 | func saveCert(netname, cert string) error { 56 | path := fmt.Sprintf("/etc/nebula/%s/neutron.crt", netname) 57 | err := ioutil.WriteFile(path, []byte(cert), 0660) 58 | return err 59 | } 60 | 61 | // get public key from CA certificate and validate cert 62 | func getPubkey(netname string) (pubkey []byte, err error) { 63 | cacert, err := ioutil.ReadFile(fmt.Sprintf("/etc/nebula/%s/ca.crt", 64 | netname)) 65 | if err != nil { 66 | return pubkey, err 67 | } 68 | nc, _, err := cert.UnmarshalNebulaCertificateFromPEM(cacert) 69 | caPool := cert.NewCAPool() 70 | // validate certificate 71 | _, err = caPool.AddCACertificate(cacert) 72 | if err != nil { 73 | log.Fatalf("Cert for network %s is not valid: %s.\n", netname, err.Error()) 74 | } 75 | // get public key and convert from edwards format to curve25519 76 | edpub := nc.Details.PublicKey 77 | pubkey = nebutils.Ed25519PublicKeyToCurve25519(edpub) 78 | return pubkey, nil 79 | 80 | } 81 | 82 | func getConfig(netname, nodename, qAddr string) { 83 | endPoint := fmt.Sprintf("%s/api/neutron/config?net=%s&node=%s", 84 | qAddr, netname, nodename) 85 | privkey, err := getKey(netname) 86 | token, err := getIdentityToken(netname, nodename, privkey) 87 | body := bytes.NewBuffer([]byte(token)) 88 | req, err := http.NewRequest("GET", endPoint, body) 89 | if err != nil { 90 | log.Fatal("Error creating request." + err.Error()) 91 | } 92 | client := &http.Client{} 93 | resp, err := client.Do(req) 94 | if err != nil { 95 | log.Fatal("Error contacting Quasar." + err.Error()) 96 | } 97 | defer resp.Body.Close() 98 | 99 | b, err := ioutil.ReadAll(resp.Body) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | if resp.StatusCode != 200 { 104 | if resp.StatusCode == 425 { 105 | log.Error("Node is not enabled - please enable it from the frontend!") 106 | return 107 | } 108 | log.Error("Error from Quasar: " + string(b)) 109 | return 110 | } 111 | pubkey, err := getPubkey(netname) 112 | if err != nil { 113 | log.Fatal("Could not get CA pubkey: " + err.Error()) 114 | } 115 | signer := xpaseto.NewSigner([]byte{}, pubkey) 116 | jsonToken, err := signer.ParsePaseto(string(b)) 117 | if err != nil { 118 | log.Fatal("Could not decode response token: " + err.Error()) 119 | } 120 | config := *new(NodeConfigSchema) 121 | err = wormhole.SchemaFromJSONToken(jsonToken, &config) 122 | if err != nil { 123 | log.Error("Can't decode node config: ", err) 124 | return 125 | } 126 | config.Netname = netname 127 | err = saveCert(netname, config.Cert) 128 | if err != nil { 129 | log.Error(err) 130 | return 131 | } 132 | err = genConfig(config) 133 | if err != nil { 134 | log.Error(err) 135 | return 136 | } 137 | log.Println("Successfully updated config.") 138 | } 139 | 140 | func loadNeutronConfig(netname string) (config NeutronConfig, err error) { 141 | path := fmt.Sprintf("/etc/nebula/%s/neutron.yml", netname) 142 | config = NeutronConfig{} 143 | file, err := os.Open(path) 144 | if err != nil { 145 | return config, err 146 | } 147 | defer file.Close() 148 | 149 | d := yaml.NewDecoder(file) 150 | 151 | if err := d.Decode(&config); err != nil { 152 | return config, err 153 | } 154 | 155 | return config, err 156 | } 157 | func update(netname string) { 158 | config, err := loadNeutronConfig(netname) 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | getConfig(netname, config.Nodename, config.Quasar) 163 | } 164 | -------------------------------------------------------------------------------- /cmd/quasar/address.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net" 4 | 5 | // https://gist.github.com/kotakanbe/d3059af990252ba89a82 6 | func possibleAddresses(cidr string) ([]string, error) { 7 | ip, ipnet, err := net.ParseCIDR(cidr) 8 | if err != nil { 9 | return nil, err 10 | } 11 | 12 | var ips []string 13 | for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { 14 | ips = append(ips, ip.String()) 15 | } 16 | // remove network address and broadcast address 17 | return ips[1 : len(ips)-1], nil 18 | } 19 | 20 | // http://play.golang.org/p/m8TNTtygK0 21 | func inc(ip net.IP) { 22 | for j := len(ip) - 1; j >= 0; j-- { 23 | ip[j]++ 24 | if ip[j] > 0 { 25 | break 26 | } 27 | } 28 | } 29 | 30 | // based off https://www.codegrepper.com/code-examples/go/golang+diff+of+string+arrays 31 | func difference(slice1 []string, slice2 []string) []string { 32 | var diff []string 33 | 34 | // Loop two times, first to find slice1 strings not in slice2, 35 | // second loop to find slice2 strings not in slice1 36 | for i := 0; i < 2; i++ { 37 | for _, s1 := range slice1 { 38 | found := false 39 | for _, s2 := range slice2 { 40 | if s1 == s2 { 41 | found = true 42 | break 43 | } 44 | } 45 | // String not found. We add it to return slice 46 | if !found { 47 | diff = append(diff, s1) 48 | } 49 | } 50 | // Swap the slices, only if it was the first loop 51 | if i == 0 { 52 | slice1, slice2 = slice2, slice1 53 | } 54 | } 55 | 56 | return diff 57 | } 58 | 59 | func newAddress(cidr string, usedAddresses []string) (address string, 60 | err error) { 61 | possible, err := possibleAddresses(cidr) 62 | if err != nil { 63 | return "", err 64 | } 65 | unused := difference(possible, usedAddresses) 66 | return unused[0], nil 67 | } 68 | -------------------------------------------------------------------------------- /cmd/quasar/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | yaml "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type Config struct { 10 | Database struct { 11 | Type string `yaml:"type"` 12 | Source string `yaml:"src"` 13 | } `yaml:"db"` 14 | Quasar struct { 15 | Name string `yaml:"name"` 16 | Listen struct { 17 | Host string `yaml:"host"` 18 | Port int `yaml:"port"` 19 | } `yaml:"listen"` 20 | } `yaml:"quasar"` 21 | Authsecret string 22 | } 23 | 24 | // https://dev.to/koddr/let-s-write-config-for-your-golang-web-app-on-right-way-yaml-5ggp 25 | func NewConfig(configPath string) (*Config, error) { 26 | config := &Config{} 27 | file, err := os.Open(configPath) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer file.Close() 32 | 33 | d := yaml.NewDecoder(file) 34 | 35 | if err := d.Decode(&config); err != nil { 36 | return nil, err 37 | } 38 | 39 | return config, nil 40 | } 41 | -------------------------------------------------------------------------------- /cmd/quasar/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/boltdb/bolt" 11 | "github.com/pkg/errors" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type database interface { 16 | connect(filepath string) error 17 | addNetwork( 18 | cacert []byte, 19 | capriv []byte, 20 | name string, 21 | cidr string, 22 | cipher string, 23 | ) error 24 | getCert(network, host string) ([]byte, error) 25 | updateLatestFetch(netname, nodename, timestamp string) error 26 | addJoinRequest(netname string, 27 | nodename string, 28 | hostname string, 29 | address string, 30 | pubkey []byte, 31 | ) error 32 | allNetworks() ( 33 | networks []NetOverviewSchema, 34 | err error, 35 | ) 36 | deleteNetwork(netname string) error 37 | networkInfo(netname string) ( 38 | network NetSchema, 39 | err error, 40 | ) 41 | updateNetwork(netname, cidr, cipher string, 42 | groups []string) error 43 | allNodes(netname string) ( 44 | nodes []NodeOverviewSchema, 45 | err error, 46 | ) 47 | updateNodeStatus(netname string, 48 | nodename string, 49 | status string) error 50 | updateNodeInfo(netname string, 51 | nodename string, 52 | node NodeSchema) error 53 | getNodeStatus(netname string, 54 | nodename string) ( 55 | status string, 56 | err error, 57 | ) 58 | getNodeInfo(netname string, nodename string) (nodeinfo NodeSchema, 59 | err error) 60 | getNodeConfig(netname, nodename string) (config NodeConfigSchema, 61 | err error) 62 | getNodePubkey(netname string, 63 | nodename string) (pubkey []byte, 64 | err error) 65 | getNetworkCA(netname string) (privkey []byte, 66 | cert []byte, 67 | err error) 68 | saveNodeCert(netname string, 69 | nodename string, 70 | cert []byte) error 71 | newAddress(netname string) (address string, 72 | err error) 73 | } 74 | 75 | // boltdb interface 76 | type boltdbi struct { 77 | db *bolt.DB 78 | } 79 | 80 | func (b *boltdbi) addNetwork(cacert []byte, 81 | capriv []byte, 82 | name string, 83 | cidr string, 84 | cipher string, 85 | ) (err error) { 86 | log.Info("Bolt adding network: ", name) 87 | err = b.db.Update(func(tx *bolt.Tx) error { 88 | nb, err := tx.CreateBucket([]byte(name)) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | err = nb.Put([]byte("NET_NAME"), []byte(name)) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | err = nb.Put([]byte("CA_PRIV_KEY"), capriv) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = nb.Put([]byte("CA_CERT"), cacert) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | err = nb.Put([]byte("CIDR"), []byte(cidr)) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | groups, err := json.Marshal([]string{}) 114 | if err != nil { 115 | return err 116 | } 117 | err = nb.Put([]byte("GROUPS"), groups) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | err = nb.Put([]byte("CIPHER"), []byte(cipher)) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | return nil 128 | 129 | }) 130 | return err 131 | } 132 | 133 | func (b *boltdbi) connect(filepath string) (err error) { 134 | log.Info("Bolt connecting ", filepath) 135 | b.db, err = bolt.Open(filepath, 136 | 0600, 137 | &bolt.Options{Timeout: 3 * time.Second}, 138 | ) 139 | return nil 140 | } 141 | 142 | func (b *boltdbi) getCert(network, host string) (cert []byte, 143 | err error) { 144 | err = b.db.View(func(tx *bolt.Tx) error { 145 | bkt := tx.Bucket([]byte(network)) 146 | if bkt == nil { 147 | return fmt.Errorf("NONETWORK") 148 | } 149 | if host != "" { 150 | nodeBkt := bkt.Bucket([]byte(host)) 151 | if nodeBkt == nil { 152 | return fmt.Errorf("NOHOST") 153 | } 154 | cert = nodeBkt.Get([]byte("cert")) 155 | return nil 156 | } 157 | cert = bkt.Get([]byte("CA_CERT")) 158 | return nil 159 | }) 160 | return cert, err 161 | } 162 | 163 | func (b *boltdbi) addJoinRequest(netname string, 164 | nodename string, 165 | hostname string, 166 | address string, 167 | pubkey []byte, 168 | ) (err error) { 169 | err = b.db.Update(func(tx *bolt.Tx) error { 170 | netBkt := tx.Bucket([]byte(netname)) 171 | if netBkt == nil { 172 | return errors.Errorf("Network does not exist.") 173 | } 174 | nodeBkt, err := netBkt.CreateBucket([]byte(nodename)) 175 | if err != nil { 176 | return errors.Errorf("Node exists in network.") 177 | } 178 | err = nodeBkt.Put([]byte("hostname"), []byte(hostname)) 179 | if err != nil { 180 | return err 181 | } 182 | err = nodeBkt.Put([]byte("address"), []byte(address)) 183 | if err != nil { 184 | return err 185 | } 186 | err = nodeBkt.Put([]byte("latest_fetch"), []byte("NEVER")) 187 | if err != nil { 188 | return err 189 | } 190 | err = nodeBkt.Put([]byte("pubkey"), pubkey) 191 | if err != nil { 192 | return err 193 | } 194 | err = nodeBkt.Put([]byte("status"), []byte("pending")) 195 | if err != nil { 196 | return err 197 | } 198 | err = nodeBkt.Put([]byte("listen_port"), []byte("0")) 199 | if err != nil { 200 | return err 201 | } 202 | groups, err := json.Marshal([]string{}) 203 | if err != nil { 204 | return err 205 | } 206 | err = nodeBkt.Put([]byte("groups"), groups) 207 | if err != nil { 208 | return err 209 | } 210 | err = nodeBkt.Put([]byte("is_lighthouse"), []byte("false")) 211 | if err != nil { 212 | return err 213 | } 214 | err = nodeBkt.Put([]byte("static_address"), []byte("")) 215 | if err != nil { 216 | return err 217 | } 218 | inbound, outbound := defaultRules() 219 | log.Println("Converting to bytes", inbound, outbound) 220 | inboundBytes, err := json.Marshal(inbound) 221 | if err != nil { 222 | return err 223 | } 224 | outboundBytes, err := json.Marshal(outbound) 225 | if err != nil { 226 | return err 227 | } 228 | err = json.Unmarshal(outboundBytes, &outbound) 229 | if err != nil { 230 | return err 231 | } 232 | err = nodeBkt.Put([]byte("firewall_outbound"), outboundBytes) 233 | if err != nil { 234 | return err 235 | } 236 | err = nodeBkt.Put([]byte("firewall_inbound"), inboundBytes) 237 | return err 238 | }) 239 | return err 240 | } 241 | 242 | func (b *boltdbi) allNetworks() (networks []NetOverviewSchema, err error) { 243 | networks = make([]NetOverviewSchema, 0) 244 | err = b.db.View(func(tx *bolt.Tx) error { 245 | tx.ForEach(func(name []byte, b *bolt.Bucket) error { 246 | netcidr := b.Get([]byte("CIDR")) 247 | n := NetOverviewSchema{ 248 | Name: string(name), 249 | Cidr: string(netcidr), 250 | } 251 | networks = append(networks, n) 252 | return nil 253 | }) 254 | return nil 255 | }) 256 | return networks, err 257 | } 258 | 259 | func (b *boltdbi) deleteNetwork(netname string) (err error) { 260 | err = b.db.Update(func(tx *bolt.Tx) error { 261 | return tx.DeleteBucket([]byte(netname)) 262 | }) 263 | return err 264 | } 265 | 266 | func (b *boltdbi) networkInfo(netname string) (network NetSchema, 267 | err error) { 268 | err = b.db.View(func(tx *bolt.Tx) error { 269 | bkt := tx.Bucket([]byte(netname)) 270 | if bkt == nil { 271 | return fmt.Errorf("NONETWORK") 272 | } 273 | netcidr := bkt.Get([]byte("CIDR")) 274 | cipher := bkt.Get([]byte("CIPHER")) 275 | groupsBytes := bkt.Get([]byte("GROUPS")) 276 | var groups []string 277 | err := json.Unmarshal(groupsBytes, &groups) 278 | if err != nil { 279 | return err 280 | } 281 | network = NetSchema{ 282 | Name: netname, 283 | Cidr: string(netcidr), 284 | Cipher: string(cipher), 285 | Groups: groups, 286 | } 287 | return nil 288 | }) 289 | return network, err 290 | } 291 | 292 | func (b *boltdbi) allNodes(netname string) (nodes []NodeOverviewSchema, 293 | err error) { 294 | nodes = make([]NodeOverviewSchema, 0) 295 | err = b.db.View(func(tx *bolt.Tx) error { 296 | bkt := tx.Bucket([]byte(netname)) 297 | if bkt == nil { 298 | return fmt.Errorf("NONETWORK") 299 | } 300 | err := bkt.ForEach(func(key, val []byte) error { 301 | if val == nil { 302 | // keyval is bucket so is node 303 | nb := bkt.Bucket(key) 304 | hostName := nb.Get([]byte("hostname")) 305 | status := nb.Get([]byte("status")) 306 | address := nb.Get([]byte("address")) 307 | pubkeyBytes := nb.Get([]byte("pubkey")) 308 | pubkey := base64.StdEncoding.EncodeToString([]byte(pubkeyBytes)) 309 | latest_fetch := nb.Get([]byte("latest_fetch")) 310 | node := NodeOverviewSchema{ 311 | Nodename: string(key), 312 | Hostname: string(hostName), 313 | Status: string(status), 314 | Address: string(address), 315 | LatestFetch: string(latest_fetch), 316 | PubKey: pubkey, 317 | } 318 | nodes = append(nodes, node) 319 | } 320 | return err 321 | }) 322 | return err 323 | }) 324 | return nodes, err 325 | 326 | } 327 | 328 | func (b *boltdbi) updateNodeStatus(netname string, 329 | nodename string, 330 | status string, 331 | ) (err error) { 332 | err = b.db.Update(func(tx *bolt.Tx) error { 333 | netBkt := tx.Bucket([]byte(netname)) 334 | if netBkt == nil { 335 | return errors.Errorf("Network does not exist.") 336 | } 337 | nodeBkt := netBkt.Bucket([]byte(nodename)) 338 | if nodeBkt == nil { 339 | return errors.Errorf("Node does not exist in network.") 340 | } 341 | err = nodeBkt.Put([]byte("status"), []byte(status)) 342 | return err 343 | }) 344 | return err 345 | 346 | } 347 | 348 | func (b *boltdbi) updateNodeInfo(netname string, 349 | nodename string, 350 | node NodeSchema, 351 | ) (err error) { 352 | err = b.db.Update(func(tx *bolt.Tx) error { 353 | netBkt := tx.Bucket([]byte(netname)) 354 | if netBkt == nil { 355 | return errors.Errorf("Network does not exist.") 356 | } 357 | nodeBkt := netBkt.Bucket([]byte(nodename)) 358 | if nodeBkt == nil { 359 | return errors.Errorf("Node does not exist in network.") 360 | } 361 | inboundBytes, err := json.Marshal(node.FirewallInbound) 362 | if err != nil { 363 | return err 364 | } 365 | outboundBytes, err := json.Marshal(node.FirewallOutbound) 366 | if err != nil { 367 | return err 368 | } 369 | err = nodeBkt.Put([]byte("firewall_outbound"), outboundBytes) 370 | if err != nil { 371 | return err 372 | } 373 | err = nodeBkt.Put([]byte("firewall_inbound"), inboundBytes) 374 | if err != nil { 375 | return err 376 | } 377 | if node.StaticAddress != "" { 378 | err = nodeBkt.Put([]byte("static_address"), []byte(node.StaticAddress)) 379 | if err != nil { 380 | return err 381 | } 382 | } 383 | err = nodeBkt.Put([]byte("is_lighthouse"), []byte(strconv.FormatBool(node.Lighthouse))) 384 | if err != nil { 385 | return err 386 | } 387 | err = nodeBkt.Put([]byte("listen_port"), []byte(fmt.Sprint(node.ListenPort))) 388 | if err != nil { 389 | return err 390 | } 391 | 392 | log.Println("Adding groups to db:", node.Groups) 393 | groupsBytes, err := json.Marshal(node.Groups) 394 | if err != nil { 395 | return err 396 | } 397 | err = nodeBkt.Put([]byte("groups"), groupsBytes) 398 | return err 399 | }) 400 | return err 401 | 402 | } 403 | 404 | func (b *boltdbi) getNodeStatus(netname string, 405 | nodename string) (status string, 406 | err error) { 407 | err = b.db.View(func(tx *bolt.Tx) error { 408 | netBkt := tx.Bucket([]byte(netname)) 409 | if netBkt == nil { 410 | return errors.Errorf("Network does not exist.") 411 | } 412 | nodeBkt := netBkt.Bucket([]byte(nodename)) 413 | if nodeBkt == nil { 414 | return errors.Errorf("Node does not exist in network.") 415 | } 416 | statusBytes := nodeBkt.Get([]byte("status")) 417 | status = string(statusBytes) 418 | return err 419 | }) 420 | return status, err 421 | } 422 | 423 | func (b *boltdbi) getNodeInfo(netname string, 424 | nodename string) (node NodeSchema, 425 | err error) { 426 | err = b.db.View(func(tx *bolt.Tx) error { 427 | netBkt := tx.Bucket([]byte(netname)) 428 | if netBkt == nil { 429 | return errors.Errorf("Network does not exist.") 430 | } 431 | nodeBkt := netBkt.Bucket([]byte(nodename)) 432 | if nodeBkt == nil { 433 | return errors.Errorf("Node does not exist in network.") 434 | } 435 | statusBytes := nodeBkt.Get([]byte("status")) 436 | hostnameBytes := nodeBkt.Get([]byte("hostname")) 437 | addressBytes := nodeBkt.Get([]byte("address")) 438 | listenPortBytes := nodeBkt.Get([]byte("listen_port")) 439 | listenPort, err := strconv.Atoi(string(listenPortBytes)) 440 | if err != nil { 441 | return err 442 | } 443 | staticAddressBytes := nodeBkt.Get([]byte("static_address")) 444 | groupsBytes := nodeBkt.Get([]byte("groups")) 445 | var groups []string 446 | err = json.Unmarshal(groupsBytes, &groups) 447 | if err != nil { 448 | return err 449 | } 450 | inboundBytes := nodeBkt.Get([]byte("firewall_inbound")) 451 | outboundBytes := nodeBkt.Get([]byte("firewall_outbound")) 452 | var inbound []FirewallRule 453 | var outbound []FirewallRule 454 | err = json.Unmarshal(inboundBytes, &inbound) 455 | if err != nil { 456 | return err 457 | } 458 | err = json.Unmarshal(outboundBytes, &outbound) 459 | if err != nil { 460 | return err 461 | } 462 | lighthouseBytes := nodeBkt.Get([]byte("is_lighthouse")) 463 | is_lighthouse, err := strconv.ParseBool(string(lighthouseBytes)) 464 | if err != nil { 465 | return err 466 | } 467 | node = NodeSchema{ 468 | Nodename: nodename, 469 | Hostname: string(hostnameBytes), 470 | Status: string(statusBytes), 471 | Address: string(addressBytes), 472 | StaticAddress: string(staticAddressBytes), 473 | ListenPort: listenPort, 474 | Lighthouse: is_lighthouse, 475 | Groups: groups, 476 | FirewallInbound: inbound, 477 | FirewallOutbound: outbound, 478 | } 479 | return err 480 | }) 481 | return node, err 482 | } 483 | 484 | func getLighthouses(bkt *bolt.Bucket) (lighthouses []string, 485 | err error) { 486 | lighthouses = []string{} 487 | err = bkt.ForEach(func(key, val []byte) error { 488 | if val == nil { 489 | nb := bkt.Bucket(key) 490 | log.Println("Checking if node is lighthouse", string(key)) 491 | lhBytes := nb.Get([]byte("is_lighthouse")) 492 | isLighthouse, err := strconv.ParseBool(string(lhBytes)) 493 | log.Println("lighthouse: ", isLighthouse, string(lhBytes)) 494 | if err != nil { 495 | return err 496 | } 497 | if isLighthouse { 498 | addressBytes := nb.Get([]byte("address")) 499 | lighthouses = append(lighthouses, string(addressBytes)) 500 | } 501 | } 502 | return err 503 | }) 504 | return lighthouses, err 505 | } 506 | 507 | func getStaticHosts(nodename string, bkt *bolt.Bucket) (hosts []StaticHost, 508 | err error) { 509 | hosts = []StaticHost{} 510 | err = bkt.ForEach(func(key, val []byte) error { 511 | if val == nil && string(key) != nodename { 512 | nb := bkt.Bucket(key) 513 | addressBytes := nb.Get([]byte("address")) 514 | staticAddressBytes := nb.Get([]byte("static_address")) 515 | staticAddress := string(staticAddressBytes) 516 | portBytes := nb.Get([]byte("listen_port")) 517 | port := string(portBytes) 518 | endpoint := fmt.Sprintf("%s:%s", staticAddress, port) 519 | if staticAddress != "" { 520 | host := StaticHost{ 521 | NebulaAddress: string(addressBytes), 522 | Endpoint: []string{endpoint}, 523 | } 524 | hosts = append(hosts, host) 525 | } 526 | } 527 | return err 528 | }) 529 | return hosts, err 530 | } 531 | 532 | func (b *boltdbi) getNodeConfig(netname string, 533 | nodename string) (config NodeConfigSchema, 534 | err error) { 535 | err = b.db.View(func(tx *bolt.Tx) error { 536 | netBkt := tx.Bucket([]byte(netname)) 537 | if netBkt == nil { 538 | return errors.Errorf("Network does not exist.") 539 | } 540 | cipher := netBkt.Get([]byte("CIPHER")) 541 | nodeBkt := netBkt.Bucket([]byte(nodename)) 542 | if nodeBkt == nil { 543 | return errors.Errorf("Node does not exist in network.") 544 | } 545 | addressBytes := nodeBkt.Get([]byte("address")) 546 | listenPortBytes := nodeBkt.Get([]byte("listen_port")) 547 | listenPort, err := strconv.Atoi(string(listenPortBytes)) 548 | groupsBytes := nodeBkt.Get([]byte("groups")) 549 | var groups []string 550 | err = json.Unmarshal(groupsBytes, &groups) 551 | lighthouseBytes := nodeBkt.Get([]byte("is_lighthouse")) 552 | is_lighthouse, err := strconv.ParseBool(string(lighthouseBytes)) 553 | var lighthouses []string 554 | if !is_lighthouse { 555 | lighthouses, err = getLighthouses(netBkt) 556 | if err != nil { 557 | return err 558 | } 559 | } else { 560 | lighthouses = []string{} 561 | } 562 | if err != nil { 563 | return err 564 | } 565 | staticHosts, err := getStaticHosts(nodename, netBkt) 566 | inboundBytes := nodeBkt.Get([]byte("firewall_inbound")) 567 | outboundBytes := nodeBkt.Get([]byte("firewall_outbound")) 568 | var inbound []FirewallRule 569 | var outbound []FirewallRule 570 | err = json.Unmarshal(inboundBytes, &inbound) 571 | if err != nil { 572 | return err 573 | } 574 | err = json.Unmarshal(outboundBytes, &outbound) 575 | if err != nil { 576 | return err 577 | } 578 | config = NodeConfigSchema{ 579 | Address: string(addressBytes), 580 | AmLighthouse: is_lighthouse, 581 | Cipher: string(cipher), 582 | Lighthouses: lighthouses, 583 | StaticHosts: staticHosts, 584 | ListenPort: listenPort, 585 | FirewallInbound: inbound, 586 | FirewallOutbound: outbound, 587 | } 588 | return err 589 | }) 590 | return config, err 591 | } 592 | 593 | func (b *boltdbi) getNodePubkey(netname string, 594 | nodename string) (pubkey []byte, 595 | err error) { 596 | err = b.db.View(func(tx *bolt.Tx) error { 597 | netBkt := tx.Bucket([]byte(netname)) 598 | if netBkt == nil { 599 | return errors.Errorf("Network does not exist.") 600 | } 601 | nodeBkt := netBkt.Bucket([]byte(nodename)) 602 | if nodeBkt == nil { 603 | return errors.Errorf("Node does not exist in network.") 604 | } 605 | pubkey = nodeBkt.Get([]byte("pubkey")) 606 | return err 607 | }) 608 | return pubkey, err 609 | } 610 | 611 | func (b *boltdbi) getNetworkCA(netname string) (privkey []byte, 612 | cert []byte, 613 | err error) { 614 | err = b.db.View(func(tx *bolt.Tx) error { 615 | bkt := tx.Bucket([]byte(netname)) 616 | if bkt == nil { 617 | return errors.Errorf("Network does not exist.") 618 | } 619 | privkey = bkt.Get([]byte("CA_PRIV_KEY")) 620 | cert = bkt.Get([]byte("CA_CERT")) 621 | return err 622 | }) 623 | return privkey, cert, err 624 | } 625 | 626 | func (b *boltdbi) saveNodeCert(netname string, 627 | nodename string, 628 | cert []byte, 629 | ) (err error) { 630 | err = b.db.Update(func(tx *bolt.Tx) error { 631 | netBkt := tx.Bucket([]byte(netname)) 632 | if netBkt == nil { 633 | return errors.Errorf("Network does not exist.") 634 | } 635 | nodeBkt := netBkt.Bucket([]byte(nodename)) 636 | if nodeBkt == nil { 637 | return errors.Errorf("Node does not exist in network.") 638 | } 639 | err = nodeBkt.Put([]byte("cert"), cert) 640 | return err 641 | }) 642 | return err 643 | } 644 | 645 | func (b *boltdbi) updateLatestFetch(netname string, 646 | nodename string, 647 | timestamp string, 648 | ) (err error) { 649 | err = b.db.Update(func(tx *bolt.Tx) error { 650 | netBkt := tx.Bucket([]byte(netname)) 651 | if netBkt == nil { 652 | return errors.Errorf("Network does not exist.") 653 | } 654 | nodeBkt := netBkt.Bucket([]byte(nodename)) 655 | if nodeBkt == nil { 656 | return errors.Errorf("Node does not exist in network.") 657 | } 658 | err = nodeBkt.Put([]byte("latest_fetch"), []byte(timestamp)) 659 | return err 660 | }) 661 | return err 662 | } 663 | 664 | func (b *boltdbi) updateNetwork(netname, cidr, 665 | cipher string, groups []string) (err error) { 666 | err = b.db.Update(func(tx *bolt.Tx) error { 667 | bkt := tx.Bucket([]byte(netname)) 668 | if bkt == nil { 669 | return errors.Errorf("Network does not exist.") 670 | } 671 | groupsBytes, err := json.Marshal(groups) 672 | if err != nil { 673 | return err 674 | } 675 | err = bkt.Put([]byte("GROUPS"), groupsBytes) 676 | if err != nil { 677 | return err 678 | } 679 | if cidr != "" { 680 | err = bkt.Put([]byte("CIDR"), []byte(cidr)) 681 | } 682 | if cipher != "" { 683 | err = bkt.Put([]byte("CIPHER"), []byte(cipher)) 684 | } 685 | return err 686 | }) 687 | return err 688 | } 689 | 690 | func (b *boltdbi) newAddress(netname string) (address string, 691 | err error) { 692 | var used []string 693 | var cidr string 694 | err = b.db.View(func(tx *bolt.Tx) error { 695 | bkt := tx.Bucket([]byte(netname)) 696 | if bkt == nil { 697 | return errors.Errorf("Network does not exist.") 698 | } 699 | cidr = string(bkt.Get([]byte("CIDR"))) 700 | err := bkt.ForEach(func(key, val []byte) error { 701 | if val == nil { 702 | // keyval is bucket so is node 703 | nb := bkt.Bucket(key) 704 | addr := nb.Get([]byte("address")) 705 | used = append(used, string(addr)) 706 | } 707 | return err 708 | }) 709 | return err 710 | }) 711 | return newAddress(cidr, used) 712 | } 713 | -------------------------------------------------------------------------------- /cmd/quasar/firewall.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type FirewallRule struct { 4 | Port string `json:"port"` 5 | Proto string `json:"proto"` 6 | Groups []string `json:"groups"` 7 | Any bool `json:"any"` 8 | } 9 | 10 | func defaultRules() (inbound, outbound []FirewallRule) { 11 | inbound = append(inbound, FirewallRule{ 12 | Port: "any", 13 | Proto: "icmp", 14 | Any: true, 15 | Groups: []string{}, 16 | }) 17 | outbound = append(outbound, FirewallRule{ 18 | Port: "any", 19 | Proto: "any", 20 | Any: true, 21 | Groups: []string{}, 22 | }) 23 | return inbound, outbound 24 | } 25 | -------------------------------------------------------------------------------- /cmd/quasar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func main() { 11 | // Set log level 12 | log.SetLevel(log.DebugLevel) 13 | 14 | // define -help command flag 15 | printUsage := flag.Bool("help", false, "Print command line usage") 16 | 17 | serveConfig := flag.String("config", 18 | "/etc/quasar/config.yml", 19 | "Quasar config file path", 20 | ) 21 | serveListenAddress := flag.String("host", 22 | "", 23 | "Quasar server listen address", 24 | ) 25 | serveListenPort := flag.Int("port", 26 | 0, 27 | "Quasar server listen port", 28 | ) 29 | flag.Parse() 30 | 31 | if *printUsage { 32 | flag.Usage() 33 | os.Exit(0) 34 | } 35 | 36 | runServe(*serveConfig, 37 | *serveListenAddress, 38 | *serveListenPort, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/quasar/networks.go: -------------------------------------------------------------------------------- 1 | // HTTP Endpoints Relating to Networks 2 | // These should be used by a client using standard auth 3 | // rather than XPASETO auth with a nebula key 4 | 5 | package main 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "crypto/rand" 10 | "encoding/json" 11 | "fmt" 12 | "net" 13 | "net/http" 14 | "time" 15 | 16 | "github.com/gorilla/mux" 17 | log "github.com/sirupsen/logrus" 18 | "github.com/slackhq/nebula/cert" 19 | ) 20 | 21 | type NetSchema struct { 22 | Name string `json:"name"` 23 | Cidr string `json:"cidr"` 24 | Cipher string `json:"cipher"` 25 | Groups []string `json:"groups"` 26 | CaFingerprint string `json:"ca_fingerprint"` 27 | } 28 | 29 | type NetOverviewSchema struct { 30 | Name string `json:"name"` 31 | Cidr string `json:"cidr"` 32 | } 33 | 34 | // /api/networks/all [GET] 35 | func (s *server) handleGetAllNetworks() http.HandlerFunc { 36 | return func(w http.ResponseWriter, r *http.Request) { 37 | log.Println("Getting all networks") 38 | networks, err := s.db.allNetworks() 39 | if err != nil { 40 | http.Error(w, err.Error(), http.StatusBadRequest) 41 | return 42 | } 43 | log.Println("Got all networks", networks) 44 | if err := json.NewEncoder(w).Encode(networks); err != nil { 45 | log.Error(err) 46 | } 47 | } 48 | } 49 | 50 | type NewNetSchema struct { 51 | Name string `json:"name"` 52 | Cidr string `json:"cidr"` 53 | } 54 | 55 | // /api/networks/new [POST] 56 | func (s *server) handleNewNetwork() http.HandlerFunc { 57 | return func(w http.ResponseWriter, r *http.Request) { 58 | // get name and cidr from request body 59 | dec := json.NewDecoder(r.Body) 60 | dec.DisallowUnknownFields() 61 | var newnet NewNetSchema 62 | err := dec.Decode(&newnet) 63 | if err != nil { 64 | http.Error(w, err.Error(), http.StatusBadRequest) 65 | return 66 | } 67 | log.Println("Creating new network: ", newnet) 68 | 69 | // generate keys 70 | pubkey, privkey, err := ed25519.GenerateKey(rand.Reader) 71 | if err != nil { 72 | log.Error("Could not generate keys: " + err.Error()) 73 | http.Error(w, err.Error(), http.StatusBadRequest) 74 | return 75 | } 76 | 77 | // generate and self-sign cert for ca key 78 | ip, cidr, err := net.ParseCIDR(newnet.Cidr) 79 | if err != nil { 80 | log.Error("Invalid cidr definition: " + newnet.Cidr) 81 | http.Error(w, err.Error(), http.StatusBadRequest) 82 | return 83 | } 84 | cidr.IP = ip 85 | subnet := cidr 86 | nc := cert.NebulaCertificate{ 87 | Details: cert.NebulaCertificateDetails{ 88 | Name: "quasar" + newnet.Name, 89 | Ips: []*net.IPNet{cidr}, 90 | Groups: []string{}, 91 | Subnets: []*net.IPNet{subnet}, 92 | NotBefore: time.Now(), 93 | NotAfter: time.Now().Add(time.Duration(time.Hour * 2190)), 94 | PublicKey: pubkey, 95 | IsCA: true, 96 | }, 97 | } 98 | 99 | err = nc.Sign(privkey) 100 | if err != nil { 101 | log.Error("Error while signing ca key: %s", err) 102 | http.Error(w, err.Error(), http.StatusBadRequest) 103 | return 104 | } 105 | 106 | certbytes, err := nc.MarshalToPEM() 107 | 108 | // write new network to database 109 | log.Println("ADDING NETWORK TO DB, priv: ", privkey) 110 | err = s.db.addNetwork(certbytes, 111 | privkey, 112 | newnet.Name, 113 | newnet.Cidr, 114 | "chachapoly", 115 | ) 116 | if err != nil { 117 | log.Error(err) 118 | http.Error(w, err.Error(), http.StatusBadRequest) 119 | return 120 | } 121 | log.Println("Added network to db: ", newnet.Name, newnet.Cidr) 122 | fmt.Fprintf(w, "SUCCESS") 123 | } 124 | } 125 | 126 | // /api/networks/{NETWORK}/update [POST] 127 | func (s *server) handleUpdateNetwork() http.HandlerFunc { 128 | return func(w http.ResponseWriter, r *http.Request) { 129 | vars := mux.Vars(r) 130 | netname := vars["NETWORK"] 131 | dec := json.NewDecoder(r.Body) 132 | dec.DisallowUnknownFields() 133 | var network NetSchema 134 | err := dec.Decode(&network) 135 | if err != nil { 136 | http.Error(w, err.Error(), http.StatusBadRequest) 137 | return 138 | } 139 | cidr := network.Cidr 140 | if cidr != "" { 141 | _, ncidr, err := net.ParseCIDR(cidr) 142 | if err != nil { 143 | http.Error(w, err.Error(), http.StatusBadRequest) 144 | return 145 | } 146 | cidr = ncidr.String() 147 | } 148 | err = s.db.updateNetwork(netname, cidr, network.Cipher, network.Groups) 149 | if err != nil { 150 | http.Error(w, err.Error(), http.StatusBadRequest) 151 | return 152 | } 153 | fmt.Fprintf(w, "SUCCESS") 154 | } 155 | } 156 | 157 | // /api/networks/{NETWORK}/delete [POST] 158 | func (s *server) handleDeleteNetwork() http.HandlerFunc { 159 | return func(w http.ResponseWriter, r *http.Request) { 160 | vars := mux.Vars(r) 161 | net := vars["NETWORK"] 162 | log.Println("Deleting network", net) 163 | err := s.db.deleteNetwork(net) 164 | if err != nil { 165 | http.Error(w, err.Error(), http.StatusBadRequest) 166 | return 167 | } 168 | fmt.Fprintf(w, "SUCCESS") 169 | } 170 | } 171 | 172 | // /api/networks/{NETWORK}/info [GET] 173 | func (s *server) handleNetworkInfo() http.HandlerFunc { 174 | return func(w http.ResponseWriter, r *http.Request) { 175 | vars := mux.Vars(r) 176 | net := vars["NETWORK"] 177 | log.Println("Getting network info for", net) 178 | network, err := s.db.networkInfo(net) 179 | if err != nil { 180 | http.Error(w, err.Error(), http.StatusBadRequest) 181 | return 182 | } 183 | _, certBytes, err := s.db.getNetworkCA(net) 184 | if err != nil { 185 | http.Error(w, err.Error(), http.StatusBadRequest) 186 | return 187 | } 188 | cert, _, err := cert.UnmarshalNebulaCertificateFromPEM(certBytes) 189 | if err != nil { 190 | http.Error(w, err.Error(), http.StatusBadRequest) 191 | return 192 | } 193 | fingerprint, err := cert.Sha256Sum() 194 | if err != nil { 195 | http.Error(w, err.Error(), http.StatusBadRequest) 196 | return 197 | } 198 | network.CaFingerprint = fingerprint 199 | if err := json.NewEncoder(w).Encode(network); err != nil { 200 | log.Error(err) 201 | http.Error(w, err.Error(), http.StatusBadRequest) 202 | } 203 | } 204 | } 205 | 206 | // /api/networks/{NETWORK}/cert [GET] 207 | func (s *server) handleGetNetworkCert() http.HandlerFunc { 208 | return func(w http.ResponseWriter, r *http.Request) { 209 | // unprotected route - anyone with network name can get ca cert 210 | // returns 404 if no network or ca cert file 211 | vars := mux.Vars(r) 212 | net := vars["NETWORK"] 213 | log.Printf("/api/networks/%s/cert requested.\n", net) 214 | cacert, err := s.db.getCert(net, "") 215 | log.Println("GOT CERT") 216 | if err != nil || string(cacert) == "" { 217 | switch errmsg := err.Error(); errmsg { 218 | case "NONETWORK": 219 | http.Error(w, "Network does not exist.", 404) 220 | default: 221 | http.Error(w, "Internal Server Error.", 500) 222 | } 223 | return 224 | } 225 | nc, _, err := cert.UnmarshalNebulaCertificateFromPEM(cacert) 226 | if err != nil { 227 | http.Error(w, "Could not decode CA Certificate", 500) 228 | } 229 | pemcert, err := nc.MarshalToPEM() 230 | if err != nil { 231 | http.Error(w, "Could not marshal CA certificate to PEM", 500) 232 | } 233 | fmt.Fprintf(w, string(pemcert)) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /cmd/quasar/neutron.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/b177y/starship/nebutils" 10 | "github.com/b177y/starship/wormhole" 11 | "github.com/b177y/starship/xpaseto" 12 | "github.com/gorilla/mux" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/slackhq/nebula/cert" 15 | ) 16 | 17 | type StaticHost struct { 18 | NebulaAddress string 19 | Endpoint []string 20 | } 21 | 22 | type NodeConfigSchema struct { 23 | Address string `json:"address"` // do i need this? 24 | Lighthouses []string `json:"lighthouses"` 25 | AmLighthouse bool `json:"am_lighthouse"` 26 | StaticHosts []StaticHost `json:"static_hosts"` 27 | ListenPort int `json:"listen_port"` 28 | 29 | FirewallInbound []FirewallRule `json:"firewall_inbound"` 30 | FirewallOutbound []FirewallRule `json:"firewall_outbound"` 31 | Cipher string `json:"cipher"` 32 | Cert string `json:"cert"` 33 | } 34 | 35 | func (s *server) CheckNodeIdentity(netname string, 36 | nodename string, token string) (err error) { 37 | log.Printf("Checking identity for node %s (network %s).\n", nodename, 38 | netname) 39 | // get node pubkey 40 | pubkey, err := s.db.getNodePubkey(netname, nodename) 41 | if err != nil { 42 | return err 43 | } 44 | log.Println("Got node pubkey: ", pubkey) 45 | signer := xpaseto.NewSigner([]byte{0}, pubkey) 46 | jsonToken, err := signer.ParsePaseto(token) 47 | if err != nil { 48 | return err 49 | } 50 | nodeIdentity := *new(wormhole.NodeIdentitySchema) 51 | err = wormhole.SchemaFromJSONToken(jsonToken, &nodeIdentity) 52 | if err != nil { 53 | return err 54 | } 55 | if nodeIdentity.Netname != netname || nodeIdentity.Nodename != nodename { 56 | log.Error("Node identity does not match url params") 57 | return fmt.Errorf("Node Identity does not match url params.") 58 | } 59 | return nil 60 | } 61 | 62 | func (s *server) SignPayload(netname string, 63 | payload interface{}) (signed string, err error) { 64 | jsonToken, err := wormhole.NewToken(payload) 65 | if err != nil { 66 | return "", err 67 | } 68 | edprivkey, _, err := s.db.getNetworkCA(netname) 69 | if err != nil { 70 | return "", err 71 | } 72 | privkey := nebutils.PrivateKeyToCurve25519(edprivkey[:32]) 73 | signer := xpaseto.NewSigner(privkey, []byte{}) 74 | token, err := signer.SignPaseto(jsonToken) 75 | if err != nil { 76 | return "", err 77 | } 78 | return token, nil 79 | } 80 | 81 | // /api/neutron/config?net=NETWORK&node=NODE [GET] 82 | func (s *server) handleGetConfig() http.HandlerFunc { 83 | return func(w http.ResponseWriter, r *http.Request) { 84 | urlParams := r.URL.Query() 85 | netname := urlParams["net"][0] 86 | nodename := urlParams["node"][0] 87 | b, err := ioutil.ReadAll(r.Body) 88 | if err != nil { 89 | http.Error(w, "Bad Request", 400) 90 | return 91 | } 92 | token := string(b) 93 | err = s.CheckNodeIdentity(netname, nodename, token) 94 | if err != nil { 95 | http.Error(w, err.Error(), 401) 96 | log.Error(err) 97 | return 98 | } 99 | status, err := s.db.getNodeStatus(netname, nodename) 100 | if err != nil { 101 | http.Error(w, "Could not get node status: "+err.Error(), 500) 102 | log.Error("Could not get node status: ", err) 103 | return 104 | } 105 | if status != "active" { 106 | http.Error(w, "425 - Node is not active. You may need to have it approved.", 425) 107 | log.Error("Node is not active, not returning cert.") 108 | return 109 | } 110 | log.Printf("Getting cert for node %s in network %s.\n", nodename, netname) 111 | err = s.signNodeCert(netname, nodename) 112 | if err != nil { 113 | http.Error(w, "Could not sign certificate", 503) 114 | return 115 | } 116 | nodeCert, err := s.db.getCert(netname, nodename) 117 | if err != nil || nodeCert == nil { 118 | http.Error(w, "Could not get certificate", 503) 119 | return 120 | } 121 | nc, err := cert.UnmarshalNebulaCertificate(nodeCert) 122 | if err != nil { 123 | http.Error(w, "Could not decode CA Certificate", 500) 124 | return 125 | } 126 | pemcert, err := nc.MarshalToPEM() 127 | if err != nil { 128 | http.Error(w, "Could not marshal CA certificate to PEM", 500) 129 | return 130 | } 131 | node, err := s.db.getNodeConfig(netname, nodename) 132 | node.Cert = string(pemcert) 133 | err = s.db.updateLatestFetch(netname, nodename, time.Now().Format(time.RFC3339)) 134 | signedResponse, err := s.SignPayload(netname, node) 135 | if err != nil { 136 | log.Error(err) 137 | http.Error(w, err.Error(), http.StatusBadRequest) 138 | return 139 | } 140 | fmt.Fprintf(w, signedResponse) 141 | } 142 | } 143 | 144 | // /api/neutron/join [POST] 145 | func (s *server) handleJoinNetwork() http.HandlerFunc { 146 | return func(w http.ResponseWriter, r *http.Request) { 147 | log.Println("Join network requested") 148 | b, err := ioutil.ReadAll(r.Body) 149 | if err != nil { 150 | http.Error(w, "Bad Request", 400) 151 | return 152 | } 153 | token := string(b) 154 | signer := xpaseto.NewSigner([]byte{0}, []byte{0}) 155 | jsonToken, err := signer.ParseSelfSigned(token) 156 | if err != nil { 157 | http.Error(w, "Invalid Signature: "+err.Error(), 503) 158 | return 159 | } 160 | joinReq := *new(wormhole.RequestJoinSchema) 161 | err = wormhole.SchemaFromJSONToken(jsonToken, &joinReq) 162 | if err != nil { 163 | http.Error(w, "Internal Server Error", 500) 164 | log.Error(err) 165 | return 166 | } 167 | pubkey, _, err := cert.UnmarshalX25519PublicKey([]byte(joinReq.PubKey)) 168 | if err != nil { 169 | http.Error(w, "Internal Server Error", 500) 170 | log.Error(err) 171 | return 172 | } 173 | address, err := s.db.newAddress(joinReq.Netname) 174 | if err != nil { 175 | http.Error(w, "Internal Server Error", 500) 176 | log.Error(err) 177 | return 178 | } 179 | err = s.db.addJoinRequest(joinReq.Netname, 180 | joinReq.Nodename, 181 | joinReq.Hostname, 182 | address, 183 | pubkey, 184 | ) 185 | if err != nil { 186 | http.Error(w, "Internal Server Error: "+err.Error(), 500) 187 | log.Error(err) 188 | return 189 | } 190 | fmt.Fprintf(w, "SUCCESS") 191 | } 192 | } 193 | 194 | // /api/neutron/leave?net=NETWORK&node=NODE [POST] 195 | func (s *server) handleLeaveNetwork() http.HandlerFunc { 196 | return func(w http.ResponseWriter, r *http.Request) { 197 | vars := mux.Vars(r) 198 | nodename := vars["NODENAME"] 199 | log.Printf("/api/neutron/%s/leave requested.\n", nodename) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /cmd/quasar/nodes.go: -------------------------------------------------------------------------------- 1 | // This contains endpoints for managing nodes through the API 2 | // using standard auth (not XPASETO auth) 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/gorilla/mux" 14 | log "github.com/sirupsen/logrus" 15 | "github.com/slackhq/nebula/cert" 16 | ) 17 | 18 | type NodeOverviewSchema struct { 19 | Nodename string `json:"name"` 20 | Hostname string `json:"hostname"` 21 | LatestFetch string `json:"latest_fetch"` 22 | Status string `json:"status"` 23 | Address string `json:"address"` 24 | PubKey string `json:"pubkey"` 25 | } 26 | 27 | type NodeSchema struct { 28 | Nodename string `json:"name"` 29 | Hostname string `json:"hostname"` 30 | Status string `json:"status"` 31 | Address string `json:"address"` 32 | StaticAddress string `json:"static_address"` 33 | ListenPort int `json:"listen_port"` 34 | Lighthouse bool `json:"is_lighthouse"` 35 | Groups []string `json:"groups"` 36 | FirewallOutbound []FirewallRule `json:"firewall_outbound"` 37 | FirewallInbound []FirewallRule `json:"firewall_inbound"` 38 | } 39 | 40 | // /api/networks/{NETWORK}/nodes/all [GET] 41 | func (s *server) handleGetAllNodes() http.HandlerFunc { 42 | return func(w http.ResponseWriter, r *http.Request) { 43 | vars := mux.Vars(r) 44 | net := vars["NETWORK"] 45 | log.Println("Getting nodes in network", net) 46 | nodes, err := s.db.allNodes(net) 47 | if err != nil { 48 | http.Error(w, err.Error(), http.StatusBadRequest) 49 | return 50 | } 51 | if err := json.NewEncoder(w).Encode(nodes); err != nil { 52 | log.Error(err) 53 | http.Error(w, err.Error(), http.StatusBadRequest) 54 | } 55 | } 56 | } 57 | 58 | func (s *server) signNodeCert(netname string, nodename string) error { 59 | log.Printf("Signing cert for node %s in network %s.", nodename, netname) 60 | pubkey, err := s.db.getNodePubkey(netname, nodename) 61 | if err != nil { 62 | return err 63 | } 64 | node, err := s.db.getNodeInfo(netname, nodename) 65 | if err != nil { 66 | return err 67 | } 68 | network, err := s.db.networkInfo(netname) 69 | if err != nil { 70 | return err 71 | } 72 | quasarPrivKey, certBytes, err := s.db.getNetworkCA(netname) 73 | log.Println("Trying to unmarshal cert") 74 | quasarCert, _, err := cert.UnmarshalNebulaCertificateFromPEM(certBytes) 75 | if err != nil { 76 | return err 77 | } 78 | log.Println("Trying to get issuer") 79 | issuer, err := quasarCert.Sha256Sum() 80 | if err != nil { 81 | return err 82 | } 83 | log.Println("cidr stuffs.") 84 | ip, cidr, err := net.ParseCIDR(network.Cidr) 85 | ip = net.ParseIP(node.Address) 86 | if err != nil { 87 | return err 88 | } 89 | cidr.IP = ip 90 | subnet := cidr 91 | exp := time.Until(quasarCert.Details.NotAfter) - time.Second*1 92 | nc := cert.NebulaCertificate{ 93 | Details: cert.NebulaCertificateDetails{ 94 | Name: nodename, 95 | Ips: []*net.IPNet{cidr}, 96 | Groups: node.Groups, 97 | Subnets: []*net.IPNet{subnet}, 98 | NotBefore: time.Now(), 99 | NotAfter: time.Now().Add(exp), 100 | PublicKey: pubkey, 101 | IsCA: false, 102 | Issuer: issuer, 103 | }, 104 | } 105 | err = nc.Sign(quasarPrivKey) 106 | if err != nil { 107 | return err 108 | } 109 | signedCertBytes, err := nc.Marshal() 110 | if err != nil { 111 | return err 112 | } 113 | err = s.db.saveNodeCert(netname, nodename, signedCertBytes) 114 | if err != nil { 115 | return err 116 | } 117 | pemBytes, _ := nc.MarshalToPEM() 118 | log.Println("Saved cert", string(pemBytes)) 119 | return nil 120 | } 121 | 122 | // /api/networks/{NETWORK}/nodes/{NODENAME}/approve [POST] 123 | func (s *server) handleApproveNode() http.HandlerFunc { 124 | return func(w http.ResponseWriter, r *http.Request) { 125 | vars := mux.Vars(r) 126 | net := vars["NETWORK"] 127 | node := vars["NODENAME"] 128 | log.Printf("Approving node %s in network %s.\n", node, net) 129 | // sign pubkey and create cert 130 | err := s.signNodeCert(net, node) 131 | if err != nil { 132 | http.Error(w, err.Error(), http.StatusBadRequest) 133 | return 134 | } 135 | // update status to active 136 | err = s.db.updateNodeStatus(net, node, "active") 137 | if err != nil { 138 | http.Error(w, err.Error(), http.StatusBadRequest) 139 | return 140 | } 141 | fmt.Fprintf(w, "SUCCESS") 142 | } 143 | } 144 | 145 | // /api/networks/{NETWORK}/nodes/{NODENAME}/update [POST] 146 | func (s *server) handleUpdateNode() http.HandlerFunc { 147 | return func(w http.ResponseWriter, r *http.Request) { 148 | vars := mux.Vars(r) 149 | netname := vars["NETWORK"] 150 | nodename := vars["NODENAME"] 151 | log.Printf("Updating node %s in network %s.\n", nodename, netname) 152 | dec := json.NewDecoder(r.Body) 153 | dec.DisallowUnknownFields() 154 | var node NodeSchema 155 | err := dec.Decode(&node) 156 | if err != nil { 157 | log.Error("Could not decode json", err.Error()) 158 | http.Error(w, err.Error(), http.StatusBadRequest) 159 | return 160 | } 161 | log.Println("UPDATING NODE WITH", node) 162 | err = s.db.updateNodeInfo(netname, nodename, node) 163 | if err != nil { 164 | http.Error(w, err.Error(), http.StatusBadRequest) 165 | return 166 | } 167 | fmt.Fprintf(w, "SUCCESS") 168 | } 169 | } 170 | 171 | // /api/networks/{NETWORK}/nodes/{NODENAME}/info [GET] 172 | func (s *server) handleNodeInfo() http.HandlerFunc { 173 | return func(w http.ResponseWriter, r *http.Request) { 174 | vars := mux.Vars(r) 175 | netname := vars["NETWORK"] 176 | nodename := vars["NODENAME"] 177 | node, err := s.db.getNodeInfo(netname, nodename) 178 | if err != nil { 179 | http.Error(w, err.Error(), http.StatusBadRequest) 180 | return 181 | } 182 | if err := json.NewEncoder(w).Encode(node); err != nil { 183 | log.Error(err) 184 | http.Error(w, err.Error(), http.StatusBadRequest) 185 | } 186 | } 187 | } 188 | 189 | // /api/networks/{NETWORK}/nodes/{NODENAME}/disable [POST] 190 | func (s *server) handleDisableNode() http.HandlerFunc { 191 | return func(w http.ResponseWriter, r *http.Request) { 192 | vars := mux.Vars(r) 193 | net := vars["NETWORK"] 194 | node := vars["NODENAME"] 195 | log.Printf("Disabling node %s in network %s.\n", node, net) 196 | err := s.db.updateNodeStatus(net, node, "disabled") 197 | if err != nil { 198 | http.Error(w, err.Error(), http.StatusBadRequest) 199 | return 200 | } 201 | fmt.Fprintf(w, "SUCCESS") 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /cmd/quasar/serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | 9 | jwtmiddleware "github.com/auth0/go-jwt-middleware" 10 | jwt "github.com/form3tech-oss/jwt-go" 11 | "github.com/gorilla/mux" 12 | negronilogrus "github.com/meatballhat/negroni-logrus" 13 | "github.com/rs/cors" 14 | log "github.com/sirupsen/logrus" 15 | "github.com/urfave/negroni" 16 | ) 17 | 18 | type server struct { 19 | db database 20 | router *mux.Router 21 | conf *Config 22 | n *negroni.Negroni 23 | jmw *jwtmiddleware.JWTMiddleware 24 | userpass map[string]string 25 | } 26 | 27 | func (s *server) serve(endpoint string) { 28 | cor := cors.New(cors.Options{ 29 | AllowedOrigins: []string{"*"}, 30 | AllowedMethods: []string{"POST", "GET", "OPTIONS", "DELETE"}, 31 | AllowedHeaders: []string{"Accept", "Accept-Language", "Content-Type", "Authorization"}, 32 | AllowCredentials: true, 33 | Debug: false, 34 | }) 35 | handler := cor.Handler(s.router) 36 | s.n.Use(negronilogrus.NewMiddleware()) 37 | s.n.UseHandler(handler) 38 | log.Fatal(http.ListenAndServe(endpoint, s.n)) 39 | } 40 | 41 | func (s *server) handleHome() http.HandlerFunc { 42 | return func(w http.ResponseWriter, r *http.Request) { 43 | log.Info("Index page / requested") 44 | fmt.Fprintf(w, "Quasar is running!") 45 | } 46 | } 47 | 48 | type User struct { 49 | Username string `json:"username"` 50 | Password string `json:"password"` 51 | } 52 | 53 | type JwtToken struct { 54 | Token string `json:"token"` 55 | } 56 | 57 | func (s *server) handleLogin() http.HandlerFunc { 58 | return func(w http.ResponseWriter, r *http.Request) { 59 | var user User 60 | json.NewDecoder(r.Body).Decode(&user) 61 | if pass, ok := s.userpass[user.Username]; ok { 62 | if pass != user.Password { 63 | http.Error(w, "Password Incorrect", 403) 64 | log.Error(fmt.Sprintf("Incorrect password for %s", user.Username)) 65 | return 66 | } 67 | } else { 68 | http.Error(w, fmt.Sprintf("No user %s exists.", user.Username), 69 | 403) 70 | log.Error(fmt.Sprintf("No user %s exists.", user.Username)) 71 | return 72 | } 73 | 74 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 75 | "username": user.Username, 76 | }) 77 | tokenString, err := token.SignedString([]byte(s.conf.Authsecret)) 78 | if err != nil { 79 | http.Error(w, err.Error(), http.StatusBadRequest) 80 | log.Error(err) 81 | return 82 | } 83 | err = json.NewEncoder(w).Encode(JwtToken{Token: tokenString}) 84 | if err != nil { 85 | http.Error(w, err.Error(), http.StatusBadRequest) 86 | log.Error(err) 87 | return 88 | } 89 | log.Println("Returning token: ", tokenString) 90 | } 91 | } 92 | 93 | func (s *server) routes() { 94 | s.router.HandleFunc("/", s.handleHome()).Methods("GET") 95 | // networks endpoints 96 | s.router.Handle("/api/login", negroni.New( 97 | negroni.Wrap(http.HandlerFunc(s.handleLogin())), 98 | )).Methods("POST") 99 | s.router.Handle("/api/networks/all", negroni.New( 100 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 101 | negroni.Wrap(http.HandlerFunc(s.handleGetAllNetworks())), 102 | )).Methods("GET") 103 | s.router.Handle("/api/networks/new", negroni.New( 104 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 105 | negroni.Wrap(http.HandlerFunc(s.handleNewNetwork())), 106 | )).Methods("POST") 107 | s.router.Handle("/api/networks/{NETWORK}/cert", negroni.New( 108 | negroni.Wrap(http.HandlerFunc(s.handleGetNetworkCert())), 109 | )).Methods("GET") 110 | s.router.Handle("/api/networks/{NETWORK}/info", negroni.New( 111 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 112 | negroni.Wrap(http.HandlerFunc(s.handleNetworkInfo())), 113 | )).Methods("GET") 114 | s.router.Handle("/api/networks/{NETWORK}/delete", negroni.New( 115 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 116 | negroni.Wrap(http.HandlerFunc(s.handleDeleteNetwork())), 117 | )).Methods("DELETE") 118 | s.router.Handle("/api/networks/{NETWORK}/update", negroni.New( 119 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 120 | negroni.Wrap(http.HandlerFunc(s.handleUpdateNetwork())), 121 | )).Methods("POST") 122 | // node management endpoints 123 | s.router.Handle("/api/networks/{NETWORK}/nodes/all", negroni.New( 124 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 125 | negroni.Wrap(http.HandlerFunc(s.handleGetAllNodes())), 126 | )).Methods("GET") 127 | s.router.Handle("/api/networks/{NETWORK}/nodes/{NODENAME}/approve", negroni.New( 128 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 129 | negroni.Wrap(http.HandlerFunc(s.handleApproveNode())), 130 | )).Methods("POST") 131 | s.router.Handle("/api/networks/{NETWORK}/nodes/{NODENAME}/info", negroni.New( 132 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 133 | negroni.Wrap(http.HandlerFunc(s.handleNodeInfo())), 134 | )).Methods("GET") 135 | s.router.Handle("/api/networks/{NETWORK}/nodes/{NODENAME}/disable", negroni.New( 136 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 137 | negroni.Wrap(http.HandlerFunc(s.handleDisableNode())), 138 | )).Methods("POST") 139 | s.router.Handle("/api/networks/{NETWORK}/nodes/{NODENAME}/update", negroni.New( 140 | negroni.HandlerFunc(s.jmw.HandlerWithNext), 141 | negroni.Wrap(http.HandlerFunc(s.handleUpdateNode())), 142 | )).Methods("POST") 143 | // neutron endpoints 144 | s.router.HandleFunc("/api/neutron/join", s.handleJoinNetwork()).Methods("POST") 145 | s.router.HandleFunc("/api/neutron/config", s.handleGetConfig()).Methods("GET") 146 | s.router.HandleFunc("/api/neutron/leave", s.handleLeaveNetwork()).Methods("POST") 147 | } 148 | 149 | func runServe(configPath string, 150 | listenAddress string, 151 | listenPort int, 152 | ) { 153 | s := new(server) 154 | var err error 155 | s.conf, err = NewConfig(configPath) 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | 160 | if listenAddress == "" { 161 | listenAddress = s.conf.Quasar.Listen.Host 162 | } 163 | if listenPort == 0 { 164 | listenPort = s.conf.Quasar.Listen.Port 165 | } 166 | 167 | endpoint := listenAddress + ":" + fmt.Sprint(listenPort) 168 | 169 | log.WithFields(log.Fields{ 170 | "config": configPath, 171 | }).Info("Loaded config") 172 | 173 | if s.conf.Database.Type == "bolt" { 174 | s.db = new(boltdbi) 175 | err = s.db.connect(s.conf.Database.Source) 176 | if err != nil { 177 | log.Fatal(err) 178 | } 179 | } else { 180 | log.WithFields(log.Fields{ 181 | "requested_db": s.conf.Database.Type, 182 | }).Fatal("Currently only bolt is supported as a database type.") 183 | } 184 | s.conf.Authsecret = os.Getenv("QUASAR_AUTHSECRET") 185 | if s.conf.Authsecret == "" { 186 | log.Fatal("Environment variable QUASAR_AUTHSECRET cannot be empty.") 187 | } 188 | adminpassword := os.Getenv("QUASAR_ADMINPASS") 189 | if adminpassword == "" { 190 | log.Fatal("Environment variable QUASAR_ADMINPASS cannot be empty.") 191 | } 192 | 193 | log.WithFields(log.Fields{ 194 | "endpoint": endpoint, 195 | }).Info("Starting Quasar server") 196 | s.router = mux.NewRouter().StrictSlash(true) 197 | s.n = negroni.New() 198 | s.userpass = make(map[string]string) 199 | s.userpass["admin"] = adminpassword 200 | s.jmw = jwtmiddleware.New(jwtmiddleware.Options{ 201 | ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { 202 | return []byte(s.conf.Authsecret), nil 203 | }, 204 | SigningMethod: jwt.SigningMethodHS256, 205 | }) 206 | s.routes() 207 | s.serve(endpoint) 208 | } 209 | -------------------------------------------------------------------------------- /docs/conclusion.adoc: -------------------------------------------------------------------------------- 1 | == Conclusions 2 | 3 | The state of the toolset as of the submission is fully functional 4 | and fulfils all requirements from the project plan. 5 | 6 | The toolset can be used to create and manage Nebula overlay networks. 7 | A demo of this (see introduction) shows that the tools successfully work 8 | together. 9 | 10 | == Future Work 11 | 12 | === Improved Authentication 13 | 14 | // management 15 | Currently there is a single user called 'admin', 16 | and the password is defined by the environment variable `QUASAR_ADMINPASS`. 17 | This is good enough for basic demonstrations and usage for a simple network 18 | such as a homelab which is managed by one person. 19 | Others can still join nodes to the network, while one person can manage access. 20 | 21 | However, for larger scale networks such as those of small corporations or academic groups, 22 | it would be useful for multiple people to be able to manage networks. 23 | This could involve role based access e.g. one user can manage all nodes in 24 | a specific network while another user can only manage a specific node. 25 | 26 | // basic auth on join request 27 | Additionally, there is no authentication on the Quasar endpoint that 28 | Neutron uses to request to join a network. 29 | This is not a direct security risk to the network as nodes 30 | must be approved by an authorised client before they can receive a certificate 31 | signed by the CA. 32 | However, if Quasar is running on the Internet, 33 | denial of service attacks could be possible as someone could repeatedly 34 | request to join a network. 35 | 36 | A possible solution is for a token to be created for each network which 37 | would be required with a join request. 38 | These tokens could be rotated at intervals such as every 24 hours. 39 | The tokens would be less sensitive than credentials for the management endpoints 40 | as nodes would still require approval. 41 | This means you could share tokens with people 42 | who you partially trust so that they can join your network, 43 | and you wouldn't have to worry about them changing firewall rules in your network. 44 | You would then only be risking a denial of service from these people 45 | you partially trust, which is a significantly smaller attack surface 46 | compared to being open to the internet. 47 | 48 | === HTTPS Support 49 | 50 | Currently HTTPS can be set up using a reverse proxy such as traefik or nginx. 51 | Using tools such as docker, this can be set up quickly and easily with a replicable 52 | setup. 53 | However, one of the big advantages of using Golang is that it compiles code to 54 | a static binary. 55 | Golang's built in HTTP server (which Quasar uses) has support for running over HTTPS. 56 | This means that it would be very easy to add support. 57 | 58 | To use HTTPS you would run the server with: 59 | 60 | [source, go] 61 | ---- 62 | http.ListenAndServeTLS(addr, certFile, keyFile, handler) 63 | ---- 64 | 65 | Instead of: 66 | 67 | [source, go] 68 | ---- 69 | http.ListenAndServe(addr, handler) 70 | ---- 71 | 72 | Adding built in support would involve adding an option to 73 | the yaml config to enable HTTPS, and to configure key and cert paths. 74 | 75 | === Input Validation 76 | 77 | When incoming JSON is decoded by the Quasar API, 78 | it isn't validated against any constraints. 79 | This means injection attacks could be attempted, for example 80 | it is possible to create a network called "". 81 | 82 | Although Svelte protects against this and renders the script tag as a string, 83 | it should be validated by Quasar to limit the possibilities of injection attacks. 84 | 85 | Golang structs allow 'tags' on attributes, which are used by the json package 86 | to decode and encode json data. 87 | Third party libraries provide the ability to use additional tags to add validators 88 | to these tags. 89 | 90 | For example when creating a new network, the `NewNetSchema` struct 91 | is used. 92 | 93 | [source,go] 94 | ---- 95 | type NewNetSchema struct { 96 | Name string `json:"name"` 97 | Cidr string `json:"cidr"` 98 | } 99 | ---- 100 | 101 | The `json:"name"` tag tells the json decoder if there is a field with the key 102 | `name`, it should use the value as the value for `NewNetSchema.Name`. 103 | Using an external library such as `go-playground/validator` you could add validators 104 | as follows: 105 | 106 | [source,go] 107 | ---- 108 | type NewNetSchema struct { 109 | Name string `json:"name" validate:"max=30,alphanum"` 110 | Cidr string `json:"cidr" validate:"cidrv4"` 111 | } 112 | ---- 113 | 114 | You could then validate requests using: 115 | 116 | [source,go] 117 | ---- 118 | // example test struct 119 | net := NewNetSchema{ 120 | Name: "testnet", 121 | Cidr: "192.168.1.0/24", 122 | } 123 | err := validate.Struct(net) 124 | if err := nil { 125 | log.Error(err) 126 | } 127 | ---- 128 | 129 | Overall, although the current project fulfils its requirements and works as 130 | intended, there are lots of improvements that can be made to improve the security 131 | and usability of the toolset. 132 | -------------------------------------------------------------------------------- /docs/hubble.adoc: -------------------------------------------------------------------------------- 1 | == Hubble 2 | 3 | === Overview 4 | 5 | Hubble is a frontend application which communicates with the Quasar API 6 | in order to manage Starship networks. 7 | It shows all available networks in a sidebar, in addition to a 'Create New' button. 8 | When you select a network it shows network settings, which you can modify. 9 | You also have the option to delete a network. 10 | 11 | .Hubble Network Page 12 | image::hubblenetwork.png[] 13 | 14 | The network settings page also shows all nodes in the network as collapsible 15 | cards, which initially show simple information such as the nodename, 16 | hostname and IP address, 17 | but can be expanded to reveal settings for the node that can be updated such 18 | as firewall rules, and groups which the node is in etc. 19 | 20 | .Hubble Node Management 21 | image:hubblenodes.png[] 22 | 23 | === Language and Paradigm Chosen 24 | 25 | // and justification why 26 | To make the application easily accessible for users, 27 | I have built it as a web application. 28 | This means it works cross platform and does not require any installation 29 | on a client device. 30 | Hubble is built with HTML, CSS and JavaScript as this is what is needed 31 | to make an interactive web application. 32 | 33 | In order to develop more efficiently, Hubble is built using a tool called 'Svelte'. 34 | Although this can be considered an alternative to frameworks such as React, 35 | Angular and Vue, it is not a framework as such but more of a compiler. 36 | It allows you to program reactively, with code broken down into components. 37 | The coding process is similar to when using frameworks such as React, 38 | but code is compiled to static HTML, CSS and native JavaScript - whereas many 39 | other frameworks bundle a full library which the client uses to interpret the 40 | code at runtime. 41 | This allows Svelte to have a small footprint in terms of both resource usage 42 | and size of the compiled site. 43 | 44 | Svelte uses a reactive programming paradigm, which is a subset of declarative 45 | paradigms. 46 | You can declare what should happen as a result of something else - to make 47 | the frontend *react* to changes as they happen. 48 | For example if a change is made to a network's settings, 49 | other parts of the interface can *react* and update to reflect the change. 50 | 51 | === Installation Instructions 52 | 53 | [source,shell] 54 | ---- 55 | cd hubble 56 | npm install 57 | 58 | # to build to static site (not needed for running dev server) 59 | npm run build 60 | ---- 61 | 62 | === Operating Instructions 63 | 64 | The build creates a `public` directory containing HTML, CSS and JavaScript files 65 | which can be served using any HTTP server. 66 | 67 | [source,shell] 68 | ---- 69 | cd hubble 70 | 71 | # to run dev server 72 | npm run dev 73 | 74 | # to run 'production' server 75 | npm run start 76 | ---- 77 | 78 | === Libraries and Tools Needed to Run 79 | 80 | * nodejs 81 | * npm - node package manager 82 | ** svelte - compiler for `.svelte` files to HTML/CSS/JS 83 | ** svelte-notifications - for notifications 84 | ** axios - for API requests to Quasar 85 | ** svelte-routing - For managing pages and navigation 86 | ** tailwindcss - simple class based css framework 87 | 88 | === Issues 89 | 90 | // A section outlining any issues that needed to be overcome during development and what mitigations were put in place. This can include things you tried but that didn’t work, things you wanted to do but couldn’t complete and the reasons why 91 | 92 | There were no major issues with the development of the Hubble frontend, 93 | but it involved the challenge of learning a new style of web app development 94 | as I had never used Svelte before. 95 | 96 | Coming from a React background meant I had to learn new concepts 97 | of global stores, event management and overall project management. 98 | However, Svelte is simple and easy to learn so I was able to pick 99 | these up reasonably quickly. 100 | -------------------------------------------------------------------------------- /docs/images/hubblenetwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b177y/starship/20f7cc871d03157ba6d6d69b0f87a32327d12fde/docs/images/hubblenetwork.png -------------------------------------------------------------------------------- /docs/images/hubblenodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b177y/starship/20f7cc871d03157ba6d6d69b0f87a32327d12fde/docs/images/hubblenodes.png -------------------------------------------------------------------------------- /docs/introduction.adoc: -------------------------------------------------------------------------------- 1 | == Introduction 2 | 3 | === Overview 4 | 5 | Nebula is "a scalable overlay networking tool" which allows you to 6 | "seamlessly connect computers anywhere in the world". 7 | footnote:[GitHub. 2021. slackhq/nebula. [online\] Available at: https://github.com/slackhq/nebula] 8 | It uses UDP hole punching to allow nodes to connect directly even if they 9 | are behind a firewall which only allows established traffic through. 10 | 11 | To run Nebula on a node, you must have the `nebula` binary, along with 12 | a private key, certificate and yaml config file. 13 | Signing certificates and customising the config files can become 14 | tedious when the size of a Nebula network grows beyond a few nodes. 15 | Often users will create keypairs and certificates for all nodes from a single host, 16 | then transfer the private keys and certificates to the correct nodes. 17 | This goes against best security practices as it involves transferring a 18 | private key, often across a network, and it means that a host, other than 19 | the node which will use the key, has had access to this private key. 20 | 21 | This toolkit aims to overcome some of these issues by making it easy to bring 22 | up a new node, 23 | provisioning a certificate and giving it configuration, without the private key 24 | leaving the node. 25 | 26 | Although Nebula can scale to support thousands of nodes, 27 | this toolkit is currently focused on (but not limited to) managing smaller networks 28 | such as homelab networks - where some hosts are based on a home network, 29 | some may be running in 'the cloud' with VPS services, and some nodes such as laptops 30 | and mobiles may be constantly moving between different private networks. 31 | 32 | The 'Starship' toolkit includes an API server (Quasar) with a database 33 | which acts as a central management system, 34 | a client tool (Neutron) used by nodes to request to join networks 35 | and update their certificates and configuration, 36 | and a web client (Hubble) which communicates with the API server in order 37 | to manage networks. 38 | The management system can support multiple networks, 39 | and the client tool will allow a node to join multiple networks. 40 | The management system will sign certificates for nodes when they 41 | have been approved using the API. 42 | 43 | A demo of this project can be found here: https://youtu.be/glIgz1huZPI 44 | 45 | === Alternatives 46 | 47 | One alternative to designing a new system for certificate signing would be 48 | to create an extension or a fork of the `step-ca` certificate management 49 | tool. 50 | This would be very powerful and useful for large datacentres 51 | as it would integrate with many different forms of authentication and identity 52 | services. 53 | It is likely that there would be large groups of nodes which could use the same 54 | configuration, 55 | meaning tools such as Ansible, Chef or Puppet would be able to set up 56 | and give the configuration files to the correct nodes. 57 | 58 | However this would lose the ability to have fine grained control over configuration. 59 | For smaller networks, this control can be very useful where each node has 60 | a specific purpose and therefore need different firewall rules. 61 | 62 | Additionally, `step-ca` is now a large and mature project which would take time to 63 | understand well enough to successfully add the ability to sign Nebula 64 | certificates. 65 | Considering that I'm working with a language that I have never used before, 66 | it made more sense to write a new program from scratch, albeit less complex and powerful. 67 | -------------------------------------------------------------------------------- /docs/neutron.adoc: -------------------------------------------------------------------------------- 1 | == Neutron 2 | 3 | === Overview 4 | 5 | Neutron is a client which Starship nodes use to request to join networks, 6 | and update their configuration and certificates. 7 | 8 | When joining a new network, Neutron will create a new Nebula keypair. 9 | It will then send a request to Quasar to join a specific network. 10 | This request includes the node name, the network it wants to join, 11 | its hostname and its Nebula public key. 12 | This information is sent as a JSON payload, signed using the Nebula 13 | private key. 14 | This is encoded similarly to a PASETO token. 15 | PASETO tokens are similar to JSON Web Tokens (JWTs), 16 | however do not suffer the same vulnerabilities JWTs suffer due to the vague 17 | protocol specification. 18 | 19 | When updating, Neutron will send requests to Quasar to obtain 20 | an updated certificate and configuration file. 21 | For Quasar to send these, Neutron must include a signed token 22 | which includes it's nodename and the network name it is trying to 23 | update, and the node must be approved and active on the Quasar server. 24 | The signature on the token is verified against the public key stored 25 | for the node on the Quasar server. 26 | 27 | The update script can be run at frequent intervals to keep the node updated 28 | with the most recent configuration changes. 29 | 30 | === Language and Paradigm Chosen 31 | 32 | Neutron is written in Golang. 33 | There were many reasons for this, but the most significant is that Golang 34 | can statically compile binaries easily. 35 | This means that a small binary can be downloaded to a node with no extra dependencies 36 | required to use the tool. 37 | 38 | Golang has many other advantages. 39 | For example, it is strongly typed, and there is little 'magic' as with 40 | languages such as Python. 41 | The go compiler is also 'fussy'. 42 | For example, it will refuse to compile when you have an unused variable declared. 43 | Although this makes it harder to work with initially, 44 | it means it is easier to write good code. 45 | 46 | Golang is an imperative language, 47 | but it supports programming in object oriented and functional paradigms. 48 | An imperative language is necessary due to the complexity and unique nature 49 | of the tools. 50 | Features of object oriented programming such as classes and inheritence are 51 | not available in golang, 52 | but other features including polymorphism (using interfaces) and methods 53 | are available and have been used in this tool. 54 | 55 | // paradigm 56 | 57 | === Installation Instructions 58 | 59 | [source,shell] 60 | ---- 61 | # build 62 | cd starship 63 | 64 | # equivalent of `go build -o neutron cmd/neutron/*.go` 65 | make neutron 66 | ---- 67 | 68 | === Operating Instructions 69 | 70 | ==== Manual install 71 | 72 | [source,shell] 73 | ---- 74 | # request to join network 75 | ./neutron join -quasar http://127.0.0.1:6947 -network NETWORK -name NAME 76 | 77 | # approve node from frontend then fetch latest config from Quasar 78 | ./neutron update -network NETWORK 79 | # send SIGHUP to nebula to force config reload 80 | pgrep nebula | xargs sudo kill -1 81 | ---- 82 | 83 | ==== Using Install Script 84 | 85 | [source, shell] 86 | ---- 87 | # quick install from release 88 | wget https://github.com/b177y/starship-public/releases/download/v0.3.0/install-neutron.sh -O /tmp/install-neutron.sh 89 | 90 | # check content 91 | less /tmp/install-neutron.sh 92 | bash /tmp/install-neutron.sh 93 | 94 | # approve node from frontend then fetch latest config from Quasar 95 | neutron update -network NETWORK 96 | 97 | # start nebula with systemd 98 | sudo systemctl start nebula@NETWORK 99 | 100 | # send SIGHUP to nebula to force config reload 101 | pgrep nebula | xargs sudo kill -1 102 | ---- 103 | 104 | === Libraries and Tools Needed to Run 105 | 106 | * Golang 107 | ** slackhq/nebula - nebula certificate tools 108 | ** sirupsen/logrus - logging library 109 | ** tetris-io/shortid - library for creating short uuids 110 | * systemd (not a hard requirement, but used for example setup) 111 | * Nebula - this is provided by the install script but otherwise must 112 | be downloaded from link:https://github.com/slackhq/nebula/releases[here] 113 | 114 | === Issues 115 | 116 | // A section outlining any issues that needed to be overcome during development and what mitigations were put in place. This can include things you tried but that didn’t work, things you wanted to do but couldn’t complete and the reasons why 117 | 118 | The keys used by nebula are saved in the Montgomery format as they are used 119 | for x25519 Diffie-Helman key exchange. 120 | This means they cannot be used to sign standard PASETO tokens - which can only use 121 | ed25519 signatures for asymmetric key authentication. 122 | This requires Edwards formatted keys rather than Montgomery. 123 | The "twisted Edwards curve used by Ed25519 and the Montgomery 124 | curve used by X25519 are birationally equivalent" 125 | footnote:[Valsorda, F. 2019. Using Ed25519 signing keys for encryption [online\] Available at: https://blog.filippo.io/using-ed25519-keys-for-encryption/] 126 | which means you can convert between the two key formats. 127 | However you can only convert directly from Edwards to Montgomery, 128 | not the other way around. 129 | 130 | To avoid having multiple private keys for each network a node is in 131 | (one for Nebula and one for communicating with Quasar), 132 | I created a library for signing and verifying 'XPASETO' tokens. 133 | These use Montgomery keys for XEdDSA signatures, outlined by Signal. 134 | footnote:[Perrin, T. 2016. The XEdDSA and VXEdDSA Signature Schemes [online\] Available at: https://signal.org/docs/specifications/xeddsa/] 135 | This package is based off an existing paseto library, 136 | footnote:[GitHub. 2021. o1egl/paseto. [online\] Available at: https://github.com/o1egl/paseto] 137 | from which functions are borrowed where it wasn't necessary to rewrite them. 138 | It should be noted that the XPASETO library does NOT conform with the PASETO 139 | standard (see https://paseto.io/rfc/ section 5.2). 140 | -------------------------------------------------------------------------------- /docs/quasar.adoc: -------------------------------------------------------------------------------- 1 | == Quasar 2 | 3 | === Overview 4 | 5 | Quasar is a Central Management System (CMS) for managing Starship networks. 6 | It provides APIs for two types of clients: 7 | 8 | * Neutron Nodes 9 | ** These authenticate by signing requests using their nebula private key 10 | * Frontend clients / management tools 11 | ** These authenticate using JSON Web Tokens 12 | 13 | Quasar can be configured using a yaml config file. 14 | By default the API listens on port `6947` as the Helix Nebula 15 | is 694.7 light years away from earth. 16 | 17 | The API for neutron nodes provides the following endpoints: 18 | 19 | * /api/neutron/join - for a node to request to join a network. 20 | This request includes the Nebula public key for the node. 21 | The request is signed by the corresponding private key. 22 | This self-signed request is verified by Quasar. 23 | * /api/neutron/update - for a node to request configuration information 24 | and a certificate. 25 | Quasar will work out the configuration options based off the node's config 26 | in the database, and the config of other nodes. 27 | 28 | The API for management clients provides endpoints for: 29 | 30 | * listing networks 31 | * getting CA cert for a network 32 | * listing nodes in a network 33 | * updating network settings 34 | * updating node settings 35 | * approving / enabling / disabling nodes 36 | 37 | === Language and Paradigm Chosen 38 | 39 | Quasar is written in Golang, for many of the same reasons as Neutron. 40 | In addition to these reasons, 41 | Nebula tools and libraries are written in Golang. 42 | Nebula has a custom certificate format (not x509 or SSH certs) 43 | and slack have made the library for interacting with these certificates 44 | open source so it is easy to include in a project. 45 | 46 | Although it would be possible to use the `nebula-cert` commandline tool 47 | with other languages using subprocesses, 48 | this would be less clean and less efficient than importing and using the 49 | native functions needed. 50 | Using a language other than Go would have to add this as an extra dependency for the tool. 51 | 52 | Furthermore, I could not find a way to use Montgomery keys for XEdDSA 53 | signatures in Python (the most likely alternative to Golang for this tool), 54 | and writing the cryptography functions from scratch myself would 55 | be a security (and time management) risk as maths and cryptography are 56 | not my areas of expertise. 57 | Golang has well maintained cryptography libraries as part of the language's 58 | standard package. 59 | Using the built in libraries in addition to some code borrowed from third party 60 | libraries, 61 | I was able to write a JSON token signing library which uses XEdDSA signatures. 62 | 63 | Golang uses an imperative programming paradigm. 64 | See the Neutron section for more on this. 65 | 66 | === Installation Instructions 67 | 68 | [source,shell] 69 | ---- 70 | make quasar 71 | ---- 72 | 73 | === Operating Instructions 74 | 75 | [source,shell] 76 | ---- 77 | # set JWT signing secret 78 | export QUASAR_AUTHSECRET=$(uuid) 79 | 80 | # set admin account password 81 | export QUASAR_ADMINPASS="password" 82 | 83 | # start server 84 | ./quasar serve -config examples/quasar.yml 85 | ---- 86 | 87 | === Libraries and Tools Needed to Run 88 | 89 | * Golang 90 | ** slackhq/nebula - nebula certificate tools 91 | ** boltdb/bolt - embedded key/value database 92 | ** gorilla/mux - http router 93 | ** urfave/negroni - http middleware manager 94 | ** meatballhat/negroni-logrus - logging middleware support 95 | ** sirupsen/logrus - logging library 96 | ** rs/cors - CORS middleware 97 | 98 | === Issues 99 | 100 | // A section outlining any issues that needed to be overcome during development and what mitigations were put in place. This can include things you tried but that didn’t work, things you wanted to do but couldn’t complete and the reasons why 101 | 102 | Part way through the project I decided to rewrite Quasar in Python, 103 | as I am more familiar with Python and I was running into time constraints. 104 | I had rewritten most of the API in Python when I tried to replicate 105 | the XPASETO library I had earlier written in Golang. 106 | I was unable to find the necessary libraries in Python to support this. 107 | Although Golang is a newer language than Python, 108 | it was created by Google and has always had a focus on security, 109 | meaning the built in crypto libraries are more advanced. 110 | 111 | Another problem I had with the Python rewrite was that I had to use 112 | the `nebula-cert` binary with subprocesses for creating and signing 113 | certificates. 114 | This adds an extra dependency to the project and is not a clean way 115 | of interacting with certificates. 116 | 117 | I decided to switch back to Golang for these reasons, 118 | but fortunately I ended up finding it easier than I thought it would be. 119 | 120 | Another problem I had was with the conversion of Edwards keys (used by the CA) 121 | to Montgomery Curve25519 keys (used by Nebula nodes). 122 | I used functions from a project by Filippo Valsorda (Go team security lead) 123 | to perform the key conversion. 124 | footnote:[GitHub. 2021. FiloSottile/age. [online\] Available at: https://github.com/FiloSottile/age/blob/bbab440e198a4d67ba78591176c7853e62d29e04/internal/age/ssh.go#L174] 125 | The function for converting public keys worked, 126 | but the private key function did not. 127 | After lots of research, I found that key clamping 128 | footnote:[Craige, J. 2021. An Explainer On Ed25519 Clamping [online\] Available at: https://www.jcraige.com/an-explainer-on-ed25519-clamping] 129 | was needed. 130 | -------------------------------------------------------------------------------- /docs/report.adoc: -------------------------------------------------------------------------------- 1 | = PLCS Report - Starship Toolset 2 | :toc: left 3 | :toclevels: 3 4 | :icons: font 5 | :experimental: 6 | :source-highlighter: pygments 7 | :pygments-style: onedark 8 | :imagesdir: ./images 9 | :pdf-themesdir: /home/billy/repos/asciidoc-themes/pdf/themes 10 | :pdf-fontsdir: /home/billy/repos/asciidoc-themes/pdf/fonts 11 | :pdf-theme: b177y 12 | 13 | include::introduction.adoc[] 14 | 15 | include::hubble.adoc[] 16 | 17 | include::neutron.adoc[] 18 | 19 | include::quasar.adoc[] 20 | 21 | include::conclusion.adoc[] 22 | 23 | // include appendices here if necessary 24 | == Appendices 25 | 26 | include::../CHANGELOG.adoc[leveloffset=+1] 27 | 28 | == References 29 | -------------------------------------------------------------------------------- /examples/install-neutron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function setupNebulaDir(){ 4 | sudo chown -R $(whoami) /etc/nebula 5 | } 6 | 7 | function download(){ 8 | # Could handle multiple platforms here 9 | cd /tmp 10 | wget https://github.com/b177y/starship/releases/download/v0.3.0/$(uname -s)-$(uname -m).tar.gz -O /tmp/starship.tar.gz 11 | rm -r /tmp/release 12 | tar -xf /tmp/starship.tar.gz 13 | release=$(echo "/tmp/release/$(uname -s)-$(uname -m)" | tr '[:upper:]' '[:lower:]') 14 | sudo mv "${release}/nebula" /usr/local/bin/nebula 15 | sudo mv "${release}/neutron" /usr/local/bin/neutron 16 | sudo chown root:root /usr/local/bin/nebula 17 | sudo chown $(whoami):$(whoami) /usr/local/bin/neutron 18 | sudo mv "${release}/nebula@.service" /etc/systemd/system/nebula@.service 19 | sudo chown root:root /etc/systemd/system/nebula@.service 20 | sudo systemctl daemon-reload 21 | cd - 22 | } 23 | 24 | function runNeutron(){ 25 | echo -n "Quasar Endpoint: " 26 | read qaddr 27 | echo -n "Network to Join: " 28 | read netname 29 | echo -n "Node Name: " 30 | read nodename 31 | /usr/local/bin/neutron join -quasar $qaddr -network $netname -name $nodename || exit 32 | echo "Run \`neutron update -network $netname\` to get the node config once you have approved the node." 33 | echo "Once the config has been fetched you can start nebula with \`systemctl start nebula@$netname\`" 34 | } 35 | 36 | function main(){ 37 | download 38 | setupNebulaDir 39 | runNeutron 40 | } 41 | 42 | main 43 | -------------------------------------------------------------------------------- /examples/nebula@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Nebula for %i 3 | Wants=basic.target 4 | After=basic.target network.target 5 | Before=sshd.service 6 | 7 | [Service] 8 | SyslogIdentifier=nebula 9 | StandardOutput=syslog 10 | StandardError=syslog 11 | ExecReload=/bin/kill -HUP $MAINPID 12 | ExecStart=/usr/local/bin/nebula -config /etc/nebula/%i/nebula.yml 13 | Restart=always 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /examples/quasar.yml: -------------------------------------------------------------------------------- 1 | db: 2 | type: "bolt" # bolt used by default, later add etcd 3 | src: "test.db" 4 | 5 | quasar: 6 | name: "Test Server" 7 | listen: 8 | host: "127.0.0.1" 9 | port: 6947 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/b177y/starship 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect 7 | github.com/auth0/go-jwt-middleware v1.0.0 8 | github.com/boltdb/bolt v1.3.1 9 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible 10 | github.com/gorilla/mux v1.8.0 11 | github.com/meatballhat/negroni-logrus v1.1.1 12 | github.com/mitchellh/mapstructure v1.4.1 // indirect 13 | github.com/o1egl/paseto v1.0.0 14 | github.com/o1egl/paseto/v2 v2.1.1 15 | github.com/pieterbork/ed25519 v0.0.0-20200301051623-f19b832d0d2e // indirect 16 | github.com/pkg/errors v0.9.1 17 | github.com/rs/cors v1.7.0 18 | github.com/signal-golang/ed25519 v0.0.0-20200301051623-f19b832d0d2e 19 | github.com/sirupsen/logrus v1.8.1 20 | github.com/slackhq/nebula v1.4.0 21 | github.com/stretchr/testify v1.6.1 22 | github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 23 | github.com/urfave/negroni v1.0.0 24 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 25 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect 26 | gopkg.in/yaml.v2 v2.4.0 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 2 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 3 | github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= 4 | github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE= 5 | github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= 6 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= 7 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= 8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 10 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 11 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 12 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 13 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 14 | github.com/auth0/go-jwt-middleware v1.0.0 h1:76t55qLQu3xjMFbkirbSCA8ZPcO1ny+20Uq1wkSTRDE= 15 | github.com/auth0/go-jwt-middleware v1.0.0/go.mod h1:nX2S0GmCyl087kdNSSItfOvMYokq5PSTG1yGIP5Le4U= 16 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 17 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 18 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 19 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 20 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 21 | github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= 22 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 23 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 24 | github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 29 | github.com/flynn/noise v0.0.0-20210331153838-4bdb43be3117/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= 30 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= 31 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 32 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 33 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 34 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 35 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 36 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 37 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 38 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 39 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 40 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 42 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 44 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 45 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 46 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 47 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 48 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 49 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 50 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 51 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 52 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= 53 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 54 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 55 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 56 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 57 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 58 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 59 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 60 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 61 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 62 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 63 | github.com/kardianos/service v1.1.0/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc= 64 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 65 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 66 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 67 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 68 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 69 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 70 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 71 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 72 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 73 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 74 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 75 | github.com/meatballhat/negroni-logrus v1.1.1 h1:eDgsDdJYy97gI9kr+YS/uDKCaqK4S6CUQLPG0vNDqZA= 76 | github.com/meatballhat/negroni-logrus v1.1.1/go.mod h1:FlwPdXB6PeT8EG/gCd/2766M2LNF7SwZiNGD6t2NRGU= 77 | github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 78 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 79 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 80 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 81 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 82 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 83 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 84 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 85 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 86 | github.com/nbrownus/go-metrics-prometheus v0.0.0-20180622211546-6e6d5173d99c/go.mod h1:1yMri853KAI2pPAUnESjaqZj9JeImOUM+6A4GuuPmTs= 87 | github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= 88 | github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= 89 | github.com/o1egl/paseto/v2 v2.1.1 h1:vWP5o9P/3UEXXQ+/BHQRrpdXpK+X9RMtD4IvB30FWF0= 90 | github.com/o1egl/paseto/v2 v2.1.1/go.mod h1:HQ4aS/uX2A/v1h/BIh5XTFStRm+eMdI7G/jBaQ0vaCA= 91 | github.com/pieterbork/ed25519 v0.0.0-20200301051623-f19b832d0d2e h1:iTTj9cGouNkvs1qyL/PexzFJ32MZu4aQsaAjS2uIwVw= 92 | github.com/pieterbork/ed25519 v0.0.0-20200301051623-f19b832d0d2e/go.mod h1:0s8sTU9YA2e8B5N+4O0BiuVuzcUbAEJGz3GyAtcqCFw= 93 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 94 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 95 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 96 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 97 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 98 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 99 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 100 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 101 | github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= 102 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 103 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 104 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 105 | github.com/prometheus/client_model v0.0.0-20191202183732-d1d2010b5bee/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 106 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 107 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 108 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 109 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 110 | github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 111 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 112 | github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 113 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 114 | github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 115 | github.com/signal-golang/ed25519 v0.0.0-20200301051623-f19b832d0d2e h1:XLsRkdVt2FOctJUbBbyBcOtmSgRv+jMQYgqESfOgTxo= 116 | github.com/signal-golang/ed25519 v0.0.0-20200301051623-f19b832d0d2e/go.mod h1:2Ad7iWk5/yN+AiUcyx6EteImlJBcxBM0Q2R/bmXoCA0= 117 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 118 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 119 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 120 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 121 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 122 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 123 | github.com/slackhq/nebula v1.4.0 h1:EwjObdoI1a0V4hXGn8cc/5gbGvMKuKBp1H+bOCnyZU8= 124 | github.com/slackhq/nebula v1.4.0/go.mod h1:N4OtbI4997CFRdZZiJSOwuQdvslvef5CkWR6Nd+tUB4= 125 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 126 | github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= 127 | github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 128 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 129 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 130 | github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= 131 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 132 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 133 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 134 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 135 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 136 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 137 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 138 | github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w= 139 | github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= 140 | github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= 141 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 142 | github.com/vishvananda/netlink v1.0.1-0.20190522153524-00009fb8606a/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= 143 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 144 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 145 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 146 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 147 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 148 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 149 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 150 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 151 | golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 152 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 153 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= 154 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 155 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 156 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 157 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 158 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 159 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 160 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 161 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 162 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 163 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 164 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 165 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 166 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 167 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 168 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 173 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 176 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 177 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 178 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= 188 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 190 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 191 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 192 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 193 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 194 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 195 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 196 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 197 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 198 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 199 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 200 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 201 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 202 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 203 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 204 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 205 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 206 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 207 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 208 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 211 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 212 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 213 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 214 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 215 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 216 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 217 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 218 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 219 | -------------------------------------------------------------------------------- /hubble/README.adoc: -------------------------------------------------------------------------------- 1 | == Hubble 2 | :imagesdir: ../docs/images 3 | 4 | === Overview 5 | 6 | Hubble is a frontend application which communicates with the Quasar API 7 | in order to manage Starship networks. 8 | It shows all available networks in a sidebar, in addition to a 'Create New' button. 9 | When you select a network it shows network settings, which you can modify. 10 | You also have the option to delete a network. 11 | 12 | .Hubble Network Page 13 | image::hubblenetwork.png[] 14 | 15 | The network settings page also shows all nodes in the network as collapsible 16 | cards, which initially show simple information such as the nodename, 17 | hostname and IP address, 18 | but can be expanded to reveal settings for the node that can be updated such 19 | as firewall rules, and groups which the node is in etc. 20 | 21 | .Hubble Node Management 22 | image:hubblenodes.png[] 23 | 24 | === Installation 25 | 26 | [source,shell] 27 | ---- 28 | cd hubble 29 | npm install 30 | 31 | # to build to static site (not needed for running dev server) 32 | npm run build 33 | ---- 34 | 35 | === Operating Instructions 36 | 37 | The build creates a `public` directory containing HTML, CSS and JavaScript files 38 | which can be served using any HTTP server. 39 | 40 | [source,shell] 41 | ---- 42 | cd hubble 43 | 44 | # to run dev server 45 | npm run dev 46 | 47 | # to run 'production' server 48 | npm run start 49 | ---- 50 | 51 | -------------------------------------------------------------------------------- /hubble/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear -s" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.0", 13 | "rollup": "^2.3.4", 14 | "rollup-plugin-css-only": "^3.1.0", 15 | "rollup-plugin-livereload": "^2.0.0", 16 | "rollup-plugin-svelte": "^7.0.0", 17 | "rollup-plugin-terser": "^7.0.0", 18 | "svelte": "^3.0.0" 19 | }, 20 | "dependencies": { 21 | "autoprefixer": "^9.8.6", 22 | "axios": "^0.21.1", 23 | "postcss": "^7.0.35", 24 | "sirv-cli": "^1.0.0", 25 | "svelte-notifications": "^0.9.9", 26 | "svelte-preprocess": "^4.7.3", 27 | "svelte-routing": "^1.6.0", 28 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.1.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hubble/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b177y/starship/20f7cc871d03157ba6d6d69b0f87a32327d12fde/hubble/public/favicon.png -------------------------------------------------------------------------------- /hubble/public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | box-sizing: border-box; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 12 | height: 100%; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /hubble/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |