VPN client: start Nebula controller locally
diff --git a/charts/vpn-mesh-config/templates/api.yaml b/charts/vpn-mesh-config/templates/api.yaml
index 9da26d7..a747f4f 100644
--- a/charts/vpn-mesh-config/templates/api.yaml
+++ b/charts/vpn-mesh-config/templates/api.yaml
@@ -27,6 +27,8 @@
metadata:
labels:
app: nebula-api
+ annotations:
+ checksum/config: {{ include (print $.Template.BasePath "/lighthouse-config-template.yaml") . | sha256sum }}
spec:
volumes:
- name: config
diff --git a/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml b/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml
index cdf18df..016ecb0 100644
--- a/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml
+++ b/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml
@@ -14,6 +14,8 @@
lighthouse:
am_lighthouse: false
interval: 60
+ hosts:
+ - "{{ .Values.lighthouse.internalIP }}"
listen:
host: "[::]"
port: 4242
@@ -28,7 +30,7 @@
tx_queue: 500
mtu: 1300
logging:
- level: info
+ level: debug
format: text
firewall:
conntrack:
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudActivity.java b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudActivity.java
index ec1c76d..330e8cd 100644
--- a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudActivity.java
+++ b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudActivity.java
@@ -1,6 +1,8 @@
package me.lekva.pcloud;
+import android.content.Intent;
import android.content.res.Configuration;
+import android.net.VpnService;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
@@ -13,6 +15,8 @@
import org.gioui.GioView;
public class PCloudActivity extends AppCompatActivity {
+ private static final int VPN_START_CODE = 0x10;
+
private GioView view;
private final ActivityResultLauncher<ScanOptions> barcodeLauncher = registerForActivityResult(new ScanContract(),
@@ -77,5 +81,19 @@
return null;
}
+ public String startVpn(String ipCidr) {
+ Intent intent = VpnService.prepare(this);
+ if (intent != null) {
+ System.out.println("#### STARTVPN");
+ intent.setAction(PCloudVPNService.ACTION_CONNECT);
+ startActivityForResult(intent, VPN_START_CODE);
+ } else {
+ intent = new Intent(this, PCloudVPNService.class);
+ startService(intent);
+ }
+
+ return null;
+ }
+
private native void qrcodeScanned(String contents);
}
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudVPNService.java b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudVPNService.java
index 43ce99f..1566ed1 100644
--- a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudVPNService.java
+++ b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudVPNService.java
@@ -1,10 +1,13 @@
package me.lekva.pcloud;
+import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.net.VpnService;
+import android.os.Build;
import android.os.Handler;
import android.os.Message;
+import android.system.OsConstants;
import androidx.annotation.NonNull;
@@ -24,7 +27,7 @@
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent != null && intent.getAction().equals(ACTION_DISCONNECT)) {
+ if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) {
stopVpn();
return Service.START_NOT_STICKY;
} else {
@@ -40,6 +43,7 @@
private void startVpn() {
System.out.println("--- START");
+ connect();
}
private void stopVpn() {
@@ -52,4 +56,23 @@
System.out.println(getString(message.what));
return true;
}
+
+ private PendingIntent configIntent() {
+ return PendingIntent.getActivity(this, 0, new Intent(this, PCloudActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ public VpnService.Builder newBuilder() {
+ VpnService.Builder builder = new VpnService.Builder()
+ .setConfigureIntent(configIntent())
+ .allowFamily(OsConstants.AF_INET)
+ .allowFamily(OsConstants.AF_INET6);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ builder.setMetered(false); // Inherit the metered status from the underlying networks.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
+ builder.setUnderlyingNetworks(null); // Use all available networks.
+ return builder;
+ }
+
+ private native void connect();
+ private native void disconnect();
}
diff --git a/core/client/cmd/pcloud/app.go b/core/client/cmd/pcloud/app.go
index 5c49644..0f0dc47 100644
--- a/core/client/cmd/pcloud/app.go
+++ b/core/client/cmd/pcloud/app.go
@@ -5,4 +5,6 @@
type App interface {
LaunchBarcodeScanner() error
OnView(app.ViewEvent) error
+ StartVPN(config []byte) error
+ Connect(service interface{}) error
}
diff --git a/core/client/cmd/pcloud/app_android.go b/core/client/cmd/pcloud/app_android.go
index 8dd7c73..c66b401 100644
--- a/core/client/cmd/pcloud/app_android.go
+++ b/core/client/cmd/pcloud/app_android.go
@@ -2,16 +2,25 @@
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
- activity jni.Object
+ jvm *jni.JVM
+ appCtx jni.Object // PCloudApp
+ activity jni.Object // PCloudActivity
+ service jni.Object // PCloudVPNService
+ nebulaConfig []byte
+ ctrl *nebula.Control
}
func createApp() App {
@@ -73,3 +82,161 @@
}
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)Z")
+ // 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
+ }
+ fd := int(tunFD)
+ fmt.Println("===========")
+ ctrl, err := nebula.Main(config, false, "pcloud", logrus.StandardLogger(), &fd)
+ if err != nil {
+ return err
+ }
+ ctrl.Start()
+ a.ctrl = ctrl
+ return nil
+ })
+
+}
diff --git a/core/client/cmd/pcloud/app_darwin.go b/core/client/cmd/pcloud/app_darwin.go
index a60ea35..c7b670f 100644
--- a/core/client/cmd/pcloud/app_darwin.go
+++ b/core/client/cmd/pcloud/app_darwin.go
@@ -20,3 +20,11 @@
func (a *darwinApp) OnView(e app.ViewEvent) error {
return nil
}
+
+func (a *darwinApp) StartVPN(config []byte) error {
+ return nil
+}
+
+func (a *darwinApp) Connect(serv interface{}) error {
+ return nil
+}
diff --git a/core/client/cmd/pcloud/callbacks_android.go b/core/client/cmd/pcloud/callbacks_android.go
index df399f9..b7060aa 100644
--- a/core/client/cmd/pcloud/callbacks_android.go
+++ b/core/client/cmd/pcloud/callbacks_android.go
@@ -17,3 +17,15 @@
code := jni.GoString(jenv, jni.String(contents))
p.InviteQRCodeScanned([]byte(code))
}
+
+//export Java_me_lekva_pcloud_PCloudVPNService_connect
+func Java_me_lekva_pcloud_PCloudVPNService_connect(env *C.JNIEnv, this C.jobject) {
+ jenv := (*jni.Env)(unsafe.Pointer(env))
+ p.ConnectRequested(jni.NewGlobalRef(jenv, jni.Object(this)))
+}
+
+//export Java_me_lekva_pcloud_PCloudVPNService_disconnect
+func Java_me_lekva_pcloud_PCloudVPNService_disconnect(env *C.JNIEnv, this C.jobject) {
+ jenv := (*jni.Env)(unsafe.Pointer(env))
+ p.DisconnectRequested(jni.NewGlobalRef(jenv, jni.Object(this)))
+}
diff --git a/core/client/cmd/pcloud/main.go b/core/client/cmd/pcloud/main.go
index 1d70e03..1e43fa5 100644
--- a/core/client/cmd/pcloud/main.go
+++ b/core/client/cmd/pcloud/main.go
@@ -7,6 +7,7 @@
"fmt"
"image"
"image/png"
+ "time"
"gioui.org/app"
"gioui.org/io/system"
@@ -27,6 +28,9 @@
inviteQrCh chan image.Image
inviteQrScannedCh chan []byte
+
+ onConnectCh chan interface{}
+ onDisconnectCh chan interface{}
}
func newProcessor() *processor {
@@ -36,6 +40,8 @@
ui: NewUI(),
inviteQrCh: make(chan image.Image, 1),
inviteQrScannedCh: make(chan []byte, 1),
+ onConnectCh: make(chan interface{}, 1),
+ onDisconnectCh: make(chan interface{}, 1),
}
}
@@ -43,6 +49,17 @@
p.inviteQrScannedCh <- code
}
+func (p *processor) ConnectRequested(service interface{}) {
+ go func() {
+ time.Sleep(1 * time.Second)
+ p.onConnectCh <- service
+ }()
+}
+
+func (p *processor) DisconnectRequested(service interface{}) {
+ p.onDisconnectCh <- service
+}
+
func (p *processor) run() error {
w := app.NewWindow(
app.Size(unit.Px(1500), unit.Px(1500)),
@@ -78,9 +95,17 @@
p.ui.InviteQRGenerated(img)
w.Invalidate()
case code := <-p.inviteQrScannedCh:
- go func() {
- p.JoinNetworkAndConnect(code)
- }()
+ // go func() {
+ fmt.Println("00000000")
+ p.JoinNetworkAndConnect(code)
+ fmt.Println("00000000")
+ // }()
+ case s := <-p.onConnectCh:
+ fmt.Println("--- 1111111111")
+ if err := p.app.Connect(s); err != nil {
+ panic(err)
+ }
+ fmt.Println("--- 1111111111")
}
}
return nil
@@ -148,8 +173,9 @@
if err != nil {
panic(err)
}
- fmt.Printf("-- VPN CONFIG %s\n", string(config))
-
+ if err := p.app.StartVPN(config); err != nil {
+ panic(err)
+ }
}
func main() {
diff --git a/core/client/go.mod b/core/client/go.mod
index 112a042..76a8d55 100644
--- a/core/client/go.mod
+++ b/core/client/go.mod
@@ -5,6 +5,7 @@
require (
gioui.org v0.0.0-20211201162354-9a5298914282
gioui.org/cmd v0.0.0-20211214104907-f5c9d2725c7b
+ 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