vpn client + api: add feature to approve one device from another
diff --git a/core/nebula/api/go.mod b/core/nebula/api/go.mod
index fa5d230..719cac7 100644
--- a/core/nebula/api/go.mod
+++ b/core/nebula/api/go.mod
@@ -2,12 +2,13 @@
 
 go 1.17
 
+replace github.com/giolekva/pcloud/core/nebula/controller => ../controller
+
 require (
 	github.com/giolekva/pcloud/core/nebula/controller v0.0.0-20211209144208-c054df13a2a1
 	github.com/gorilla/mux v1.8.0
 	github.com/jinzhu/copier v0.3.4
 	github.com/slackhq/nebula v1.5.0
-	inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
 	k8s.io/api v0.23.0
 	k8s.io/apimachinery v0.23.0
 	k8s.io/client-go v0.23.0
@@ -27,8 +28,6 @@
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
-	go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
 	golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 // indirect
 	golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
diff --git a/core/nebula/api/go.sum b/core/nebula/api/go.sum
index 950363a..797fc8f 100644
--- a/core/nebula/api/go.sum
+++ b/core/nebula/api/go.sum
@@ -75,7 +75,6 @@
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -95,8 +94,6 @@
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/giolekva/pcloud/core/nebula/controller v0.0.0-20211209144208-c054df13a2a1 h1:GyB1dLK2oWARCPXBpnXDI1sr2I9MhFuetrmUexUvUCs=
-github.com/giolekva/pcloud/core/nebula/controller v0.0.0-20211209144208-c054df13a2a1/go.mod h1:IasYiN/e2eUZKb6bPsG3NIOZ6Jpz8y7LuieA15TWqNs=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -329,10 +326,6 @@
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
-go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
-go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA=
-go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -728,8 +721,6 @@
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=
-inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
 k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8=
 k8s.io/api v0.23.0 h1:WrL1gb73VSC8obi8cuYETJGXEoFNEh3LU0Pt+Sokgro=
 k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg=
diff --git a/core/nebula/api/main.go b/core/nebula/api/main.go
index 1b74d8b..906a674 100644
--- a/core/nebula/api/main.go
+++ b/core/nebula/api/main.go
@@ -1,12 +1,18 @@
 package main
 
 import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
 	"embed"
 	"encoding/base64"
 	"encoding/json"
 	"flag"
 	"fmt"
 	"html/template"
+	"io"
 	"io/ioutil"
 	"log"
 	"net/http"
@@ -83,35 +89,6 @@
 	w.Write(qr)
 }
 
-func (h *Handler) handleSignNode(w http.ResponseWriter, r *http.Request) {
-	if err := r.ParseForm(); err != nil {
-		http.Error(w, err.Error(), http.StatusBadRequest)
-		return
-	}
-	_, _, err := h.mgr.CreateNode(
-		r.FormValue("node-namespace"),
-		r.FormValue("node-name"),
-		r.FormValue("ca-namespace"),
-		r.FormValue("ca-name"),
-		r.FormValue("ip-cidr"),
-		r.FormValue("pub-key"),
-	)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	http.Redirect(w, r, "/", http.StatusSeeOther)
-}
-
-func (h *Handler) getNextIP(w http.ResponseWriter, r *http.Request) {
-	ip, err := h.mgr.getNextIP()
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	fmt.Fprint(w, ip)
-}
-
 type signReq struct {
 	Message []byte `json:"message"`
 }
@@ -149,9 +126,6 @@
 	IPCidr    string `json:"ip_cidr"`
 }
 
-type joinResp struct {
-}
-
 func (h *Handler) join(w http.ResponseWriter, r *http.Request) {
 	var req joinReq
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -174,6 +148,7 @@
 		*caName,
 		req.IPCidr,
 		string(req.PublicKey),
+		nil,
 	)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -200,6 +175,111 @@
 	}
 }
 
