vpn client + api: add feature to approve one device from another
diff --git a/charts/nebula/crds/nebula-node.yaml b/charts/nebula/crds/nebula-node.yaml
index d59963f..ddb79de 100644
--- a/charts/nebula/crds/nebula-node.yaml
+++ b/charts/nebula/crds/nebula-node.yaml
@@ -34,6 +34,8 @@
                   type: string
                 pubKey:
                   type: string
+                encPubKey:
+                  type: string
                 secretName:
                   type: string
             status:
diff --git a/charts/nebula/crds/nebula.crds.yaml b/charts/nebula/crds/nebula.crds.yaml
deleted file mode 100644
index f6b8411..0000000
--- a/charts/nebula/crds/nebula.crds.yaml
+++ /dev/null
@@ -1,37 +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
diff --git "a/core/client/cmd/pcloud/\043app_android.go\043" "b/core/client/cmd/pcloud/\043app_android.go\043"
new file mode 100644
index 0000000..2b28a11
--- /dev/null
+++ "b/core/client/cmd/pcloud/\043app_android.go\043"
@@ -0,0 +1,241 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"time"
+	"unsafe"
+
+	"gioui.org/app"
+	"github.com/giolekva/pcloud/core/client/jni"
+	"github.com/sirupsen/logrus"
+	"github.com/slackhq/nebula"
+	"github.com/slackhq/nebula/cert"
+	nc "github.com/slackhq/nebula/config"
+)
+
+type androidApp struct {
+	jvm          *jni.JVM
+	appCtx       jni.Object // PCloudApp
+	activity     jni.Object // PCloudActivity
+	service      jni.Object // PCloudVPNService
+	nebulaConfig []byte
+	ctrl         *nebula.Control
+}
+
+func createApp() App {
+	return &androidApp{
+		jvm:    (*jni.JVM)(unsafe.Pointer(app.JavaVM())),
+		appCtx: jni.Object(app.AppContext()),
+	}
+}
+
+func (a *androidApp) LaunchBarcodeScanner() error {
+	return jni.Do(a.jvm, func(env *jni.Env) error {
+		cls := jni.GetObjectClass(env, a.activity)
+		m := jni.GetMethodID(env, cls, "launchBarcodeScanner", "()Ljava/lang/String;")
+		_, err := jni.CallObjectMethod(env, a.activity, m)
+		return err
+	})
+}
+
+func (a *androidApp) OnView(e app.ViewEvent) error {
+	a.deleteActivityRef()
+	view := jni.Object(e.View)
+	if view == 0 {
+		return nil
+	}
+	activity, err := a.contextForView(view)
+	if err != nil {
+		return err
+	}
+	a.activity = activity
+	return nil
+}
+
+func (a *androidApp) deleteActivityRef() {
+	if a.activity == 0 {
+		return
+	}
+	jni.Do(a.jvm, func(env *jni.Env) error {
+		jni.DeleteGlobalRef(env, a.activity)
+		return nil
+	})
+	a.activity = 0
+}
+
+func (a *androidApp) contextForView(view jni.Object) (jni.Object, error) {
+	if view == 0 {
+		return 0, errors.New("Should not reach")
+	}
+	var ctx jni.Object
+	err := jni.Do(a.jvm, func(env *jni.Env) error {
+		cls := jni.GetObjectClass(env, view)
+		m := jni.GetMethodID(env, cls, "getContext", "()Landroid/content/Context;")
+		var err error
+		ctx, err = jni.CallObjectMethod(env, view, m)
+		ctx = jni.NewGlobalRef(env, ctx)
+		return err
+	})
+	if err != nil {
+		return 0, err
+	}
+	return ctx, nil
+}
+
+func (a *androidApp) StartVPN(config []byte) error {
+	fmt.Println("2222222")
+	a.nebulaConfig = config
+	fmt.Println(string(a.nebulaConfig))
+	return jni.Do(a.jvm, func(env *jni.Env) error {
+		fmt.Println(123123)
+		cls := jni.GetObjectClass(env, a.activity)
+		m := jni.GetMethodID(env, cls, "startVpn", "(Ljava/lang/String;)Ljava/lang/String;")
+		jConfig := jni.JavaString(env, string(config))
+		_, err := jni.CallObjectMethod(env, a.activity, m, jni.Value(jConfig))
+		fmt.Println(123123123)
+		return err
+
+	})
+}
+
+func (a *androidApp) Connect(serv interface{}) error {
+	s, ok := serv.(jni.Object)
+	if !ok {
+		return fmt.Errorf("Unexpected service type: %T", serv)
+	}
+	jni.Do(a.jvm, func(env *jni.Env) error {
+		if jni.IsSameObject(env, s, a.service) {
+			// We already have a reference.
+			jni.DeleteGlobalRef(env, s)
+			return nil
+		}
+		if a.service != 0 {
+			jni.DeleteGlobalRef(env, a.service)
+		}
+		// netns.SetAndroidProtectFunc(func(fd int) error {
+		// 	return jni.Do(a.jvm, func(env *jni.Env) error {
+		// 		// Call https://developer.android.com/reference/android/net/VpnService#protect(int)
+		// 		// to mark fd as a socket that should bypass the VPN and use the underlying network.
+		// 		cls := jni.GetObjectClass(env, s)
+		// 		m := jni.GetMethodID(env, cls, "protect", "(I)Z")
+		// 		ok, err := jni.CallBooleanMethod(env, s, m, jni.Value(fd))
+		// 		// TODO(bradfitz): return an error back up to netns if this fails, once
+		// 		// we've had some experience with this and analyzed the logs over a wide
+		// 		// range of Android phones. For now we're being paranoid and conservative
+		// 		// and do the JNI call to protect best effort, only logging if it fails.
+		// 		// The risk of returning an error is that it breaks users on some Android
+		// 		// versions even when they're not using exit nodes. I'd rather the
+		// 		// relatively few number of exit node users file bug reports if Tailscale
+		// 		// doesn't work and then we can look for this log print.
+		// 		if err != nil || !ok {
+		// 			log.Printf("[unexpected] VpnService.protect(%d) = %v, %v", fd, ok, err)
+		// 		}
+		// 		return nil // even on error. see big TODO above.
+		// 	})
+		// })
+		a.service = s
+		return nil
+	})
+	return a.buildVPNConfigurationAndConnect()
+}
+
+func (a *androidApp) buildVPNConfigurationAndConnect() error {
+	if string(a.nebulaConfig) == "" {
+		return nil
+	}
+	fmt.Println(333333333)
+	fmt.Println(string(a.nebulaConfig))
+	// return nil
+	// if err := a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;I)V",
+	// 	jni.Value(act), jni.Value(requestPrepareVPN)); err != nil {
+	// 	return nil
+	// }
+
+	config := nc.NewC(logrus.StandardLogger())
+	if err := config.LoadString(string(a.nebulaConfig)); err != nil {
+		return err
+	}
+	pki := config.GetMap("pki", nil)
+	hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(pki["cert"].(string)))
+	if err != nil {
+		panic(err)
+	}
+	t := time.Now()
+	fmt.Println("#########3")
+	fmt.Println(t.String())
+	fmt.Println(hostCert.Details.NotBefore.String())
+	fmt.Println(hostCert.Details.NotAfter.String())
+	fmt.Println(t.Before(hostCert.Details.NotBefore))
+	fmt.Println(t.After(hostCert.Details.NotAfter))
+	fmt.Println("#########3")
+	// return nil
+	return jni.Do(a.jvm, func(env *jni.Env) error {
+		fmt.Println("---------")
+		cls := jni.GetObjectClass(env, a.service)
+		m := jni.GetMethodID(env, cls, "newBuilder", "()Landroid/net/VpnService$Builder;")
+		b, err := jni.CallObjectMethod(env, a.service, m)
+		if err != nil {
+			return fmt.Errorf("PCloudVPNService.newBuilder: %v", err)
+		}
+		fmt.Println("---------")
+		bcls := jni.GetObjectClass(env, b)
+		addAddress := jni.GetMethodID(env, bcls, "addAddress", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
+		addRoute := jni.GetMethodID(env, bcls, "addRoute", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
+		for _, ipNet := range hostCert.Details.Ips {
+			ip := ipNet.IP.String()
+			prefix, _ := ipNet.Mask.Size()
+			_, err := jni.CallObjectMethod(
+				env,
+				b,
+				addAddress,
+				jni.Value(jni.JavaString(env, ip)),
+				jni.Value(jni.Value(prefix)))
+			if err != nil {
+				return err
+			}
+			_, err = jni.CallObjectMethod(
+				env,
+				b,
+				addRoute,
+				jni.Value(jni.JavaString(env, ip)),
+				jni.Value(jni.Value(prefix)))
+		}
+		tun := config.GetMap("tun", nil)
+		setMtu := jni.GetMethodID(env, bcls, "setMtu", "(I)Landroid/net/VpnService$Builder;")
+		if _, err := jni.CallObjectMethod(env, b, setMtu, jni.Value(tun["mtu"].(int))); err != nil {
+			return err
+		}
+		establish := jni.GetMethodID(env, bcls, "establish", "()Landroid/os/ParcelFileDescriptor;")
+		parcelFD, err := jni.CallObjectMethod(env, b, establish)
+		if err != nil {
+			return err
+		}
+		parcelCls := jni.GetObjectClass(env, parcelFD)
+		detachFd := jni.GetMethodID(env, parcelCls, "detachFd", "()I")
+		tunFD, err := jni.CallIntMethod(env, parcelFD, detachFd)
+		if err != nil {
+			return fmt.Errorf("detachFd: %v", err)
+		}
+		fd := int(tunFD)
+		protect := jni.GetMethodID(env, cls, "protect", "(I)")
+		ok, err := jni.CallBooleanMethod(env, a.service, protect, jni.Value(fd))
+		if err != nil || !ok {
+			return fmt.Errorf("protect: %v %v", err, ok)
+		}
+		// getFd := jni.GetMethodID(env, parcelCls, "getFd", "()I")
+		// tunFD, err := jni.CallIntMethod(env, parcelFD, getFd)
+		// if err != nil {
+		// 	return err
+		// }
+
+		fmt.Println("===========")
+		ctrl, err := nebula.Main(config, false, "pcloud", logrus.StandardLogger(), &fd)
+		if err != nil {
+			return err
+		}
+		a.ctrl = ctrl
+		return nil
+	})
+
+}
diff --git a/core/client/cmd/pcloud/app.go b/core/client/cmd/pcloud/app.go
index a0cab6c..5020e68 100644
--- a/core/client/cmd/pcloud/app.go
+++ b/core/client/cmd/pcloud/app.go
@@ -3,6 +3,7 @@
 import "gioui.org/app"
 
 type App interface {
+	Capabilities() DeviceCapabilities
 	LaunchBarcodeScanner() error
 	OnView(app.ViewEvent) error
 	UpdateService(service interface{}) error
diff --git a/core/client/cmd/pcloud/app_android.go b/core/client/cmd/pcloud/app_android.go
index 3d18663..8feb2e7 100644
--- a/core/client/cmd/pcloud/app_android.go
+++ b/core/client/cmd/pcloud/app_android.go
@@ -28,6 +28,12 @@
 	}
 }
 
