GoBackend: integrate into app

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jason A. Donenfeld 2018-02-07 19:19:20 +01:00
parent b923f7bc57
commit 0ea6f73332
15 changed files with 348 additions and 59 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "app/tools/wireguard"] [submodule "app/tools/wireguard"]
path = app/tools/wireguard path = app/tools/wireguard
url = https://git.zx2c4.com/WireGuard url = https://git.zx2c4.com/WireGuard
[submodule "app/tools/wireguard-go"]
path = app/tools/wireguard-go
url = https://git.zx2c4.com/wireguard-go

View File

@ -1,9 +1,8 @@
# Android GUI for [WireGuard](https://www.wireguard.com/) # Android GUI for [WireGuard](https://www.wireguard.com/)
##### [Test this app on the Play Store](https://play.google.com/apps/testing/com.wireguard.android). ### [Test this app on the Play Store](https://play.google.com/apps/testing/com.wireguard.android).
This is a work in progress Android GUI for [WireGuard](https://www.wireguard.com/). The ultimate goal is to [opportunistically use the kernel implementation](https://git.zx2c4.com/android_kernel_wireguard/about/), and fallback to using the non-root userspace implementation. At the time of writing, this only supports using the kernel module, but this should change in the near future.
This is an Android GUI for [WireGuard](https://www.wireguard.com/). It [opportunistically uses the kernel implementation](https://git.zx2c4.com/android_kernel_wireguard/about/), and falls back to using the non-root [userspace implementation](https://git.zx2c4.com/wireguard-go/about/).
## License ## License

View File

@ -8,6 +8,7 @@ import android.os.Looper;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import com.wireguard.android.backend.Backend; import com.wireguard.android.backend.Backend;
import com.wireguard.android.backend.GoBackend;
import com.wireguard.android.backend.WgQuickBackend; import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.configStore.ConfigStore; import com.wireguard.android.configStore.ConfigStore;
import com.wireguard.android.configStore.FileConfigStore; import com.wireguard.android.configStore.FileConfigStore;
@ -16,6 +17,7 @@ import com.wireguard.android.util.AsyncWorker;
import com.wireguard.android.util.RootShell; import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller; import com.wireguard.android.util.ToolsInstaller;
import java.io.File;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import javax.inject.Qualifier; import javax.inject.Qualifier;
@ -56,6 +58,8 @@ public class Application extends android.app.Application {
ToolsInstaller getToolsInstaller(); ToolsInstaller getToolsInstaller();
TunnelManager getTunnelManager(); TunnelManager getTunnelManager();
Class getBackendType();
} }
@Qualifier @Qualifier
@ -83,7 +87,16 @@ public class Application extends android.app.Application {
public static Backend getBackend(@ApplicationContext final Context context, public static Backend getBackend(@ApplicationContext final Context context,
final RootShell rootShell, final RootShell rootShell,
final ToolsInstaller toolsInstaller) { final ToolsInstaller toolsInstaller) {
return new WgQuickBackend(context, rootShell, toolsInstaller); if (new File("/sys/module/wireguard").exists())
return new WgQuickBackend(context, rootShell, toolsInstaller);
else
return new GoBackend(context);
}
@ApplicationScope
@Provides
public static Class getBackendType(final Backend backend) {
return backend.getClass();
} }
@ApplicationScope @ApplicationScope

View File

@ -2,9 +2,12 @@ package com.wireguard.android.activity;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import com.wireguard.android.Application;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.backend.WgQuickBackend;
/** /**
* Interface for changing application-global persistent settings. * Interface for changing application-global persistent settings.
@ -26,6 +29,11 @@ public class SettingsActivity extends Activity {
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
if (Application.getComponent().getBackendType() != WgQuickBackend.class) {
final Preference toolsInstaller =
getPreferenceManager().findPreference("tools_installer");
getPreferenceScreen().removePreference(toolsInstaller);
}
} }
} }
} }

View File

@ -0,0 +1,125 @@
package com.wireguard.android.backend;
import android.content.Context;
import android.support.v4.util.ArraySet;
import android.util.Log;
import com.wireguard.android.model.Tunnel;
import com.wireguard.android.model.Tunnel.State;
import com.wireguard.android.model.Tunnel.Statistics;
import com.wireguard.config.Config;
import com.wireguard.config.Interface;
import com.wireguard.config.Peer;
import com.wireguard.crypto.KeyEncoding;
import java.util.Collections;
import java.util.Formatter;
import java.util.Set;
public final class GoBackend implements Backend {
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
static {
System.loadLibrary("wg-go");
}
private final Context context;
private Tunnel currentTunnel;
public GoBackend(final Context context) {
this.context = context;
}
private static native int wgGetSocketV4(int handle);
private static native int wgGetSocketV6(int handle);
private static native void wgTurnOff(int handle);
private static native int wgTurnOn(String ifName, int tunFd, String settings);
@Override
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
if (tunnel.getState() == State.UP) {
// Restart the tunnel to apply the new config.
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
try {
setStateInternal(tunnel, config, State.UP);
} catch (final Exception e) {
// The new configuration didn't work, so try to go back to the old one.
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
throw e;
}
}
return config;
}
@Override
public Set<String> enumerate() {
if (currentTunnel != null) {
final Set<String> runningTunnels = new ArraySet<>();
runningTunnels.add(currentTunnel.getName());
return runningTunnels;
}
return Collections.emptySet();
}
@Override
public State getState(final Tunnel tunnel) {
return currentTunnel == tunnel ? State.UP : State.DOWN;
}
@Override
public Statistics getStatistics(final Tunnel tunnel) {
return new Statistics();
}
@Override
public State setState(final Tunnel tunnel, State state) throws Exception {
final State originalState = getState(tunnel);
if (state == State.TOGGLE)
state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState)
return originalState;
if (state == State.UP && currentTunnel != null)
throw new IllegalStateException("Only one userspace tunnel can run at a time");
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
setStateInternal(tunnel, tunnel.getConfig(), state);
return getState(tunnel);
}
private void setStateInternal(final Tunnel tunnel, final Config config, final State state)
throws Exception {
if (state == State.UP) {
// Do something (context.startService()...).
currentTunnel = tunnel;
Formatter fmt = new Formatter(new StringBuilder());
final Interface iface = config.getInterface();
fmt.format("replace_peers=true\n");
if (iface.getPrivateKey() != null)
fmt.format("private_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(iface.getPrivateKey())));
if (iface.getListenPort() != null)
fmt.format("listen_port=%d\n", Integer.parseInt(config.getInterface().getListenPort()));
for (final Peer peer : config.getPeers()) {
if (peer.getPublicKey() != null)
fmt.format("public_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPublicKey())));
if (peer.getPreSharedKey() != null)
fmt.format("preshared_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPreSharedKey())));
if (peer.getEndpoint() != null)
fmt.format("endpoint=%s\n", peer.getEndpoint());
if (peer.getPersistentKeepalive() != null)
fmt.format("persistent_keepalive_interval=%d\n", Integer.parseInt(peer.getPersistentKeepalive()));
if (peer.getAllowedIPs() != null) {
for (final String allowedIp : peer.getAllowedIPs().split(" *, *")) {
fmt.format("allowed_ip=%s\n", allowedIp);
}
}
}
wgTurnOn(tunnel.getName(), -1, fmt.toString());
} else {
// Do something else.
currentTunnel = null;
}
}
}

View File

@ -88,8 +88,6 @@ public final class WgQuickBackend implements Backend {
state = originalState == State.UP ? State.DOWN : State.UP; state = originalState == State.UP ? State.DOWN : State.UP;
if (state == originalState) if (state == originalState)
return originalState; return originalState;
if (state == State.UP && !new File("/sys/module/wireguard").exists())
throw new ModuleNotLoadedException("WireGuard module not loaded");
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state); Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
toolsInstaller.ensureToolsAvailable(); toolsInstaller.ensureToolsAvailable();
setStateInternal(tunnel, tunnel.getConfig(), state); setStateInternal(tunnel, tunnel.getConfig(), state);
@ -113,14 +111,4 @@ public final class WgQuickBackend implements Backend {
if (result != 0) if (result != 0)
throw new Exception("Unable to configure tunnel (wg-quick returned " + result + ')'); throw new Exception("Unable to configure tunnel (wg-quick returned " + result + ')');
} }
public static class ModuleNotLoadedException extends Exception {
public ModuleNotLoadedException(final String message, final Throwable cause) {
super(message, cause);
}
public ModuleNotLoadedException(final String message) {
super(message);
}
}
} }

View File

@ -1,18 +1,13 @@
package com.wireguard.android.fragment; package com.wireguard.android.fragment;
import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.databinding.DataBindingUtil; import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding; import android.databinding.ViewDataBinding;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView;
import com.commonsware.cwac.crossport.design.widget.Snackbar; import com.commonsware.cwac.crossport.design.widget.Snackbar;
import com.wireguard.android.R; import com.wireguard.android.R;
import com.wireguard.android.backend.WgQuickBackend;
import com.wireguard.android.databinding.TunnelDetailFragmentBinding; import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
import com.wireguard.android.databinding.TunnelListItemBinding; import com.wireguard.android.databinding.TunnelListItemBinding;
import com.wireguard.android.model.Tunnel; import com.wireguard.android.model.Tunnel;
@ -47,26 +42,11 @@ public final class TunnelController {
if (throwable == null) if (throwable == null)
return; return;
final Context context = view.getContext(); final Context context = view.getContext();
if (ExceptionLoggers.unwrap(throwable) final String error = ExceptionLoggers.unwrap(throwable).getMessage();
instanceof WgQuickBackend.ModuleNotLoadedException) { final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = context.getString(R.string.not_supported_message); final String message = context.getString(messageResId, error);
final String title = context.getString(R.string.not_supported_title); Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
final AlertDialog dialog = new AlertDialog.Builder(context) Log.e(TAG, message, throwable);
.setMessage(Html.fromHtml(message))
.setPositiveButton(R.string.ok, null)
.setTitle(title)
.show();
// Make links work.
((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
Log.e(TAG, title, throwable);
} else {
final String error = ExceptionLoggers.unwrap(throwable).getMessage();
final int messageResId = checked ? R.string.error_up : R.string.error_down;
final String message = context.getString(messageResId, error);
Snackbar.make(view, message, Snackbar.LENGTH_LONG).show();
Log.e(TAG, message, throwable);
}
}); });
} }
} }

View File

@ -39,23 +39,6 @@
<string name="listen_port">Listen port</string> <string name="listen_port">Listen port</string>
<string name="mtu">MTU</string> <string name="mtu">MTU</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="not_supported_message" tools:ignore="TypographyQuotes"><![CDATA[
<p>Your Android device does not <em>currently</em> have the WireGuard kernel module. Please
talk to the manufacturer of your Android device or the author of your device&rsquo;s ROM
about including the WireGuard kernel module.</p>
<p>Fortunately, we are in the process of implementing support for WireGuard in a way that
will work on all devices, without any need for the kernel module. This means that while you
may not be able to use WireGuard today, you will very likely be able to use WireGuard in
several weeks. Things are looking up!</p>
<p>Sorry for the wait. In the mean time, you may stay up to date on the latest project news
by <a href="https://lists.zx2c4.com/mailman/listinfo/wireguard">subscribing to our mailing
list</a>. General information about the project is available at
<a href="https://www.wireguard.com/">WireGuard.com</a>.</p>
]]></string>
<string name="not_supported_title">WireGuard not installed</string>
<string name="ok">OK</string>
<string name="peer">Peer</string> <string name="peer">Peer</string>
<string name="persistent_keepalive">Persistent keepalive</string> <string name="persistent_keepalive">Persistent keepalive</string>
<string name="pre_shared_key">Pre-shared key</string> <string name="pre_shared_key">Pre-shared key</string>

View File

@ -5,5 +5,5 @@
android:key="restore_on_boot" android:key="restore_on_boot"
android:summary="@string/restore_on_boot_summary" android:summary="@string/restore_on_boot_summary"
android:title="@string/restore_on_boot_title" /> android:title="@string/restore_on_boot_title" />
<com.wireguard.android.preference.ToolsInstallerPreference /> <com.wireguard.android.preference.ToolsInstallerPreference android:key="tools_installer" />
</PreferenceScreen> </PreferenceScreen>

View File

@ -10,3 +10,16 @@ target_compile_options(libwg-quick.so PUBLIC -O3 -std=gnu11 -Wall -pedantic -Wno
file(GLOB WG_SOURCES wireguard/src/tools/*.c libmnl/src/*.c) file(GLOB WG_SOURCES wireguard/src/tools/*.c libmnl/src/*.c)
add_executable(libwg.so ${WG_SOURCES}) add_executable(libwg.so ${WG_SOURCES})
target_compile_options(libwg.so PUBLIC "-I${CMAKE_CURRENT_SOURCE_DIR}libmnl/src/" "-I${CMAKE_CURRENT_SOURCE_DIR}/libmnl/include/" "-I${CMAKE_CURRENT_SOURCE_DIR}/wireguard/src/tools/" -O3 -std=gnu11 -D_GNU_SOURCE -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"\\\"/data/data/com.wireguard.android/cache\\\"\" -Wno-pointer-arith -Wno-unused-parameter) target_compile_options(libwg.so PUBLIC "-I${CMAKE_CURRENT_SOURCE_DIR}libmnl/src/" "-I${CMAKE_CURRENT_SOURCE_DIR}/libmnl/include/" "-I${CMAKE_CURRENT_SOURCE_DIR}/wireguard/src/tools/" -O3 -std=gnu11 -D_GNU_SOURCE -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"\\\"/data/data/com.wireguard.android/cache\\\"\" -Wno-pointer-arith -Wno-unused-parameter)
add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND make
ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
ANDROID_C_COMPILER=${ANDROID_C_COMPILER}
ANDROID_TOOLCHAIN_ROOT=${ANDROID_TOOLCHAIN_ROOT}
ANDROID_LLVM_TRIPLE=${ANDROID_LLVM_TRIPLE}
ANDROID_SYSROOT=${ANDROID_SYSROOT}
CFLAGS=${CMAKE_C_FLAGS}\ -Wno-unused-command-line-argument
LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}\ -fuse-ld=gold
DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
)
# Hack to make it actually build as part of the default target
add_dependencies(libwg.so libwg-go.so)

4
app/tools/libwg-go/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
go/
*.go
libwg-go.h
jni.o

View File

@ -0,0 +1,21 @@
containing = $(foreach v,$2,$(if $(findstring $1,$v),$v))
FILES := $(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go)
FILES := $(filter-out %/main.go $(filter-out %_linux.go,$(call containing,_,$(FILES))),$(FILES))
export GOPATH := $(CURDIR)/go
CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
export CC := $(ANDROID_C_COMPILER)
GO_ARCH_FILTER := case "$(ANDROID_ARCH_NAME)" in x86) echo 386 ;; x86_64) echo amd64 ;; *) echo $(ANDROID_ARCH_NAME) ;; esac
export GOARCH := $(shell $(GO_ARCH_FILTER))
export GOOS := android
export CGO_ENABLED := 1
$(DESTDIR)/libwg-go.so: $(FILES) api-android.go jni.c
find . -name '*.go' -type l -delete
find . -type d -empty -delete
mkdir -p $(subst ../wireguard-go/,./,$(dir $(FILES)))
$(foreach FILE,$(FILES),ln -sfrt $(subst ../wireguard-go/,./,$(dir $(FILE))) $(FILE);)
go get -v -d
go build -v -o $(DESTDIR)/libwg-go.so -buildmode c-shared

View File

@ -0,0 +1,111 @@
package main
// #cgo LDFLAGS: -llog
// #include <android/log.h>
import "C"
import (
"bufio"
"io/ioutil"
"log"
"math"
"os"
"strings"
)
type AndroidLogger struct {
level C.int
interfaceName string
}
func (l AndroidLogger) Write(p []byte) (int, error) {
C.__android_log_write(l.level, C.CString("WireGuard/GoBackend/"+l.interfaceName), C.CString(string(p)))
return len(p), nil
}
var tunnelHandles map[int32]*Device
func init() {
tunnelHandles = make(map[int32]*Device)
}
//export wgTurnOn
func wgTurnOn(ifnameRef string, tun_fd int32, settings string) int32 {
interfaceName := string([]byte(ifnameRef))
logger := &Logger{
Debug: log.New(&AndroidLogger{level: C.ANDROID_LOG_DEBUG, interfaceName: interfaceName}, "", 0),
Info: log.New(&AndroidLogger{level: C.ANDROID_LOG_INFO, interfaceName: interfaceName}, "", 0),
Error: log.New(&AndroidLogger{level: C.ANDROID_LOG_ERROR, interfaceName: interfaceName}, "", 0),
}
logger.Debug.Println("Debug log enabled")
tun := &NativeTun{
fd: os.NewFile(uintptr(tun_fd), ""),
events: make(chan TUNEvent, 5),
errors: make(chan error, 5),
}
device := NewDevice(tun, logger)
device.tun.mtu = DefaultMTU //TODO: make dynamic
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(settings)), bufio.NewWriter(ioutil.Discard))
setError := ipcSetOperation(device, bufferedSettings)
if setError != nil {
logger.Debug.Println(setError)
return -1
}
device.Up()
logger.Info.Println("Device started")
var i int32
for i = 0; i < math.MaxInt32; i++ {
if _, exists := tunnelHandles[i]; !exists {
break
}
}
if i == math.MaxInt32 {
return -1
}
tunnelHandles[i] = device
return i
}
//export wgTurnOff
func wgTurnOff(tunnelHandle int32) {
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return
}
delete(tunnelHandles, tunnelHandle)
device.Close()
}
//export wgGetSocketV4
func wgGetSocketV4(tunnelHandle int32) int32 {
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return -1
}
native, ok := device.net.bind.(NativeBind)
if !ok {
return -1
}
return int32(native.sock4)
}
//export wgGetSocketV6
func wgGetSocketV6(tunnelHandle int32) int32 {
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return -1
}
native, ok := device.net.bind.(NativeBind)
if !ok {
return -1
}
return int32(native.sock6)
}
func main() {}

40
app/tools/libwg-go/jni.c Normal file
View File

@ -0,0 +1,40 @@
#include <jni.h>
struct go_string { const char *str; long n; };
extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settings);
extern void wgTurnOff(int handle);
extern int wgGetSocketV4(int handle);
extern int wgGetSocketV6(int handle);
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
{
const char *ifname_str = (*env)->GetStringUTFChars(env, ifname, 0);
size_t ifname_len = (*env)->GetStringUTFLength(env, ifname);
const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0);
size_t settings_len = (*env)->GetStringUTFLength(env, settings);
int ret = wgTurnOn((struct go_string){
.str = ifname_str,
.n = ifname_len
}, tun_fd, (struct go_string){
.str = settings_str,
.n = settings_len
});
(*env)->ReleaseStringUTFChars(env, ifname, ifname_str);
(*env)->ReleaseStringUTFChars(env, settings, settings_str);
return ret;
}
JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOff(JNIEnv *env, jclass c, jint handle)
{
wgTurnOff(handle);
}
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV4(JNIEnv *env, jclass c, jint handle)
{
return wgGetSocketV4(handle);
}
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV6(JNIEnv *env, jclass c, jint handle)
{
return wgGetSocketV6(handle);
}

@ -0,0 +1 @@
Subproject commit 8f1d1b8c54d747309d9fdf06b157823af2a823bd