+type getResp struct {
+	Key   []byte `json:"key"`
+	Nonce []byte `json:"nonce"`
+	Data  []byte `json:"data"`
+}
+
+func (h *Handler) get(w http.ResponseWriter, r *http.Request) {
+	fmt.Println("##### GET")
+	vars := mux.Vars(r)
+	pubKey, err := h.mgr.GetNodeEncryptionPublicKey(*namespace, vars["name"])
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	fmt.Println("Got key")
+	key := make([]byte, 32)
+	if _, err := io.ReadFull(rand.Reader, key); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	aesgcm, err := cipher.NewGCM(block)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	nonce := make([]byte, 12)
+	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+		panic(err.Error())
+	}
+	for {
+		time.Sleep(1 * time.Second)
+		cfg, err := h.mgr.GetNodeConfig(*namespace, vars["name"])
+		if err != nil {
+			fmt.Println(err.Error())
+			continue
+		}
+		cfgBytes, err := yaml.Marshal(cfg)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		cfgEnc := aesgcm.Seal(nil, nonce, cfgBytes, nil)
+		keyEnc, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, key, []byte(""))
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		nonceEnc, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, nonce, []byte(""))
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		w.Header().Set("Content-Type", "application/json")
+		resp := getResp{
+			Key:   keyEnc,
+			Nonce: nonceEnc,
+			Data:  cfgEnc,
+		}
+		if err := json.NewEncoder(w).Encode(&resp); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		break
+	}
+}
+
+type approveReq struct {
+	EncPublicKey []byte `json:"enc_public_key"`
+	Name         string `json:"name"`
+	NetPublicKey []byte `json:"net_public_key"`
+	IPCidr       string `json:"ip_cidr"`
+}
+
+type approveResp struct {
+}
+
+func (h *Handler) approve(w http.ResponseWriter, r *http.Request) {
+	var req approveReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	fmt.Println("---- APPROVE")
+	fmt.Printf("%#v\n", req)
+	_, _, err := h.mgr.CreateNode(
+		*namespace,
+		req.Name,
+		*namespace,
+		*caName,
+		req.IPCidr,
+		string(req.NetPublicKey),
+		req.EncPublicKey,
+	)
+	if err != nil {
+		fmt.Println(err.Error())
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func loadConfigTemplate(path string) (map[string]interface{}, error) {
 	tmpl, err := ioutil.ReadFile(path)
 	if err != nil {
@@ -243,12 +323,12 @@
 		tmpls: t,
 	}
 	r := mux.NewRouter()
-	r.HandleFunc("/api/ip", handler.getNextIP)
 	r.HandleFunc("/api/sign", handler.sign)
 	r.HandleFunc("/api/join", handler.join)
+	r.HandleFunc("/api/approve", handler.approve)
+	r.HandleFunc("/api/get/{name:[a-zA-z0-9-]+}", handler.get)
 	r.HandleFunc("/node/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleNode)
 	r.HandleFunc("/ca/{namespace:[a-zA-z0-9-]+}/{name:[a-zA-z0-9-]+}", handler.handleCA)
-	r.HandleFunc("/sign-node", handler.handleSignNode)
 	r.HandleFunc("/", handler.handleIndex)
 	http.Handle("/", r)
 	fmt.Printf("Starting HTTP server on port: %d\n", *port)
diff --git a/core/nebula/api/manager.go b/core/nebula/api/manager.go
index 790a01e..962b3b7 100644
--- a/core/nebula/api/manager.go
+++ b/core/nebula/api/manager.go
@@ -5,11 +5,11 @@
 	"crypto"
 	"crypto/ed25519"
 	"crypto/rand"
-	"errors"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/base64"
 	"fmt"
 
-	"inet.af/netaddr"
-
 	"github.com/jinzhu/copier"
 	"github.com/slackhq/nebula/cert"
 
@@ -72,7 +72,7 @@
 	return ret, nil
 }
 
-func (m *Manager) CreateNode(namespace, name, caNamespace, caName, ipCidr, pubKey string) (string, string, error) {
+func (m *Manager) CreateNode(namespace, name, caNamespace, caName, ipCidr, pubKey string, encPubKey []byte) (string, string, error) {
 	node := &nebulav1.NebulaNode{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
@@ -86,6 +86,9 @@
 			SecretName:  fmt.Sprintf("%s-cert", name),
 		},
 	}
+	if encPubKey != nil {
+		node.Spec.EncPubKey = base64.StdEncoding.EncodeToString(encPubKey)
+	}
 	node, err := m.nebulaClient.LekvaV1().NebulaNodes(namespace).Create(context.TODO(), node, metav1.CreateOptions{})
 	if err != nil {
 		return "", "", err
@@ -128,25 +131,6 @@
 	return secret.Data["ca.png"], nil
 }
 
-func (m *Manager) getNextIP() (netaddr.IP, error) {
-	nodes, err := m.nebulaClient.LekvaV1().NebulaNodes(m.namespace).List(context.TODO(), metav1.ListOptions{})
-	if err != nil {
-		return netaddr.IP{}, err
-	}
-	var max netaddr.IP
-	for _, node := range nodes.Items {
-		ip := netaddr.MustParseIPPrefix(node.Spec.IPCidr)
-		if max.Less(ip.IP()) {
-			max = ip.IP()
-		}
-	}
-	n := max.Next()
-	if n.IsZero() {
-		return n, errors.New("IP address range exhausted")
-	}
-	return n, nil
-}
-
 func (m *Manager) Sign(message []byte) ([]byte, error) {
 	secret, err := m.getCASecret(m.namespace, m.caName)
 	if err != nil {
@@ -186,3 +170,15 @@
 	}
 	return m.kubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), node.Spec.SecretName, metav1.GetOptions{})
 }
+
+func (m *Manager) GetNodeEncryptionPublicKey(namespace, name string) (*rsa.PublicKey, error) {
+	node, err := m.nebulaClient.LekvaV1().NebulaNodes(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+	k, err := base64.StdEncoding.DecodeString(node.Spec.EncPubKey)
+	if err != nil {
+		return nil, err
+	}
+	return x509.ParsePKCS1PublicKey(k)
+}
diff --git a/core/nebula/controller/apis/nebula/v1/types.go b/core/nebula/controller/apis/nebula/v1/types.go
index c4e03f2..60545c0 100644
--- a/core/nebula/controller/apis/nebula/v1/types.go
+++ b/core/nebula/controller/apis/nebula/v1/types.go
@@ -55,6 +55,7 @@
 	CANamespace string `json:"caNamespace"`
 	IPCidr      string `json:"ipCidr"`
 	PubKey      string `json:"pubKey"`
+	EncPubKey   string `json:"encPubKey"`
 	SecretName  string `json:"secretName"`
 }
 
diff --git a/core/nebula/controller/crds/nebula.crds.yaml b/core/nebula/controller/crds/nebula.crds.yaml
deleted file mode 100644
index c8de194..0000000
--- a/core/nebula/controller/crds/nebula.crds.yaml
+++ /dev/null
@@ -1,83 +0,0 @@
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
-  name: nebulacas.lekva.me
-spec:
-  group: lekva.me
-  scope: Namespaced
-  names:
-    kind: NebulaCA
-    listKind: NebulaCAList
-    plural: nebulacas
-    singular: nebulaca
-    shortNames:
-      - nca
-      - ncas
-  versions:
-    - name: v1
-      served: true
-      storage: true
-      subresources:
-        status: {}
-      schema:
-        openAPIV3Schema:
-          type: object
-          properties:
-            spec:
-              type: object
-              properties:
-                secretName:
-                  type: string
-            status:
-              type: object
-              properties:
-                state:
-                  type: string
-                message:
-                  type: string
----
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
-  name: nebulanodes.lekva.me
-spec:
-  group: lekva.me
-  scope: Namespaced
-  names:
-    kind: NebulaNode
-    listKind: NebulaNodeList
-    plural: nebulanodes
-    singular: nebulanode
-    shortNames:
-      - nnode
-      - nnodes
-  versions:
-    - name: v1
-      served: true
-      storage: true
-      subresources:
-        status: {}
-      schema:
-        openAPIV3Schema:
-          type: object
-          properties:
-            spec:
-              type: object
-              properties:
-                caName:
-                  type: string
-                caNamespace:
-                  type: string
-                ipCidr:
-                  type: string
-                pubKey:
-                  type: string
-                secretName:
-                  type: string
-            status:
-              type: object
-              properties:
-                state:
-                  type: string
-                message:
-                  type: string