+func (a *androidApp) Capabilities() DeviceCapabilities {
+	return DeviceCapabilities{
+		HasCamera: true,
+	}
+}
+
 func (a *androidApp) CreateStorage() Storage {
 	return CreateStorage(a.jvm, a.appCtx)
 }
@@ -134,11 +140,11 @@
 }
 
 func (a *androidApp) Connect(config Config) error {
-	if config.Network == nil {
+	if config.Network.Config == nil {
 		return nil
 	}
 	nebulaConfig := nc.NewC(logrus.StandardLogger())
-	if err := nebulaConfig.LoadString(string(config.Network)); err != nil {
+	if err := nebulaConfig.LoadString(string(config.Network.Config)); err != nil {
 		return err
 	}
 	pki := nebulaConfig.GetMap("pki", nil)
diff --git a/core/client/cmd/pcloud/app_darwin.go b/core/client/cmd/pcloud/app_darwin.go
deleted file mode 100644
index 3e4bd04..0000000
--- a/core/client/cmd/pcloud/app_darwin.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package main
-
-import (
-	"errors"
-
-	"gioui.org/app"
-)
-
-type darwinApp struct {
-}
-
-func createApp() App {
-	return &darwinApp{}
-}
-
-func (a *darwinApp) LaunchBarcodeScanner() error {
-	return errors.New("no camera")
-}
-
-func (a *darwinApp) OnView(e app.ViewEvent) error {
-	return nil
-}
-
-func (a *darwinApp) Connect(config Config) error {
-	return nil
-}
-
-func (a *darwinApp) UpdateService(serv interface{}) error {
-	return nil
-}
-
-func (a *darwinApp) TriggerService() error {
-	return nil
-}
-
-func (a *darwinApp) CreateStorage() Storage {
-	return nil
-}
-
-func (a *darwinApp) GetHostname() (string, error) {
-	return "", nil
-}
diff --git a/core/client/cmd/pcloud/app_macos.go b/core/client/cmd/pcloud/app_macos.go
new file mode 100644
index 0000000..3a231d3
--- /dev/null
+++ b/core/client/cmd/pcloud/app_macos.go
@@ -0,0 +1,70 @@
+//go:build darwin && !ios
+// +build darwin,!ios
+
+package main
+
+import (
+	"errors"
+	"os"
+
+	"gioui.org/app"
+	"github.com/sirupsen/logrus"
+	"github.com/slackhq/nebula"
+	nc "github.com/slackhq/nebula/config"
+)
+
+type macosApp struct {
+	ctrl *nebula.Control
+}
+
+func createApp() App {
+	return &macosApp{}
+}
+
+func (a *macosApp) Capabilities() DeviceCapabilities {
+	return DeviceCapabilities{
+		HasCamera: false,
+	}
+}
+
+func (a *macosApp) LaunchBarcodeScanner() error {
+	return errors.New("no camera")
+}
+
+func (a *macosApp) OnView(e app.ViewEvent) error {
+	return nil
+}
+
+func (a *macosApp) Connect(config Config) error {
+	if config.Network.Config == nil {
+		return nil
+	}
+	nebulaConfig := nc.NewC(logrus.StandardLogger())
+	if err := nebulaConfig.LoadString(string(config.Network.Config)); err != nil {
+		return err
+	}
+	ctrl, err := nebula.Main(nebulaConfig, false, "pcloud", logrus.StandardLogger(), nil)
+	if err != nil {
+		return err
+	}
+	ctrl.Start()
+	a.ctrl = ctrl
+	return nil
+}
+
+func (a *macosApp) UpdateService(serv interface{}) error {
+	return nil
+}
+
+func (a *macosApp) TriggerService() error {
+	p.ConnectRequested(nil)
+	return nil
+}
+
+func (a *macosApp) CreateStorage() Storage {
+	return CreateStorage()
+}
+
+func (a *macosApp) GetHostname() (string, error) {
+	return os.Hostname()
+}
diff --git a/core/client/cmd/pcloud/callbacks_android.go b/core/client/cmd/pcloud/callbacks_android.go
index b7060aa..4664538 100644
--- a/core/client/cmd/pcloud/callbacks_android.go
+++ b/core/client/cmd/pcloud/callbacks_android.go
@@ -15,7 +15,7 @@
 func Java_me_lekva_pcloud_PCloudActivity_qrcodeScanned(env *C.JNIEnv, this C.jobject, contents C.jobject) {
 	jenv := (*jni.Env)(unsafe.Pointer(env))
 	code := jni.GoString(jenv, jni.String(contents))
-	p.InviteQRCodeScanned([]byte(code))
+	p.QRCodeScanned([]byte(code))
 }
 
 //export Java_me_lekva_pcloud_PCloudVPNService_connect
diff --git a/core/client/cmd/pcloud/client.go b/core/client/cmd/pcloud/client.go
index 78a31f1..f755281 100644
--- a/core/client/cmd/pcloud/client.go
+++ b/core/client/cmd/pcloud/client.go
@@ -2,10 +2,15 @@
 
 import (
 	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
 	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
 	"crypto/tls"
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"io"
 	"net/http"
 
@@ -16,7 +21,9 @@
 
 type VPNClient interface {
 	Sign(apiAddr string, message []byte) ([]byte, error)
-	Join(apiAddr, hostname string, message, signature []byte) ([]byte, error)
+	Join(apiAddr, hostname string, publicKey, privateKey []byte, message, signature []byte) ([]byte, error)
+	Approve(apiAddr, hostname, ipCidr string, encPublicKey, netPublicKey []byte) error
+	Get(apiAddr, hostname string, encPrivateKey *rsa.PrivateKey, netPrivateKey []byte) ([]byte, error)
 }
 
 type directVPNClient struct {
@@ -69,16 +76,12 @@
 	cfgYamlB64 string
 }
 
-func (c *directVPNClient) Join(apiAddr, hostname string, message, signature []byte) ([]byte, error) {
-	pubKey, privKey, err := x25519Keypair()
-	if err != nil {
-		return nil, err
-	}
+func (c *directVPNClient) Join(apiAddr, hostname string, publicKey, privateKey []byte, message, signature []byte) ([]byte, error) {
 	req := joinReq{
 		message,
 		signature,
 		hostname,
-		cert.MarshalX25519PublicKey(pubKey),
+		cert.MarshalX25519PublicKey(publicKey),
 		"111.0.0.13/24",
 	}
 	var data bytes.Buffer
@@ -111,7 +114,101 @@
 	if pki, ok = cfgMap["pki"].(map[string]interface{}); !ok {
 		panic("Must not reach")
 	}
-	pki["key"] = string(cert.MarshalX25519PrivateKey(privKey))
+	pki["key"] = string(cert.MarshalX25519PrivateKey(privateKey))
+	return yaml.Marshal(cfgMap)
+}
+
+type approveReq struct {
+	EncPublicKey []byte `json:"enc_public_key"`
+	Name         string `json:"name"`
+	NetPublicKey []byte `json:"net_public_key"`
+	IPCidr       string `json:"ip_cidr"`
+}
+
+func (c *directVPNClient) Approve(apiAddr, hostname, ipCidr string, encPublicKey, netPublicKey []byte) error {
+	req := approveReq{
+		encPublicKey,
+		hostname,
+		cert.MarshalX25519PublicKey(netPublicKey),
+		ipCidr,
+	}
+	var data bytes.Buffer
+	if err := json.NewEncoder(&data).Encode(req); err != nil {
+		return err
+	}
+	client := &http.Client{
+		// TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+	_, err := client.Post(apiAddr+"/api/approve", "application/json", &data)
+	return err
+}
+
+type getResp struct {
+	Key   []byte `json:"key"`
+	Nonce []byte `json:"nonce"`
+	Data  []byte `json:"data"`
+}
+
+func (c *directVPNClient) Get(apiAddr, hostname string, encPrivateKey *rsa.PrivateKey, netPrivateKey []byte) ([]byte, error) {
+	client := &http.Client{
+		// TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+	r, err := client.Get(apiAddr + "/api/get/" + hostname)
+	if err != nil {
+		fmt.Println(err.Error())
+		return nil, err
+	}
+	var resp getResp
+	if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
+		fmt.Println(err.Error())
+		return nil, err
+	}
+	// TODO(giolekva): encrypt key and nonce together
+	key, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, encPrivateKey, resp.Key, []byte(""))
+	if err != nil {
+		fmt.Println(err.Error())
+		return nil, err
+	}
+	nonce, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, encPrivateKey, resp.Nonce, []byte(""))
+	if err != nil {
+		fmt.Println(1123123)
+		fmt.Println(err.Error())
+		return nil, err
+	}
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		fmt.Println(err.Error())
+		return nil, err
+	}
+	aesgcm, err := cipher.NewGCM(block)
+	if err != nil {
+		fmt.Println(err.Error())
+		return nil, err
+	}
+
+	cfgYaml, err := aesgcm.Open(nil, nonce, resp.Data, nil)
+	if err != nil {
+		fmt.Println(22222)
+		fmt.Println(err.Error())
+		return nil, err
+	}
+	var cfgMap map[string]interface{}
+	if err := yaml.Unmarshal(cfgYaml, &cfgMap); err != nil {
+		fmt.Println(err.Error())
+		return nil, err
+	}
+	var pki map[string]interface{}
+	var ok bool
+	if pki, ok = cfgMap["pki"].(map[string]interface{}); !ok {
+		panic("Must not reach")
+	}
+	pki["key"] = string(cert.MarshalX25519PrivateKey(netPrivateKey))
 	return yaml.Marshal(cfgMap)
 }
 
diff --git a/core/client/cmd/pcloud/config.go b/core/client/cmd/pcloud/config.go
index 0cf551d..6a12ada 100644
--- a/core/client/cmd/pcloud/config.go
+++ b/core/client/cmd/pcloud/config.go
@@ -1,6 +1,14 @@
 package main
 
 type Config struct {
-	ApiAddr string `json:"api_addr"`
-	Network []byte `json:"network"`
+	ApiAddr string `json:"api_addr,omitempty"`
+	Enc     struct {
+		PublicKey  []byte `json:"public_key,omitempty"`
+		PrivateKey []byte `json:"private_key,omitempty"`
+	} `json:"encyption,omitempty"`
+	Network struct {
+		PublicKey  []byte `json:"public_key,omitempty"`
+		PrivateKey []byte `json:"private_key,omitempty"`
+		Config     []byte `json:"network,omitempty"`
+	} `json:"network,omitempty"`
 }
diff --git a/core/client/cmd/pcloud/main.go b/core/client/cmd/pcloud/main.go
index 287407b..38c55ff 100644
--- a/core/client/cmd/pcloud/main.go
+++ b/core/client/cmd/pcloud/main.go
@@ -2,7 +2,11 @@
 
 import (
 	"bytes"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
 	"encoding/json"
+	"errors"
 	"flag"
 	"fmt"
 	"image"
@@ -24,14 +28,23 @@
 
 var p *processor
 
+type ScanQRFor int
+
+const (
+	ScanQRForJoin    ScanQRFor = 0
+	ScanQRForApprove           = 1
+)
+
 type processor struct {
 	vc  VPNClient
 	app App
 	st  Storage
 	ui  *UI
 
-	inviteQrCh        chan image.Image
-	inviteQrScannedCh chan []byte
+	scanQRFor   ScanQRFor
+	inviteQrCh  chan image.Image
+	qrScannedCh chan []byte
+	joinQrCh    chan image.Image
 
 	onConnectCh    chan interface{}
 	onDisconnectCh chan interface{}
@@ -43,20 +56,21 @@
 	th := material.NewTheme(gofont.Collection())
 	app := createApp()
 	return &processor{
-		vc:                NewDirectVPNClient(*vpnApiAddr),
-		app:               app,
-		st:                app.CreateStorage(),
-		ui:                NewUI(th),
-		inviteQrCh:        make(chan image.Image, 1),
-		inviteQrScannedCh: make(chan []byte, 1),
-		onConnectCh:       make(chan interface{}, 1),
-		onDisconnectCh:    make(chan interface{}, 1),
-		onConfigCh:        make(chan struct{}, 1),
+		vc:             NewDirectVPNClient(*vpnApiAddr),
+		app:            app,
+		st:             app.CreateStorage(),
+		ui:             NewUI(th, app.Capabilities()),
+		inviteQrCh:     make(chan image.Image, 1),
+		qrScannedCh:    make(chan []byte, 1),
+		joinQrCh:       make(chan image.Image, 1),
+		onConnectCh:    make(chan interface{}, 1),
+		onDisconnectCh: make(chan interface{}, 1),
+		onConfigCh:     make(chan struct{}, 1),
 	}
 }
 
-func (p *processor) InviteQRCodeScanned(code []byte) {
-	p.inviteQrScannedCh <- code
+func (p *processor) QRCodeScanned(code []byte) {
+	p.qrScannedCh <- code
 }
 
 func (p *processor) ConnectRequested(service interface{}) {
@@ -70,7 +84,31 @@
 	p.onDisconnectCh <- service
 }
 
+func (p *processor) generatePublicPrivateKey() error {
+	config, err := p.st.Get()
+	if err != nil {
+		return err
+	}
+	if config.Enc.PrivateKey != nil {
+		return nil
+	}
+	privKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		panic(err)
+	}
+	config.Enc.PrivateKey = x509.MarshalPKCS1PrivateKey(privKey)
+	config.Enc.PublicKey = x509.MarshalPKCS1PublicKey(&privKey.PublicKey)
+	config.Network.PublicKey, config.Network.PrivateKey, err = x25519Keypair()
+	if err != nil {
+		return err
+	}
+	return p.st.Store(config)
+}
+
 func (p *processor) run() error {
+	if err := p.generatePublicPrivateKey(); err != nil {
+		return err
+	}
 	w := app.NewWindow(
 		app.Size(unit.Px(1500), unit.Px(1500)),
 		app.Title("PCloud"),
@@ -89,7 +127,7 @@
 				if config, err := p.st.Get(); err != nil {
 					return err
 				} else {
-					if config.Network != nil {
+					if config.Network.Config != nil {
 						p.onConfigCh <- struct{}{}
 					}
 				}
@@ -111,10 +149,57 @@
 		case img := <-p.inviteQrCh:
 			p.ui.InviteQRGenerated(img)
 			w.Invalidate()
-		case code := <-p.inviteQrScannedCh:
-			if err := p.JoinAndGetNetworkConfig(code); err != nil {
-				return err
+		case code := <-p.qrScannedCh:
+			switch p.scanQRFor {
+			case ScanQRForJoin:
+				if err := p.JoinAndGetNetworkConfig(code); err != nil {
+					return err
+				}
+			case ScanQRForApprove:
+				if err := p.ApproveOther(code); err != nil {
+					return err
+				}
+			default:
+				return errors.New("Must not reach!")
 			}
+		case img := <-p.joinQrCh:
+			p.ui.JoinQRGenerated(img)
+			w.Invalidate()
+			go func() {
+				cnt := 0
+				for {
+					fmt.Println(cnt)
+					cnt++
+					if cnt > 5 {
+						break
+					}
+					time.Sleep(time.Second)
+					config, err := p.st.Get()
+					if err != nil {
+						continue
+					}
+					privKey, err := x509.ParsePKCS1PrivateKey(config.Enc.PrivateKey)
+					if err != nil {
+						continue
+					}
+					hostname, err := p.app.GetHostname()
+					if err != nil {
+						continue
+					}
+					hostname = sanitizeHostname(hostname)
+					network, err := p.vc.Get("https://vpn.lekva.me", hostname, privKey, config.Network.PrivateKey)
+					if err != nil {
+						continue
+					}
+					config.ApiAddr = "https://vpn.lekva.me"
+					config.Network.Config = network
+					if err := p.st.Store(config); err != nil {
+						continue
+					}
+					p.onConfigCh <- struct{}{}
+					break
+				}
+			}()
 		case <-p.onConfigCh:
 			if err := p.app.TriggerService(); err != nil {
 				return err
@@ -147,8 +232,25 @@
 					panic(err)
 				}
 			}()
+		case EventGetJoinQRCode:
+			go func() {
+				if img, err := p.generateJoinQRCode(); err == nil {
+					p.joinQrCh <- img
+				} else {
+					// TODO(giolekva): do not panic
+					panic(err)
+				}
+			}()
+		case EventApproveOther:
+			p.scanQRFor = ScanQRForApprove
+			if err := p.app.LaunchBarcodeScanner(); err != nil {
+				return err
+			}
 		case EventScanBarcode:
-			return p.app.LaunchBarcodeScanner()
+			p.scanQRFor = ScanQRForJoin
+			if err := p.app.LaunchBarcodeScanner(); err != nil {
+				return err
+			}
 		default:
 			return fmt.Errorf("Unhandled event: %#v", e)
 		}
@@ -156,12 +258,19 @@
 	return nil
 }
 
-type qrCodeData struct {
+type inviteQrCodeData struct {
 	VPNApiAddr string `json:"vpn_api_addr"`
 	Message    []byte `json:"message"`
 	Signature  []byte `json:"signature"`
 }
 
+type joinQrCodeData struct {
+	EncPublicKey []byte `json:"enc_public_key"`
+	Name         string `json:"name"`
+	NetPublicKey []byte `json:"net_public_key"`
+	IPCidr       string `json:"ip_cidr"`
+}
+
 func (p *processor) generateInviteQRCode() (image.Image, error) {
 	config, err := p.st.Get()
 	if err != nil {
@@ -172,7 +281,7 @@
 	if err != nil {
 		return nil, err
 	}
-	c := qrCodeData{
+	c := inviteQrCodeData{
 		config.ApiAddr,
 		message,
 		signature,
@@ -192,8 +301,43 @@
 	return img, nil
 }
 
+func (p *processor) generateJoinQRCode() (image.Image, error) {
+	config, err := p.st.Get()
+	if err != nil {
+		return nil, err
+	}
+	hostname, err := p.app.GetHostname()
+	if err != nil {
+		return nil, err
+	}
+	hostname = sanitizeHostname(hostname)
+	c := joinQrCodeData{
+		EncPublicKey: config.Enc.PublicKey,
+		Name:         hostname,
+		NetPublicKey: config.Network.PublicKey,
+		IPCidr:       "111.0.0.14/24",
+	}
+	var data bytes.Buffer
+	if err := json.NewEncoder(&data).Encode(c); err != nil {
+		return nil, err
+	}
+	qr, err := qrcode.Encode(data.String(), qrcode.Medium, 1024)
+	if err != nil {
+		return nil, err
+	}
+	img, err := png.Decode(bytes.NewReader(qr))
+	if err != nil {
+		return nil, err
+	}
+	return img, nil
+}
+
 func (p *processor) JoinAndGetNetworkConfig(code []byte) error {
-	var invite qrCodeData
+	config, err := p.st.Get()
+	if err != nil {
+		return err
+	}
+	var invite inviteQrCodeData
 	if err := json.NewDecoder(bytes.NewReader(code)).Decode(&invite); err != nil {
 		return err
 	}
@@ -201,19 +345,39 @@
 	if err != nil {
 		return err
 	}
-	hostname = strings.ToLower(strings.ReplaceAll(hostname, " ", "-"))
-	fmt.Printf("------ %s\n", hostname)
-	network, err := p.vc.Join(invite.VPNApiAddr, hostname, invite.Message, invite.Signature)
+	hostname = sanitizeHostname(hostname)
+	network, err := p.vc.Join(invite.VPNApiAddr, hostname, config.Network.PublicKey, config.Network.PrivateKey, invite.Message, invite.Signature)
 	if err != nil {
 		return err
 	}
-	if err := p.st.Store(Config{invite.VPNApiAddr, network}); err != nil {
+	config.ApiAddr = invite.VPNApiAddr
+	config.Network.Config = network
+	if err := p.st.Store(config); err != nil {
 		return err
 	}
 	p.onConfigCh <- struct{}{}
 	return nil
 }
 
+func (p *processor) ApproveOther(code []byte) error {
+	config, err := p.st.Get()
+	if err != nil {
+		return err
+	}
+	var approve joinQrCodeData
+	if err := json.NewDecoder(bytes.NewReader(code)).Decode(&approve); err != nil {
+		return err
+	}
+	return p.vc.Approve(config.ApiAddr, approve.Name, approve.IPCidr, approve.EncPublicKey, approve.NetPublicKey)
+}
+
+func sanitizeHostname(hostname string) string {
+	return strings.ToLower(
+		strings.ReplaceAll(
+			strings.ReplaceAll(hostname, " ", "-"),
+			".", "-"))
+}
+
 func main() {
 	flag.Parse()
 	p = newProcessor()
diff --git a/core/client/cmd/pcloud/storage_darwin.go b/core/client/cmd/pcloud/storage_darwin.go
index 8fabc98..d45d34a 100644
--- a/core/client/cmd/pcloud/storage_darwin.go
+++ b/core/client/cmd/pcloud/storage_darwin.go
@@ -1,6 +1,11 @@
 package main
 
-import "errors"
+import (
+	"bytes"
+	"encoding/json"
+
+	"github.com/keybase/go-keychain"
+)
 
 type darwinStorage struct {
 }
@@ -10,9 +15,43 @@
 }
 
 func (s *darwinStorage) Get() (Config, error) {
-	return nil, errors.New("Not implemented")
+	q := configItem()
+	q.SetMatchLimit(keychain.MatchLimitOne)
+	q.SetReturnData(true)
+	results, err := keychain.QueryItem(q)
+	if err != nil {
+		return Config{}, err
+	} else if len(results) != 1 {
+		return Config{}, nil
+	}
+	var config Config
+	err = json.NewDecoder(bytes.NewReader(results[0].Data)).Decode(&config)
+	return config, err
 }
 
 func (s *darwinStorage) Store(config Config) error {
-	return errors.New("Not implemented")
+	var data bytes.Buffer
+	if err := json.NewEncoder(&data).Encode(config); err != nil {
+		return err
+	}
+	q := configItem()
+	item := configItem()
+	item.SetData(data.Bytes())
+	if err := keychain.UpdateItem(q, item); err == keychain.ErrorItemNotFound {
+		return keychain.AddItem(item)
+	} else {
+		return err
+	}
+}
+
+func configItem() keychain.Item {
+	item := keychain.NewItem()
+	item.SetSecClass(keychain.SecClassGenericPassword)
+	item.SetService("pcloud")
+	item.SetAccount("pcloud")
+	item.SetLabel("pcloud-config")
+	item.SetAccessGroup("me.lekva.pcloud")
+	item.SetSynchronizable(keychain.SynchronizableNo)
+	item.SetAccessible(keychain.AccessibleWhenUnlocked)
+	return item
 }
diff --git a/core/client/cmd/pcloud/ui.go b/core/client/cmd/pcloud/ui.go
index 78afa76..8f35a3b 100644
--- a/core/client/cmd/pcloud/ui.go
+++ b/core/client/cmd/pcloud/ui.go
@@ -17,33 +17,51 @@
 	D = layout.Dimensions
 )
 
+type DeviceCapabilities struct {
+	HasCamera bool
+}
+
 type UI struct {
-	th     *material.Theme
+	th  *material.Theme
+	cap DeviceCapabilities
+
 	invite struct {
 		open widget.Clickable
 		show bool
 		qr   image.Image
 	}
 
+	approve struct {
+		open widget.Clickable
+		show bool
+	}
+
 	join struct {
-		open   widget.Clickable
-		show   bool
-		qrcode string
+		open widget.Clickable
+		show bool
+		qr   image.Image
 	}
 }
 
-func NewUI(th *material.Theme) *UI {
-	return &UI{th: th}
+func NewUI(th *material.Theme, cap DeviceCapabilities) *UI {
+	return &UI{th: th, cap: cap}
 }
 
 func (ui *UI) InviteQRGenerated(img image.Image) {
 	ui.invite.qr = img
 }
 
+func (ui *UI) JoinQRGenerated(img image.Image) {
+	ui.join.qr = img
+}
+
 func (ui *UI) OnBack() bool {
 	if ui.invite.show {
 		ui.invite.show = false
 		return true
+	} else if ui.approve.show {
+		ui.approve.show = false
+		return true
 	} else if ui.join.show {
 		ui.join.show = false
 		return true
@@ -54,14 +72,22 @@
 func (ui *UI) Layout(gtx C) []UIEvent {
 	var events []UIEvent
 	if ui.invite.open.Clicked() {
-		ui.join.show = false
 		ui.invite.show = true
+		ui.approve.show = false
+		ui.join.show = false
 		ui.invite.qr = nil
 		events = append(events, EventGetInviteQRCode{})
+	} else if ui.approve.open.Clicked() {
+		events = append(events, EventApproveOther{})
 	} else if ui.join.open.Clicked() {
-		// ui.invite.show = false
-		// ui.join.show = true
-		events = append(events, EventScanBarcode{})
+		if ui.cap.HasCamera {
+			events = append(events, EventScanBarcode{})
+		} else {
+			ui.invite.show = false
+			ui.approve.show = false
+			ui.join.show = true
+			events = append(events, EventGetJoinQRCode{})
+		}
 	}
 	if ui.invite.show {
 		ui.layout(gtx, ui.layoutInvite)
@@ -96,13 +122,18 @@
 }
 
 func (ui *UI) layoutActions(gtx C) D {
-	return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceAround, WeightSum: 2.2}.Layout(gtx,
+	return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceAround, WeightSum: 3.2}.Layout(gtx,
 		layout.Flexed(1, func(gtx C) D {
 			b := material.Button(ui.th, &ui.invite.open, "Invite")
 			b.CornerRadius = unit.Px(20)
 			return b.Layout(gtx)
 		}),
 		layout.Flexed(1, func(gtx C) D {
+			b := material.Button(ui.th, &ui.approve.open, "Approve")
+			b.CornerRadius = unit.Px(20)
+			return b.Layout(gtx)
+		}),
+		layout.Flexed(1, func(gtx C) D {
 			b := material.Button(ui.th, &ui.join.open, "Join")
 			b.CornerRadius = unit.Px(20)
 			return b.Layout(gtx)
@@ -110,24 +141,26 @@
 	)
 }
 
-func (ui *UI) layoutInvite(gtx C) D {
-	if ui.invite.qr == nil {
+func layoutQR(gtx C, qr image.Image) D {
+	if qr == nil {
 		return ColorBox(gtx, gtx.Constraints.Max, color.NRGBA{})
 	}
-	d := ui.invite.qr.Bounds().Max.Sub(ui.invite.qr.Bounds().Min)
+	d := qr.Bounds().Max.Sub(qr.Bounds().Min)
 	return layout.Inset{
 		Left: unit.Px(0.5 * float32(gtx.Constraints.Max.X-d.X)),
 		Top:  unit.Px(0.5 * float32(gtx.Constraints.Max.Y-d.Y)),
 	}.Layout(gtx, func(gtx C) D {
-		paint.NewImageOp(ui.invite.qr).Add(gtx.Ops)
+		paint.NewImageOp(qr).Add(gtx.Ops)
 		paint.PaintOp{}.Add(gtx.Ops)
 		return D{Size: gtx.Constraints.Max}
 	})
+
+}
+
+func (ui *UI) layoutInvite(gtx C) D {
+	return layoutQR(gtx, ui.invite.qr)
 }
 
 func (ui *UI) layoutJoin(gtx C) D {
-	if ui.join.qrcode == "" {
-		return ColorBox(gtx, gtx.Constraints.Min, color.NRGBA{R: 255, A: 255})
-	}
-	return ColorBox(gtx, gtx.Constraints.Min, color.NRGBA{R: 255, G: 255, B: 255, A: 255})
+	return layoutQR(gtx, ui.join.qr)
 }
diff --git a/core/client/cmd/pcloud/ui_events.go b/core/client/cmd/pcloud/ui_events.go
index 581c5d4..e59be5b 100644
--- a/core/client/cmd/pcloud/ui_events.go
+++ b/core/client/cmd/pcloud/ui_events.go
@@ -5,3 +5,7 @@
 type EventScanBarcode struct{}
 
 type EventGetInviteQRCode struct{}
+
+type EventGetJoinQRCode struct{}
+
+type EventApproveOther struct{}
diff --git a/core/client/go.mod b/core/client/go.mod
index c14150b..f419e35 100644
--- a/core/client/go.mod
+++ b/core/client/go.mod
@@ -5,11 +5,11 @@
 require (
 	gioui.org v0.0.0-20211201162354-9a5298914282
 	gioui.org/cmd v0.0.0-20211214104907-f5c9d2725c7b
+	github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621
 	github.com/sirupsen/logrus v1.8.1
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/slackhq/nebula v1.5.1
 	golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
-	golang.org/x/exp v0.0.0-20210722180016-6781d3edade3
 	golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 // indirect
 	sigs.k8s.io/yaml v1.1.0
 )
diff --git a/core/client/go.sum b/core/client/go.sum
index f062a62..2a36dad 100644
--- a/core/client/go.sum
+++ b/core/client/go.sum
@@ -253,6 +253,9 @@
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
+github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621 h1:aMQ7pA4f06yOVXSulygyGvy4xA94fyzjUGs0iqQdMOI=
+github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621/go.mod h1:enrU/ug069Om7vWxuFE6nikLI2BZNwevMiGSo43Kt5w=
+github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -399,6 +402,7 @@
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -448,6 +452,7 @@
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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