summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--build.gradle.kts4
-rw-r--r--gradle.properties5
-rw-r--r--gradle/libs.versions.toml16
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin62076 -> 63375 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xgradlew5
-rw-r--r--keystore/debug.keystorebin0 -> 2107 bytes
-rw-r--r--settings.gradle.kts1
-rw-r--r--tunnel/build.gradle.kts61
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Backend.java5
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Bgp.java285
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java66
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java665
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Statistics.java28
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java17
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java6
-rw-r--r--tunnel/src/main/java/com/wireguard/android/util/RootShell.java4
-rw-r--r--tunnel/src/main/java/com/wireguard/config/BadConfigException.java9
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Config.java12
-rw-r--r--tunnel/src/main/java/com/wireguard/config/HttpProxy.java78
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetEndpoint.java37
-rw-r--r--tunnel/src/main/java/com/wireguard/config/InetNetwork.java2
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Interface.java33
-rw-r--r--tunnel/src/main/java/com/wireguard/config/Peer.java13
-rw-r--r--tunnel/src/main/java/com/wireguard/util/Resolver.java140
-rw-r--r--tunnel/src/main/proto/libwg.proto139
-rw-r--r--tunnel/tools/libwg-go/.gitignore3
-rw-r--r--tunnel/tools/libwg-go/Makefile27
-rw-r--r--tunnel/tools/libwg-go/api-android.go69
-rw-r--r--tunnel/tools/libwg-go/dhcp.go193
-rw-r--r--tunnel/tools/libwg-go/go.mod23
-rw-r--r--tunnel/tools/libwg-go/go.sum179
-rw-r--r--tunnel/tools/libwg-go/http-proxy.go736
-rw-r--r--tunnel/tools/libwg-go/jni.c32
-rw-r--r--tunnel/tools/libwg-go/service.go342
-rw-r--r--ui/build.gradle.kts17
-rw-r--r--ui/src/debug/res/values/strings.xml2
-rw-r--r--ui/src/main/AndroidManifest.xml7
-rw-r--r--ui/src/main/java/com/wireguard/android/QuickTileService.kt12
-rw-r--r--ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt7
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt10
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt27
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt131
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt5
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/Updater.kt14
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt22
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt72
-rw-r--r--ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt85
-rw-r--r--ui/src/main/res/layout/http_proxy_menu_item.xml8
-rw-r--r--ui/src/main/res/layout/tunnel_detail_fragment.xml111
-rw-r--r--ui/src/main/res/layout/tunnel_detail_peer.xml6
-rw-r--r--ui/src/main/res/layout/tunnel_editor_fragment.xml105
-rw-r--r--ui/src/main/res/values-da-rDK/strings.xml4
-rw-r--r--ui/src/main/res/values-et-rEE/strings.xml4
-rw-r--r--ui/src/main/res/values-ja/strings.xml15
-rw-r--r--ui/src/main/res/values-sv-rSE/strings.xml21
-rw-r--r--ui/src/main/res/values-vi-rVN/strings.xml28
-rw-r--r--ui/src/main/res/values-zh-rCN/strings.xml10
-rw-r--r--ui/src/main/res/values/strings.xml8
60 files changed, 3790 insertions, 183 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index acd4e797..017b2dea 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,7 +7,7 @@ plugins {
tasks {
wrapper {
- gradleVersion = "8.1.1"
- distributionSha256Sum = "e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f"
+ gradleVersion = "8.2.1"
+ distributionSha256Sum = "03ec176d388f2aa99defcadc3ac6adf8dd2bce5145a129659537c0874dea5ad1"
}
}
diff --git a/gradle.properties b/gradle.properties
index 77edff12..c2e14998 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,7 @@
-wireguardVersionCode=506
-wireguardVersionName=1.0.20230526
+wireguardVersionCode=509
+wireguardVersionName=1.0.20230707
wireguardPackageName=com.wireguard.android
+wireguardApplicationID=eu.m7n.wireguard.android
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 982a7ae5..d6849b75 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,10 +1,14 @@
[versions]
-agp = "8.2.0-alpha05"
+agp = "8.2.0-alpha15"
grgit = "5.2.0"
-kotlin = "1.8.21"
+grpc = "1.55.1"
+kotlin = "1.9.0"
+protobuf = "0.9.3"
+protoc = "3.22.4"
+protocgengrpc = '1.55.1'
[libraries]
-androidx-activity-ktx = "androidx.activity:activity-ktx:1.7.1"
+androidx-activity-ktx = "androidx.activity:activity-ktx:1.7.2"
androidx-annotation = "androidx.annotation:annotation:1.6.0"
androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
androidx-biometric = "androidx.biometric:biometric:1.1.0"
@@ -18,6 +22,11 @@ androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1
androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.0"
desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3"
google-material = "com.google.android.material:material:1.9.0"
+grpc-android = "io.grpc:grpc-android:1.55.1"
+grpc-okhttp = "io.grpc:grpc-okhttp:1.55.1"
+grpc-protobuf-lite = "io.grpc:grpc-protobuf-lite:1.55.1"
+grpc-stub = "io.grpc:grpc-stub:1.55.1"
+javax-annotation-api = "javax.annotation:javax.annotation-api:1.3.2"
jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
junit = "junit:junit:4.13.2"
kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0"
@@ -26,6 +35,7 @@ zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
+google-protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
ajoberstar-grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index c1962a79..033e24c4 100644
--- a/gradle/wrapper/gradle-wrapper.jar
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2c3425d4..c2447881 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
+distributionSha256Sum=03ec176d388f2aa99defcadc3ac6adf8dd2bce5145a129659537c0874dea5ad1
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index aeb74cbb..fcb6fca1 100755
--- a/gradlew
+++ b/gradlew
@@ -130,10 +130,13 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
diff --git a/keystore/debug.keystore b/keystore/debug.keystore
new file mode 100644
index 00000000..c2655107
--- /dev/null
+++ b/keystore/debug.keystore
Binary files differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 91bc0b90..e87d2a0d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,4 +19,5 @@ dependencyResolutionManagement {
rootProject.name = "wireguard-android"
include(":tunnel")
+include(":bgp-java")
include(":ui")
diff --git a/tunnel/build.gradle.kts b/tunnel/build.gradle.kts
index fff3857d..44a10696 100644
--- a/tunnel/build.gradle.kts
+++ b/tunnel/build.gradle.kts
@@ -1,5 +1,6 @@
@file:Suppress("UnstableApiUsage")
+import com.google.protobuf.gradle.*
import org.gradle.api.tasks.testing.logging.TestLogEvent
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
@@ -7,11 +8,12 @@ val pkg: String = providers.gradleProperty("wireguardPackageName").get()
plugins {
alias(libs.plugins.android.library)
`maven-publish`
+ alias(libs.plugins.google.protobuf)
signing
}
android {
- compileSdk = 33
+ compileSdk = 34
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -65,12 +67,69 @@ android {
}
dependencies {
+ implementation(project(":bgp-java"))
implementation(libs.androidx.annotation)
implementation(libs.androidx.collection)
+ implementation(libs.grpc.android)
+ implementation(libs.grpc.okhttp)
+ implementation(libs.grpc.protobuf.lite)
+ implementation(libs.grpc.stub)
+ compileOnly(libs.javax.annotation.api)
compileOnly(libs.jsr305)
testImplementation(libs.junit)
}
+protobuf {
+ protoc {
+ // The artifact spec for the Protobuf Compiler
+ artifact = "com.google.protobuf:protoc:${libs.versions.protoc.get()}"
+ }
+ plugins {
+ // Optional: an artifact spec for a protoc plugin, with "grpc" as
+ // the identifier, which can be referred to in the "plugins"
+ // container of the "generateProtoTasks" closure.
+ id("grpc") {
+ artifact = "io.grpc:protoc-gen-grpc-java:${libs.versions.protocgengrpc.get()}"
+ }
+ id("java") {
+ }
+ }
+ generateProtoTasks {
+ all().forEach { task ->
+ task.plugins{
+ id("grpc") {
+ option("lite")
+ }
+ id("java") {
+ option("lite")
+ }
+ }
+ }
+ }
+}
+
+afterEvaluate({ ->
+ // All custom configurations created by the protobuf plugin,
+ // are only available at this point.
+ var protoc = configurations.named("protobufToolsLocator_protoc")
+ var protocCache = "${gradle.gradleUserHomeDir}/caches/protoc-bin-${libs.versions.protoc.get()}"
+
+ tasks.register("copyProtoc", Copy::class) {
+ // Used by tunnel/tools/libwg-go/Makefile run in tools/CMakeLists.txt
+ from(protoc)
+ into(protocCache)
+ rename("protoc-.*", "protoc")
+ setFileMode(7 * 64 + 7 * 8 + 5) // 0775
+ }
+
+ tasks.named("preBuild").get().dependsOn("copyProtoc")
+
+ // Extract duration.proto used by external library in libwg.proto
+ tasks.named("preDebugBuild").get().dependsOn(tasks.named("extractIncludeDebugProto").get())
+ tasks.named("preReleaseBuild").get().dependsOn(tasks.named("extractIncludeReleaseProto").get())
+ println("done afterEvaluate")
+})
+
publishing {
publications {
register<MavenPublication>("release") {
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Backend.java b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
index edf98b9e..5ffdf8e2 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Backend.java
@@ -6,8 +6,11 @@
package com.wireguard.android.backend;
import com.wireguard.config.Config;
+import com.wireguard.config.InetNetwork;
+import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
+import java.util.List;
import java.util.Set;
import androidx.annotation.Nullable;
@@ -64,4 +67,6 @@ public interface Backend {
* @throws Exception Exception raised while changing state.
*/
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
+
+ void addAllowedIps(Tunnel tunnel, Key publicKey, List<InetNetwork> addNetworks);
}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java b/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java
new file mode 100644
index 00000000..a6a8b420
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java
@@ -0,0 +1,285 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import android.net.TrafficStats;
+import android.util.Log;
+
+import com.lumaserv.bgp.BGPListener;
+import com.lumaserv.bgp.BGPServer;
+import com.lumaserv.bgp.BGPSession;
+import com.lumaserv.bgp.BGPSessionConfiguration;
+import com.lumaserv.bgp.protocol.AFI;
+import com.lumaserv.bgp.protocol.BGPPacket;
+import com.lumaserv.bgp.protocol.IPPrefix;
+import com.lumaserv.bgp.protocol.SAFI;
+import com.lumaserv.bgp.protocol.attribute.ASPathAttribute;
+import com.lumaserv.bgp.protocol.attribute.MPReachableNLRIAttribute;
+import com.lumaserv.bgp.protocol.attribute.NextHopAttribute;
+import com.lumaserv.bgp.protocol.attribute.OriginAttribute;
+import com.lumaserv.bgp.protocol.attribute.PathAttribute;
+import com.lumaserv.bgp.protocol.attribute.TunnelEncapsAttribute;
+import com.lumaserv.bgp.protocol.message.BGPUpdate;
+
+import com.wireguard.android.backend.Backend;
+import com.wireguard.config.InetEndpoint;
+import com.wireguard.config.InetNetwork;
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyFormatException;
+
+import io.grpc.ManagedChannel;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.net.SocketFactory;
+
+public class Bgp implements BGPListener {
+ private static final String TAG = "WireGuard/Bgp";
+ private static final String SESSION = "demosession";
+ private static final int MY_ASN = (int)4200000201L;
+ private static final int REMOTE_ASN = (int)4200000010L;
+ private static final String REMOTE_ADDR = "10.49.32.1";
+ private static final String REMOTE_ID = "10.49.160.1";
+ private static final String LOCAL_ID = "10.49.33.218";
+ private static final int PORT = 0;
+ private static final int STATS_TAG = 1; // FIXME
+
+ private final Backend backend;
+ private final ManagedChannel channel;
+ private final Tunnel tunnel;
+ private final int tunnelHandle;
+ private BGPServer server;
+
+ public Bgp(Backend backend, ManagedChannel channel, Tunnel tunnel, int tunnelHandle) {
+ this.backend = backend;
+ this.channel = channel;
+ this.tunnel = tunnel;
+ this.tunnelHandle = tunnelHandle;
+ }
+
+ @Override
+ public void onOpen(BGPSession session) {
+ // DO WHAT YOU WANT
+ Log.i(TAG, "onOpen");
+
+ // BGPUpdate update;
+ // {
+ // List<IPPrefix> prefixes = new ArrayList<>(1);
+ // prefixes.add(new IPPrefix(new byte[]{10, 49, 124, 105}, 32));
+ // List<PathAttribute> attrs = new ArrayList<>();
+ // attrs.add(new OriginAttribute(OriginAttribute.Origin.IGP));
+ // attrs.add(new NextHopAttribute().setAddress(new byte[]{10, 49, 125, 105}));
+ // ASPathAttribute.Segment seg = new ASPathAttribute.Segment();
+ // seg.setType(ASPathAttribute.Segment.Type.SEQUENCE);
+ // seg.getAsns().add(MY_ASN);
+ // ASPathAttribute asPath = new ASPathAttribute(session);
+ // asPath.getSegments().add(seg);
+ // attrs.add(asPath);
+ // update = new BGPUpdate().setAttributes(attrs).setPrefixes(prefixes);
+ // }
+
+ // BGPUpdate update2;
+ // try {
+ // List<PathAttribute> attrs = new ArrayList<>();
+ // attrs.add(new OriginAttribute(OriginAttribute.Origin.IGP));
+ // ASPathAttribute.Segment seg = new ASPathAttribute.Segment();
+ // seg.setType(ASPathAttribute.Segment.Type.SEQUENCE);
+ // seg.getAsns().add(MY_ASN);
+ // ASPathAttribute asPath = new ASPathAttribute(session);
+ // List<IPPrefix> prefixes = new ArrayList<>(1);
+ // prefixes.add(new IPPrefix(new byte[]{0x20, 0x1, 0x04, 0x70, (byte)0xdf, (byte)0xae, 0x63, 0, 0, 0, 0, 0, 0, 0, 0x01, 0x05}, 128));
+ // MPReachableNLRIAttribute mpr = new MPReachableNLRIAttribute();
+ // mpr.setAfi(AFI.IPV6).setSafi(SAFI.UNICAST).setNextHop(InetAddress.getByName("2001:470:dfae:6300::1:105")).setNlriPrefixes(prefixes);
+ // attrs.add(mpr);
+ // asPath.getSegments().add(seg);
+ // attrs.add(asPath);
+ // update2 = new BGPUpdate().setAttributes(attrs);
+ // } catch (UnknownHostException ex) {
+ // throw new RuntimeException(ex);
+ // }
+
+ // try {
+ // session.sendUpdate(update);
+ // session.sendUpdate(update2);
+ // } catch (IOException ex) {
+ // throw new RuntimeException(ex);
+ // }
+ }
+
+ @Override
+ public void onUpdate(BGPSession session, BGPUpdate update) {
+ // DO WHAT YOU WANT
+ Log.i(TAG, "onUpdate: " + update.getPrefixes() + ",-" + update.getWithdrawnPrefixes() + "," + update.getAttributes());
+ MPReachableNLRIAttribute mpr = null;
+ TunnelEncapsAttribute te = null;
+
+ for (PathAttribute attr: update.getAttributes()) {
+ if (attr instanceof TunnelEncapsAttribute) {
+ te = (TunnelEncapsAttribute)attr;
+ } else if (attr instanceof MPReachableNLRIAttribute) {
+ mpr = (MPReachableNLRIAttribute)attr;
+ }
+ }
+
+ if (te == null) {
+ return;
+ }
+
+ TunnelEncapsAttribute.WireGuard wg = null;
+ TunnelEncapsAttribute.Color col = null;
+ TunnelEncapsAttribute.EgressEndpoint ep = null;
+ TunnelEncapsAttribute.UDPDestinationPort port = null;
+
+ for (TunnelEncapsAttribute.Tunnel t: te.getTunnels()) {
+ if (t.getType() != 51820) {
+ continue;
+ }
+
+ for (TunnelEncapsAttribute.SubTlv st: t.getSubTlvs()) {
+ if (st instanceof TunnelEncapsAttribute.WireGuard) {
+ wg = (TunnelEncapsAttribute.WireGuard)st;
+ } else if (st instanceof TunnelEncapsAttribute.Color) {
+ col = (TunnelEncapsAttribute.Color)st;
+ } else if (st instanceof TunnelEncapsAttribute.EgressEndpoint) {
+ ep = (TunnelEncapsAttribute.EgressEndpoint)st;
+ } else if (st instanceof TunnelEncapsAttribute.UDPDestinationPort) {
+ port = (TunnelEncapsAttribute.UDPDestinationPort)st;
+ }
+ }
+ }
+
+ if (wg == null) {
+ return;
+ }
+
+ try {
+ Key publicKey = Key.fromBytes(wg.getPublicKey());
+ InetEndpoint endpoint = null;
+
+ if (ep != null && port != null) {
+ endpoint = InetEndpoint.fromAddress(ep.getAddress(), port.getPort());
+ }
+
+ tunnel.onEndpointChange(publicKey, endpoint);
+
+ List<InetNetwork> addNetworks = new ArrayList<>();
+ List<InetNetwork> removeNetworks = new ArrayList<>();
+
+ for (IPPrefix prefix: update.getPrefixes()) {
+ try {
+ addNetworks.add(new InetNetwork(InetAddress.getByAddress(prefix.getAddress()), prefix.getLength()));
+ } catch (UnknownHostException ignore) {
+ }
+ }
+
+ for (IPPrefix prefix: update.getWithdrawnPrefixes()) {
+ try {
+ removeNetworks.add(new InetNetwork(InetAddress.getByAddress(prefix.getAddress()), prefix.getLength()));
+ } catch (UnknownHostException ignore) {
+ }
+ }
+
+ if (mpr != null && (mpr.getAfi() == AFI.IPV6 || mpr.getAfi() == AFI.IPV4) && mpr.getSafi() == SAFI.UNICAST) {
+ for (IPPrefix prefix: mpr.getNlriPrefixes()) {
+ try {
+ addNetworks.add(new InetNetwork(InetAddress.getByAddress(prefix.getAddress()), prefix.getLength()));
+ } catch (UnknownHostException ignore) {
+ }
+ }
+ }
+
+ tunnel.onAllowedIpsChange(publicKey, addNetworks, removeNetworks);
+ // backend.addAllowedIps(tunnel, publicKey, addNetworks);
+ // backend.removeAllowedIps(tunnel, publicKey, addNetworks); // TODO
+ } catch (KeyFormatException ex) {
+ Log.w(TAG, "Key.fromBytes " + ex);
+ }
+ }
+
+ @Override
+ public void onClose(BGPSession session) {
+ // NOT YET IMPLEMENTED
+ Log.i(TAG, "onClose");
+ }
+
+ public boolean startServer() {
+ stopServer();
+ try {
+ SocketFactory factory = new SocketFactory() {
+ private Socket taggedSocket(Socket sock) throws SocketException {
+ TrafficStats.tagSocket(sock);
+ return sock;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ TrafficStats.setThreadStatsTag(STATS_TAG);
+ return taggedSocket(new Socket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ TrafficStats.setThreadStatsTag(STATS_TAG);
+ return taggedSocket(new Socket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+ TrafficStats.setThreadStatsTag(STATS_TAG);
+ return taggedSocket(new Socket(address, port, localAddress, localPort));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
+ TrafficStats.setThreadStatsTag(STATS_TAG);
+ return taggedSocket(new Socket(host, port, localHost, localPort));
+ }
+ };
+
+ BGPSessionConfiguration config =
+ new BGPSessionConfiguration(SESSION,
+ MY_ASN,
+ ip(LOCAL_ID),
+ REMOTE_ASN,
+ ip(REMOTE_ID),
+ null, // Remote address
+ factory,
+ this);
+ TrafficStats.setThreadStatsTag(STATS_TAG);
+ ServerSocket socket = new ServerSocket(PORT);
+ //TrafficStats.tagSocket(socket);
+ // Set
+ server = new BGPServer(socket);
+ server.getSessionConfigurations().add(config);
+ server.connect(config, REMOTE_ADDR);
+ return true;
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+
+ public void stopServer() {
+ if (server != null) {
+ server.shutdown();
+ server = null;
+ }
+ }
+
+ private static byte[] ip(String s) throws UnknownHostException {
+ InetAddress addr = InetAddress.getByName(s);
+ byte[] data = addr.getAddress();
+ if (data.length != 4)
+ throw new UnknownHostException(s + ": Not an IPv4 address");
+ return data;
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java
new file mode 100644
index 00000000..41060ddf
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.backend;
+
+import com.wireguard.config.InetNetwork;
+import com.wireguard.util.NonNullForAll;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Class representing DHCP info for a {@link Tunnel} instance.
+ */
+@NonNullForAll
+public class Dhcp {
+ private Set<Lease> leases = new LinkedHashSet<>();
+
+ public class Lease {
+ private InetNetwork address;
+ private Duration validLt;
+ private Duration preferredLt;
+ private Instant validTs;
+ private Instant preferredTs;
+
+ public Lease(InetNetwork address, Duration validLt, Duration preferredLt) {
+ this.address = address;
+ this.validLt = validLt;
+ this.preferredLt = preferredLt;
+
+ Instant now = Instant.now();
+ this.validTs = now.plus(validLt);
+ this.preferredTs = now.plus(preferredLt);
+ }
+
+ public final InetNetwork getAddress() {
+ return this.address;
+ }
+
+ public String toString() {
+ ZoneId zone = ZoneId.systemDefault();
+ LocalTime validLocal = validTs.atZone(zone).toLocalTime().withNano(0);
+ LocalTime preferredLocal = preferredTs.atZone(zone).toLocalTime().withNano(0);
+ // TODO add date when needed
+ return address.toString() + " (valid " + validLocal + ", preferred " + preferredLocal + ")";
+ }
+ }
+
+ public void addLease(InetNetwork address, Duration valid, Duration preferred) {
+ this.leases.add(new Lease(address, valid, preferred));
+ }
+
+ public final Set<Lease> getLeases() {
+ return leases;
+ }
+
+ public String toString() {
+ return "DHCP";
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
index 429cb1f1..2af43fb2 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
@@ -5,33 +5,99 @@
package com.wireguard.android.backend;
+import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.LocalSocketAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.ProxyInfo;
+import android.net.TrafficStats;
+import android.net.Uri;
import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SystemClock;
import android.system.OsConstants;
import android.util.Log;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Empty;
+
import com.wireguard.android.backend.BackendException.Reason;
import com.wireguard.android.backend.Tunnel.State;
+import com.wireguard.android.backend.gen.CapabilitiesChangedRequest;
+import com.wireguard.android.backend.gen.CapabilitiesChangedResponse;
+import com.wireguard.android.backend.gen.DhcpRequest;
+import com.wireguard.android.backend.gen.DhcpResponse;
+import com.wireguard.android.backend.gen.GetConnectionOwnerUidResponse;
+import com.wireguard.android.backend.gen.IpcSetRequest;
+import com.wireguard.android.backend.gen.IpcSetResponse;
+import com.wireguard.android.backend.gen.Lease;
+import com.wireguard.android.backend.gen.LibwgGrpc;
+import com.wireguard.android.backend.gen.ReverseRequest;
+import com.wireguard.android.backend.gen.ReverseResponse;
+import com.wireguard.android.backend.gen.StartHttpProxyRequest;
+import com.wireguard.android.backend.gen.StartHttpProxyResponse;
+import com.wireguard.android.backend.gen.StopHttpProxyRequest;
+import com.wireguard.android.backend.gen.StopHttpProxyResponse;
+import com.wireguard.android.backend.gen.TunnelHandle;
+import com.wireguard.android.backend.gen.VersionRequest;
+import com.wireguard.android.backend.gen.VersionResponse;
import com.wireguard.android.util.SharedLibraryLoader;
import com.wireguard.config.Config;
+import com.wireguard.config.HttpProxy;
import com.wireguard.config.InetEndpoint;
import com.wireguard.config.InetNetwork;
import com.wireguard.config.Peer;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.util.NonNullForAll;
-
+import com.wireguard.util.Resolver;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.android.UdsChannelBuilder;
+import io.grpc.okhttp.OkHttpChannelBuilder;
+import io.grpc.stub.StreamObserver;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.net.URL;
+import java.time.Duration;
import java.time.Instant;
+import java.nio.ByteOrder;
import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.net.SocketFactory;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
@@ -44,12 +110,25 @@ import androidx.collection.ArraySet;
public final class GoBackend implements Backend {
private static final int DNS_RESOLUTION_RETRIES = 10;
private static final String TAG = "WireGuard/GoBackend";
+ private static final int STATS_TAG = 2;
+ private static final int MSG_DHCP_EXPIRE = 1;
+ private static final int MSG_CAPABILITIES_CHANGED = 2;
@Nullable private static AlwaysOnCallback alwaysOnCallback;
private static GhettoCompletableFuture<VpnService> vpnService = new GhettoCompletableFuture<>();
private final Context context;
@Nullable private Config currentConfig;
@Nullable private Tunnel currentTunnel;
private int currentTunnelHandle = -1;
+ private ConnectivityManager connectivityManager;
+ private ConnectivityManager.NetworkCallback myNetworkCallback = new MyNetworkCallback();
+ private ConnectivityManager.NetworkCallback vpnNetworkCallback;
+ @Nullable private Network activeNetwork;
+ private ManagedChannel channel;
+ private boolean obtainDhcpLease = false;
+ @Nullable private Bgp bgp;
+ private HandlerThread thread;
+ private Handler handler;
+ private NetworkCapabilities activeNetworkCapabilities;
/**
* Public constructor for GoBackend.
@@ -59,6 +138,11 @@ public final class GoBackend implements Backend {
public GoBackend(final Context context) {
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
this.context = context;
+ connectivityManager = context.getSystemService(ConnectivityManager.class);
+ File socketFile = new File(context.getCacheDir(), "libwg.sock");
+ String socketName = socketFile.getAbsolutePath();
+ Log.i(TAG, "wgStartGrpc: " + wgStartGrpc(socketName));
+ channel = UdsChannelBuilder.forPath(socketName, LocalSocketAddress.Namespace.FILESYSTEM).build();
}
/**
@@ -77,12 +161,18 @@ public final class GoBackend implements Backend {
private static native int wgGetSocketV6(int handle);
+ private static native int wgSetConfig(int handle, String settings);
+
+ private static native void wgSetFd(int handle, int tunFd);
+
private static native void wgTurnOff(int handle);
private static native int wgTurnOn(String ifName, int tunFd, String settings);
private static native String wgVersion();
+ private static native int wgStartGrpc(String sockName);
+
/**
* Method to get the names of running tunnels.
*
@@ -185,7 +275,10 @@ public final class GoBackend implements Backend {
*/
@Override
public String getVersion() {
- return wgVersion();
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ VersionRequest request = VersionRequest.newBuilder().build();
+ VersionResponse resp = stub.version(request);
+ return resp.getVersion();
}
/**
@@ -224,78 +317,306 @@ public final class GoBackend implements Backend {
return getState(tunnel);
}
- private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
- throws Exception {
- Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
+ @Override
+ public void addAllowedIps(Tunnel tunnel, Key publicKey, List<InetNetwork> addNetworks) {
+ if (tunnel != currentTunnel) {
+ // TODO logerror and/or return error/throw.
+ return;
+ }
- if (state == State.UP) {
- if (config == null)
- throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
+ StringBuffer sb = new StringBuffer();
+ sb.append("public_key=").append(publicKey.toHex()).append('\n');
+ for (final InetNetwork allowedIp: addNetworks) {
+ sb.append("allowed_ip=").append(allowedIp).append('\n');
+ }
- if (VpnService.prepare(context) != null)
- throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
+ String goConfig = sb.toString();
+ // TODO removed removeNetworks
+ Log.w(TAG, "Wg user string: " + goConfig);
- final VpnService service;
- if (!vpnService.isDone()) {
- Log.d(TAG, "Requesting to start VpnService");
- context.startService(new Intent(context, VpnService.class));
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ TunnelHandle handle = TunnelHandle.newBuilder().setHandle(currentTunnelHandle).build();
+ IpcSetRequest request = IpcSetRequest.newBuilder().setTunnel(handle).setConfig(goConfig).build();
+ IpcSetResponse resp = stub.ipcSet(request);
+ }
+
+ private static String downloadPacFile(Network network, Uri pacFileUrl) {
+ HttpURLConnection urlConnection = null;
+ StringBuffer buf = new StringBuffer();
+ try {
+ URL url = new URL(pacFileUrl.toString());
+ TrafficStats.setThreadStatsTag(STATS_TAG);
+ urlConnection = (HttpURLConnection) network.openConnection(url);
+
+ InputStream in = urlConnection.getInputStream();
+ InputStreamReader isw = new InputStreamReader(in);
+
+ int data = isw.read();
+ while (data != -1) {
+ char current = (char) data;
+ data = isw.read();
+ buf.append(current);
+ }
+ } catch (Exception e) {
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
}
+ }
- try {
- service = vpnService.get(2, TimeUnit.SECONDS);
- } catch (final TimeoutException e) {
- final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
- be.initCause(e);
- throw be;
+ return buf.toString();
+ }
+
+ private void capabilitiesChanged(NetworkCapabilities capabilities) {
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ CapabilitiesChangedRequest.Builder reqBuilder = CapabilitiesChangedRequest.newBuilder();
+
+ if (capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
+ reqBuilder.addCapabilities(CapabilitiesChangedRequest.Capability.NOT_METERED);
+ }
+
+ CapabilitiesChangedResponse resp = stub.capabilitiesChanged(reqBuilder.build());
+ Log.i(TAG, "Capabilities change: " + resp.getError().getMessage());
+ }
+
+ private int startHttpProxy(String pacFile) {
+ LibwgGrpc.LibwgStub asyncStub = LibwgGrpc.newStub(channel);
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ StartHttpProxyRequest.Builder reqBuilder = StartHttpProxyRequest.newBuilder();
+ if (pacFile != null && pacFile != "") {
+ reqBuilder.setPacFileContent(pacFile);
+ }
+
+ Thread streamer = new Thread(new Runnable() {
+ public void run() {
+ try {
+ Log.i(TAG, "Before streamReverse");
+ streamReverse(asyncStub);
+ Log.i(TAG, "After streamReverse");
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
}
- service.setOwner(this);
+ });
+
+ StartHttpProxyRequest req = reqBuilder.build();
+ StartHttpProxyResponse resp = stub.startHttpProxy(req);
+ Log.i(TAG, "Start http proxy listen_port:" + resp.getListenPort() + ", error:" + resp.getError().getMessage());
+ capabilitiesChanged(activeNetworkCapabilities);
+ streamer.start();
+ return resp.getListenPort();
+ }
- if (currentTunnelHandle != -1) {
- Log.w(TAG, "Tunnel already up");
- return;
+ private void stopHttpProxy() {
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ StopHttpProxyRequest req = StopHttpProxyRequest.newBuilder().build();
+ StopHttpProxyResponse resp = stub.stopHttpProxy(req);
+ Log.i(TAG, "Stop http proxy: " + resp.getError().getMessage());
+ }
+
+ private static InetSocketAddress toInetSocketAddress(com.wireguard.android.backend.gen.InetSocketAddress sockAddr) {
+ try {
+ return new InetSocketAddress(InetAddress.getByAddress(sockAddr.getAddress().getAddress().toByteArray()), sockAddr.getPort());
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void Dhcp(VpnService service) throws Exception{
+ if (currentConfig == null || currentTunnel == null || currentTunnelHandle < 0) {
+ return;
+ }
+
+ obtainDhcpLease = false;
+
+ // Heuristics: Use first ULA address as client address
+ com.wireguard.android.backend.gen.InetAddress source = null;
+
+ for (final InetNetwork net : currentConfig.getInterface().getAddresses()) {
+ InetAddress addr = net.getAddress();
+ if (addr instanceof Inet6Address) {
+ if (Resolver.isULA((Inet6Address)addr)) {
+ source = com.wireguard.android.backend.gen.InetAddress.newBuilder().setAddress(ByteString.copyFrom(addr.getAddress())).build();
+ }
}
+ }
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ DhcpRequest.Builder requestBuilder = DhcpRequest.newBuilder();
+ if (source != null) {
+ requestBuilder.setSource(source);
+ }
+ DhcpRequest request = requestBuilder.build();
+ DhcpResponse resp = stub.dhcp(request);
+ Log.i(TAG, "Dhcp: " + resp.getError().getMessage());
- dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) {
- // Pre-resolve IPs so they're cached when building the userspace string
- for (final Peer peer : config.getPeers()) {
- final InetEndpoint ep = peer.getEndpoint().orElse(null);
- if (ep == null)
- continue;
- if (ep.getResolved().orElse(null) == null) {
- if (i < DNS_RESOLUTION_RETRIES - 1) {
- Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again");
- Thread.sleep(1000);
- continue dnsRetry;
- } else
- throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost());
- }
+ Dhcp dhcp = new Dhcp();
+ long delayMillis = 1000 * 3600 * 12; // Max renew 12h
+
+ if (resp.getLeasesList() != null) {
+ for (final Lease lease: resp.getLeasesList()) {
+ try {
+ InetAddress addr = InetAddress.getByAddress(lease.getAddress().getAddress().toByteArray());
+ Log.i(TAG, "Lease: " + addr + " " + lease.getValidLifetime().getSeconds() + " " + lease.getPreferredLifetime().getSeconds());
+ dhcp.addLease(new InetNetwork(addr, 128),
+ Duration.ofSeconds(lease.getValidLifetime().getSeconds(), lease.getValidLifetime().getNanos()),
+ Duration.ofSeconds(lease.getPreferredLifetime().getSeconds(), lease.getPreferredLifetime().getNanos()));
+ long leaseDelayMillis = lease.getValidLifetime().getSeconds() * 1000 + lease.getValidLifetime().getNanos() / 1000;
+ delayMillis = Math.min(delayMillis, leaseDelayMillis);
+ } catch (UnknownHostException ex) {
+ // Ignore
}
- break;
}
+ }
- // Build config
- final String goConfig = config.toWgUserspaceString();
+ // Replace the vpn tunnel
+ final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getLeases());
- // Create the vpn tunnel with android API
+ Log.i(TAG, "Builder: " + builder);
+
+ try (final ParcelFileDescriptor tun = builder.establish()) {
+ if (tun == null)
+ throw new BackendException(Reason.TUN_CREATION_ERROR);
+ Log.d(TAG, "Go backend " + wgVersion());
+ // SetFd
+ wgSetFd(currentTunnelHandle, tun.detachFd());
+ }
+ if (currentTunnelHandle < 0)
+ throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
+
+ service.protect(wgGetSocketV4(currentTunnelHandle));
+ service.protect(wgGetSocketV6(currentTunnelHandle));
+ Log.i(TAG, "Dhcp done");
+
+ AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
+ am.cancel(alarmListener);
+ am.setWindow(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + delayMillis * 3 / 4, delayMillis / 4, null, alarmListener, handler);
+
+ if (bgp != null) {
+ bgp = new Bgp(this, channel, currentTunnel, currentTunnelHandle);
+ bgp.startServer();
+ }
+
+ currentTunnel.onDhcpChange(dhcp);
+ }
+
+ private int getConnectionOwnerUid(int protocol, InetSocketAddress local, InetSocketAddress remote) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+ return connectivityManager.getConnectionOwnerUid(protocol, local, remote);
+ else
+ return Process.INVALID_UID;
+ }
+
+ private void streamReverse(LibwgGrpc.LibwgStub asyncStub) throws InterruptedException {
+ Log.i(TAG, "In streamReverse");
+ final CountDownLatch finishLatch = new CountDownLatch(1);
+ final AtomicReference<StreamObserver<ReverseRequest>> atomicRequestObserver = new AtomicReference<StreamObserver<ReverseRequest>>();
+ // Throwable failed = null;
+
+ StreamObserver<ReverseResponse> responseObserver = new StreamObserver<ReverseResponse>() {
+ @Override
+ public void onNext(ReverseResponse resp) {
+ try {
+ String pkg = "";
+ int uid = getConnectionOwnerUid(resp.getUid().getProtocol(), toInetSocketAddress(resp.getUid().getLocal()), toInetSocketAddress(resp.getUid().getRemote()));
+ if (uid != Process.INVALID_UID) {
+ PackageManager pm = context.getPackageManager();
+ pkg = pm.getNameForUid(uid);
+ String[] pkgs = pm.getPackagesForUid(uid);
+ Log.i(TAG, "reverse onNext uid:" + uid + " package:" + pkg);
+ if (pkgs != null) {
+ for (int i=0; i < pkgs.length; i++) {
+ Log.i(TAG, "getPackagesForUid() = " + pkgs[i]);
+ }
+ }
+ } else {
+ Log.i(TAG, "Connection not found");
+ }
+
+ ReverseRequest req = ReverseRequest.newBuilder()
+ .setUid(GetConnectionOwnerUidResponse.newBuilder()
+ .setUid(uid)
+ .setPackage(pkg != null ? pkg: "")
+ .build())
+ .build();
+
+ io.grpc.Context.current().fork().run(new Runnable() {
+ public void run() {
+ atomicRequestObserver.get().onNext(req);
+ }
+ });
+ } catch (RuntimeException ex) {
+ Log.i(TAG, "onNext " + ex);
+ throw ex;
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ // failed = t;
+ Log.i(TAG, "streamReverse error: " + t);
+ finishLatch.countDown();
+ }
+
+ @Override
+ public void onCompleted() {
+ Log.i(TAG, "streamReverse completed");
+ finishLatch.countDown();
+ }
+ };
+ StreamObserver<ReverseRequest> requestObserver = asyncStub.reverse(responseObserver);
+ atomicRequestObserver.set(requestObserver);
+
+ // Mark the end of requests
+ //requestObserver.onCompleted();
+
+ //requestObserver.onNext(ReverseRequest.getDefaultInstance());
+
+ Log.i(TAG, "Waiting streamReverse");
+ // Receiving happens asynchronously
+ finishLatch.await();
+
+ // if (failed != null) {
+ // throw new RuntimeException(failed);
+ // }
+ Log.i(TAG, "Exit streamReverse");
+ }
+
+ private VpnService.Builder getBuilder(final String name, @Nullable final Config config, final VpnService service, @Nullable final Set<Dhcp.Lease> leases) throws PackageManager.NameNotFoundException {
+ Log.i(TAG, "Builder 1");
final VpnService.Builder builder = service.getBuilder();
- builder.setSession(tunnel.getName());
+ Log.i(TAG, "Builder 2");
+ builder.setSession(name);
+ Log.i(TAG, "Builder 3");
for (final String excludedApplication : config.getInterface().getExcludedApplications())
builder.addDisallowedApplication(excludedApplication);
+ Log.i(TAG, "Builder 4");
for (final String includedApplication : config.getInterface().getIncludedApplications())
builder.addAllowedApplication(includedApplication);
+ Log.i(TAG, "Builder 5");
+ if (leases != null) {
+ for (final Dhcp.Lease lease: leases) {
+ InetNetwork addr = lease.getAddress();
+ builder.addAddress(addr.getAddress(), addr.getMask());
+ }
+ }
+
+ Log.i(TAG, "Builder 6");
for (final InetNetwork addr : config.getInterface().getAddresses())
builder.addAddress(addr.getAddress(), addr.getMask());
+ Log.i(TAG, "Builder 7");
for (final InetAddress addr : config.getInterface().getDnsServers())
builder.addDnsServer(addr.getHostAddress());
+ Log.i(TAG, "Builder 8");
for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains())
builder.addSearchDomain(dnsSearchDomain);
+ Log.i(TAG, "Builder 9");
boolean sawDefaultRoute = false;
for (final Peer peer : config.getPeers()) {
for (final InetNetwork addr : peer.getAllowedIps()) {
@@ -305,20 +626,115 @@ public final class GoBackend implements Backend {
}
}
+ Log.i(TAG, "Builder 10");
// "Kill-switch" semantics
if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
builder.allowFamily(OsConstants.AF_INET);
builder.allowFamily(OsConstants.AF_INET6);
}
+ Log.i(TAG, "Builder 11");
builder.setMtu(config.getInterface().getMtu().orElse(1280));
+ Log.i(TAG, "Builder 12");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
builder.setMetered(false);
+ Log.i(TAG, "Builder 13");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
service.setUnderlyingNetworks(null);
+ Log.i(TAG, "Builder 14");
+ Optional<HttpProxy> proxy = config.getInterface().getHttpProxy();
+ if (proxy.isPresent()) {
+ ProxyInfo pi = proxy.get().getProxyInfo();
+ Uri pacFileUrl = pi.getPacFileUrl();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ if (pacFileUrl != null && pacFileUrl != Uri.EMPTY) {
+ String pacFile = downloadPacFile(activeNetwork, pacFileUrl);
+ int listenPort = startHttpProxy(pacFile);
+ ProxyInfo localPi = ProxyInfo.buildDirectProxy("localhost", listenPort);
+ builder.setHttpProxy(localPi);
+ } else {
+ builder.setHttpProxy(pi);
+ }
+ }
+ }
+
+ Log.i(TAG, "Builder 15");
builder.setBlocking(true);
+ return builder;
+ }
+
+ private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state)
+ throws Exception {
+ Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
+
+ if (state == State.UP) {
+ if (config == null)
+ throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
+
+ if (VpnService.prepare(context) != null)
+ throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
+
+ final VpnService service;
+ if (!vpnService.isDone()) {
+ Log.d(TAG, "Requesting to start VpnService");
+ context.startService(new Intent(context, VpnService.class));
+ }
+
+ try {
+ service = vpnService.get(2, TimeUnit.SECONDS);
+ } catch (final TimeoutException e) {
+ final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
+ be.initCause(e);
+ throw be;
+ }
+ service.setOwner(this);
+
+ if (currentTunnelHandle != -1) {
+ Log.w(TAG, "Tunnel already up");
+ return;
+ }
+
+
+ activeNetwork = connectivityManager.getActiveNetwork();
+ if (activeNetwork == null) {
+ Log.w(TAG, "Null activeNetwork");
+ activeNetwork = null;
+ }
+ else if (!connectivityManager.getNetworkCapabilities(activeNetwork).hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
+ Log.w(TAG, "VPN network is active, null activeNetwork");
+ activeNetwork = null;
+ }
+ final Resolver resolver = new Resolver(activeNetwork, connectivityManager.getLinkProperties(activeNetwork));
+ dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) {
+ // Pre-resolve IPs so they're cached when building the userspace string
+ for (final Peer peer : config.getPeers()) {
+ final InetEndpoint ep = peer.getEndpoint().orElse(null);
+ if (ep == null)
+ continue;
+ // FIXME
+ tunnel.onEndpointChange(peer.getPublicKey(), ep);
+ Log.i(TAG, "onEndpointChange " + peer.getPublicKey() + ", " + ep);
+ if (ep.getResolved(resolver, true).orElse(null) == null) {
+ if (i < DNS_RESOLUTION_RETRIES - 1) {
+ Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again");
+ Thread.sleep(1000);
+ continue dnsRetry;
+ } else
+ throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost());
+ }
+ }
+ break;
+ }
+
+ // Build config
+ final String goConfig = config.toWgUserspaceString(resolver);
+
+ // Create the vpn tunnel with android API
+ final VpnService.Builder builder = getBuilder(tunnel.getName(), config, service, null);
+
try (final ParcelFileDescriptor tun = builder.establish()) {
if (tun == null)
throw new BackendException(Reason.TUN_CREATION_ERROR);
@@ -333,6 +749,46 @@ public final class GoBackend implements Backend {
service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle));
+
+ obtainDhcpLease = true;
+
+ thread = new HandlerThread("GoBackend HandlerThread");
+ thread.start();
+ handler = new Handler(thread.getLooper()) {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DHCP_EXPIRE:
+ Log.w(TAG, "DHCP expire: " + ((activeNetwork != null) ? activeNetwork : "disabled"));
+ try {
+ if (activeNetwork != null) {
+ Log.w(TAG, "DHCP before");
+ Dhcp(service); // Renew addresses
+ Log.w(TAG, "DHCP after");
+ } else {
+ Log.w(TAG, "DHCP delay obtain lease");
+ obtainDhcpLease = true;
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "DHCP failed: " + ex);
+ }
+ break;
+ case MSG_CAPABILITIES_CHANGED:
+ Log.w(TAG, "Msg: capabilities changed: " + activeNetworkCapabilities);
+ capabilitiesChanged(activeNetworkCapabilities);
+ break;
+ default:
+ Log.w(TAG, "Unknown message: " + msg.what);
+ break;
+ }
+ }
+ };
+
+ NetworkRequest req = new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build();
+ connectivityManager.requestNetwork(req, myNetworkCallback);
+
+ NetworkRequest vpnReq = new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_VPN).removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN).build();
+ vpnNetworkCallback = new VpnNetworkCallback(service);
+ connectivityManager.requestNetwork(vpnReq, vpnNetworkCallback);
} else {
if (currentTunnelHandle == -1) {
Log.w(TAG, "Tunnel already down");
@@ -342,6 +798,24 @@ public final class GoBackend implements Backend {
currentTunnel = null;
currentTunnelHandle = -1;
currentConfig = null;
+
+ AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
+ am.cancel(alarmListener);
+ if (thread != null) {
+ handler = null;
+ thread.quit();
+ thread = null;
+ }
+ if (bgp != null) {
+ bgp.stopServer();
+ bgp = null;
+ }
+ stopHttpProxy();
+ if (vpnNetworkCallback != null)
+ connectivityManager.unregisterNetworkCallback(vpnNetworkCallback);
+ vpnNetworkCallback = null;
+ connectivityManager.unregisterNetworkCallback(myNetworkCallback);
+ activeNetwork = null;
wgTurnOff(handleToClose);
try {
vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf();
@@ -407,10 +881,29 @@ public final class GoBackend implements Backend {
@Override
public void onDestroy() {
if (owner != null) {
+ if (owner.bgp != null) {
+ owner.bgp.stopServer();
+ owner.bgp = null;
+ }
+ AlarmManager am = (AlarmManager)owner.context.getSystemService(Context.ALARM_SERVICE);
+ am.cancel(owner.alarmListener);
+ if (owner.thread != null) {
+ owner.handler = null;
+ owner.thread.quit();
+ owner.thread = null;
+ }
+
+ owner.stopHttpProxy();
final Tunnel tunnel = owner.currentTunnel;
if (tunnel != null) {
- if (owner.currentTunnelHandle != -1)
+ if (owner.currentTunnelHandle != -1) {
+ if (owner.vpnNetworkCallback != null)
+ owner.connectivityManager.unregisterNetworkCallback(owner.vpnNetworkCallback);
+ owner.vpnNetworkCallback = null;
+ owner.connectivityManager.unregisterNetworkCallback(owner.myNetworkCallback);
+ owner.activeNetwork = null;
wgTurnOff(owner.currentTunnelHandle);
+ }
owner.currentTunnel = null;
owner.currentTunnelHandle = -1;
owner.currentConfig = null;
@@ -436,4 +929,88 @@ public final class GoBackend implements Backend {
this.owner = owner;
}
}
+
+ private class VpnNetworkCallback extends ConnectivityManager.NetworkCallback {
+ private VpnService service;
+ public VpnNetworkCallback(VpnService service) {
+ this.service = service;
+ }
+ @Override
+ public void onAvailable(Network network) {
+ Log.w(TAG, "VPN onAvailable: " + network);
+ if (obtainDhcpLease && activeNetwork != null) {
+ Log.w(TAG, "Obtaindhcplease");
+ try {
+ Log.w(TAG, "Before Dhcp");
+ Dhcp(service);
+ Log.w(TAG, "After Dhcp");
+ } catch (Exception ex) {
+ Log.e(TAG, "DHCP failed: " + ex);
+ }
+ }
+ }
+ }
+
+ private class MyNetworkCallback extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(Network network) {
+ activeNetwork = network;
+ Log.w(TAG, "onAvailable: " + activeNetwork);
+ }
+
+ @Override
+ public void onLosing(Network network, int maxMsToLive) {
+ Log.w(TAG, "onLosing: " + network + " maxMsToLive: " + maxMsToLive);
+ // TODO release DHCP?
+ }
+
+ @Override
+ public void onLost(Network network) {
+ Log.w(TAG, "onLost: " + network + " (" + activeNetwork + ")");
+ if (network.equals(activeNetwork)) {
+ activeNetwork = null;
+ }
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
+ Log.w(TAG, "onLinkPropertiesChanged: " + network + " is default:" + (network.equals(activeNetwork)));
+ if (network.equals(activeNetwork) && currentConfig != null && currentTunnelHandle > -1) {
+ final Resolver resolver = new Resolver(network, linkProperties);
+ final String goConfig = currentConfig.toWgEndpointsUserspaceString(resolver);
+ Log.w(TAG, "is default network, config:" + goConfig);
+
+ LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel);
+ TunnelHandle tunnel = TunnelHandle.newBuilder().setHandle(currentTunnelHandle).build();
+ IpcSetRequest request = IpcSetRequest.newBuilder().setTunnel(tunnel).setConfig(goConfig).build();
+ IpcSetResponse resp = stub.ipcSet(request);
+
+ for (final Peer peer : currentConfig.getPeers()) {
+ final InetEndpoint ep = peer.getEndpoint().orElse(null);
+ if (ep == null)
+ continue;
+ currentTunnel.onEndpointChange(peer.getPublicKey(), ep);
+ }
+ }
+ }
+
+ @Override
+ public void onCapabilitiesChanged (Network network, NetworkCapabilities networkCapabilities) {
+ boolean hasCapNotMetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ Log.w(TAG, "onCapabilitiesChanged: " + network + " is not metered:" + (hasCapNotMetered));
+ if (network.equals(activeNetwork) && handler != null) {
+ activeNetworkCapabilities = networkCapabilities;
+ handler.sendEmptyMessage(MSG_CAPABILITIES_CHANGED);
+ }
+ }
+ }
+
+ private AlarmManager.OnAlarmListener alarmListener = new AlarmManager.OnAlarmListener() {
+ @Override
+ public void onAlarm() {
+ if (handler != null) {
+ handler.sendEmptyMessage(MSG_DHCP_EXPIRE);
+ }
+ }
+ };
}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
index 9fc92c53..08b84949 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
@@ -21,31 +21,7 @@ import androidx.annotation.Nullable;
*/
@NonNullForAll
public class Statistics {
-
- // TODO: switch to Java Record class once R8 supports desugaring those.
- public final class PeerStats {
- public final long rxBytes, txBytes, latestHandshakeEpochMillis;
-
- PeerStats(final long rxBytes, final long txBytes, final long latestHandshakeEpochMillis) {
- this.rxBytes = rxBytes;
- this.txBytes = txBytes;
- this.latestHandshakeEpochMillis = latestHandshakeEpochMillis;
- }
-
- @Override public boolean equals(final Object o) {
- if (this == o)
- return true;
- if (o == null || getClass() != o.getClass())
- return false;
- final PeerStats stats = (PeerStats) o;
- return rxBytes == stats.rxBytes && txBytes == stats.txBytes && latestHandshakeEpochMillis == stats.latestHandshakeEpochMillis;
- }
-
- @Override public int hashCode() {
- return Objects.hash(rxBytes, txBytes, latestHandshakeEpochMillis);
- }
- }
-
+ public record PeerStats(long rxBytes, long txBytes, long latestHandshakeEpochMillis) { }
private final Map<Key, PeerStats> stats = new HashMap<>();
private long lastTouched = SystemClock.elapsedRealtime();
@@ -85,7 +61,7 @@ public class Statistics {
*/
@Nullable
public PeerStats peer(final Key peer) {
- return this.stats.get(peer);
+ return stats.get(peer);
}
/**
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
index 766df443..fc94375b 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
@@ -5,8 +5,14 @@
package com.wireguard.android.backend;
+import androidx.annotation.Nullable;
+
+import com.wireguard.config.InetEndpoint;
+import com.wireguard.config.InetNetwork;
+import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
+import java.util.List;
import java.util.regex.Pattern;
/**
@@ -54,4 +60,15 @@ public interface Tunnel {
return running ? UP : DOWN;
}
}
+
+ /**
+ * React to a change of DHCP of the tunnel. Should only be directly called by Backend.
+ *
+ * @param newDhcp The new DHCP info of the tunnel.
+ */
+ void onDhcpChange(Dhcp newDhcp);
+
+ void onEndpointChange(Key publicKey, @Nullable InetEndpoint newEndpoint);
+
+ void onAllowedIpsChange(Key publicKey, @Nullable List<InetNetwork> addNetworks, @Nullable List<InetNetwork> removeNetworks);
}
diff --git a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
index 023743a8..2a3ee588 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java
@@ -14,6 +14,7 @@ import com.wireguard.android.backend.Tunnel.State;
import com.wireguard.android.util.RootShell;
import com.wireguard.android.util.ToolsInstaller;
import com.wireguard.config.Config;
+import com.wireguard.config.InetNetwork;
import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
@@ -167,6 +168,11 @@ public final class WgQuickBackend implements Backend {
return state;
}
+ @Override
+ public void addAllowedIps(Tunnel tunnel, Key publicKey, List<InetNetwork> addNetworks) {
+ throw new RuntimeException("Not implemented");
+ }
+
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + ' ' + state);
diff --git a/tunnel/src/main/java/com/wireguard/android/util/RootShell.java b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
index e123733b..67bff568 100644
--- a/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
+++ b/tunnel/src/main/java/com/wireguard/android/util/RootShell.java
@@ -46,8 +46,8 @@ public class RootShell {
final String packageName = context.getPackageName();
if (packageName.contains("'"))
throw new RuntimeException("Impossibly invalid package name contains a single quote");
- preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE package_name='%s'\" >/dev/null 2>&1; id -u\n",
- packageName, localBinaryDir, localTemporaryDir, packageName);
+ preamble = String.format("export CALLING_PACKAGE='%s' PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE uid=%d\" >/dev/null 2>&1; id -u\n",
+ packageName, localBinaryDir, localTemporaryDir, android.os.Process.myUid());
}
private static boolean isExecutableInPath(final String name) {
diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
index db022e14..0d41cc05 100644
--- a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
+++ b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java
@@ -8,6 +8,8 @@ package com.wireguard.config;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.util.NonNullForAll;
+import java.net.MalformedURLException;
+
import androidx.annotation.Nullable;
@NonNullForAll
@@ -44,6 +46,12 @@ public class BadConfigException extends Exception {
}
public BadConfigException(final Section section, final Location location,
+ @Nullable final CharSequence text,
+ final MalformedURLException cause) {
+ this(section, location, Reason.INVALID_VALUE, text, cause);
+ }
+
+ public BadConfigException(final Section section, final Location location,
final ParseException cause) {
this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
}
@@ -73,6 +81,7 @@ public class BadConfigException extends Exception {
ENDPOINT("Endpoint"),
EXCLUDED_APPLICATIONS("ExcludedApplications"),
INCLUDED_APPLICATIONS("IncludedApplications"),
+ HTTP_PROXY("HttpProxy"),
LISTEN_PORT("ListenPort"),
MTU("MTU"),
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
diff --git a/tunnel/src/main/java/com/wireguard/config/Config.java b/tunnel/src/main/java/com/wireguard/config/Config.java
index ee9cebce..12ddb242 100644
--- a/tunnel/src/main/java/com/wireguard/config/Config.java
+++ b/tunnel/src/main/java/com/wireguard/config/Config.java
@@ -9,6 +9,7 @@ import com.wireguard.config.BadConfigException.Location;
import com.wireguard.config.BadConfigException.Reason;
import com.wireguard.config.BadConfigException.Section;
import com.wireguard.util.NonNullForAll;
+import com.wireguard.util.Resolver;
import java.io.BufferedReader;
import java.io.IOException;
@@ -173,12 +174,19 @@ public final class Config {
*
* @return the {@code Config} represented as a series of "key=value" lines
*/
- public String toWgUserspaceString() {
+ public String toWgUserspaceString(Resolver resolver) {
final StringBuilder sb = new StringBuilder();
sb.append(interfaze.toWgUserspaceString());
sb.append("replace_peers=true\n");
for (final Peer peer : peers)
- sb.append(peer.toWgUserspaceString());
+ sb.append(peer.toWgUserspaceString(resolver));
+ return sb.toString();
+ }
+
+ public String toWgEndpointsUserspaceString(Resolver resolver) {
+ final StringBuilder sb = new StringBuilder();
+ for (final Peer peer : peers)
+ sb.append(peer.toWgEndpointsUserspaceString(resolver));
return sb.toString();
}
diff --git a/tunnel/src/main/java/com/wireguard/config/HttpProxy.java b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java
new file mode 100644
index 00000000..0a9d19f6
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.config;
+
+import com.wireguard.config.BadConfigException.Location;
+import com.wireguard.config.BadConfigException.Section;
+import com.wireguard.util.NonNullForAll;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import android.net.ProxyInfo;
+import android.net.Uri;
+
+@NonNullForAll
+public final class HttpProxy {
+ public static final int DEFAULT_PROXY_PORT = 8080;
+
+ private ProxyInfo pi;
+
+ protected HttpProxy(ProxyInfo pi) {
+ this.pi = pi;
+ }
+
+ public ProxyInfo getProxyInfo() {
+ return pi;
+ }
+
+ public String getHost() {
+ return pi.getHost();
+ }
+
+ public Uri getPacFileUrl() {
+ return pi.getPacFileUrl();
+ }
+
+ public int getPort() {
+ return pi.getPort();
+ }
+
+ public static HttpProxy parse(final String httpProxy) throws BadConfigException {
+ try {
+ if (httpProxy.startsWith("pac:")) {
+ return new HttpProxy(ProxyInfo.buildPacProxy(Uri.parse(httpProxy.substring(4))));
+ } else {
+ final String urlStr;
+ if (!httpProxy.contains("://")) {
+ urlStr = "http://" + httpProxy;
+ } else {
+ urlStr = httpProxy;
+ }
+ URL url = new URL(urlStr);
+ return new HttpProxy(ProxyInfo.buildDirectProxy(url.getHost(), url.getPort() <= 0 ? DEFAULT_PROXY_PORT : url.getPort()));
+ }
+ } catch (final MalformedURLException e) {
+ throw new BadConfigException(Section.INTERFACE, Location.HTTP_PROXY, httpProxy, e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ if (pi.getPacFileUrl() != null && pi.getPacFileUrl() != Uri.EMPTY)
+ sb.append("pac:").append(pi.getPacFileUrl());
+ else {
+ sb.append("http://").append(pi.getHost()).append(':');
+ if (pi.getPort() <= 0)
+ sb.append(DEFAULT_PROXY_PORT);
+ else
+ sb.append(pi.getPort());
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
index d1db432b..f9db5779 100644
--- a/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
+++ b/tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
@@ -6,6 +6,7 @@
package com.wireguard.config;
import com.wireguard.util.NonNullForAll;
+import com.wireguard.util.Resolver;
import java.net.Inet4Address;
import java.net.InetAddress;
@@ -64,6 +65,10 @@ public final class InetEndpoint {
}
}
+ public static InetEndpoint fromAddress(final InetAddress address, final int port) {
+ return new InetEndpoint(address.getHostAddress(), true, port);
+ }
+
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof InetEndpoint))
@@ -80,6 +85,14 @@ public final class InetEndpoint {
return port;
}
+ public Optional<InetEndpoint> getResolved() {
+ if (isResolved) {
+ return Optional.of(this);
+ } else {
+ return Optional.ofNullable(resolved);
+ }
+ }
+
/**
* Generate an {@code InetEndpoint} instance with the same port and the host resolved using DNS
* to a numeric address. If the host is already numeric, the existing instance may be returned.
@@ -87,24 +100,22 @@ public final class InetEndpoint {
*
* @return the resolved endpoint, or {@link Optional#empty()}
*/
- public Optional<InetEndpoint> getResolved() {
- if (isResolved)
+ public Optional<InetEndpoint> getResolved(Resolver resolver) {
+ return getResolved(resolver, false);
+ }
+
+ public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force) {
+ if (!force && isResolved)
return Optional.of(this);
synchronized (lock) {
//TODO(zx2c4): Implement a real timeout mechanism using DNS TTL
- if (Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
+ if (force || Duration.between(lastResolution, Instant.now()).toMinutes() > 1) {
try {
- // Prefer v4 endpoints over v6 to work around DNS64 and IPv6 NAT issues.
- final InetAddress[] candidates = InetAddress.getAllByName(host);
- InetAddress address = candidates[0];
- for (final InetAddress candidate : candidates) {
- if (candidate instanceof Inet4Address) {
- address = candidate;
- break;
- }
- }
- resolved = new InetEndpoint(address.getHostAddress(), true, port);
+ InetAddress address = resolver.resolve(host);
+ InetEndpoint resolvedNow = new InetEndpoint(address.getHostAddress(), true, port);
lastResolution = Instant.now();
+
+ resolved = resolvedNow;
} catch (final UnknownHostException e) {
resolved = null;
}
diff --git a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
index 4a918044..02ccd946 100644
--- a/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
+++ b/tunnel/src/main/java/com/wireguard/config/InetNetwork.java
@@ -20,7 +20,7 @@ public final class InetNetwork {
private final InetAddress address;
private final int mask;
- private InetNetwork(final InetAddress address, final int mask) {
+ public InetNetwork(final InetAddress address, final int mask) {
this.address = address;
this.mask = mask;
}
diff --git a/tunnel/src/main/java/com/wireguard/config/Interface.java b/tunnel/src/main/java/com/wireguard/config/Interface.java
index bebca2e5..e47e0be3 100644
--- a/tunnel/src/main/java/com/wireguard/config/Interface.java
+++ b/tunnel/src/main/java/com/wireguard/config/Interface.java
@@ -46,6 +46,7 @@ public final class Interface {
private final KeyPair keyPair;
private final Optional<Integer> listenPort;
private final Optional<Integer> mtu;
+ private final Optional<HttpProxy> httpProxy;
private Interface(final Builder builder) {
// Defensively copy to ensure immutability even if the Builder is reused.
@@ -57,6 +58,7 @@ public final class Interface {
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
listenPort = builder.listenPort;
mtu = builder.mtu;
+ httpProxy = builder.httpProxy;
}
/**
@@ -92,6 +94,9 @@ public final class Interface {
case "mtu":
builder.parseMtu(attribute.getValue());
break;
+ case "httpproxy":
+ builder.parseHttpProxy(attribute.getValue());
+ break;
case "privatekey":
builder.parsePrivateKey(attribute.getValue());
break;
@@ -115,7 +120,8 @@ public final class Interface {
&& includedApplications.equals(other.includedApplications)
&& keyPair.equals(other.keyPair)
&& listenPort.equals(other.listenPort)
- && mtu.equals(other.mtu);
+ && mtu.equals(other.mtu)
+ && httpProxy.equals(other.httpProxy);
}
/**
@@ -195,6 +201,15 @@ public final class Interface {
return mtu;
}
+ /**
+ * Returns the HTTP proxy used for the WireGuard interface.
+ *
+ * @return the HTTP proxy, or {@code Optional.empty()} if none is configured
+ */
+ public Optional<HttpProxy> getHttpProxy() {
+ return httpProxy;
+ }
+
@Override
public int hashCode() {
int hash = 1;
@@ -205,6 +220,7 @@ public final class Interface {
hash = 31 * hash + keyPair.hashCode();
hash = 31 * hash + listenPort.hashCode();
hash = 31 * hash + mtu.hashCode();
+ hash = 31 * hash + httpProxy.hashCode();
return hash;
}
@@ -244,6 +260,7 @@ public final class Interface {
sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n');
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
+ httpProxy.ifPresent(p -> sb.append("HttpProxy = ").append(p).append('\n'));
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
return sb.toString();
}
@@ -279,6 +296,8 @@ public final class Interface {
private Optional<Integer> listenPort = Optional.empty();
// Defaults to not present.
private Optional<Integer> mtu = Optional.empty();
+ // Defaults to not present.
+ private Optional<HttpProxy> httpProxy = Optional.empty();
public Builder addAddress(final InetNetwork address) {
addresses.add(address);
@@ -391,6 +410,10 @@ public final class Interface {
}
}
+ public Builder parseHttpProxy(final String httpProxy) throws BadConfigException {
+ return setHttpProxy(HttpProxy.parse(httpProxy));
+ }
+
public Builder parsePrivateKey(final String privateKey) throws BadConfigException {
try {
return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
@@ -419,5 +442,13 @@ public final class Interface {
this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
return this;
}
+
+ public Builder setHttpProxy(final HttpProxy httpProxy) throws BadConfigException {
+ if (httpProxy == null)
+ throw new BadConfigException(Section.INTERFACE, Location.HTTP_PROXY,
+ Reason.INVALID_VALUE, String.valueOf(httpProxy));
+ this.httpProxy = httpProxy == null ? Optional.empty() : Optional.of(httpProxy);
+ return this;
+ }
}
}
diff --git a/tunnel/src/main/java/com/wireguard/config/Peer.java b/tunnel/src/main/java/com/wireguard/config/Peer.java
index 8a0fd763..1d20a961 100644
--- a/tunnel/src/main/java/com/wireguard/config/Peer.java
+++ b/tunnel/src/main/java/com/wireguard/config/Peer.java
@@ -11,6 +11,7 @@ import com.wireguard.config.BadConfigException.Section;
import com.wireguard.crypto.Key;
import com.wireguard.crypto.KeyFormatException;
import com.wireguard.util.NonNullForAll;
+import com.wireguard.util.Resolver;
import java.util.Collection;
import java.util.Collections;
@@ -190,18 +191,26 @@ public final class Peer {
*
* @return the {@code Peer} represented as a series of "key=value" lines
*/
- public String toWgUserspaceString() {
+ public String toWgUserspaceString(Resolver resolver) {
final StringBuilder sb = new StringBuilder();
// The order here is important: public_key signifies the beginning of a new peer.
sb.append("public_key=").append(publicKey.toHex()).append('\n');
for (final InetNetwork allowedIp : allowedIps)
sb.append("allowed_ip=").append(allowedIp).append('\n');
- endpoint.flatMap(InetEndpoint::getResolved).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
+ endpoint.flatMap(ep -> ep.getResolved(resolver)).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
persistentKeepalive.ifPresent(pk -> sb.append("persistent_keepalive_interval=").append(pk).append('\n'));
preSharedKey.ifPresent(psk -> sb.append("preshared_key=").append(psk.toHex()).append('\n'));
return sb.toString();
}
+ public String toWgEndpointsUserspaceString(Resolver resolver) {
+ final StringBuilder sb = new StringBuilder();
+ // The order here is important: public_key signifies the beginning of a new peer.
+ sb.append("public_key=").append(publicKey.toHex()).append('\n');
+ endpoint.flatMap(ep -> ep.getResolved(resolver, true)).ifPresent(ep -> sb.append("endpoint=").append(ep).append('\n'));
+ return sb.toString();
+ }
+
@SuppressWarnings("UnusedReturnValue")
public static final class Builder {
// See wg(8)
diff --git a/tunnel/src/main/java/com/wireguard/util/Resolver.java b/tunnel/src/main/java/com/wireguard/util/Resolver.java
new file mode 100644
index 00000000..24f5ab88
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/util/Resolver.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright © 2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.util;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.TrafficStats;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+@NonNullForAll
+public class Resolver {
+ private static final String TAG = "WireGuard/Resolver";
+ private static final int STATS_TAG = 3; // FIXME
+ @Nullable private final Network network;
+ @Nullable private final LinkProperties linkProps;
+ @Nullable private IpPrefix nat64Prefix;
+
+ public Resolver(Network network, LinkProperties linkProps) {
+ this.network = network;
+ this.linkProps = linkProps;
+ if (linkProps != null) {
+ this.nat64Prefix = linkProps.getNat64Prefix();
+ }
+ }
+
+ public static boolean isULA(Inet6Address addr) {
+ byte[] raw = addr.getAddress();
+ return ((raw[0] & 0xfe) == 0xfc);
+ }
+
+ boolean isWithinNAT64Prefix(Inet6Address address) {
+ if (nat64Prefix == null)
+ return false;
+
+ int prefixLength = nat64Prefix.getPrefixLength();
+ byte[] rawAddr = address.getAddress();
+ byte[] rawPrefix = nat64Prefix.getRawAddress();
+
+ for (int i=0; i < prefixLength/8; i++) {
+ if (rawAddr[i] != rawPrefix[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ boolean isPreferredIPv6(Inet6Address local, Inet6Address remote) {
+ if (linkProps == null) {
+ // Prefer IPv4 if there are not link properties that can
+ // be tested.
+ return false;
+ }
+
+ // * Prefer IPv4 if local or remote address is ULA
+ // * Prefer IPv4 if remote IPv6 is within NAT64 prefix.
+ // * Otherwise prefer IPv6
+ boolean isLocalULA = isULA(local);
+ boolean isRemoteULA = isULA(remote);
+
+ if (isLocalULA || isRemoteULA) {
+ return false;
+ }
+
+ if (isWithinNAT64Prefix(remote)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public InetAddress resolve(String host) throws UnknownHostException {
+ TrafficStats.setThreadStatsTag(STATS_TAG);
+ final InetAddress[] candidates = network != null ? network.getAllByName(host) : InetAddress.getAllByName(host);
+ InetAddress address = candidates[0];
+ for (final InetAddress candidate : candidates) {
+ DatagramSocket sock;
+
+ try {
+ sock = new DatagramSocket();
+ TrafficStats.tagDatagramSocket(sock);
+ if (network != null) {
+ network.bindSocket(sock);
+ }
+ } catch (SocketException e) {
+ // Return first candidate as fallback
+ Log.w(TAG, "DatagramSocket failed, fallback to: \"" + address);
+ return address;
+ } catch (IOException e) {
+ // Return first candidate as fallback
+ Log.w(TAG, "BindSocket failed, fallback to: \"" + address);
+ return address;
+ }
+
+ sock.connect(candidate, 51820);
+
+ if (sock.getLocalAddress().isAnyLocalAddress()) {
+ // Connect didn't find a local address.
+ Log.w(TAG, "No local address");
+ sock.close();
+ continue;
+ }
+
+ Log.w(TAG, "Local address: " + sock.getLocalAddress());
+
+ if (candidate instanceof Inet4Address) {
+ // Accept IPv4 as preferred address.
+ address = candidate;
+ sock.close();
+ break;
+ }
+
+ Inet6Address local = (Inet6Address)sock.getLocalAddress();
+ InetSocketAddress remoteSockAddr = (InetSocketAddress)sock.getRemoteSocketAddress();
+ Inet6Address remote = (Inet6Address)remoteSockAddr.getAddress();
+ sock.close();
+
+ if (isPreferredIPv6(local, remote)) {
+ address = candidate;
+ break;
+ }
+ }
+ Log.w(TAG, "Resolved \"" + host + "\" to: " + address);
+ return address;
+ }
+}
diff --git a/tunnel/src/main/proto/libwg.proto b/tunnel/src/main/proto/libwg.proto
new file mode 100644
index 00000000..bf471599
--- /dev/null
+++ b/tunnel/src/main/proto/libwg.proto
@@ -0,0 +1,139 @@
+syntax = "proto3";
+
+import "google/protobuf/duration.proto";
+
+option java_multiple_files = true;
+option java_package = 'com.wireguard.android.backend.gen';
+option java_outer_classname = "LibwgProto";
+option java_generic_services = true;
+option go_package = 'golang.zx2c4.com/wireguard/android/gen';
+
+package api;
+
+service Libwg {
+ rpc StopGrpc(StopGrpcRequest) returns (StopGrpcResponse);
+ rpc Version(VersionRequest) returns (VersionResponse);
+ rpc StartHttpProxy(StartHttpProxyRequest) returns (StartHttpProxyResponse);
+ rpc StopHttpProxy(StopHttpProxyRequest) returns (StopHttpProxyResponse);
+ rpc Reverse(stream ReverseRequest) returns (stream ReverseResponse);
+ rpc IpcSet(IpcSetRequest) returns (IpcSetResponse);
+ rpc Dhcp(DhcpRequest) returns (DhcpResponse);
+ rpc CapabilitiesChanged(CapabilitiesChangedRequest) returns (CapabilitiesChangedResponse);
+}
+
+message TunnelHandle { int32 handle = 1; }
+
+message Error {
+ enum Code {
+ NO_ERROR = 0;
+ UNSPECIFIED = 1;
+ INVALID_PROTOCOL_BUFFER = 2;
+ INVALID_RESPONSE = 3;
+ }
+ Code code = 1;
+ string message = 2;
+}
+
+message InetAddress {
+ bytes address = 1;
+}
+
+message InetSocketAddress {
+ InetAddress address = 1;
+ uint32 port = 2;
+}
+
+message StopGrpcRequest {
+}
+
+message StopGrpcResponse {
+}
+
+message VersionRequest {
+}
+
+message VersionResponse {
+ string version = 1;
+}
+
+message StartHttpProxyRequest {
+ oneof pacFile {
+ string pacFileUrl = 1;
+ string pacFileContent = 2;
+ }
+}
+
+message StartHttpProxyResponse {
+ uint32 listen_port = 1;
+ Error error = 2;
+}
+
+message StopHttpProxyRequest {
+}
+
+message StopHttpProxyResponse {
+ Error error = 1;
+}
+
+message ReverseRequest {
+ oneof response {
+ GetConnectionOwnerUidResponse uid = 1;
+ }
+}
+
+message ReverseResponse {
+ oneof request {
+ GetConnectionOwnerUidRequest uid = 1;
+ }
+}
+
+message GetConnectionOwnerUidRequest {
+ // ConnectivityManager.getConnectionOwnerUid(int protocol,
+ // InetSocketAddress local, InetSocketAddress remote)
+ int32 protocol = 1;
+ InetSocketAddress local = 2;
+ InetSocketAddress remote = 3;
+}
+
+message GetConnectionOwnerUidResponse {
+ int32 uid = 1;
+ string package = 2; // context.getPackageManager().getNameForUid()
+}
+
+message IpcSetRequest {
+ TunnelHandle tunnel = 1;
+ string config = 2;
+}
+
+message IpcSetResponse {
+ Error error = 1;
+}
+
+message Lease {
+ InetAddress address = 1;
+ google.protobuf.Duration preferred_lifetime = 2;
+ google.protobuf.Duration valid_lifetime = 3;
+}
+
+message DhcpRequest {
+ InetAddress relay = 1;
+ InetAddress source = 2;
+}
+
+message DhcpResponse {
+ Error error = 1;
+ repeated Lease leases = 2;
+}
+
+message CapabilitiesChangedRequest {
+ enum Capability {
+ NONE = 0;
+ NOT_METERED = 11;
+ };
+
+ repeated Capability capabilities = 1;
+}
+
+message CapabilitiesChangedResponse {
+ Error error = 1;
+}
diff --git a/tunnel/tools/libwg-go/.gitignore b/tunnel/tools/libwg-go/.gitignore
index d1638636..d69ee823 100644
--- a/tunnel/tools/libwg-go/.gitignore
+++ b/tunnel/tools/libwg-go/.gitignore
@@ -1 +1,2 @@
-build/ \ No newline at end of file
+build/
+/gen/
diff --git a/tunnel/tools/libwg-go/Makefile b/tunnel/tools/libwg-go/Makefile
index f1936e1d..5ae4548d 100644
--- a/tunnel/tools/libwg-go/Makefile
+++ b/tunnel/tools/libwg-go/Makefile
@@ -19,6 +19,7 @@ export CGO_LDFLAGS := $(CLANG_FLAGS) $(patsubst -Wl$(comma)--build-id=%,-Wl$(com
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
export GOOS := android
export CGO_ENABLED := 1
+export GOPATH ?= $(HOME)/go
GO_VERSION := 1.20.3
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
@@ -27,6 +28,13 @@ GO_HASH_darwin-amd64 := c1e1161d6d859deb576e6cfabeb40e3d042ceb1c6f444f617c3c9d76
GO_HASH_darwin-arm64 := 86b0ed0f2b2df50fa8036eea875d1cf2d76cefdacf247c44639a1464b7e36b95
GO_HASH_linux-amd64 := 979694c2c25c735755bf26f4f45e19e64e4811d661dd07b8c010f7a8e18adfca
+PROTOC_VERSION := 3.22.4
+PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go
+PROTOC := $(GRADLE_USER_HOME)/caches/protoc-bin-$(PROTOC_VERSION)/protoc
+PROTODIR = $(CURDIR)/../../src/main/proto
+PROTO_INCLUDEDIR = $(CURDIR)/../../build/extracted-include-protos/debug
+PBDIR = $(GOPATH)/pkg/mod
+
default: $(DESTDIR)/libwg-go.so
$(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
@@ -45,8 +53,25 @@ $(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_T
patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \
touch "$@"'
+$(PROTOC_GEN_GO): export GOARCH :=
+$(PROTOC_GEN_GO): export GOOS :=
+$(PROTOC_GEN_GO): export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
+$(PROTOC_GEN_GO): $(BUILDDIR)/go-$(GO_VERSION)/.prepared Makefile
+ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
+ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
+
+gen/%.pb.go: export PATH := $(GOPATH)/bin:$(PATH)
+gen/%.pb.go: $(PROTODIR)/%.proto $(BUILDDIR)/go-$(GO_VERSION)/.prepared $(PROTOC_GEN_GO)
+ test -d gen || mkdir gen
+ $(PROTOC) -I $(PROTODIR) -I $(PROTO_INCLUDEDIR) --go_out=paths=source_relative:./gen $<
+
+gen/%_grpc.pb.go: export PATH := $(GOPATH)/bin:$(PATH)
+gen/%_grpc.pb.go: $(PROTODIR)/%.proto $(BUILDDIR)/go-$(GO_VERSION)/.prepared $(PROTOC_GEN_GO)
+ test -d gen || mkdir gen
+ $(PROTOC) -I $(PROTODIR) -I $(PROTO_INCLUDEDIR) --go-grpc_out=./gen --go-grpc_opt=paths=source_relative $<
+
$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
-$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
+$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod api-android.go dhcp.go http-proxy.go service.go gen/libwg.pb.go gen/libwg_grpc.pb.go jni.c
go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared
.DELETE_ON_ERROR:
diff --git a/tunnel/tools/libwg-go/api-android.go b/tunnel/tools/libwg-go/api-android.go
index d47c5d76..5d9e8b96 100644
--- a/tunnel/tools/libwg-go/api-android.go
+++ b/tunnel/tools/libwg-go/api-android.go
@@ -48,10 +48,17 @@ func (l AndroidLogger) Printf(format string, args ...interface{}) {
type TunnelHandle struct {
device *device.Device
uapi net.Listener
+ logger *device.Logger
+ tun *tun.NativeTun
}
var tunnelHandles map[int32]TunnelHandle
+func GetTunnel(handle int32) (tunnelHandle TunnelHandle, ok bool) {
+ tunnelHandle, ok = tunnelHandles[handle]
+ return
+}
+
func init() {
tunnelHandles = make(map[int32]TunnelHandle)
signals := make(chan os.Signal)
@@ -142,10 +149,29 @@ func wgTurnOn(interfaceName string, tunFd int32, settings string) int32 {
device.Close()
return -1
}
- tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
+ tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi, tun: tun}
return i
}
+//export wgSetFd
+func wgSetFd(tunnelHandle int32, tunFd int32) {
+ tag := cstring(fmt.Sprintf("WireGuard/GoBackend/%x", tunnelHandle))
+ logger := &device.Logger{
+ Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf,
+ Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf,
+ }
+
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ unix.Close(int(tunFd))
+ logger.Errorf("Tunnel not found")
+ return
+ }
+
+ handle.tun.SetFd(int(tunFd))
+ logger.Verbosef("wgSetFd: %v", tunFd)
+}
+
//export wgTurnOff
func wgTurnOff(tunnelHandle int32) {
handle, ok := tunnelHandles[tunnelHandle]
@@ -208,20 +234,53 @@ func wgGetConfig(tunnelHandle int32) *C.char {
//export wgVersion
func wgVersion() *C.char {
+ return C.CString(Version())
+}
+
+func Version() string {
info, ok := debug.ReadBuildInfo()
if !ok {
- return C.CString("unknown")
+ return "unknown"
}
for _, dep := range info.Deps {
if dep.Path == "golang.zx2c4.com/wireguard" {
parts := strings.Split(dep.Version, "-")
if len(parts) == 3 && len(parts[2]) == 12 {
- return C.CString(parts[2][:7])
+ return parts[2][:7]
}
- return C.CString(dep.Version)
+ return dep.Version
}
}
- return C.CString("unknown")
+ return "unknown"
+}
+
+//export wgStartGrpc
+func wgStartGrpc(sock_path string) C.int{
+ tag := cstring("WireGuard/GoBackend/gRPC")
+ logger := &device.Logger{
+ Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf,
+ Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf,
+ }
+
+ res, errmsg := StartGrpc(sock_path, logger)
+ if res < 0 {
+ logger.Verbosef("wgStartGrpc: %v (%v)", errmsg, res)
+ }
+
+ return C.int(res)
+}
+
+//export wgSetConfig
+func wgSetConfig(tunnelHandle int32, settings string) int32 {
+ handle, ok := tunnelHandles[tunnelHandle]
+ if !ok {
+ return -1
+ }
+ err := handle.device.IpcSet(settings)
+ if err != nil {
+ return -1
+ }
+ return 0
}
func main() {}
diff --git a/tunnel/tools/libwg-go/dhcp.go b/tunnel/tools/libwg-go/dhcp.go
new file mode 100644
index 00000000..4af5b0cb
--- /dev/null
+++ b/tunnel/tools/libwg-go/dhcp.go
@@ -0,0 +1,193 @@
+package main
+
+import (
+ "context"
+ "net"
+ "net/netip"
+ "time"
+
+ "github.com/insomniacslk/dhcp/dhcpv4"
+ "github.com/insomniacslk/dhcp/dhcpv6"
+ "github.com/insomniacslk/dhcp/dhcpv6/nclient6"
+ "github.com/insomniacslk/dhcp/iana"
+
+ gen "golang.zx2c4.com/wireguard/android/gen"
+ "google.golang.org/protobuf/types/known/durationpb"
+)
+
+const (
+ ENABLE_PD = true
+)
+
+type dhcp struct {
+ fqdn string
+ hwAddr net.HardwareAddr
+ conn *net.UDPConn
+ serverAddr net.UDPAddr
+ client *nclient6.Client
+ linkAddr net.IP
+ peerAddr net.IP
+}
+
+func newClientIDOpt(duid dhcpv6.DUID) dhcpv4.Option {
+ iaid := []byte{0, 0, 0, 3}
+ ident := []byte{255} // Type IAID+DUID
+ ident = append(ident, iaid...) // IAID
+ ident = append(ident, duid.ToBytes()...) // DUID
+ return dhcpv4.OptClientIdentifier(ident)
+}
+
+func getDuid(hwAddr net.HardwareAddr) dhcpv6.DUID {
+ duid := &dhcpv6.DUIDLLT{
+ HWType: iana.HWTypeEthernet,
+ Time: uint32(time.Now().Unix()),
+ LinkLayerAddr: hwAddr,
+ }
+
+ return duid
+}
+
+func (d *dhcp) encapSendAndRead(ctx context.Context, msg *dhcpv6.Message, match nclient6.Matcher) (*dhcpv6.Message, error) {
+ packet, err := dhcpv6.EncapsulateRelay(msg, dhcpv6.MessageTypeRelayForward, d.linkAddr, d.peerAddr)
+ if err != nil {
+ return nil, err
+ }
+
+ relay, err := d.client.SendAndReadRelay(ctx, &d.serverAddr, packet, match)
+ if err != nil {
+ return nil, err
+ }
+
+ inner, err := relay.GetInnerMessage()
+ if err != nil {
+ return nil, err
+ }
+
+ return inner, nil
+}
+
+// isRelayMessageType returns a matcher that checks for the message type.
+func isRelayMessageType(t dhcpv6.MessageType, tt ...dhcpv6.MessageType) nclient6.Matcher {
+ return func(p dhcpv6.DHCPv6) bool {
+ inner, err := p.GetInnerMessage()
+ if err != nil {
+ return false
+ }
+ if inner.Type() == t {
+ return true
+ }
+ for _, mt := range tt {
+ if inner.Type() == mt {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+// func New() *dhcp {
+// }
+
+func RunDhcp(ctx context.Context, laddr, raddr netip.Addr) ([]*gen.Lease, error) {
+ d := &dhcp{}
+
+ d.linkAddr = net.ParseIP("fe80::101")
+ d.peerAddr = net.ParseIP("::1")
+
+ // TODO generate hostname and hwAddr from public key
+ hostName := "foobar"
+ d.fqdn = hostName + ".m7n.se"
+ d.hwAddr = []byte{41, 42, 43, 44, 45, 46}
+
+
+ src := net.UDPAddr{IP: laddr.AsSlice(),
+ Port: 0, // Use non-restrict UDP source port
+ }
+
+
+ d.serverAddr = net.UDPAddr{IP: raddr.AsSlice(),
+ Port: 547,
+ }
+
+ err := d.Start(&src)
+ if err != nil {
+ return nil, err
+ }
+
+ defer d.Close()
+
+ reply, err := d.ObtainLease(ctx) // Use reply
+ if err != nil {
+ return nil, err
+ }
+
+ return getAddressesFromReply(reply), nil
+}
+
+func getAddressesFromReply(reply *dhcpv6.Message) []*gen.Lease{
+ var leases []*gen.Lease = make([]*gen.Lease, 0, 4)
+
+ if opt := reply.GetOneOption(dhcpv6.OptionIANA); opt != nil {
+ iana := opt.(*dhcpv6.OptIANA)
+ ianaOpts := iana.Options.Get(dhcpv6.OptionIAAddr)
+
+ for _, opt := range ianaOpts {
+ addrOpt := opt.(*dhcpv6.OptIAAddress)
+ lease := &gen.Lease{
+ Address: &gen.InetAddress{
+ Address: addrOpt.IPv6Addr,
+ },
+ PreferredLifetime: durationpb.New(addrOpt.PreferredLifetime),
+ ValidLifetime: durationpb.New(addrOpt.ValidLifetime),
+ }
+ leases = append(leases, lease)
+ }
+ }
+
+ return leases
+}
+
+func (d *dhcp) Start(localAddr *net.UDPAddr) error {
+ conn, err := net.ListenUDP("udp6", localAddr)
+ if err != nil {
+ return err
+ }
+
+ d.client, err = nclient6.NewWithConn(conn, d.hwAddr, nclient6.WithDebugLogger())
+ return err
+}
+
+func (d *dhcp) Close() error {
+ err := d.client.Close()
+
+ d.client = nil
+ return err
+}
+
+func (d *dhcp) ObtainLease(ctx context.Context) (*dhcpv6.Message, error){
+ duidOpt := dhcpv6.WithClientID(getDuid(d.hwAddr))
+ fqdnOpt := dhcpv6.WithFQDN(0x1, d.fqdn)
+
+ solicit, err := dhcpv6.NewSolicit(d.hwAddr, duidOpt, fqdnOpt, dhcpv6.WithRapidCommit)
+ if err != nil {
+ return nil, err
+ }
+
+ msg, err := d.encapSendAndRead(ctx, solicit, isRelayMessageType(dhcpv6.MessageTypeReply, dhcpv6.MessageTypeAdvertise))
+ if err != nil {
+ return nil, err
+ }
+
+ if msg.Type() == dhcpv6.MessageTypeReply {
+ // We got RapidCommitted.
+ return msg, nil
+ }
+
+ // We didn't get RapidCommitted. Request regular lease.
+ req, err := dhcpv6.NewRequestFromAdvertise(msg, fqdnOpt)
+ if err != nil {
+ return nil, err
+ }
+
+ return d.encapSendAndRead(ctx, req, nil)
+}
diff --git a/tunnel/tools/libwg-go/go.mod b/tunnel/tools/libwg-go/go.mod
index 9318ebcf..8f17923c 100644
--- a/tunnel/tools/libwg-go/go.mod
+++ b/tunnel/tools/libwg-go/go.mod
@@ -3,12 +3,27 @@ module golang.zx2c4.com/wireguard/android
go 1.20
require (
- golang.org/x/sys v0.6.0
- golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675
+ github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b
+ github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
+ golang.org/x/net v0.7.0
+ golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89
+ golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d
+ google.golang.org/grpc v1.42.0-dev.0.20211020220737-f00baa6c3c84
+ google.golang.org/protobuf v1.27.1
+ gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440
)
require (
- golang.org/x/crypto v0.7.0 // indirect
- golang.org/x/net v0.8.0 // indirect
+ github.com/golang/protobuf v1.5.2 // indirect
+ github.com/josharian/native v1.1.0 // indirect
+ github.com/pierrec/lz4/v4 v4.1.14 // indirect
+ github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
+ golang.org/x/crypto v0.6.0 // indirect
+ golang.org/x/text v0.7.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
+ google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect
)
+
+replace github.com/insomniacslk/dhcp => /src/insomniacslk-dhcp
+
+replace golang.zx2c4.com/wireguard => /src/wireguard-go
diff --git a/tunnel/tools/libwg-go/go.sum b/tunnel/tools/libwg-go/go.sum
index 3b49b497..dfd31072 100644
--- a/tunnel/tools/libwg-go/go.sum
+++ b/tunnel/tools/libwg-go/go.sum
@@ -1,10 +1,173 @@
-golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
-golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b h1:1XqENn2YoYZd6w3Awx+7oa+aR87DFIZJFLF2n1IojA0=
+github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
+github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
+github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
+github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
+github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
+github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+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/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
+github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89 h1:260HNjMTPDya+jq5AM1zZLgG9pv9GASPAGiEEJUbRg4=
+golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
-golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU=
-golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f h1:YORWxaStkWBnWgELOHTmDrqNlFXuVGEbhwbB5iK94bQ=
+google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.42.0-dev.0.20211020220737-f00baa6c3c84 h1:hZAzgyItS2MPyqvdC8wQZI99ZLGP9Vwijyfr0dmYWc4=
+google.golang.org/grpc v1.42.0-dev.0.20211020220737-f00baa6c3c84/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440 h1:SxFAMd+8zfpL/Rk4pgdb8leeZDiL3M/gCWCbBvmLkoE=
+gopkg.in/olebedev/go-duktape.v3 v3.0.0-20210326210528-650f7c854440/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/tunnel/tools/libwg-go/http-proxy.go b/tunnel/tools/libwg-go/http-proxy.go
new file mode 100644
index 00000000..11ab0716
--- /dev/null
+++ b/tunnel/tools/libwg-go/http-proxy.go
@@ -0,0 +1,736 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/netip"
+ "net/url"
+ "strings"
+ "sync"
+
+ "github.com/elazarl/goproxy"
+
+ "golang.zx2c4.com/wireguard/device"
+
+ net_proxy "golang.org/x/net/proxy"
+
+ "gopkg.in/olebedev/go-duktape.v3"
+)
+
+const (
+ // Imported from Firefox' ProxyAutoConfig.cpp
+ // https://searchfox.org/mozilla-central/source/netwerk/base/ProxyAutoConfig.cpp
+ // This Source Code Form is subject to the terms of the Mozilla Public
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
+ // file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ ASCII_PAC_UTILS = "function dnsDomainIs(host, domain) {\n" +
+ " return (host.length >= domain.length &&\n" +
+ " host.substring(host.length - domain.length) == domain);\n" +
+ "}\n" +
+ "" +
+ "function dnsDomainLevels(host) {\n" +
+ " return host.split('.').length - 1;\n" +
+ "}\n" +
+ "" +
+ "function isValidIpAddress(ipchars) {\n" +
+ " var matches = " +
+ "/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(ipchars);\n" +
+ " if (matches == null) {\n" +
+ " return false;\n" +
+ " } else if (matches[1] > 255 || matches[2] > 255 || \n" +
+ " matches[3] > 255 || matches[4] > 255) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " return true;\n" +
+ "}\n" +
+ "" +
+ "function convert_addr(ipchars) {\n" +
+ " var bytes = ipchars.split('.');\n" +
+ " var result = ((bytes[0] & 0xff) << 24) |\n" +
+ " ((bytes[1] & 0xff) << 16) |\n" +
+ " ((bytes[2] & 0xff) << 8) |\n" +
+ " (bytes[3] & 0xff);\n" +
+ " return result;\n" +
+ "}\n" +
+ "" +
+ "function isInNet(ipaddr, pattern, maskstr) {\n" +
+ " if (!isValidIpAddress(pattern) || !isValidIpAddress(maskstr)) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " if (!isValidIpAddress(ipaddr)) {\n" +
+ " ipaddr = dnsResolve(ipaddr);\n" +
+ " if (ipaddr == null) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " }\n" +
+ " var host = convert_addr(ipaddr);\n" +
+ " var pat = convert_addr(pattern);\n" +
+ " var mask = convert_addr(maskstr);\n" +
+ " return ((host & mask) == (pat & mask));\n" +
+ " \n" +
+ "}\n" +
+ "" +
+ "function isPlainHostName(host) {\n" +
+ " return (host.search('(\\\\.)|:') == -1);\n" +
+ "}\n" +
+ "" +
+ "function isResolvable(host) {\n" +
+ " var ip = dnsResolve(host);\n" +
+ " return (ip != null);\n" +
+ "}\n" +
+ "" +
+ "function localHostOrDomainIs(host, hostdom) {\n" +
+ " return (host == hostdom) ||\n" +
+ " (hostdom.lastIndexOf(host + '.', 0) == 0);\n" +
+ "}\n" +
+ "" +
+ "function shExpMatch(url, pattern) {\n" +
+ " pattern = pattern.replace(/\\./g, '\\\\.');\n" +
+ " pattern = pattern.replace(/\\*/g, '.*');\n" +
+ " pattern = pattern.replace(/\\?/g, '.');\n" +
+ " var newRe = new RegExp('^'+pattern+'$');\n" +
+ " return newRe.test(url);\n" +
+ "}\n" +
+ "" +
+ "var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};\n" +
+ "var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, " +
+ "AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};\n" +
+ "" +
+ "function weekdayRange() {\n" +
+ " function getDay(weekday) {\n" +
+ " if (weekday in wdays) {\n" +
+ " return wdays[weekday];\n" +
+ " }\n" +
+ " return -1;\n" +
+ " }\n" +
+ " var date = new Date();\n" +
+ " var argc = arguments.length;\n" +
+ " var wday;\n" +
+ " if (argc < 1)\n" +
+ " return false;\n" +
+ " if (arguments[argc - 1] == 'GMT') {\n" +
+ " argc--;\n" +
+ " wday = date.getUTCDay();\n" +
+ " } else {\n" +
+ " wday = date.getDay();\n" +
+ " }\n" +
+ " var wd1 = getDay(arguments[0]);\n" +
+ " var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;\n" +
+ " return (wd1 == -1 || wd2 == -1) ? false\n" +
+ " : (wd1 <= wd2) ? (wd1 <= wday && wday " +
+ "<= wd2)\n" +
+ " : (wd2 >= wday || wday " +
+ ">= wd1);\n" +
+ "}\n" +
+ "" +
+ "function dateRange() {\n" +
+ " function getMonth(name) {\n" +
+ " if (name in months) {\n" +
+ " return months[name];\n" +
+ " }\n" +
+ " return -1;\n" +
+ " }\n" +
+ " var date = new Date();\n" +
+ " var argc = arguments.length;\n" +
+ " if (argc < 1) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " var isGMT = (arguments[argc - 1] == 'GMT');\n" +
+ "\n" +
+ " if (isGMT) {\n" +
+ " argc--;\n" +
+ " }\n" +
+ " // function will work even without explict handling of this case\n" +
+ " if (argc == 1) {\n" +
+ " var tmp = parseInt(arguments[0]);\n" +
+ " if (isNaN(tmp)) {\n" +
+ " return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==\n" +
+ " getMonth(arguments[0]));\n" +
+ " } else if (tmp < 32) {\n" +
+ " return ((isGMT ? date.getUTCDate() : date.getDate()) == " +
+ "tmp);\n" +
+ " } else { \n" +
+ " return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) " +
+ "==\n" +
+ " tmp);\n" +
+ " }\n" +
+ " }\n" +
+ " var year = date.getFullYear();\n" +
+ " var date1, date2;\n" +
+ " date1 = new Date(year, 0, 1, 0, 0, 0);\n" +
+ " date2 = new Date(year, 11, 31, 23, 59, 59);\n" +
+ " var adjustMonth = false;\n" +
+ " for (var i = 0; i < (argc >> 1); i++) {\n" +
+ " var tmp = parseInt(arguments[i]);\n" +
+ " if (isNaN(tmp)) {\n" +
+ " var mon = getMonth(arguments[i]);\n" +
+ " date1.setMonth(mon);\n" +
+ " } else if (tmp < 32) {\n" +
+ " adjustMonth = (argc <= 2);\n" +
+ " date1.setDate(tmp);\n" +
+ " } else {\n" +
+ " date1.setFullYear(tmp);\n" +
+ " }\n" +
+ " }\n" +
+ " for (var i = (argc >> 1); i < argc; i++) {\n" +
+ " var tmp = parseInt(arguments[i]);\n" +
+ " if (isNaN(tmp)) {\n" +
+ " var mon = getMonth(arguments[i]);\n" +
+ " date2.setMonth(mon);\n" +
+ " } else if (tmp < 32) {\n" +
+ " date2.setDate(tmp);\n" +
+ " } else {\n" +
+ " date2.setFullYear(tmp);\n" +
+ " }\n" +
+ " }\n" +
+ " if (adjustMonth) {\n" +
+ " date1.setMonth(date.getMonth());\n" +
+ " date2.setMonth(date.getMonth());\n" +
+ " }\n" +
+ " if (isGMT) {\n" +
+ " var tmp = date;\n" +
+ " tmp.setFullYear(date.getUTCFullYear());\n" +
+ " tmp.setMonth(date.getUTCMonth());\n" +
+ " tmp.setDate(date.getUTCDate());\n" +
+ " tmp.setHours(date.getUTCHours());\n" +
+ " tmp.setMinutes(date.getUTCMinutes());\n" +
+ " tmp.setSeconds(date.getUTCSeconds());\n" +
+ " date = tmp;\n" +
+ " }\n" +
+ " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" +
+ " : (date2 >= date) || (date >= date1);\n" +
+ "}\n" +
+ "" +
+ "function timeRange() {\n" +
+ " var argc = arguments.length;\n" +
+ " var date = new Date();\n" +
+ " var isGMT= false;\n" +
+ "" +
+ " if (argc < 1) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " if (arguments[argc - 1] == 'GMT') {\n" +
+ " isGMT = true;\n" +
+ " argc--;\n" +
+ " }\n" +
+ "\n" +
+ " var hour = isGMT ? date.getUTCHours() : date.getHours();\n" +
+ " var date1, date2;\n" +
+ " date1 = new Date();\n" +
+ " date2 = new Date();\n" +
+ "\n" +
+ " if (argc == 1) {\n" +
+ " return (hour == arguments[0]);\n" +
+ " } else if (argc == 2) {\n" +
+ " return ((arguments[0] <= hour) && (hour <= arguments[1]));\n" +
+ " } else {\n" +
+ " switch (argc) {\n" +
+ " case 6:\n" +
+ " date1.setSeconds(arguments[2]);\n" +
+ " date2.setSeconds(arguments[5]);\n" +
+ " case 4:\n" +
+ " var middle = argc >> 1;\n" +
+ " date1.setHours(arguments[0]);\n" +
+ " date1.setMinutes(arguments[1]);\n" +
+ " date2.setHours(arguments[middle]);\n" +
+ " date2.setMinutes(arguments[middle + 1]);\n" +
+ " if (middle == 2) {\n" +
+ " date2.setSeconds(59);\n" +
+ " }\n" +
+ " break;\n" +
+ " default:\n" +
+ " throw 'timeRange: bad number of arguments'\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ " if (isGMT) {\n" +
+ " date.setFullYear(date.getUTCFullYear());\n" +
+ " date.setMonth(date.getUTCMonth());\n" +
+ " date.setDate(date.getUTCDate());\n" +
+ " date.setHours(date.getUTCHours());\n" +
+ " date.setMinutes(date.getUTCMinutes());\n" +
+ " date.setSeconds(date.getUTCSeconds());\n" +
+ " }\n" +
+ " return (date1 <= date2) ? (date1 <= date) && (date <= date2)\n" +
+ " : (date2 >= date) || (date >= date1);\n" +
+ "\n" +
+ "}\n"
+)
+
+type HttpProxy struct {
+ listener net.Listener
+ tlsListener net.Listener
+ logger *device.Logger
+ addrPort netip.AddrPort
+ tlsAddrPort netip.AddrPort
+ ctx *duktape.Context
+ defaultProxy *goproxy.ProxyHttpServer
+ uidRequest chan UidRequest
+ handlers []*HttpHandler
+}
+
+func NewHttpProxy(uidRequest chan UidRequest, logger *device.Logger) *HttpProxy {
+ logger.Verbosef("NewHttpProxy")
+ return &HttpProxy{
+ listener: nil,
+ logger: logger,
+ uidRequest: uidRequest,
+ handlers: make([]*HttpHandler, 0, 2),
+ }
+}
+
+var (
+ ASCII_PAC_UTILS_NAMES = []string{"dnsDomainIs", "dnsDomainLevels", "isValidIpAddress", "convert_addr", "isInNet", "isPlainHostName", "isResolvable", "localHostOrDomainIs", "shExpMatch", "weekdayRange", "dateRange", "timeRange"}
+)
+
+func (p *HttpProxy) GetAddrPort() netip.AddrPort {
+ return p.addrPort
+}
+
+type Logger struct {
+ logger *device.Logger
+}
+
+func (l *Logger) Printf(format string, v ...interface{}) {
+ l.logger.Verbosef(format, v...)
+}
+
+func (p *HttpProxy) newGoProxy(cat, proxyUrl string) (*goproxy.ProxyHttpServer, error) {
+ proxy := goproxy.NewProxyHttpServer()
+ proxy.Logger = &Logger{logger: p.logger}
+ proxy.Verbose = true
+ if cat == "SOCKS5" {
+ socksDialer, err := net_proxy.SOCKS5("tcp", proxyUrl, nil, nil)
+ if err != nil {
+ return nil, err
+ }
+ proxy.Tr.Dial = socksDialer.Dial
+ }
+ proxy.NonproxyHandler = http.HandlerFunc(func (w http.ResponseWriter, req *http.Request) {
+ if req.Host == "" {
+ fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0")
+ return
+ }
+ req.URL.Scheme = "http"
+ req.URL.Host = req.Host
+ proxy.ServeHTTP(w, req)
+ })
+
+ if cat == "PROXY" {
+ proxy.Tr.Proxy = func(req *http.Request) (*url.URL, error) {
+ return url.Parse(proxyUrl)
+ }
+ proxy.ConnectDial = proxy.NewConnectDialToProxy(proxyUrl)
+ }
+
+ return proxy, nil
+}
+
+func FindProxyForURL(ctx *duktape.Context, url, host string, logger *device.Logger) (res string, err error) {
+ if !ctx.GetGlobalString("FindProxyForURL") {
+ ctx.Pop()
+ return "", fmt.Errorf("FindProxyForURL not found")
+ }
+
+ ctx.PushString(url)
+ ctx.PushString(host)
+
+ res = "DIRECT"
+ logger.Verbosef("Before pcall")
+ r := ctx.Pcall(2)
+ logger.Verbosef("After pcall")
+ if r == 0 {
+ res = ctx.GetString(-1)
+ } else if ctx.IsError(-1) {
+ ctx.GetPropString(-1, "stack")
+ err = fmt.Errorf("Error: %v", ctx.SafeToString(-1))
+ } else {
+ err = fmt.Errorf("Error: %v", ctx.SafeToString(-1))
+ }
+ ctx.Pop()
+ return
+}
+
+
+func FindProxyForPkg(ctx *duktape.Context, pkg string, logger *device.Logger) (res string, err error) {
+ if !ctx.GetGlobalString("FindProxyForPkg") {
+ ctx.Pop()
+ return "", fmt.Errorf("FindProxyForPkg not found")
+ }
+
+ ctx.PushString(pkg)
+
+ res = "DIRECT"
+ logger.Verbosef("Before pcall")
+ r := ctx.Pcall(1)
+ logger.Verbosef("After pcall")
+ if r == 0 {
+ res = ctx.GetString(-1)
+ } else if ctx.IsError(-1) {
+ ctx.GetPropString(-1, "stack")
+ err = fmt.Errorf("Error: %v", ctx.SafeToString(-1))
+ } else {
+ err = fmt.Errorf("Error: %v", ctx.SafeToString(-1))
+ }
+ ctx.Pop()
+ return
+}
+
+func newPacBodyCtx(body string, logger *device.Logger) *duktape.Context {
+ ctx := duktape.New()
+ ctx.PushGlobalGoFunction("dnsResolve",
+ func(ctx *duktape.Context) int {
+ // Check stack
+ host := ctx.GetString(-1)
+ ctx.Pop()
+ ips, err := net.LookupIP(host)
+ if err != nil {
+ return 0
+ }
+
+ for _, ip := range(ips) {
+ ipStr := string(ip)
+ if strings.Contains(ipStr, ".") {
+ // Found IPv4
+ ctx.PushString(ipStr)
+ return 1
+ }
+ }
+ // No IPv4
+ return 0
+ })
+
+ ctx.PevalString(ASCII_PAC_UTILS)
+ logger.Verbosef("ASCII_PAC_UTILS result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop())
+ ctx.Pop()
+
+ ctx.PevalString(string(body))
+ logger.Verbosef("result is: %v stack:%v", ctx.GetType(-1), ctx.GetTop())
+ ctx.Pop()
+ FindProxyForURL(ctx, "http://www.jabra.se/", "www.jabra.se", logger)
+ return ctx
+}
+
+func (p *HttpProxy) newPacFileCtx(pacFileUrl *url.URL) (*duktape.Context, error) {
+ var pacFileBody = ""
+ if pacFileUrl == nil {
+ return nil, nil
+ }
+
+ resp, err := http.Get(pacFileUrl.String())
+ p.logger.Verbosef("pacFile: %v, %v", resp, err)
+
+ if err == nil {
+ defer resp.Body.Close()
+ ct, ok := resp.Header["Content-Type"]
+ if ok && len(ct) == 1 && ct[0] == "application/x-ns-proxy-autoconfig" {
+ body, err := io.ReadAll(resp.Body)
+ if err == nil {
+ pacFileBody = string(body)
+ return newPacBodyCtx(pacFileBody, p.logger), nil
+ }
+ }
+ }
+
+ return nil, err
+}
+
+func (p *HttpProxy) SetPacFileUrl(pacFileUrl *url.URL) error {
+ ctx, err := p.newPacFileCtx(pacFileUrl)
+ if err != nil {
+ return err
+ }
+
+ for _, handler := range(p.handlers) {
+ handler.setPacCtx(ctx)
+ }
+
+ if p.ctx != nil {
+ p.ctx.Destroy()
+ }
+ p.ctx = ctx
+ p.logger.Verbosef("SetNotMetered(false) from SetPacFileUrl")
+ p.SetNotMetered(false)
+ return nil
+}
+
+func (p *HttpProxy) SetPacFileContent(pacFile string) error {
+ ctx := newPacBodyCtx(pacFile, p.logger)
+
+ for _, handler := range(p.handlers) {
+ handler.setPacCtx(ctx)
+ }
+
+ if p.ctx != nil {
+ p.ctx.Destroy()
+ }
+ p.ctx = ctx
+ p.logger.Verbosef("SetNotMetered(false) from SetPacFileContent")
+ p.SetNotMetered(false)
+ return nil
+}
+
+func (p *HttpProxy) SetNotMetered(notMetered bool) error {
+ p.logger.Verbosef("SetNotMetered: %v", notMetered)
+
+ if p.ctx == nil {
+ return fmt.Errorf("No ductape context")
+ }
+
+ p.ctx.PushGlobalObject()
+ p.ctx.PushBoolean(notMetered)
+ p.ctx.PutPropString(-2, "notMetered")
+ p.ctx.Pop()
+
+ return nil
+}
+
+func (p *HttpProxy) Start() (listen_port uint16, err error) {
+ p.logger.Verbosef("HttpProxy.Start()")
+ listen_port = 0
+
+
+ proxyMap := make(map[string]*goproxy.ProxyHttpServer)
+
+ p.defaultProxy, err = p.newGoProxy("", "")
+ if err != nil {
+ return
+ }
+ proxyMap[""] = p.defaultProxy
+
+ listen_port, handler, err := p.startRegularProxy(proxyMap)
+ if err != nil {
+ return
+ }
+ p.handlers = append(p.handlers, handler)
+
+ return
+}
+
+func (p *HttpProxy) startRegularProxy(proxyMap map[string]*goproxy.ProxyHttpServer) (listen_port uint16, handler *HttpHandler, err error) {
+ p.listener, err = net.Listen("tcp", "[::]:")
+ if err != nil {
+ return
+ }
+
+ p.addrPort, err = netip.ParseAddrPort(p.listener.Addr().String())
+ if err != nil {
+ return
+ }
+
+ listen_port = p.addrPort.Port()
+
+ handler = NewHttpHandler(p, p.defaultProxy, proxyMap, p.logger)
+
+ go http.Serve(NewListener(p.listener, handler, p.logger), handler)
+ return
+}
+
+func (p *HttpProxy) Stop() {
+ if p.listener != nil {
+ p.logger.Verbosef("Close: %v", p.listener)
+ p.listener.Close()
+// p.listener = nil
+ }
+ if p.tlsListener != nil {
+ p.logger.Verbosef("Close: %v", p.tlsListener)
+ p.tlsListener.Close()
+// p.tlsListener = nil
+ }
+ if p.ctx != nil {
+ p.ctx.DestroyHeap()
+ p.ctx = nil
+ }
+}
+
+type HttpHandler struct {
+ p *HttpProxy
+ defaultProxy *goproxy.ProxyHttpServer
+ logger *device.Logger
+ remoteAddrPkgMapMutex sync.RWMutex
+ remoteAddrPkgMap map[string]*goproxy.ProxyHttpServer
+ uidRequest chan UidRequest
+ ctx *duktape.Context
+ proxyMap map[string]*goproxy.ProxyHttpServer // FIXME include type in map key
+}
+
+func NewHttpHandler(p *HttpProxy, defaultProxy *goproxy.ProxyHttpServer, proxyMap map[string]*goproxy.ProxyHttpServer, logger *device.Logger) *HttpHandler{
+ h := &HttpHandler{
+ p: p,
+ defaultProxy: defaultProxy,
+ logger: logger,
+ remoteAddrPkgMapMutex: sync.RWMutex{},
+ remoteAddrPkgMap: make(map[string]*goproxy.ProxyHttpServer),
+ uidRequest: p.uidRequest,
+ proxyMap: proxyMap,
+ }
+
+ return h
+}
+
+// TODO fix multi-threading
+func (h *HttpHandler) setPacCtx(ctx *duktape.Context) {
+ h.ctx = ctx
+}
+
+func (h *HttpHandler) addConnToProxyMap(c net.Conn) error {
+ h.logger.Verbosef("Accept: %v -> %v", c.RemoteAddr().String(), c.LocalAddr().String())
+ local, err := netip.ParseAddrPort(c.RemoteAddr().String())
+ if err != nil {
+ return fmt.Errorf("Bad local address (%v): %v", c.RemoteAddr(), err)
+ }
+ // Remove IPv6 zone, NOOP on IPv4
+ local = netip.AddrPortFrom(local.Addr().WithZone(""), local.Port())
+
+ remote, err := netip.ParseAddrPort(c.LocalAddr().String())
+ if err != nil {
+ return fmt.Errorf("Bad remote address (%v): %v", c.LocalAddr(), err)
+ }
+
+ h.logger.Verbosef("uidRequest: %v -> %v", local, remote)
+ addrPortPair := AddrPortPair{local: local, remote: remote}
+ retCh := make(chan string)
+ h.uidRequest <- UidRequest{Data: addrPortPair, RetCh: retCh}
+
+ select {
+ case pkg := <-retCh:
+ h.logger.Verbosef("uidResponse: '%v'", pkg)
+
+ // TODO add NotMetered as
+ proxy, ok := h.proxyMap[pkg]
+ if !ok {
+ proxy, err = h.findProxyForPkg(pkg)
+ if err != nil {
+ return err
+ }
+ }
+
+ if proxy != nil {
+ h.remoteAddrPkgMapMutex.Lock()
+ h.remoteAddrPkgMap[c.RemoteAddr().String()] = proxy
+ h.remoteAddrPkgMapMutex.Unlock()
+ }
+ }
+
+ return nil
+}
+
+func (h *HttpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ h.remoteAddrPkgMapMutex.Lock()
+ proxy, ok := h.remoteAddrPkgMap[req.RemoteAddr]
+ if ok && proxy != nil {
+ delete(h.remoteAddrPkgMap, req.RemoteAddr)
+ h.remoteAddrPkgMapMutex.Unlock()
+
+ proxyStr := "nil"
+ proxyUrl, err := proxy.Tr.Proxy(req)
+ if err == nil && proxyUrl != nil {
+ proxyStr = proxyUrl.String()
+ }
+ h.logger.Verbosef("ServeHTTP remote:%s proxy:%v", req.RemoteAddr, proxyStr)
+
+ proxy.ServeHTTP(rw, req)
+ } else {
+ h.remoteAddrPkgMapMutex.Unlock()
+ h.defaultProxy.ServeHTTP(rw, req)
+ }
+}
+
+func (h *HttpHandler) findProxyForPkg(pkg string) (*goproxy.ProxyHttpServer, error) {
+ if h.ctx == nil {
+ return nil, fmt.Errorf("Null context")
+ }
+
+ find := func() (cat string, res string, err error) {
+ h.logger.Verbosef("Call FindProxyForPkg %v %v", pkg)
+ res, err = FindProxyForPkg(h.ctx, pkg, h.logger)
+ h.logger.Verbosef("FindProxyForPkg res %v %v", res, err)
+ if err != nil {
+ h.logger.Verbosef("FindProxyForPkg result is: %v stack:%v", res, h.ctx.GetTop())
+ return "", "", err
+ } else if res == "" {
+ return "DIRECT", "", nil
+ } else {
+ values := strings.Split(strings.Trim(res, " "), ";")
+ for _, v := range values {
+ value := strings.Trim(v, " ")
+ parts := strings.SplitN(value, " ", 2)
+ if parts[0] == "PROXY" || parts[0] == "HTTP" {
+ return "PROXY", "http://" + parts[1], nil
+ } else if parts[0] == "HTTPS" {
+ return "PROXY", "https://" + parts[1], nil
+ } else if parts[0] == "DIRECT" {
+ return parts[0], "", nil
+ } else if parts[0] == "SOCKS" || parts[0] == "SOCKS5" {
+ return "SOCKS5", parts[1], nil
+ }
+ }
+ }
+ return "", "", fmt.Errorf("No result")
+ }
+ cat, res, err := find()
+ if err != nil {
+ return nil, err
+ }
+
+ var proxy *goproxy.ProxyHttpServer
+ if cat == "DIRECT" {
+ proxy = h.defaultProxy
+ } else {
+ proxy, err = h.p.newGoProxy(cat, res)
+ if err != nil {
+ return nil, err
+ }
+ }
+ h.proxyMap[pkg] = proxy
+
+ return proxy, nil
+}
+
+type AddrPortPair struct {
+ local netip.AddrPort
+ remote netip.AddrPort
+}
+
+// Listener
+type Listener struct {
+ l net.Listener
+ handler *HttpHandler
+ logger *device.Logger
+}
+
+func NewListener(listener net.Listener, handler *HttpHandler, logger *device.Logger) *Listener{
+ l := &Listener{
+ l: listener,
+ handler: handler,
+ logger: logger,
+ }
+
+ return l
+}
+
+func (l *Listener) Accept() (net.Conn, error) {
+ c, err := l.l.Accept()
+ if err != nil {
+ l.logger.Verbosef("Accept failed: %v", err)
+ return c, err
+ }
+
+ if err := l.handler.addConnToProxyMap(c); err != nil {
+ err = fmt.Errorf("Reject connection (%v): %v", c, err)
+ c.Close()
+ return nil, err
+ }
+
+ return c, nil
+}
+
+func (l *Listener) Close() error {
+ return l.l.Close()
+}
+
+func (l *Listener) Addr() net.Addr {
+ return l.l.Addr()
+}
diff --git a/tunnel/tools/libwg-go/jni.c b/tunnel/tools/libwg-go/jni.c
index 7ad94d35..443848e8 100644
--- a/tunnel/tools/libwg-go/jni.c
+++ b/tunnel/tools/libwg-go/jni.c
@@ -14,6 +14,9 @@ extern int wgGetSocketV4(int handle);
extern int wgGetSocketV6(int handle);
extern char *wgGetConfig(int handle);
extern char *wgVersion();
+extern int wgSetConfig(int handle, struct go_string settings);
+extern int wgStartGrpc();
+extern int wgSetFd(int handle, int tun_fd);
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
{
@@ -69,3 +72,32 @@ JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion
free(version);
return ret;
}
+
+JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgSetConfig(JNIEnv *env, jclass c, jint handle, jstring settings)
+{
+ const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0);
+ size_t settings_len = (*env)->GetStringUTFLength(env, settings);
+ int ret = wgSetConfig(handle, (struct go_string){
+ .str = settings_str,
+ .n = settings_len
+ });
+ (*env)->ReleaseStringUTFChars(env, settings, settings_str);
+ return ret;
+}
+
+JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgStartGrpc(JNIEnv *env, jclass c, jstring sockname)
+{
+ const char *sockname_str = (*env)->GetStringUTFChars(env, sockname, 0);
+ size_t sockname_len = (*env)->GetStringUTFLength(env, sockname);
+ jint res = wgStartGrpc((struct go_string){
+ .str = sockname_str,
+ .n = sockname_len
+ });
+ (*env)->ReleaseStringUTFChars(env, sockname, sockname_str);
+ return res;
+}
+
+JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgSetFd(JNIEnv *env, jclass c, jint handle, jint tun_fd)
+{
+ wgSetFd(handle, tun_fd);
+}
diff --git a/tunnel/tools/libwg-go/service.go b/tunnel/tools/libwg-go/service.go
new file mode 100644
index 00000000..41834fe0
--- /dev/null
+++ b/tunnel/tools/libwg-go/service.go
@@ -0,0 +1,342 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "net/netip"
+ "net/url"
+ "os"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
+ gen "golang.zx2c4.com/wireguard/android/gen"
+ "golang.zx2c4.com/wireguard/device"
+)
+
+const (
+ IPPROTO_TCP = 6
+)
+
+type UidRequest struct {
+ Data AddrPortPair
+ RetCh chan string
+}
+
+type LibwgServiceImpl struct {
+ gen.UnimplementedLibwgServer
+ logger *device.Logger
+ httpProxy *HttpProxy
+ uidRequest chan UidRequest
+ stopReverse chan bool
+}
+
+var service *LibwgServiceImpl
+var server *grpc.Server
+
+func NewLibwgService(logger *device.Logger) gen.LibwgServer {
+ return &LibwgServiceImpl{
+ logger: logger,
+ uidRequest: make(chan UidRequest),
+ stopReverse: make(chan bool),
+ }
+}
+
+func StartGrpc(sock_path string, logger *device.Logger) (int, string) {
+ if server != nil {
+ return -1, "Already started"
+ }
+
+ if _, err := os.Stat(sock_path); err == nil {
+ if err := os.RemoveAll(sock_path); err != nil {
+ return -1, fmt.Sprintf("Cleanup failed: %v %v", sock_path, err)
+ }
+ }
+
+ listener, err := net.Listen("unix", sock_path)
+ if err != nil {
+ return -1, fmt.Sprintf("Listen failed: %v %v", sock_path, err)
+ }
+
+ server = grpc.NewServer()
+ service = NewLibwgService(logger).(*LibwgServiceImpl)
+
+ gen.RegisterLibwgServer(server, service)
+
+ go func() {
+ server.Serve(listener)
+ }()
+
+ logger.Verbosef("gRPC started")
+
+ return 0, ""
+}
+
+func (e *LibwgServiceImpl) Version(ctx context.Context, req *gen.VersionRequest) (*gen.VersionResponse, error) {
+
+ r := &gen.VersionResponse{
+ Version: Version(),
+ }
+
+ return r, nil
+}
+
+func (e *LibwgServiceImpl) StopGrpc(ctx context.Context, req *gen.StopGrpcRequest) (*gen.StopGrpcResponse, error) {
+ if server != nil {
+ server.Stop()
+ server = nil
+ service = nil
+ }
+
+ r := &gen.StopGrpcResponse{
+ }
+
+ return r, nil
+}
+
+func buildStartHttpProxyError(message string) (*gen.StartHttpProxyResponse, error) {
+ r := &gen.StartHttpProxyResponse{
+ Error: &gen.Error{
+ Message: message,
+ },
+ }
+ return r, nil
+}
+
+func (e *LibwgServiceImpl) StartHttpProxy(ctx context.Context, req *gen.StartHttpProxyRequest) (*gen.StartHttpProxyResponse, error) {
+ var listenPort uint16
+ if e.httpProxy == nil {
+ e.httpProxy = NewHttpProxy(e.uidRequest, e.logger)
+ var err error
+ listenPort, err = e.httpProxy.Start()
+
+ if err != nil {
+ e.httpProxy = nil
+ return buildStartHttpProxyError(fmt.Sprintf("Http proxy start failed: %v", err))
+ }
+ } else {
+ listenPort = e.httpProxy.GetAddrPort().Port()
+ }
+
+ content := req.GetPacFileContent()
+ if content != "" {
+ err := e.httpProxy.SetPacFileContent(content)
+ if err != nil {
+ return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileContent: %v (%s)", content))
+ }
+ } else {
+ pacFileUrl, err := url.Parse(req.GetPacFileUrl())
+ if err != nil {
+ return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileUrl: %v (%s)", err, req.GetPacFileUrl()))
+ }
+ err = e.httpProxy.SetPacFileUrl(pacFileUrl)
+ if err != nil {
+ return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileUrl: %v (%s)", err, pacFileUrl))
+ }
+ }
+
+ r := &gen.StartHttpProxyResponse{
+ ListenPort: uint32(listenPort),
+ }
+ return r, nil
+}
+
+func (e *LibwgServiceImpl) StopHttpProxy(ctx context.Context, req *gen.StopHttpProxyRequest) (*gen.StopHttpProxyResponse, error) {
+ if e.httpProxy == nil {
+ r := &gen.StopHttpProxyResponse{
+ Error: &gen.Error{
+ Message: fmt.Sprintf("Http proxy not running"),
+ },
+ }
+ return r, nil
+ }
+
+ e.httpProxy.Stop()
+ e.httpProxy = nil
+ e.stopReverse <- true
+ r := &gen.StopHttpProxyResponse{}
+ return r, nil
+}
+
+func (e *LibwgServiceImpl) Reverse(stream gen.Libwg_ReverseServer) error {
+ e.logger.Verbosef("Reverse enter loop")
+ for e.httpProxy != nil {
+ var err error
+
+ // err := contextError(stream.Context())
+ err = stream.Context().Err()
+ if err != nil {
+ e.logger.Verbosef("Reverse: context: %v", err)
+ return err
+ }
+
+ select {
+ case <-e.stopReverse:
+ e.logger.Verbosef("Reverse: stop")
+ break
+ case uidReq := <-e.uidRequest:
+ addrPortPair := uidReq.Data
+ local := addrPortPair.local
+ remote := addrPortPair.remote
+ r := &gen.ReverseResponse{
+ Request: &gen.ReverseResponse_Uid{
+ Uid: &gen.GetConnectionOwnerUidRequest{
+ Protocol: IPPROTO_TCP,
+ Local: &gen.InetSocketAddress{
+ Address: &gen.InetAddress{
+ Address: local.Addr().AsSlice(),
+ },
+ Port: uint32(local.Port()),
+ },
+ Remote: &gen.InetSocketAddress{
+ Address: &gen.InetAddress{
+ Address: remote.Addr().AsSlice(),
+ },
+ Port: uint32(remote.Port()),
+ },
+ },
+ },
+ }
+
+ stream.Send(r)
+
+ req, err := stream.Recv()
+ if err == io.EOF {
+ e.logger.Verbosef("no more data")
+ uidReq.RetCh <- ""
+ break
+ }
+ if err != nil {
+ err = status.Errorf(codes.Unknown, "cannot receive stream request: %v", err)
+ e.logger.Verbosef("Reverse: %v", err)
+ uidReq.RetCh <- ""
+ return err
+ }
+
+ e.logger.Verbosef("Reverse: received, wait: %v", req)
+ uidReq.RetCh <- req.GetUid().GetPackage()
+ }
+ }
+
+
+ e.logger.Verbosef("Reverse returns")
+ return nil
+}
+
+func (e *LibwgServiceImpl) IpcSet(ctx context.Context, req *gen.IpcSetRequest) (*gen.IpcSetResponse, error) {
+ tunnel, ok := GetTunnel(req.GetTunnel().GetHandle())
+ if !ok {
+ r := &gen.IpcSetResponse{
+ Error: &gen.Error{
+ Message: fmt.Sprintf("Invalid tunnel"),
+ },
+ }
+ return r, nil
+ }
+
+ err := tunnel.device.IpcSet(req.GetConfig())
+ if err != nil {
+ r := &gen.IpcSetResponse{
+ Error: &gen.Error{
+ Message: fmt.Sprintf("IpcSet failed: %v", err),
+ },
+ }
+ return r, nil
+ }
+
+ r := &gen.IpcSetResponse{
+ }
+
+ return r, nil
+}
+
+func (e *LibwgServiceImpl) Dhcp(ctx context.Context, req *gen.DhcpRequest) (*gen.DhcpResponse, error) {
+ var relayAddr netip.Addr
+ var sourceAddr netip.Addr
+
+ source := req.GetSource()
+ if source != nil {
+ sourceAddr, _ = netip.AddrFromSlice(source.GetAddress())
+ }
+
+ if !sourceAddr.IsValid() || !sourceAddr.Is6() {
+ r := &gen.DhcpResponse{
+ Error: &gen.Error{
+ Message: fmt.Sprintf("DHCPv6 source address missing"),
+ },
+ }
+ return r, nil
+ }
+
+ relay := req.GetRelay()
+ if relay != nil {
+ relayAddr, _ = netip.AddrFromSlice(relay.GetAddress())
+ } else {
+ // Construct relay address from source prefix
+ relayRaw := source.GetAddress()[:8]
+ relayRaw = append(relayRaw, 0)
+ relayRaw = append(relayRaw, 0)
+ relayRaw = append(relayRaw, 0)
+ relayRaw = append(relayRaw, 0)
+ relayRaw = append(relayRaw, 0)
+ relayRaw = append(relayRaw, 0)
+ relayRaw = append(relayRaw, 0)
+ relayRaw = append(relayRaw, 1)
+ relayAddr, _ = netip.AddrFromSlice(relayRaw)
+ }
+
+ if !relayAddr.IsValid() || !relayAddr.Is6() {
+ r := &gen.DhcpResponse{
+ Error: &gen.Error{
+ Message: fmt.Sprintf("DHCPv6 relay address calculation failed"),
+ },
+ }
+ return r, nil
+ }
+
+ e.logger.Verbosef("RunDhcp %v %v", sourceAddr, relayAddr)
+
+ leases, err := RunDhcp(ctx, sourceAddr, relayAddr)
+ if err != nil {
+ r := &gen.DhcpResponse{
+ Error: &gen.Error{
+ Message: fmt.Sprintf("RunDhcp failed: %v", err),
+ },
+ }
+ return r, nil
+ }
+
+ r := &gen.DhcpResponse{
+ Leases: leases,
+ }
+ return r, nil
+}
+
+func (e *LibwgServiceImpl) CapabilitiesChanged(ctx context.Context, req *gen.CapabilitiesChangedRequest) (*gen.CapabilitiesChangedResponse, error) {
+ if e.httpProxy == nil {
+ r := &gen.CapabilitiesChangedResponse{
+ Error: &gen.Error{
+ Message: fmt.Sprintf("Http Proxy not running"),
+ },
+ }
+ return r, nil
+ }
+
+ notMetered := false
+
+ for _, cap := range req.Capabilities {
+ if cap == gen.CapabilitiesChangedRequest_NOT_METERED {
+ notMetered = true
+ }
+ }
+
+ e.logger.Verbosef("Set not metered %v", notMetered)
+ e.httpProxy.SetNotMetered(notMetered)
+
+ r := &gen.CapabilitiesChangedResponse{
+ }
+ return r, nil
+}
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
index 34a32d5b..fff3acd9 100644
--- a/ui/build.gradle.kts
+++ b/ui/build.gradle.kts
@@ -2,9 +2,9 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import com.android.build.gradle.tasks.ExtractSupportedLocalesTask
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
+val appID: String = providers.gradleProperty("wireguardApplicationID").get()
plugins {
alias(libs.plugins.android.application)
@@ -14,7 +14,7 @@ plugins {
}
android {
- compileSdk = 33
+ compileSdk = 34
buildFeatures {
buildConfig = true
dataBinding = true
@@ -22,9 +22,9 @@ android {
}
namespace = pkg
defaultConfig {
- applicationId = pkg
+ applicationId = appID
minSdk = 21
- targetSdk = 33
+ targetSdk = 34
versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
versionName = grgit.describe {
tags = true
@@ -37,6 +37,14 @@ android {
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
+ signingConfigs {
+ named("debug") {
+ storeFile = File("/src/wireguard-android/keystore/debug.keystore")
+ storePassword = "android"
+ keyAlias = "androiddebugkey"
+ keyPassword = "android"
+ }
+ }
buildTypes {
release {
isMinifyEnabled = true
@@ -53,6 +61,7 @@ android {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
+ signingConfig = signingConfigs.getByName("debug")
}
create("googleplay") {
initWith(getByName("release"))
diff --git a/ui/src/debug/res/values/strings.xml b/ui/src/debug/res/values/strings.xml
index 947b7381..af58a39d 100644
--- a/ui/src/debug/res/values/strings.xml
+++ b/ui/src/debug/res/values/strings.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string name="app_name" translatable="false">WireGuard β</string>
+ <string name="app_name" translatable="false">(m7n.eu) WireGuard β</string>
</resources>
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index 72617f3b..8f779f79 100644
--- a/ui/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -3,7 +3,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -159,5 +161,10 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
+
+ <intent>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+ </intent>
</queries>
</manifest>
diff --git a/ui/src/main/java/com/wireguard/android/QuickTileService.kt b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
index 7cd13e6e..f74c8bdf 100644
--- a/ui/src/main/java/com/wireguard/android/QuickTileService.kt
+++ b/ui/src/main/java/com/wireguard/android/QuickTileService.kt
@@ -4,6 +4,7 @@
*/
package com.wireguard.android
+import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
@@ -53,7 +54,12 @@ class QuickTileService : TileService() {
null -> {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivityAndCollapse(intent)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startActivityAndCollapse(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE))
+ } else {
+ @Suppress("DEPRECATION")
+ startActivityAndCollapse(intent)
+ }
}
else -> {
unlockAndRun {
@@ -82,13 +88,13 @@ class QuickTileService : TileService() {
val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme))
icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */
icon.setSlashed(false)
- var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) ?: return
+ var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
var c = Canvas(b)
icon.setBounds(0, 0, c.width, c.height)
icon.draw(c)
iconOn = Icon.createWithBitmap(b)
icon.setSlashed(true)
- b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888) ?: return
+ b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
c = Canvas(b)
icon.setBounds(0, 0, c.width, c.height)
icon.draw(c)
diff --git a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
index afba41cb..6b2040e1 100644
--- a/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
+++ b/ui/src/main/java/com/wireguard/android/databinding/BindingAdapters.kt
@@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.wireguard.android.BR
import com.wireguard.android.R
+import com.wireguard.android.backend.Dhcp;
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
import com.wireguard.android.widget.ToggleSwitch
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
@@ -170,6 +171,12 @@ object BindingAdapters {
}
@JvmStatic
+ @BindingAdapter("android:text")
+ fun setDhcpLeaseSetText(view: TextView, dhcp: Dhcp?) {
+ view.text = if (dhcp?.leases != null) Attribute.join(dhcp.leases.map { it }) else ""
+ }
+
+ @JvmStatic
fun tryParseInt(s: String?): Int {
if (s == null)
return 0
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
index 81d8e8c6..fce14d20 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt
@@ -5,6 +5,7 @@
package com.wireguard.android.fragment
import android.os.Bundle
+import android.util.Log;
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@@ -21,6 +22,7 @@ import com.wireguard.android.databinding.TunnelDetailFragmentBinding
import com.wireguard.android.databinding.TunnelDetailPeerBinding
import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.QuantityFormatter
+import com.wireguard.android.viewmodel.ConfigDetail
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -79,7 +81,9 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
} else {
lifecycleScope.launch {
try {
- binding.config = newTunnel.getConfigAsync()
+ var config = newTunnel.getConfigDetailAsync()
+ binding.config = config
+ Log.i(TAG, "onSelectedTunnelChanged " + config + ", " + config.config)
} catch (_: Throwable) {
binding.config = null
}
@@ -147,4 +151,8 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
}
}
}
+
+ companion object {
+ private const val TAG = "WireGuard/TunnelDetailFragment"
+ }
}
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
index edf4b226..6be27f94 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -16,13 +16,17 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
+import android.widget.ArrayAdapter
+import android.widget.AutoCompleteTextView
import android.widget.EditText
+import android.widget.Filter
import android.widget.Toast
import androidx.core.os.BundleCompat
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.textfield.TextInputLayout
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.backend.Tunnel
@@ -32,6 +36,7 @@ import com.wireguard.android.util.AdminKnobs
import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.viewmodel.ConfigProxy
+import com.wireguard.android.viewmodel.Constants
import com.wireguard.config.Config
import kotlinx.coroutines.launch
@@ -43,6 +48,21 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
private var binding: TunnelEditorFragmentBinding? = null
private var tunnel: ObservableTunnel? = null
+ private class MaterialSpinnerAdapter<T>(context: Context, resource: Int, private val objects: List<T>) : ArrayAdapter<T>(context, resource, objects) {
+ private val _filter: Filter by lazy {
+ object : Filter() {
+ override fun performFiltering(constraint: CharSequence?): FilterResults {
+ return FilterResults()
+ }
+
+ override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
+ }
+ }
+ }
+
+ override fun getFilter(): Filter = _filter
+ }
+
private fun onConfigLoaded(config: Config) {
binding?.config = ConfigProxy(config)
}
@@ -80,6 +100,13 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
executePendingBindings()
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
}
+
+ var httpProxyMenu = binding?.root?.findViewById<TextInputLayout>(R.id.http_proxy_menu)
+ var httpProxyItems = listOf(Constants.HTTP_PROXY_NONE, Constants.HTTP_PROXY_MANUAL, Constants.HTTP_PROXY_PAC)
+ var httpProxyAdapter = MaterialSpinnerAdapter(requireContext(), R.layout.http_proxy_menu_item, httpProxyItems)
+ var httpProxyMenuText = httpProxyMenu?.editText as? AutoCompleteTextView
+ httpProxyMenuText?.setAdapter(httpProxyAdapter)
+
return binding?.root
}
diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
index aa237aee..55c84df5 100644
--- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
+++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
@@ -7,12 +7,20 @@ package com.wireguard.android.model
import android.util.Log
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
+import com.wireguard.android.Application
import com.wireguard.android.BR
+import com.wireguard.android.backend.Dhcp
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.Keyed
import com.wireguard.android.util.applicationScope
+import com.wireguard.android.viewmodel.ConfigDetail
+import com.wireguard.android.viewmodel.PeerDetail
import com.wireguard.config.Config
+import com.wireguard.config.InetEndpoint
+import com.wireguard.config.InetNetwork
+import com.wireguard.crypto.Key
+import java.util.Optional
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -55,7 +63,18 @@ class ObservableTunnel internal constructor(
}
fun onStateChanged(state: Tunnel.State): Tunnel.State {
- if (state != Tunnel.State.UP) onStatisticsChanged(null)
+ if (state != Tunnel.State.UP) {
+ onStatisticsChanged(null)
+ onDhcpChanged(null)
+ Application.getCoroutineScope().launch {
+ onPeersReset()
+ }
+ } else {
+ configDetail?.peers?.forEach {
+ var endpoint: InetEndpoint? = it.peer?.endpoint?.orElse(null)
+ it.endpoint = Optional.ofNullable(endpoint?.getResolved()?.orElse(null));
+ }
+ }
this.state = state
notifyPropertyChanged(BR.state)
return state
@@ -68,6 +87,7 @@ class ObservableTunnel internal constructor(
this@ObservableTunnel.state
}
+ private var configDetail: ConfigDetail? = if (config != null) ConfigDetail(config) else null
@get:Bindable
var config = config
@@ -86,7 +106,11 @@ class ObservableTunnel internal constructor(
private set
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
- config ?: manager.getTunnelConfig(this@ObservableTunnel)
+ config ?: manager.getTunnelConfig(this@ObservableTunnel).config!!
+ }
+
+ suspend fun getConfigDetailAsync(): ConfigDetail = withContext(Dispatchers.Main.immediate) {
+ configDetail ?: manager.getTunnelConfig(this@ObservableTunnel)
}
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
@@ -98,10 +122,11 @@ class ObservableTunnel internal constructor(
}
}
- fun onConfigChanged(config: Config?): Config? {
+ fun onConfigChanged(config: Config?): ConfigDetail? {
+ this.configDetail = ConfigDetail(config)
this.config = config
notifyPropertyChanged(BR.config)
- return config
+ return configDetail
}
@@ -136,6 +161,104 @@ class ObservableTunnel internal constructor(
return statistics
}
+ @get:Bindable
+ var dhcp: Dhcp? = null
+ private set
+
+ override fun onDhcpChange(newDhcp: Dhcp) {
+ onDhcpChanged(newDhcp)
+ }
+
+ fun onDhcpChanged(dhcp: Dhcp?): Dhcp? {
+ this.dhcp = dhcp
+ notifyPropertyChanged(BR.dhcp)
+ return dhcp
+ }
+
+ // Remove dynamic peers, and reset static peers
+ fun onPeersReset() {
+ Log.i(TAG, "ObservableTunnel onPeersReset")
+ var toRemove: MutableList<PeerDetail> = ArrayList()
+
+ configDetail?.peers?.forEach {
+ if (it.peer == null) {
+ toRemove.add(it)
+ } else {
+ it.endpoint = Optional.empty()
+ }
+ }
+
+ toRemove.forEach {
+ Log.i(TAG, "ObservableTunnel remove " + it)
+ configDetail?.peers?.remove(it)
+ }
+ }
+
+ override fun onEndpointChange(publicKey: Key, newEndpoint: InetEndpoint?) {
+ Application.getCoroutineScope().launch {
+ onEndpointChanged(publicKey, newEndpoint)
+ }
+ }
+
+ private fun onEndpointChanged(publicKey: Key, newEndpoint: InetEndpoint?) {
+
+ Log.i(TAG, "ObservableTunnel onEndpointChange " + newEndpoint)
+ var peer: PeerDetail? = null
+
+ configDetail?.peers?.forEach {
+ if (it.publicKey.equals(publicKey) == true) {
+ Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer)
+ peer = it;
+ }
+ }
+
+ if (peer == null) {
+ Log.i(TAG, "ObservableTunnel create peer " + publicKey)
+ peer = PeerDetail(publicKey)
+ configDetail?.peers?.add(peer)
+ }
+
+ var peer2: PeerDetail = peer!!
+
+ if (newEndpoint != null) {
+ peer2.endpoint = newEndpoint.getResolved()
+ } else {
+ var peer3 = peer2.peer
+ peer2.endpoint = if (peer3 != null) peer3.endpoint else Optional.empty()
+ }
+ }
+
+ fun lookupPeer(publicKey: Key): PeerDetail {
+ configDetail?.peers?.forEach {
+ if (it.publicKey.equals(publicKey) == true) {
+ Log.i(TAG, "ObservableTunnel peer " + it + ", " + it.peer)
+ return it
+ }
+ }
+
+ Log.i(TAG, "ObservableTunnel create peer " + publicKey)
+ var peer: PeerDetail = PeerDetail(publicKey)
+ configDetail?.peers?.add(peer)
+
+ return peer
+ }
+
+ override fun onAllowedIpsChange(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) {
+ Application.getCoroutineScope().launch {
+ onAllowedIpsChanged(publicKey, addNetworks, removeNetworks)
+ }
+ }
+
+ private fun onAllowedIpsChanged(publicKey: Key, addNetworks: List<InetNetwork>?, removeNetworks: List<InetNetwork>?) {
+ var peer: PeerDetail = lookupPeer(publicKey)
+
+ removeNetworks?.let() {
+ peer.allowedIps.removeAll(removeNetworks)
+ }
+ addNetworks?.let() {
+ peer.allowedIps.addAll(addNetworks)
+ }
+ }
suspend fun deleteAsync() = manager.delete(this)
diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
index ba873fa6..960adcaa 100644
--- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
+++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
@@ -24,6 +24,7 @@ import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.UserKnobs
import com.wireguard.android.util.applicationScope
+import com.wireguard.android.viewmodel.ConfigDetail
import com.wireguard.config.Config
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
@@ -94,7 +95,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
}
- suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
+ suspend fun getTunnelConfig(tunnel: ObservableTunnel): ConfigDetail = withContext(Dispatchers.Main.immediate) {
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
}
@@ -156,7 +157,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
getBackend().setState(tunnel, tunnel.state, config)
configStore.save(tunnel.name, config)
- })!!
+ })!!.config!!
}
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
diff --git a/ui/src/main/java/com/wireguard/android/updater/Updater.kt b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
index adcb3836..87adad96 100644
--- a/ui/src/main/java/com/wireguard/android/updater/Updater.kt
+++ b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
@@ -386,13 +386,15 @@ object Updater {
context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
}.requestedPermissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES)
) {
- updaterScope.launch {
- val update = try {
- checkForUpdates()
- } catch (_: Throwable) {
- null
+ if (installer(context).isNotEmpty()) {
+ updaterScope.launch {
+ val update = try {
+ checkForUpdates()
+ } catch (_: Throwable) {
+ null
+ }
+ emitProgress(Progress.Corrupt(update?.fileName))
}
- emitProgress(Progress.Corrupt(update?.fileName))
}
return
}
diff --git a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
index d617adec..97be8c99 100644
--- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
+++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
@@ -122,6 +122,8 @@ object ErrorMessages {
return resources.getString(R.string.bad_config_explanation_udp_port)
} else if (bce.location == BadConfigException.Location.MTU) {
return resources.getString(R.string.bad_config_explanation_positive_number)
+ } else if (bce.location == BadConfigException.Location.HTTP_PROXY) {
+ return resources.getString(R.string.bad_config_explanation_http_proxy)
} else if (bce.location == BadConfigException.Location.PERSISTENT_KEEPALIVE) {
return resources.getString(R.string.bad_config_explanation_pka)
}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt
new file mode 100644
index 00000000..af95a86a
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigDetail.kt
@@ -0,0 +1,22 @@
+package com.wireguard.android.viewmodel
+
+import androidx.databinding.ObservableArrayList
+import androidx.databinding.ObservableList
+
+import com.wireguard.config.Config
+
+class ConfigDetail {
+ val config: Config?
+ val peers: ObservableList<PeerDetail> = ObservableArrayList()
+
+ constructor(other: Config?) {
+ config = other
+ if (other != null) {
+ other.peers.forEach {
+ val detail = PeerDetail(it)
+ peers.add(detail)
+ detail.bind(this)
+ }
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
index 004ebed1..81a548f9 100644
--- a/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/InterfaceProxy.kt
@@ -4,6 +4,8 @@
*/
package com.wireguard.android.viewmodel
+import android.net.Uri
+import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.databinding.BaseObservable
@@ -18,6 +20,12 @@ import com.wireguard.crypto.Key
import com.wireguard.crypto.KeyFormatException
import com.wireguard.crypto.KeyPair
+object Constants {
+ const val HTTP_PROXY_NONE = "None"
+ const val HTTP_PROXY_MANUAL = "Manual"
+ const val HTTP_PROXY_PAC = "Proxy Auto-Config"
+}
+
class InterfaceProxy : BaseObservable, Parcelable {
@get:Bindable
val excludedApplications: ObservableList<String> = ObservableArrayList()
@@ -54,6 +62,44 @@ class InterfaceProxy : BaseObservable, Parcelable {
}
@get:Bindable
+ var httpProxyMenu: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyMenu)
+ notifyPropertyChanged(BR.httpProxyManualVisibility)
+ notifyPropertyChanged(BR.httpProxyPacVisibility)
+ }
+
+ @get:Bindable
+ var httpProxyManualVisibility: Int = 0
+ get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_MANUAL) android.view.View.VISIBLE else android.view.View.GONE)
+
+ @get:Bindable
+ var httpProxyHostname: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyHostname)
+ }
+
+ @get:Bindable
+ var httpProxyPort: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyPort)
+ }
+
+ @get:Bindable
+ var httpProxyPac: String = ""
+ set(value) {
+ field = value
+ notifyPropertyChanged(BR.httpProxyPac)
+ }
+
+ @get:Bindable
+ var httpProxyPacVisibility: Int = 0
+ get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) android.view.View.GONE else (if (httpProxyMenu == Constants.HTTP_PROXY_PAC) android.view.View.VISIBLE else android.view.View.GONE)
+
+ @get:Bindable
var privateKey: String = ""
set(value) {
field = value
@@ -76,6 +122,10 @@ class InterfaceProxy : BaseObservable, Parcelable {
parcel.readStringList(includedApplications)
listenPort = parcel.readString() ?: ""
mtu = parcel.readString() ?: ""
+ httpProxyMenu = parcel.readString() ?: ""
+ httpProxyHostname = parcel.readString() ?: ""
+ httpProxyPort = parcel.readString() ?: ""
+ httpProxyPac = parcel.readString() ?: ""
privateKey = parcel.readString() ?: ""
}
@@ -87,6 +137,10 @@ class InterfaceProxy : BaseObservable, Parcelable {
includedApplications.addAll(other.includedApplications)
listenPort = other.listenPort.map { it.toString() }.orElse("")
mtu = other.mtu.map { it.toString() }.orElse("")
+ httpProxyHostname = other.httpProxy.map { if (it.getHost().startsWith('[') && it.getHost().endsWith(']')) it.getHost().substring(1, it.getHost().length-1) else it.getHost() }.orElse("")
+ httpProxyPort = other.httpProxy.map { if (it.getPort() <= 0) "8080" else it.getPort().toString() }.orElse("")
+ httpProxyPac = other.httpProxy.map { it.getPacFileUrl().toString() }.orElse("")
+ httpProxyMenu = other.httpProxy.map { if (it.getPacFileUrl() != Uri.EMPTY) Constants.HTTP_PROXY_PAC else if (it.getHost() != "") Constants.HTTP_PROXY_MANUAL else Constants.HTTP_PROXY_NONE }.orElse(Constants.HTTP_PROXY_NONE)
val keyPair = other.keyPair
privateKey = keyPair.privateKey.toBase64()
}
@@ -111,6 +165,20 @@ class InterfaceProxy : BaseObservable, Parcelable {
if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications)
if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort)
if (mtu.isNotEmpty()) builder.parseMtu(mtu)
+ if (Constants.HTTP_PROXY_MANUAL.equals(httpProxyMenu) && httpProxyHostname.isNotEmpty()) {
+ var httpProxy: String
+ if (httpProxyHostname.contains(":")) {
+ httpProxy = "[" + httpProxyHostname + "]"
+ } else {
+ httpProxy = httpProxyHostname
+ }
+ if (httpProxyPort.isNotEmpty()) {
+ httpProxy += ":" + httpProxyPort;
+ }
+ builder.parseHttpProxy(httpProxy)
+ } else if (Constants.HTTP_PROXY_PAC.equals(httpProxyMenu) && httpProxyPac.isNotEmpty()) {
+ builder.parseHttpProxy("pac:" + httpProxyPac)
+ }
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
return builder.build()
}
@@ -122,6 +190,10 @@ class InterfaceProxy : BaseObservable, Parcelable {
dest.writeStringList(includedApplications)
dest.writeString(listenPort)
dest.writeString(mtu)
+ dest.writeString(httpProxyMenu)
+ dest.writeString(httpProxyHostname)
+ dest.writeString(httpProxyPort)
+ dest.writeString(httpProxyPac)
dest.writeString(privateKey)
}
diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt
new file mode 100644
index 00000000..80b32fd5
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/viewmodel/PeerDetail.kt
@@ -0,0 +1,85 @@
+package com.wireguard.android.viewmodel
+
+import android.util.Log
+import androidx.databinding.BaseObservable
+import androidx.databinding.Bindable
+import androidx.databinding.Observable
+import androidx.databinding.ObservableList
+import androidx.databinding.ObservableArrayList
+
+import com.wireguard.android.BR
+import com.wireguard.config.InetEndpoint
+import com.wireguard.config.InetNetwork
+import com.wireguard.config.Peer
+import com.wireguard.crypto.Key;
+
+import java.util.Optional;
+
+import kotlin.collections.LinkedHashSet
+
+
+class PeerDetail : BaseObservable {
+ var peer: Peer?
+ private var owner: ConfigDetail? = null
+
+ @get:Bindable
+ var publicKey: Key
+
+ @get:Bindable
+ var allowedIps: ObservableList<InetNetwork> = ObservableArrayList<InetNetwork>()
+
+ @get:Bindable
+ var endpoint: Optional<InetEndpoint> = Optional.empty()
+ get() {
+ if (!field.isEmpty()) {
+ return field
+ } else {
+ return Optional.ofNullable(peer?.endpoint?.get())
+ }
+ }
+
+ set(value) {
+ Log.i(TAG, "notifyPropertyChanged endpoint " + this + ", " + value)
+ field = value
+ notifyPropertyChanged(BR.endpoint)
+ }
+
+ @get:Bindable
+ var persistentKeepalive: Optional<Int> = Optional.empty()
+
+ constructor(other: Peer) {
+ peer = other
+ publicKey = other.getPublicKey()
+ allowedIps.addAll(other.getAllowedIps())
+ endpoint = other.getEndpoint();
+ persistentKeepalive = other.getPersistentKeepalive()
+ }
+
+ constructor(publicKey: Key) {
+ peer = null
+ this.publicKey = publicKey
+ }
+
+ fun bind(owner: ConfigDetail) {
+ this.owner = owner
+ }
+
+ override fun addOnPropertyChangedCallback (callback: Observable.OnPropertyChangedCallback) {
+ Log.i(TAG, "addOnPropertyChangedCallback " + this + ", " + callback)
+ super.addOnPropertyChangedCallback(callback)
+ }
+
+ /**
+ * Converts the {@code Peer} into a string suitable for debugging purposes. The {@code Peer} is
+ * identified by its public key and (if known) its endpoint.
+ *
+ * @return a concise single-line identifier for the {@code Peer}
+ */
+ override fun toString(): String {
+ return "(Peer " + publicKey.toBase64() + ")"
+ }
+
+ companion object {
+ private const val TAG = "WireGuard/PeerDetail"
+ }
+}
diff --git a/ui/src/main/res/layout/http_proxy_menu_item.xml b/ui/src/main/res/layout/http_proxy_menu_item.xml
new file mode 100644
index 00000000..8ad5c026
--- /dev/null
+++ b/ui/src/main/res/layout/http_proxy_menu_item.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+/>
diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml
index 332df04a..9b17a06c 100644
--- a/ui/src/main/res/layout/tunnel_detail_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml
@@ -5,6 +5,8 @@
<data>
+ <import type="android.os.Build" />
+
<import type="com.wireguard.android.backend.Tunnel.State" />
<import type="com.wireguard.android.util.ClipboardUtils" />
@@ -19,7 +21,7 @@
<variable
name="config"
- type="com.wireguard.config.Config" />
+ type="com.wireguard.android.viewmodel.ConfigDetail" />
</data>
<ScrollView
@@ -116,7 +118,7 @@
android:nextFocusForward="@id/addresses_text"
android:onClick="@{ClipboardUtils::copyTextView}"
android:singleLine="true"
- android:text="@{config.interface.keyPair.publicKey.toBase64}"
+ android:text="@{config.config.interface.keyPair.publicKey.toBase64}"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/public_key_label"
@@ -129,7 +131,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/addresses_text"
android:text="@string/addresses"
- android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
@@ -139,14 +141,41 @@
android:layout_height="wrap_content"
android:contentDescription="@string/addresses"
android:nextFocusUp="@id/public_key_text"
+ android:nextFocusDown="@id/dynamic_addresses_text"
+ android:nextFocusForward="@id/dynamic_addresses_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.config.interface.addresses}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/addresses_label"
+ tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
+
+ <TextView
+ android:id="@+id/dynamic_addresses_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/dynamic_addresses_text"
+ android:text="@string/dynamic_addresses"
+ android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/addresses_text" />
+
+ <TextView
+ android:id="@+id/dynamic_addresses_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/dynamic_addresses"
+ android:nextFocusUp="@id/addresses_text"
android:nextFocusDown="@id/dns_servers_text"
android:nextFocusForward="@id/dns_servers_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.addresses}"
+ android:text="@{tunnel.dhcp}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{tunnel.dhcp == null ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/addresses_label"
+ app:layout_constraintTop_toBottomOf="@+id/dynamic_addresses_label"
tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" />
<TextView
@@ -156,22 +185,22 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_servers_text"
android:text="@string/dns_servers"
- android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/addresses_text" />
+ app:layout_constraintTop_toBottomOf="@id/dynamic_addresses_text" />
<TextView
android:id="@+id/dns_servers_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/dns_servers"
- android:nextFocusUp="@id/addresses_text"
+ android:nextFocusUp="@id/dynamic_addresses_text"
android:nextFocusDown="@id/dns_search_domains_text"
android:nextFocusForward="@id/dns_search_domains_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.dnsServers}"
+ android:text="@{config.config.interface.dnsServers}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dns_servers_label"
tools:text="8.8.8.8, 8.8.4.4" />
@@ -183,7 +212,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/dns_search_domain_text"
android:text="@string/dns_search_domains"
- android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dns_servers_text" />
@@ -196,9 +225,9 @@
android:nextFocusDown="@id/listen_port_text"
android:nextFocusForward="@id/listen_port_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.dnsSearchDomains}"
+ android:text="@{config.config.interface.dnsSearchDomains}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dns_search_domains_label"
tools:text="zx2c4.com" />
@@ -210,7 +239,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/listen_port_text"
android:text="@string/listen_port"
- android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toStartOf="parent"
@@ -223,12 +252,12 @@
android:contentDescription="@string/listen_port"
android:nextFocusRight="@id/mtu_text"
android:nextFocusUp="@id/dns_search_domains_text"
- android:nextFocusDown="@id/applications_text"
+ android:nextFocusDown="@id/http_proxy_text"
android:nextFocusForward="@id/mtu_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.listenPort}"
+ android:text="@{config.config.interface.listenPort}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/mtu_label"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toStartOf="parent"
@@ -242,7 +271,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/mtu_text"
android:text="@string/mtu"
- android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintLeft_toRightOf="@id/listen_port_label"
@@ -256,11 +285,11 @@
android:contentDescription="@string/mtu"
android:nextFocusLeft="@id/listen_port_text"
android:nextFocusUp="@id/dns_servers_text"
- android:nextFocusForward="@id/applications_text"
+ android:nextFocusForward="@id/http_proxy_text"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.mtu}"
+ android:text="@{config.config.interface.mtu}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!config.config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="0.5"
app:layout_constraintStart_toEndOf="@id/listen_port_label"
@@ -276,15 +305,42 @@
app:constraint_referenced_ids="listen_port_text,mtu_text" />
<TextView
+ android:id="@+id/http_proxy_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/http_proxy_text"
+ android:text="@string/http_proxy"
+ android:visibility="@{(Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" />
+
+ <TextView
+ android:id="@+id/http_proxy_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/http_proxy"
+ android:nextFocusUp="@id/listen_port_text"
+ android:nextFocusDown="@id/applications_text"
+ android:nextFocusForward="@id/applications_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:text="@{config.config.interface.httpProxy}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:visibility="@{(Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.Q || !config.config.interface.httpProxy.isPresent()) ? android.view.View.GONE : android.view.View.VISIBLE}"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_label"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:text="http://example.com:8888" />
+
+ <TextView
android:id="@+id/applications_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/applications_text"
android:text="@string/applications"
- android:visibility="@{config.interface.includedApplications.isEmpty() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.includedApplications.isEmpty() &amp;&amp; config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/listen_port_mtu_barrier" />
+ app:layout_constraintTop_toBottomOf="@+id/http_proxy_text" />
<TextView
android:id="@+id/applications_text"
@@ -295,9 +351,9 @@
android:nextFocusDown="@id/peers_layout"
android:nextFocusForward="@id/peers_layout"
android:onClick="@{ClipboardUtils::copyTextView}"
- android:text="@{config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.interface.excludedApplications.size(), config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.interface.includedApplications.size(), config.interface.includedApplications.size())}"
+ android:text="@{config.config.interface.includedApplications.isEmpty() ? @plurals/n_excluded_applications(config.config.interface.excludedApplications.size(), config.config.interface.excludedApplications.size()) : @plurals/n_included_applications(config.config.interface.includedApplications.size(), config.config.interface.includedApplications.size())}"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{config.interface.includedApplications.isEmpty() &amp;&amp; config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{config.config.interface.includedApplications.isEmpty() &amp;&amp; config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/applications_label"
tools:text="8 excluded" />
@@ -311,6 +367,7 @@
android:layout_marginTop="8dp"
android:divider="@null"
android:orientation="vertical"
+ app:fragment="@{fragment}"
app:items="@{config.peers}"
app:layout="@{@layout/tunnel_detail_peer}"
app:layout_constraintStart_toStartOf="parent"
@@ -318,4 +375,4 @@
tools:ignore="UselessLeaf" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
-</layout> \ No newline at end of file
+</layout>
diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml
index 25081cea..89bb85ec 100644
--- a/ui/src/main/res/layout/tunnel_detail_peer.xml
+++ b/ui/src/main/res/layout/tunnel_detail_peer.xml
@@ -9,7 +9,7 @@
<variable
name="item"
- type="com.wireguard.config.Peer" />
+ type="com.wireguard.android.viewmodel.PeerDetail" />
</data>
<com.google.android.material.card.MaterialCardView
@@ -64,7 +64,7 @@
android:layout_marginTop="8dp"
android:labelFor="@+id/pre_shared_key_text"
android:text="@string/pre_shared_key"
- android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/public_key_text" />
@@ -81,7 +81,7 @@
android:singleLine="true"
android:text="@string/pre_shared_key_enabled"
android:textAppearance="?attr/textAppearanceBodyLarge"
- android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
+ android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pre_shared_key_label"
tools:text="8VyS8W8XeMcBWfKp1GuG3/fZlnUQFkqMNbrdmZtVQIM=" />
diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml
index 0350486b..42222399 100644
--- a/ui/src/main/res/layout/tunnel_editor_fragment.xml
+++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml
@@ -5,6 +5,8 @@
<data>
+ <import type="android.os.Build" />
+
<import type="com.wireguard.android.util.ClipboardUtils" />
<import type="com.wireguard.android.widget.KeyInputFilter" />
@@ -210,7 +212,7 @@
android:imeOptions="actionNext"
android:inputType="textNoSuggestions|textVisiblePassword"
android:nextFocusUp="@id/addresses_label_text"
- android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusDown="@id/http_proxy_hostname_text"
android:nextFocusForward="@id/mtu_text"
android:text="@={config.interface.dnsServers}" />
</com.google.android.material.textfield.TextInputLayout>
@@ -235,19 +237,112 @@
android:imeOptions="actionDone"
android:inputType="number"
android:nextFocusUp="@id/listen_port_text"
- android:nextFocusDown="@id/set_excluded_applications"
- android:nextFocusForward="@id/set_excluded_applications"
+ android:nextFocusDown="@id/http_proxy_hostname_text"
+ android:nextFocusForward="@id/http_proxy_hostname_text"
android:text="@={config.interface.mtu}"
android:textAlignment="center" />
</com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_menu"
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/dns_servers_label_layout">
+
+ <AutoCompleteTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="none"
+ android:text="@={config.interface.httpProxyMenu}"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_hostname_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy_hostname"
+ android:visibility="@{config.interface.httpProxyManualVisibility}"
+ app:layout_constraintEnd_toStartOf="@id/http_proxy_port_label_layout"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ app:layout_constraintHorizontal_weight="0.7"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_menu">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/http_proxy_hostname_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/mtu_text"
+ android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusForward="@id/http_proxy_port_text"
+ android:text="@={config.interface.httpProxyHostname}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_port_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy_port"
+ android:visibility="@{config.interface.httpProxyManualVisibility}"
+ app:expandedHintEnabled="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_weight="0.3"
+ app:layout_constraintStart_toEndOf="@id/http_proxy_hostname_label_layout"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_menu">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/http_proxy_port_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionDone"
+ android:nextFocusUp="@id/mtu_text"
+ android:nextFocusDown="@id/http_proxy_pac_label_layout"
+ android:nextFocusForward="@id/http_proxy_pac_label_layout"
+ android:text="@={config.interface.httpProxyPort}"
+ android:textAlignment="center" />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/http_proxy_pac_label_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:hint="@string/http_proxy_pac"
+ android:visibility="@{config.interface.httpProxyPacVisibility}"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_hostname_label_layout">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/http_proxy_pac_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions|textVisiblePassword"
+ android:nextFocusUp="@id/http_proxy_hostname_text"
+ android:nextFocusDown="@id/set_excluded_applications"
+ android:nextFocusForward="@id/set_excluded_applications"
+ android:text="@={config.interface.httpProxyPac}" />
+ </com.google.android.material.textfield.TextInputLayout>
+
<com.google.android.material.button.MaterialButton
android:id="@+id/set_excluded_applications"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
- android:nextFocusUp="@id/dns_servers_text"
+ android:nextFocusUp="@id/http_proxy_hostname_text"
android:nextFocusDown="@id/peers_layout"
android:nextFocusForward="@id/peers_layout"
android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}"
@@ -256,7 +351,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/mtu_label_layout"
+ app:layout_constraintTop_toBottomOf="@id/http_proxy_pac_label_layout"
app:rippleColor="?attr/colorSecondary"
tools:text="4 excluded applications" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/ui/src/main/res/values-da-rDK/strings.xml b/ui/src/main/res/values-da-rDK/strings.xml
index 98f8de6e..88f13dc1 100644
--- a/ui/src/main/res/values-da-rDK/strings.xml
+++ b/ui/src/main/res/values-da-rDK/strings.xml
@@ -51,6 +51,9 @@
<string name="add_peer">Tilføj modpart</string>
<string name="addresses">Adresser</string>
<string name="applications">Applikationer</string>
+ <string name="allow_remote_control_intents_summary_off">Eksterne apps kan ikke slå tunneler til/fra (Anbefales)</string>
+ <string name="allow_remote_control_intents_summary_on">Eksterne apps må slå tunneler til/fra (Avanceret)</string>
+ <string name="allow_remote_control_intents_title">Tillad fjernstyring fra eksterne apps</string>
<string name="allowed_ips">Tilladte IP-adresser</string>
<string name="bad_config_context">%1$s\'s %2$s</string>
<string name="bad_config_context_top_level">%s</string>
@@ -85,6 +88,7 @@
<string name="tv_delete">Vælg tunnel du vil slette</string>
<string name="tv_select_a_storage_drive">Vælg et lagerdrev</string>
<string name="tv_add_tunnel_get_started">Tilføj en tunnel for at komme i gang</string>
+ <string name="donate_title">♥ Donér til WireGuard projektet</string>
<string name="disable_config_export_title">Deaktivér eksportering af konfiguration</string>
<string name="dns_servers">DNS-servere</string>
<string name="dns_search_domains">DNS-søgedomæner</string>
diff --git a/ui/src/main/res/values-et-rEE/strings.xml b/ui/src/main/res/values-et-rEE/strings.xml
index 692a0b8a..9beaafd9 100644
--- a/ui/src/main/res/values-et-rEE/strings.xml
+++ b/ui/src/main/res/values-et-rEE/strings.xml
@@ -185,6 +185,7 @@ Aitäh veelkord sinu panuse eest.</string>
<string name="private_key">Privaatvõti</string>
<string name="public_key">Avalik võti</string>
<string name="qr_code_hint">Vihje: tekita käsuga `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_action">Lülita tunnel</string>
<string name="restore_on_boot_summary_off">Ei lülita seadme käivitumisel lubatud tunneleid sisse</string>
<string name="restore_on_boot_summary_on">Lülitab seadme käivitumisel lubatud tunnelid sisse</string>
<string name="restore_on_boot_title">Taasta seadme käivitumisel</string>
@@ -236,6 +237,9 @@ Aitäh veelkord sinu panuse eest.</string>
<string name="updater_download_progress_nototal">Uuenduse allalaadimine: %s</string>
<string name="updater_installing">Uuenduse paigaldamine…</string>
<string name="updater_failure">Uuendamine ebaõnnestus: %s. Uus katse hetke pärast…</string>
+ <string name="updater_corrupt_title">Rakendus rikutud</string>
+ <string name="updater_corrupt_message">See rakendus on rikutud. Palun laadi APK uuesti allpool lingitud veebilehelt. Pärast seda desinstalli rakendus ja installi allalaaditud APK uuesti.</string>
+ <string name="updater_corrupt_navigate">Ava veebileht</string>
<string name="version_summary">%1$s taustsüsteem %2$s</string>
<string name="version_summary_checking">Kontrollin %s taustsüsteemi versiooni</string>
<string name="version_summary_unknown">Tundmatu %s versioon</string>
diff --git a/ui/src/main/res/values-ja/strings.xml b/ui/src/main/res/values-ja/strings.xml
index c320d476..26e99af3 100644
--- a/ui/src/main/res/values-ja/strings.xml
+++ b/ui/src/main/res/values-ja/strings.xml
@@ -96,6 +96,7 @@
<string name="tv_add_tunnel_get_started">トンネルを追加して開始する</string>
<string name="donate_title">♥ WireGuard プロジェクトに寄付する</string>
<string name="donate_summary">すべての貢献が役立ちます</string>
+ <string name="donate_google_play_disappointment">WireGuard プロジェクトを支援していただきありがとうございます!\n\n残念ながら、Google のポリシーの影響で寄付のページへのリンクを記載することができません。見つけていただけることを願っています。\n\nもう一度、あなたの貢献に深く感謝します。</string>
<string name="disable_config_export_title">設定のエクスポートを無効にする</string>
<string name="disable_config_export_description">設定のエクスポートを無効にすると、秘密鍵にアクセスされにくくなります</string>
<string name="dns_servers">DNS サーバ</string>
@@ -170,6 +171,10 @@
<string name="private_key">秘密鍵</string>
<string name="public_key">公開鍵</string>
<string name="qr_code_hint">Tip: `qrencode -t ansiutf8 &lt; tunnel.conf` で生成できます</string>
+ <string name="quick_settings_tile_add_title">クイック設定パネルを追加</string>
+ <string name="quick_settings_tile_add_summary">ショートカットタイルを使用すると、最新のトンネルに切り替わります</string>
+ <string name="quick_settings_tile_add_failure">ショートカットタイルを追加できません: エラー %d</string>
+ <string name="quick_settings_tile_action">トンネルを切り替え</string>
<string name="restore_on_boot_summary_off">起動時にトンネルを有効化しない</string>
<string name="restore_on_boot_summary_on">起動時に、前回有効だったトンネルを有効化する</string>
<string name="restore_on_boot_title">起動時に復元</string>
@@ -214,6 +219,16 @@
<string name="type_name_go_userspace">Go ユーザースペース</string>
<string name="type_name_kernel_module">カーネルモジュール</string>
<string name="unknown_error">未知のエラー</string>
+ <string name="updater_avalable">アプリを更新できます。今すぐ更新してください。</string>
+ <string name="updater_action">ダウンロードして更新</string>
+ <string name="updater_rechecking">更新のメタデータを取得しています…</string>
+ <string name="updater_download_progress">更新のダウンロード中: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">更新のダウンロード中: %s</string>
+ <string name="updater_installing">更新をインストール中…</string>
+ <string name="updater_failure">更新に失敗しました: %s. 一定時間後に再試行します…</string>
+ <string name="updater_corrupt_title">アプリケーションが破損しています</string>
+ <string name="updater_corrupt_message">このアプリケーションは破損しています。下記のリンク先のウェブサイトから APK を再ダウンロードしてください。その後、このアプリケーションをアンインストールし、ダウンロードした APK を再インストールしてください。</string>
+ <string name="updater_corrupt_navigate">ウェブサイトを開く</string>
<string name="version_summary">%1$s バックエンド %2$s</string>
<string name="version_summary_checking">%s バックエンドのバージョンを確認中</string>
<string name="version_summary_unknown">未知の %s バージョン</string>
diff --git a/ui/src/main/res/values-sv-rSE/strings.xml b/ui/src/main/res/values-sv-rSE/strings.xml
index c919d15c..910f9e05 100644
--- a/ui/src/main/res/values-sv-rSE/strings.xml
+++ b/ui/src/main/res/values-sv-rSE/strings.xml
@@ -107,6 +107,9 @@
<string name="tv_select_a_storage_drive">Välj en lagringsenhet</string>
<string name="tv_no_file_picker">Installera ett filhanteringsverktyg för att bläddra bland filer</string>
<string name="tv_add_tunnel_get_started">Lägg till en tunnel för att komma igång</string>
+ <string name="donate_title">♥ Donera till WireGuard Projektet</string>
+ <string name="donate_summary">Varje bidrag hjälper</string>
+ <string name="donate_google_play_disappointment">Tack för att du stödjer WireGuard Projektet!\n\nPå grund av Googles policyer får vi dessvärre inte till den del av projektets webbsida där du kan göra en donation. Förhoppningsvis kan du hitta dit ändå!\n\nTack igen för ditt bidrag.</string>
<string name="disable_config_export_title">Inaktivera export av konfiguration</string>
<string name="disable_config_export_description">Inaktivering av konfigurationsexport gör privata nycklar mindre tillgängliga</string>
<string name="dns_servers">DNS-servrar</string>
@@ -116,6 +119,7 @@
<string name="error_down">Fel vid nedtagning av tunnel: %s</string>
<string name="error_fetching_apps">Fel vid hämtning av applista: %s</string>
<string name="error_root">Vänligen få rootbehörighet och försök igen</string>
+ <string name="error_prepare">Fel vid förberedelse av tunnel: %s</string>
<string name="error_up">Fel vid uppstart av tunnel: %s</string>
<string name="exclude_private_ips">Uteslut privata IP-adresser</string>
<string name="generate_new_private_key">Skapa ny privat nyckel</string>
@@ -135,6 +139,8 @@
<string name="key_length_explanation_base64">: WireGuard base64 nycklar måste vara 44 tecken (32 bytes)</string>
<string name="key_length_explanation_binary">: WireGuard nycklar måste vara 32 bytes</string>
<string name="key_length_explanation_hex">: WireGuard hex nycklar måste vara 64 tecken (32 bytes)</string>
+ <string name="latest_handshake">Senaste handskakning</string>
+ <string name="latest_handshake_ago">%s sedan</string>
<string name="listen_port">Lyssningsport</string>
<string name="log_export_error">Kan inte exportera loggen: %s</string>
<string name="log_export_subject">WireGuard Android loggfil</string>
@@ -178,6 +184,10 @@
<string name="private_key">Privat nyckel</string>
<string name="public_key">Offentlig nyckel</string>
<string name="qr_code_hint">Tips: generera med `qrencode -t ansiutf8 &lt; tunnel.conf`.</string>
+ <string name="quick_settings_tile_add_title">Lägg till tile i snabbinställningarna</string>
+ <string name="quick_settings_tile_add_summary">Tilen växlar din senaste tunnel mellan på och av</string>
+ <string name="quick_settings_tile_add_failure">Misslyckades med att skapa tile: fel %d</string>
+ <string name="quick_settings_tile_action">Växla tunnel på/av</string>
<string name="restore_on_boot_summary_off">Kommer inte ta upp aktiverade tunnlar vid uppstart</string>
<string name="restore_on_boot_summary_on">Kommer ta upp aktiverade tunnlar vid uppstart</string>
<string name="restore_on_boot_title">Återställ vid uppstart</string>
@@ -213,6 +223,7 @@
<string name="tunnel_create_success">Lyckades skapa tunnel “%s”</string>
<string name="tunnel_error_already_exists">Tunnel ”%s” finns redan</string>
<string name="tunnel_error_invalid_name">Ogiltigt namn</string>
+ <string name="tunnel_list_placeholder">Lägg till en tunnel med knappen nedan</string>
<string name="tunnel_name">Tunnelns namn</string>
<string name="tunnel_on_error">Kunde inte aktivera tunneln (wgTurnOn returnerade %d)</string>
<string name="tunnel_dns_failure">Det går inte att lösa DNS-värdnamn: ”%s”</string>
@@ -221,6 +232,16 @@
<string name="type_name_go_userspace">Användarutrymme för Go</string>
<string name="type_name_kernel_module">Kärnmodul</string>
<string name="unknown_error">Okänt fel</string>
+ <string name="updater_avalable">Det finns en uppdatering till appen. Vänligen uppdatera nu.</string>
+ <string name="updater_action">Ladda ner &amp; uppdatera</string>
+ <string name="updater_rechecking">Hämtar uppdateringens metadata…</string>
+ <string name="updater_download_progress">Laddar ner uppdatering: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Laddar ner uppdatering: %s</string>
+ <string name="updater_installing">Installerar uppdatering…</string>
+ <string name="updater_failure">Uppdatering misslyckades: %s. Försöker igen inom kort…</string>
+ <string name="updater_corrupt_title">Applikationen är korrupt</string>
+ <string name="updater_corrupt_message">Applikationen är korrupt. Vänligen ladda ner en APK från hemsidan länkad nedan. Avinstallera därefter denna applikation och installera den nerladdade APKn.</string>
+ <string name="updater_corrupt_navigate">Öppna hemsida</string>
<string name="version_summary">%1$s bakstycke %2$s</string>
<string name="version_summary_checking">Kontrollerar %s backstycke utgåva</string>
<string name="version_summary_unknown">Okänd %s utgåva</string>
diff --git a/ui/src/main/res/values-vi-rVN/strings.xml b/ui/src/main/res/values-vi-rVN/strings.xml
index 8149d7e5..79d8d6c3 100644
--- a/ui/src/main/res/values-vi-rVN/strings.xml
+++ b/ui/src/main/res/values-vi-rVN/strings.xml
@@ -94,6 +94,9 @@
<string name="tv_select_a_storage_drive">Chọn bộ lưu trữ</string>
<string name="tv_no_file_picker">Vui lòng cài đặt tệp tiện ích lưu trữ để tìm kiếm các tệp</string>
<string name="tv_add_tunnel_get_started">Thêm một tunnel để bắt đầu</string>
+ <string name="donate_title">❤️ Đóng góp cho Dự án Wireguard</string>
+ <string name="donate_summary">Mọi đóng góp đều giúp ích</string>
+ <string name="donate_google_play_disappointment">Cảm ơn bạn đã ủng hộ WireGuard!\n\nThật tiếc, dựa trên điều khoản của Google, chúng tôi không thể đưa vào liên kết dẫn đến trang đóng góp ở trang chủ của dự án. Mong rằng bạn có thể tìm cách cho việc này!\n\nXin cảm ơn bạn một lần nữa vì đã đóng góp.</string>
<string name="disable_config_export_title">Vô hiệu hóa xuất cấu hình</string>
<string name="disable_config_export_description">Vô hiệu hóa xuất cấu hình sẽ giúp giảm khả năng truy cập vào private keys</string>
<string name="dns_servers">DNS servers</string>
@@ -103,9 +106,34 @@
<string name="error_down">Có lỗi khi tắt tunnel: %s</string>
<string name="error_fetching_apps">Lỗi khi lấy danh sách ứng dụng: %s</string>
<string name="error_root">Vui lòng truy cập bằng quyền root và thử lại</string>
+ <string name="error_prepare">Lỗi khi chuẩn bị tunnel: %s</string>
<string name="error_up">Có lỗi khi bật tunnel: %s</string>
<string name="exclude_private_ips">Loại trừ IPs private</string>
<string name="generate_new_private_key">Tạo private key mới</string>
+ <string name="generic_error">Lỗi \"%s\" không xác định</string>
+ <string name="hint_automatic">(tự động)</string>
+ <string name="hint_generated">(được tạo tự động)</string>
+ <string name="hint_optional">(tùy chọn)</string>
+ <string name="hint_optional_discouraged">(tùy chọn, không khuyến khích)</string>
+ <string name="hint_random">(ngẫu nhiên)</string>
+ <string name="illegal_filename_error">Tên file không hợp lệ \"%s\"</string>
+ <string name="import_error">Không thể nhập tunnel: %s</string>
+ <string name="import_from_qr_code">Nhập tunnel từ mã QR</string>
+ <string name="import_success">Đã nhập \"%s\"</string>
+ <string name="interface_title">Giao diện</string>
+ <string name="key_contents_error">Kí tự không hợp lệ trong khoá</string>
+ <string name="key_length_error">Độ dài khoá không hợp lệ</string>
+ <string name="key_length_explanation_base64">: Khoá WireGuard base64 phải đủ 44 ký tự (32 bytes)</string>
+ <string name="key_length_explanation_binary">: Khoá WireGuard phải đủ 32 bytes</string>
+ <string name="key_length_explanation_hex">: Khoá WireGuard hex phải đủ 64 ký tự (32 bytes)</string>
+ <string name="latest_handshake">Lần bắt tay cuối</string>
+ <string name="latest_handshake_ago">%s giây trước</string>
+ <string name="listen_port">Cổng</string>
+ <string name="log_export_error">Không thể xuất nhật ký: %s</string>
+ <string name="log_export_subject">File nhật ký WireGuard Android</string>
+ <string name="log_export_success">Đã lưu vào \"%s\"</string>
+ <string name="log_export_title">Xuất file nhật ký</string>
+ <string name="log_saver_activity_label">Lưu nhật ký</string>
<string name="parse_error_inet_address">Địa chỉ IP</string>
<string name="peer">Đồng trang lứa</string>
</resources>
diff --git a/ui/src/main/res/values-zh-rCN/strings.xml b/ui/src/main/res/values-zh-rCN/strings.xml
index cd282317..7757e2fe 100644
--- a/ui/src/main/res/values-zh-rCN/strings.xml
+++ b/ui/src/main/res/values-zh-rCN/strings.xml
@@ -172,9 +172,9 @@
<string name="public_key">公钥</string>
<string name="qr_code_hint">提示:使用命令 `qrencode -t ansiutf8 &lt; tunnel.conf` 生成二维码</string>
<string name="quick_settings_tile_add_title">添加磁贴到快速设置面板</string>
- <string name="quick_settings_tile_add_summary">快捷方式磁贴开/关最近使用的隧道。</string>
- <string name="quick_settings_tile_add_failure">无法添加快捷方式磁贴:错误 %d</string>
- <string name="quick_settings_tile_action">开/关隧道</string>
+ <string name="quick_settings_tile_add_summary">通过快捷磁贴开启/关闭上次使用的隧道</string>
+ <string name="quick_settings_tile_add_failure">无法添加快捷磁贴:错误 %d</string>
+ <string name="quick_settings_tile_action">开启/关闭隧道</string>
<string name="restore_on_boot_summary_off">未启用</string>
<string name="restore_on_boot_summary_on">设备启动时自动开启上次使用的隧道</string>
<string name="restore_on_boot_title">启动时恢复</string>
@@ -226,8 +226,8 @@
<string name="updater_download_progress_nototal">正在下载更新:%s</string>
<string name="updater_installing">正在安装更新…</string>
<string name="updater_failure">更新失败:%s。将在稍后重试…</string>
- <string name="updater_corrupt_title">应用程序损坏</string>
- <string name="updater_corrupt_message">此应用程序已损坏。请从下面链接的网站重新下载APK。然后卸载此应用程序并从下载的APK重新安装它。</string>
+ <string name="updater_corrupt_title">应用损坏</string>
+ <string name="updater_corrupt_message">此应用已损坏。请从下方链接的网站中重新下载 APK,然后卸载此应用并重新安装。</string>
<string name="updater_corrupt_navigate">打开网站</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">正在检查 %s backend 版本</string>
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index df3d3340..1cb642e2 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -63,10 +63,11 @@
<string name="allow_remote_control_intents_summary_on">External apps may toggle tunnels (advanced)</string>
<string name="allow_remote_control_intents_title">Allow remote control apps</string>
<string name="allowed_ips">Allowed IPs</string>
- <string name="app_name" translatable="false">WireGuard</string>
+ <string name="app_name" translatable="false">(m7n.eu) WireGuard</string>
<string name="bad_config_context">%1$s\'s %2$s</string>
<string name="bad_config_context_top_level">%s</string>
<string name="bad_config_error">%1$s in %2$s</string>
+ <string name="bad_config_explanation_http_proxy">: Must be valid proxy hostname and port</string>
<string name="bad_config_explanation_pka">: Must be positive and no more than 65535</string>
<string name="bad_config_explanation_positive_number">: Must be positive</string>
<string name="bad_config_explanation_udp_port">: Must be a valid UDP port number</string>
@@ -104,6 +105,7 @@
<string name="dark_theme_summary_on">Currently using dark (night) theme</string>
<string name="dark_theme_title">Use dark theme</string>
<string name="delete">Delete</string>
+ <string name="dynamic_addresses">Dynamic addresses</string>
<string name="tv_delete">Select tunnel to delete</string>
<string name="tv_select_a_storage_drive">Select a storage drive</string>
<string name="tv_no_file_picker">Please install a file management utility to browse files</string>
@@ -130,6 +132,10 @@
<string name="hint_optional">(optional)</string>
<string name="hint_optional_discouraged">(optional, not recommended)</string>
<string name="hint_random">(random)</string>
+ <string name="http_proxy">Proxy</string>
+ <string name="http_proxy_hostname">Proxy hostname</string>
+ <string name="http_proxy_pac">Proxy Auto-Config URL</string>
+ <string name="http_proxy_port">Proxy port</string>
<string name="illegal_filename_error">Illegal file name “%s”</string>
<string name="import_error">Unable to import tunnel: %s</string>
<string name="import_from_qr_code">Import Tunnel from QR Code</string>