client-android: store network config as app prefs
diff --git a/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml b/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml
index 016ecb0..f76f526 100644
--- a/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml
+++ b/charts/vpn-mesh-config/templates/lighthouse-config-template.yaml
@@ -28,7 +28,7 @@
drop_local_broadcast: false
drop_multicast: false
tx_queue: 500
- mtu: 1300
+ mtu: 576
logging:
level: debug
format: text
diff --git a/charts/vpn-mesh-config/templates/lighthouse-service.yaml b/charts/vpn-mesh-config/templates/lighthouse-service.yaml
index 267d0f5..b23d99d 100644
--- a/charts/vpn-mesh-config/templates/lighthouse-service.yaml
+++ b/charts/vpn-mesh-config/templates/lighthouse-service.yaml
@@ -5,6 +5,7 @@
namespace: {{ .Release.Namespace }}
spec:
type: LoadBalancer
+ externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-private
diff --git a/core/client/android/app/build.gradle b/core/client/android/app/build.gradle
index 9102497..6a8c04f 100644
--- a/core/client/android/app/build.gradle
+++ b/core/client/android/app/build.gradle
@@ -47,4 +47,5 @@
implementation 'androidx.camera:camera-core:1.1.0-alpha11'
implementation 'androidx.camera:camera-lifecycle:1.1.0-alpha11'
implementation 'androidx.camera:camera-view:1.0.0-alpha31'
+ implementation "androidx.security:security-crypto:1.1.0-alpha03"
}
\ No newline at end of file
diff --git a/core/client/android/app/src/main/AndroidManifest.xml b/core/client/android/app/src/main/AndroidManifest.xml
index a75444d..ccdff2c 100644
--- a/core/client/android/app/src/main/AndroidManifest.xml
+++ b/core/client/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".PCloudApp"
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 330e8cd..5e7dedd 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
@@ -81,18 +81,15 @@
return null;
}
- public String startVpn(String ipCidr) {
+ public void startVpn() {
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/PCloudApp.java b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudApp.java
index 1e2b64a..b7647db 100644
--- a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudApp.java
+++ b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudApp.java
@@ -1,13 +1,66 @@
package me.lekva.pcloud;
import android.app.Application;
+import android.app.NotificationChannel;
+import android.content.SharedPreferences;
+import android.os.Build;
+
+import androidx.core.app.NotificationManagerCompat;
+import androidx.security.crypto.EncryptedSharedPreferences;
+import androidx.security.crypto.MasterKey;
import org.gioui.Gio;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
public class PCloudApp extends Application {
+ static final String STATUS_CHANNEL_ID = "pcloud-status";
+ static final int STATUS_NOTIFICATION_ID = 1;
+
+ static final String NOTIFY_CHANNEL_ID = "pcloud-notify";
+ static final int NOTIFY_NOTIFICATION_ID = 2;
+
@Override
public void onCreate() {
super.onCreate();
Gio.init(this);
+
+ createNotificationChannel(NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT);
+ createNotificationChannel(STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW);
+ }
+
+ private void createNotificationChannel(String id, String name, int importance) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return;
+ }
+ NotificationChannel channel = new NotificationChannel(id, name, importance);
+ NotificationManagerCompat nm = NotificationManagerCompat.from(this);
+ nm.createNotificationChannel(channel);
+ }
+
+ // encryptToPref a byte array of data using the Jetpack Security
+ // library and writes it to a global encrypted preference store.
+ public void encryptToPref(String prefKey, String plaintext) throws IOException, GeneralSecurityException {
+ getEncryptedPrefs().edit().putString(prefKey, plaintext).commit();
+ }
+
+ // decryptFromPref decrypts a encrypted preference using the Jetpack Security
+ // library and returns the plaintext.
+ public String decryptFromPref(String prefKey) throws IOException, GeneralSecurityException {
+ return getEncryptedPrefs().getString(prefKey, null);
+ }
+
+ private SharedPreferences getEncryptedPrefs() throws IOException, GeneralSecurityException {
+ MasterKey key = new MasterKey.Builder(this)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build();
+ return EncryptedSharedPreferences.create(
+ this,
+ "secret_shared_prefs",
+ key,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ );
}
}
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 1566ed1..91a4a38 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
@@ -10,11 +10,15 @@
import android.system.OsConstants;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
public class PCloudVPNService extends VpnService implements Handler.Callback {
public static final String ACTION_CONNECT = "CONNECT";
public static final String ACTION_DISCONNECT = "DISCONNECT";
+ private PendingIntent configureIntent;
+
private boolean running = false;
private Handler handler = null;
@@ -23,6 +27,9 @@
if (handler == null) {
handler = new Handler(this);
}
+
+ configureIntent = PendingIntent.getActivity(this, 0, new Intent(this, PCloudActivity.class),
+ PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
@@ -41,8 +48,10 @@
stopVpn();
}
+ @RequiresApi(api = Build.VERSION_CODES.O)
private void startVpn() {
System.out.println("--- START");
+ updateForegroundNotification();
connect();
}
@@ -73,6 +82,15 @@
return builder;
}
+ private void updateForegroundNotification() {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, PCloudApp.STATUS_CHANNEL_ID)
+ .setContentTitle("PCloud")
+ .setContentText("hiiii")
+ .setContentIntent(configureIntent)
+ .setPriority(NotificationCompat.PRIORITY_LOW);
+ startForeground(PCloudApp.STATUS_NOTIFICATION_ID, builder.build());
+ }
+
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 0f0dc47..e54710b 100644
--- a/core/client/cmd/pcloud/app.go
+++ b/core/client/cmd/pcloud/app.go
@@ -5,6 +5,8 @@
type App interface {
LaunchBarcodeScanner() error
OnView(app.ViewEvent) error
- StartVPN(config []byte) error
- Connect(service interface{}) error
+ UpdateService(service interface{}) error
+ TriggerService() error
+ Connect(config Config) error
+ CreateStorage() Storage
}
diff --git a/core/client/cmd/pcloud/app_android.go b/core/client/cmd/pcloud/app_android.go
index 61737ee..ce90588 100644
--- a/core/client/cmd/pcloud/app_android.go
+++ b/core/client/cmd/pcloud/app_android.go
@@ -14,12 +14,11 @@
)
type androidApp struct {
- jvm *jni.JVM
- appCtx jni.Object // PCloudApp
- activity jni.Object // PCloudActivity
- service jni.Object // PCloudVPNService
- nebulaConfig []byte
- ctrl *nebula.Control
+ jvm *jni.JVM
+ appCtx jni.Object // PCloudApp
+ activity jni.Object // PCloudActivity
+ service jni.Object // PCloudVPNService
+ ctrl *nebula.Control
}
func createApp() App {
@@ -29,6 +28,10 @@
}
}
+func (a *androidApp) CreateStorage() Storage {
+ return CreateStorage(a.jvm, a.appCtx)
+}
+
func (a *androidApp) LaunchBarcodeScanner() error {
return jni.Do(a.jvm, func(env *jni.Env) error {
cls := jni.GetObjectClass(env, a.activity)
@@ -82,24 +85,20 @@
return ctx, nil
}
-func (a *androidApp) StartVPN(config []byte) error {
- a.nebulaConfig = config
+func (a *androidApp) TriggerService() error {
return jni.Do(a.jvm, func(env *jni.Env) error {
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))
- return err
-
+ m := jni.GetMethodID(env, cls, "startVpn", "()V")
+ return jni.CallVoidMethod(env, a.activity, m)
})
}
-func (a *androidApp) Connect(serv interface{}) error {
+func (a *androidApp) UpdateService(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 {
+ return 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)
@@ -132,18 +131,17 @@
a.service = s
return nil
})
- return a.buildVPNConfigurationAndConnect()
}
-func (a *androidApp) buildVPNConfigurationAndConnect() error {
- if string(a.nebulaConfig) == "" {
+func (a *androidApp) Connect(config Config) error {
+ if config.Network == nil {
return nil
}
- config := nc.NewC(logrus.StandardLogger())
- if err := config.LoadString(string(a.nebulaConfig)); err != nil {
+ nebulaConfig := nc.NewC(logrus.StandardLogger())
+ if err := nebulaConfig.LoadString(string(config.Network)); err != nil {
return err
}
- pki := config.GetMap("pki", nil)
+ pki := nebulaConfig.GetMap("pki", nil)
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(pki["cert"].(string)))
if err != nil {
panic(err)
@@ -177,7 +175,7 @@
jni.Value(jni.JavaString(env, ip)),
jni.Value(jni.Value(prefix)))
}
- tun := config.GetMap("tun", nil)
+ tun := nebulaConfig.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
@@ -205,7 +203,7 @@
return err
}
fd := int(tunFD)
- ctrl, err := nebula.Main(config, false, "pcloud", logrus.StandardLogger(), &fd)
+ ctrl, err := nebula.Main(nebulaConfig, false, "pcloud", logrus.StandardLogger(), &fd)
if err != nil {
return err
}
diff --git a/core/client/cmd/pcloud/app_darwin.go b/core/client/cmd/pcloud/app_darwin.go
index c7b670f..28532d0 100644
--- a/core/client/cmd/pcloud/app_darwin.go
+++ b/core/client/cmd/pcloud/app_darwin.go
@@ -21,10 +21,18 @@
return nil
}
-func (a *darwinApp) StartVPN(config []byte) error {
+func (a *darwinApp) Connect(config Config) error {
return nil
}
-func (a *darwinApp) Connect(serv interface{}) error {
+func (a *darwinApp) UpdateService(serv interface{}) error {
+ return nil
+}
+
+func (a *darwinApp) TriggerService() error {
+ return nil
+}
+
+func (a *darwinApp) CreateStorage() Storage {
return nil
}
diff --git a/core/client/cmd/pcloud/client.go b/core/client/cmd/pcloud/client.go
index 0d059ff..6763f17 100644
--- a/core/client/cmd/pcloud/client.go
+++ b/core/client/cmd/pcloud/client.go
@@ -6,7 +6,6 @@
"crypto/tls"
"encoding/base64"
"encoding/json"
- "errors"
"io"
"net/http"
@@ -16,8 +15,7 @@
)
type VPNClient interface {
- Address() string
- Sign(message []byte) ([]byte, error)
+ Sign(apiAddr string, message []byte) ([]byte, error)
Join(apiAddr string, message, signature []byte) ([]byte, error)
}
@@ -29,10 +27,6 @@
return &directVPNClient{addr}
}
-func (c *directVPNClient) Address() string {
- return c.addr
-}
-
type signReq struct {
Message []byte `json:"message"`
}
@@ -41,7 +35,7 @@
Signature []byte `json:"signature"`
}
-func (c *directVPNClient) Sign(message []byte) ([]byte, error) {
+func (c *directVPNClient) Sign(apiAddr string, message []byte) ([]byte, error) {
var data bytes.Buffer
if err := json.NewEncoder(&data).Encode(signReq{message}); err != nil {
return nil, err
@@ -52,7 +46,7 @@
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
- r, err := client.Post(c.addr+"/api/sign", "application/json", &data)
+ r, err := client.Post(apiAddr+"/api/sign", "application/json", &data)
if err != nil {
return nil, err
}
@@ -76,9 +70,6 @@
}
func (c *directVPNClient) Join(apiAddr string, message, signature []byte) ([]byte, error) {
- if c.addr != "" {
- return nil, errors.New("Already joined")
- }
pubKey, privKey, err := x25519Keypair()
if err != nil {
return nil, err
@@ -121,7 +112,6 @@
panic("Must not reach")
}
pki["key"] = string(cert.MarshalX25519PrivateKey(privKey))
- c.addr = apiAddr
return yaml.Marshal(cfgMap)
}
diff --git a/core/client/cmd/pcloud/config.go b/core/client/cmd/pcloud/config.go
new file mode 100644
index 0000000..0cf551d
--- /dev/null
+++ b/core/client/cmd/pcloud/config.go
@@ -0,0 +1,6 @@
+package main
+
+type Config struct {
+ ApiAddr string `json:"api_addr"`
+ Network []byte `json:"network"`
+}
diff --git a/core/client/cmd/pcloud/main.go b/core/client/cmd/pcloud/main.go
index fa013c9..1440af6 100644
--- a/core/client/cmd/pcloud/main.go
+++ b/core/client/cmd/pcloud/main.go
@@ -26,6 +26,7 @@
type processor struct {
vc VPNClient
app App
+ st Storage
ui *UI
inviteQrCh chan image.Image
@@ -33,18 +34,23 @@
onConnectCh chan interface{}
onDisconnectCh chan interface{}
+
+ onConfigCh chan struct{}
}
func newProcessor() *processor {
th := material.NewTheme(gofont.Collection())
+ app := createApp()
return &processor{
vc: NewDirectVPNClient(*vpnApiAddr),
- app: createApp(),
+ 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),
}
}
@@ -79,6 +85,13 @@
} else {
w.Invalidate()
}
+ if config, err := p.st.Get(); err != nil {
+ return err
+ } else {
+ if config.Network != nil {
+ p.onConfigCh <- struct{}{}
+ }
+ }
case *system.CommandEvent:
if e.Type == system.CommandBack {
if p.ui.OnBack() {
@@ -98,11 +111,24 @@
p.ui.InviteQRGenerated(img)
w.Invalidate()
case code := <-p.inviteQrScannedCh:
- p.JoinNetworkAndConnect(code)
- case s := <-p.onConnectCh:
- if err := p.app.Connect(s); err != nil {
+ if err := p.JoinAndGetNetworkConfig(code); err != nil {
return err
}
+ case <-p.onConfigCh:
+ if err := p.app.TriggerService(); err != nil {
+ return err
+ }
+ case s := <-p.onConnectCh:
+ if err := p.app.UpdateService(s); err != nil {
+ return err
+ }
+ if config, err := p.st.Get(); err != nil {
+ return err
+ } else {
+ if err := p.app.Connect(config); err != nil {
+ return err
+ }
+ }
}
}
return nil
@@ -136,13 +162,17 @@
}
func (p *processor) generateInviteQRCode() (image.Image, error) {
+ config, err := p.st.Get()
+ if err != nil {
+ return nil, err
+ }
message := []byte("Hello PCloud")
- signature, err := p.vc.Sign(message)
+ signature, err := p.vc.Sign(config.ApiAddr, message)
if err != nil {
return nil, err
}
c := qrCodeData{
- p.vc.Address(),
+ config.ApiAddr,
message,
signature,
}
@@ -161,18 +191,20 @@
return img, nil
}
-func (p *processor) JoinNetworkAndConnect(code []byte) {
+func (p *processor) JoinAndGetNetworkConfig(code []byte) error {
var invite qrCodeData
if err := json.NewDecoder(bytes.NewReader(code)).Decode(&invite); err != nil {
panic(err)
}
- config, err := p.vc.Join(invite.VPNApiAddr, invite.Message, invite.Signature)
+ network, err := p.vc.Join(invite.VPNApiAddr, invite.Message, invite.Signature)
if err != nil {
- panic(err)
+ return err
}
- if err := p.app.StartVPN(config); err != nil {
- panic(err)
+ if err := p.st.Store(Config{invite.VPNApiAddr, network}); err != nil {
+ return err
}
+ p.onConfigCh <- struct{}{}
+ return nil
}
func main() {
@@ -185,11 +217,3 @@
}()
app.Main()
}
-
-// fmt.Println(m["pki"])
-// c := nc.NewC(logrus.StandardLogger())
-// if err := c.LoadString(string(tmpl)); err != nil {
-// return nil, err
-// }
-// fmt.Println(c.Settings["pki"])
-// return c, nil
diff --git a/core/client/cmd/pcloud/storage.go b/core/client/cmd/pcloud/storage.go
new file mode 100644
index 0000000..824cc84
--- /dev/null
+++ b/core/client/cmd/pcloud/storage.go
@@ -0,0 +1,6 @@
+package main
+
+type Storage interface {
+ Get() (Config, error)
+ Store(Config) error
+}
diff --git a/core/client/cmd/pcloud/storage_android.go b/core/client/cmd/pcloud/storage_android.go
new file mode 100644
index 0000000..6e787da
--- /dev/null
+++ b/core/client/cmd/pcloud/storage_android.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+
+ "github.com/giolekva/pcloud/core/client/jni"
+)
+
+type androidStorage struct {
+ jvm *jni.JVM
+ appCtx jni.Object
+ encrypt jni.MethodID
+ decrypt jni.MethodID
+}
+
+func CreateStorage(jvm *jni.JVM, appCtx jni.Object) Storage {
+ s := &androidStorage{jvm: jvm, appCtx: appCtx}
+ jni.Do(jvm, func(env *jni.Env) error {
+ appCls := jni.GetObjectClass(env, appCtx)
+ s.encrypt = jni.GetMethodID(
+ env, appCls,
+ "encryptToPref", "(Ljava/lang/String;Ljava/lang/String;)V",
+ )
+ s.decrypt = jni.GetMethodID(
+ env, appCls,
+ "decryptFromPref", "(Ljava/lang/String;)Ljava/lang/String;",
+ )
+ return nil
+ })
+ return s
+}
+
+func (s *androidStorage) Get() (Config, error) {
+ var data []byte
+ err := jni.Do(s.jvm, func(env *jni.Env) error {
+ jfile := jni.JavaString(env, "config")
+ plain, err := jni.CallObjectMethod(env, s.appCtx, s.decrypt,
+ jni.Value(jfile))
+ if err != nil {
+ panic(err)
+ return err
+ }
+ b64 := jni.GoString(env, jni.String(plain))
+ if b64 == "" {
+ return nil
+ }
+ data, err = base64.RawStdEncoding.DecodeString(b64)
+ return err
+ })
+ var config Config
+ if data != nil {
+ err = json.NewDecoder(bytes.NewReader(data)).Decode(&config)
+ }
+ return config, err
+}
+
+func (s *androidStorage) Store(config Config) error {
+ var data bytes.Buffer
+ if err := json.NewEncoder(&data).Encode(config); err != nil {
+ return err
+ }
+ bs64 := base64.RawStdEncoding.EncodeToString(data.Bytes())
+ return jni.Do(s.jvm, func(env *jni.Env) error {
+ jfile := jni.JavaString(env, "config")
+ jplain := jni.JavaString(env, bs64)
+ return jni.CallVoidMethod(env, s.appCtx, s.encrypt,
+ jni.Value(jfile), jni.Value(jplain))
+ })
+}
diff --git a/core/client/cmd/pcloud/storage_darwin.go b/core/client/cmd/pcloud/storage_darwin.go
new file mode 100644
index 0000000..8fabc98
--- /dev/null
+++ b/core/client/cmd/pcloud/storage_darwin.go
@@ -0,0 +1,18 @@
+package main
+
+import "errors"
+
+type darwinStorage struct {
+}
+
+func CreateStorage() Storage {
+ return &darwinStorage{}
+}
+
+func (s *darwinStorage) Get() (Config, error) {
+ return nil, errors.New("Not implemented")
+}
+
+func (s *darwinStorage) Store(config Config) error {
+ return errors.New("Not implemented")
+}
diff --git a/core/client/cmd/pcloud/ui.go b/core/client/cmd/pcloud/ui.go
index 6f805fa..78afa76 100644
--- a/core/client/cmd/pcloud/ui.go
+++ b/core/client/cmd/pcloud/ui.go
@@ -96,12 +96,16 @@
}
func (ui *UI) layoutActions(gtx C) D {
- return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
+ return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceAround, WeightSum: 2.2}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
- return material.Button(ui.th, &ui.invite.open, "Invite").Layout(gtx)
+ 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 {
- return material.Button(ui.th, &ui.join.open, "Join").Layout(gtx)
+ b := material.Button(ui.th, &ui.join.open, "Join")
+ b.CornerRadius = unit.Px(20)
+ return b.Layout(gtx)
}),
)
}