summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--build.gradle12
-rw-r--r--gradle.properties6
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin61608 -> 62076 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties4
-rwxr-xr-xgradlew7
-rw-r--r--settings.gradle4
-rw-r--r--tunnel/build.gradle59
-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.java31
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java555
-rw-r--r--tunnel/src/main/java/com/wireguard/android/backend/Statistics.java86
-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.java13
-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.java49
-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.java138
-rw-r--r--tunnel/src/main/proto/libwg.proto125
-rw-r--r--tunnel/tools/CMakeLists.txt2
-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.go56
-rw-r--r--tunnel/tools/libwg-go/dhcp.go192
-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.go716
-rw-r--r--tunnel/tools/libwg-go/jni.c19
-rw-r--r--tunnel/tools/libwg-go/service.go316
-rw-r--r--tunnel/tools/ndk-compat/compat.c52
-rw-r--r--tunnel/tools/ndk-compat/compat.h6
-rw-r--r--ui/build.gradle2
-rw-r--r--ui/proguard-android-optimize.txt3
-rw-r--r--ui/proguard-rules.pro14
-rw-r--r--ui/src/main/AndroidManifest.xml9
-rw-r--r--ui/src/main/java/com/wireguard/android/Application.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt14
-rw-r--r--ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt3
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelDetailFragment.kt35
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt27
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt3
-rw-r--r--ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt133
-rw-r--r--ui/src/main/java/com/wireguard/android/model/TunnelManager.kt5
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt16
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/Ed25519.java2507
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt156
-rw-r--r--ui/src/main/java/com/wireguard/android/updater/Updater.kt401
-rw-r--r--ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt2
-rw-r--r--ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt45
-rw-r--r--ui/src/main/java/com/wireguard/android/util/UserKnobs.kt30
-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.xml135
-rw-r--r--ui/src/main/res/layout/tunnel_detail_peer.xml62
-rw-r--r--ui/src/main/res/layout/tunnel_editor_fragment.xml105
-rw-r--r--ui/src/main/res/values-ar-rSA/strings.xml4
-rw-r--r--ui/src/main/res/values-da-rDK/strings.xml4
-rw-r--r--ui/src/main/res/values-no-rNO/strings.xml12
-rw-r--r--ui/src/main/res/values-ru/strings.xml7
-rw-r--r--ui/src/main/res/values-zh-rCN/strings.xml3
-rw-r--r--ui/src/main/res/values/dimens.xml1
-rw-r--r--ui/src/main/res/values/strings.xml15
-rw-r--r--version.gradle5
69 files changed, 6771 insertions, 310 deletions
diff --git a/build.gradle b/build.gradle
index b127172e..7bdd260e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
buildscript {
ext {
- activityVersion = '1.7.0'
+ activityVersion = '1.7.1'
annotationsVersion = '1.6.0'
appcompatVersion = '1.6.1'
biometricVersion = '1.1.0'
@@ -11,12 +11,16 @@ buildscript {
coroutinesVersion = '1.6.4'
datastoreVersion = '1.0.0'
desugarVersion = '1.1.5'
- fragmentVersion = '1.5.6'
+ fragmentVersion = '1.5.7'
+ grpcVersion = '1.55.0-SNAPSHOT'
jsr305Version = '3.0.2'
junitVersion = '4.13.2'
lifecycleRuntimeKtxVersion = '2.6.1'
materialComponentsVersion = '1.8.0'
preferenceVersion = '1.2.0'
+ protobufGradleVersion = '0.9.0'
+ protocVersion = '3.22.2'
+ protocGenGrpcVersion = '1.54.0'
zxingEmbeddedVersion = '4.3.0'
groupName = 'com.wireguard.android'
@@ -25,8 +29,8 @@ buildscript {
tasks {
wrapper {
- gradleVersion = "8.0.2"
- distributionSha256Sum = "ff7bf6a86f09b9b2c40bb8f48b25fc19cf2b2664fd1d220cd7ab833ec758d0d7"
+ gradleVersion = "8.1.1"
+ distributionSha256Sum = "e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f"
}
}
diff --git a/gradle.properties b/gradle.properties
index 2ecf213a..db5593b3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -42,8 +42,6 @@ android.generateManifestClass=true
android.experimental.enableSourceSetPathsMap=true
# Use relative paths for better Gradle caching of library build tasks
android.experimental.cacheCompileLibResources=true
-# Disable automatic creation of publishing components
-android.disableAutomaticComponentCreation=true
# Default Android build features
# Disable BuildConfig generation by default
@@ -61,8 +59,8 @@ android.library.defaults.buildfeatures.androidresources=false
# Suppress warnings for some features that aren't yet stabilized
android.suppressUnsupportedOptionWarnings=android.keepWorkerActionServicesBetweenBuilds,\
- android.experimental.enableNewResourceShrinker,android.experimental.enableNewResourceShrinker.preciseShrinking,\
- android.enableAppCompileTimeRClass,android.experimental.enableSourceSetPathsMap,android.experimental.cacheCompileLibResources,\
+ android.experimental.enableNewResourceShrinker.preciseShrinking,\
+ android.enableAppCompileTimeRClass,\
android.suppressUnsupportedOptionWarnings
# OSSRH sometimes struggles with slow deployments, so this makes Gradle
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index ccebba77..c1962a79 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 19acfb4e..2c3425d4 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=ff7bf6a86f09b9b2c40bb8f48b25fc19cf2b2664fd1d220cd7ab833ec758d0d7
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
+distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 79a61d42..aeb74cbb 100755
--- a/gradlew
+++ b/gradlew
@@ -85,9 +85,6 @@ done
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -197,6 +194,10 @@ if "$cygwin" || "$msys" ; then
done
fi
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
diff --git a/settings.gradle b/settings.gradle
index e6dd51a5..648ecad2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,5 +1,5 @@
pluginManagement {
- def agpVersion = '7.4.2'
+ def agpVersion = '8.0.0'
def kotlinVersion = "1.8.0"
repositories {
gradlePluginPortal()
@@ -19,10 +19,12 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ mavenLocal()
}
}
rootProject.name = "wireguard-android"
include ':tunnel'
+include ':bgp-java'
include ':ui'
diff --git a/tunnel/build.gradle b/tunnel/build.gradle
index 894762c9..591cebe1 100644
--- a/tunnel/build.gradle
+++ b/tunnel/build.gradle
@@ -1,5 +1,12 @@
+buildscript {
+ dependencies {
+ classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufGradleVersion"
+ }
+}
+
plugins {
id 'com.android.library'
+ id 'com.google.protobuf' version "$protobufGradleVersion"
}
version wireguardVersionName
@@ -63,10 +70,62 @@ android {
}
dependencies {
+ implementation project(":bgp-java")
implementation "androidx.annotation:annotation:$annotationsVersion"
implementation "androidx.collection:collection:$collectionVersion"
+ implementation "io.grpc:grpc-android:$grpcVersion"
+ implementation "io.grpc:grpc-okhttp:$grpcVersion"
+ implementation "io.grpc:grpc-protobuf-lite:$grpcVersion"
+ implementation "io.grpc:grpc-stub:$grpcVersion"
compileOnly "com.google.code.findbugs:jsr305:$jsr305Version"
+ compileOnly "javax.annotation:javax.annotation-api:1.2"
testImplementation "junit:junit:$junitVersion"
}
+protobuf {
+ protoc {
+ // You still need protoc like in the non-Android case
+ artifact = "com.google.protobuf:protoc:$protocVersion"
+ }
+ plugins {
+ grpc {
+ artifact = "io.grpc:protoc-gen-grpc-java:$protocGenGrpcVersion"
+ }
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ java {
+ option 'lite'
+ }
+ }
+ task.plugins {
+ grpc {
+ option 'lite'
+ }
+ }
+ }
+ }
+}
+
+afterEvaluate({ project ->
+ // All custom configurations created by the protobuf plugin,
+ // are only available at this point.
+ def protoc = configurations.getByName('protobufToolsLocator_protoc')
+
+ task copyProtoc(type: Copy) {
+ // Used by tunnel/tools/libwg-go/Makefile run in tools/CMakeLists.txt
+ from protoc
+ into "${gradle.gradleUserHomeDir}/caches/protoc-${protocVersion}"
+ rename 'protoc-.*', 'protoc'
+ fileMode 0775
+ }
+
+ preBuild.dependsOn copyProtoc
+
+ // Extract duration.proto used by external library in libwg.proto
+ preDebugBuild.dependsOn extractIncludeDebugProto
+ preReleaseBuild.dependsOn extractIncludeReleaseProto
+})
+
apply from: "publish.gradle"
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..59a3e69c
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Dhcp.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util.Set;
+
+/**
+ * Class representing DHCP info for a {@link Tunnel} instance.
+ */
+@NonNullForAll
+public class Dhcp {
+ private Set<InetNetwork> addresses;
+
+ Dhcp(Set<InetNetwork> addresses) {
+ this.addresses = addresses;
+ }
+
+ public Set<InetNetwork> getAddresses() {
+ return addresses;
+ }
+
+ 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 670f04a1..24f1adc8 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/GoBackend.java
@@ -7,30 +7,90 @@ package com.wireguard.android.backend;
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.ParcelFileDescriptor;
+import android.os.Process;
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.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.Instant;
+import java.nio.ByteOrder;
import java.util.Collections;
+import java.util.LinkedHashSet;
+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;
@@ -43,12 +103,20 @@ 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;
@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 ManagedChannel channel;
+ private ConnectivityManager connectivityManager;
+ private ConnectivityManager.NetworkCallback myNetworkCallback = new MyNetworkCallback();
+ private ConnectivityManager.NetworkCallback vpnNetworkCallback;
+ @Nullable private Network activeNetwork;
+ private boolean obtainDhcpLease = false;
+ @Nullable private Bgp bgp;
/**
* Public constructor for GoBackend.
@@ -58,6 +126,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();
}
/**
@@ -76,12 +149,16 @@ public final class GoBackend implements Backend {
private static native int wgGetSocketV6(int handle);
+ 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.
*
@@ -125,12 +202,14 @@ public final class GoBackend implements Backend {
Key key = null;
long rx = 0;
long tx = 0;
+ long latestHandshakeMSec = 0;
for (final String line : config.split("\\n")) {
if (line.startsWith("public_key=")) {
if (key != null)
- stats.add(key, rx, tx);
+ stats.add(key, rx, tx, latestHandshakeMSec);
rx = 0;
tx = 0;
+ latestHandshakeMSec = 0;
try {
key = Key.fromHex(line.substring(11));
} catch (final KeyFormatException ignored) {
@@ -152,10 +231,26 @@ public final class GoBackend implements Backend {
} catch (final NumberFormatException ignored) {
tx = 0;
}
+ } else if (line.startsWith("last_handshake_time_sec=")) {
+ if (key == null)
+ continue;
+ try {
+ latestHandshakeMSec += Long.parseLong(line.substring(24)) * 1000;
+ } catch (final NumberFormatException ignored) {
+ latestHandshakeMSec = 0;
+ }
+ } else if (line.startsWith("last_handshake_time_nsec=")) {
+ if (key == null)
+ continue;
+ try {
+ latestHandshakeMSec += Long.parseLong(line.substring(25)) / 1000000;
+ } catch (final NumberFormatException ignored) {
+ latestHandshakeMSec = 0;
+ }
}
}
if (key != null)
- stats.add(key, rx, tx);
+ stats.add(key, rx, tx, latestHandshakeMSec);
return stats;
}
@@ -166,7 +261,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();
}
/**
@@ -205,78 +303,278 @@ 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 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);
+ });
- if (currentTunnelHandle != -1) {
- Log.w(TAG, "Tunnel already up");
- return;
+ StartHttpProxyRequest req = reqBuilder.build();
+ StartHttpProxyResponse resp = stub.startHttpProxy(req);
+ Log.i(TAG, "Start http proxy listen_port:" + resp.getListenPort() + ", error:" + resp.getError().getMessage());
+ streamer.start();
+ return resp.getListenPort();
+ }
+
+ 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{
+ 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());
- }
+ Set<InetNetwork> addresses = new LinkedHashSet<>();
+ if (resp.getLeasesList() != null) {
+ for (final Lease lease: resp.getLeasesList()) {
+ try {
+ InetAddress addr = InetAddress.getByAddress(lease.getAddress().getAddress().toByteArray());
+ Log.i(TAG, "Lease: " + addr);
+ addresses.add(new InetNetwork(addr, 128));
+ } catch (UnknownHostException ex) {
+ // Ignore
}
- break;
}
+ }
- // Build config
- final String goConfig = config.toWgUserspaceString();
+ Dhcp dhcp = new Dhcp(addresses);
- // Create the vpn tunnel with android API
+ // Replace the vpn tunnel
+ final VpnService.Builder builder = getBuilder(currentTunnel.getName(), currentConfig, service, dhcp.getAddresses());
+
+ 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");
+
+ 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<InetNetwork> 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 InetNetwork lease: leases) {
+ builder.addAddress(lease.getAddress(), lease.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()) {
@@ -286,20 +584,111 @@ 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 (!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);
@@ -314,6 +703,15 @@ public final class GoBackend implements Backend {
service.protect(wgGetSocketV4(currentTunnelHandle));
service.protect(wgGetSocketV6(currentTunnelHandle));
+
+ obtainDhcpLease = true;
+
+ 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");
@@ -323,6 +721,16 @@ public final class GoBackend implements Backend {
currentTunnel = null;
currentTunnelHandle = -1;
currentConfig = 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();
@@ -388,10 +796,21 @@ public final class GoBackend implements Backend {
@Override
public void onDestroy() {
if (owner != null) {
+ if (owner.bgp != null) {
+ owner.bgp.stopServer();
+ owner.bgp = 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;
@@ -417,4 +836,48 @@ 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) {
+ 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 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.toWgUserspaceStringWithChangedEndpoints(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);
+ }
+ }
+ }
}
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 35a472bd..9fc92c53 100644
--- a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
+++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java
@@ -6,36 +6,65 @@
package com.wireguard.android.backend;
import android.os.SystemClock;
-import android.util.Pair;
import com.wireguard.crypto.Key;
import com.wireguard.util.NonNullForAll;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
+
+import androidx.annotation.Nullable;
/**
* Class representing transfer statistics for a {@link Tunnel} instance.
*/
@NonNullForAll
public class Statistics {
- private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
+
+ // 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);
+ }
+ }
+
+ private final Map<Key, PeerStats> stats = new HashMap<>();
private long lastTouched = SystemClock.elapsedRealtime();
Statistics() {
}
/**
- * Add a peer and its current data usage to the internal map.
+ * Add a peer and its current stats to the internal map.
*
- * @param key A WireGuard public key bound to a particular peer
- * @param rx The received traffic for the {@link com.wireguard.config.Peer} referenced by
- * the provided {@link Key}. This value is in bytes
- * @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
- * the provided {@link Key}. This value is in bytes.
+ * @param key A WireGuard public key bound to a particular peer
+ * @param rxBytes The received traffic for the {@link com.wireguard.config.Peer} referenced by
+ * the provided {@link Key}. This value is in bytes
+ * @param txBytes The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
+ * the provided {@link Key}. This value is in bytes.
+ * @param latestHandshake The timestamp of the latest handshake for the {@link com.wireguard.config.Peer}
+ * referenced by the provided {@link Key}. The value is in epoch milliseconds.
*/
- void add(final Key key, final long rx, final long tx) {
- peerBytes.put(key, Pair.create(rx, tx));
+ void add(final Key key, final long rxBytes, final long txBytes, final long latestHandshake) {
+ stats.put(key, new PeerStats(rxBytes, txBytes, latestHandshake));
lastTouched = SystemClock.elapsedRealtime();
}
@@ -49,31 +78,14 @@ public class Statistics {
}
/**
- * Get the received traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
- * the provided {@link Key}
- *
- * @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
- * @return a long representing the number of bytes received by this peer.
- */
- public long peerRx(final Key peer) {
- final Pair<Long, Long> rxTx = peerBytes.get(peer);
- if (rxTx == null)
- return 0;
- return rxTx.first;
- }
-
- /**
- * Get the transmitted traffic (in bytes) for the {@link com.wireguard.config.Peer} referenced by
- * the provided {@link Key}
+ * Get the statistics for the {@link com.wireguard.config.Peer} referenced by the provided {@link Key}
*
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
- * @return a long representing the number of bytes transmitted by this peer.
+ * @return a {@link PeerStats} representing various statistics about this peer.
*/
- public long peerTx(final Key peer) {
- final Pair<Long, Long> rxTx = peerBytes.get(peer);
- if (rxTx == null)
- return 0;
- return rxTx.second;
+ @Nullable
+ public PeerStats peer(final Key peer) {
+ return this.stats.get(peer);
}
/**
@@ -83,7 +95,7 @@ public class Statistics {
* {@link com.wireguard.config.Peer}s
*/
public Key[] peers() {
- return peerBytes.keySet().toArray(new Key[0]);
+ return stats.keySet().toArray(new Key[0]);
}
/**
@@ -93,8 +105,8 @@ public class Statistics {
*/
public long totalRx() {
long rx = 0;
- for (final Pair<Long, Long> val : peerBytes.values()) {
- rx += val.first;
+ for (final PeerStats val : stats.values()) {
+ rx += val.rxBytes;
}
return rx;
}
@@ -106,8 +118,8 @@ public class Statistics {
*/
public long totalTx() {
long tx = 0;
- for (final Pair<Long, Long> val : peerBytes.values()) {
- tx += val.second;
+ for (final PeerStats val : stats.values()) {
+ tx += val.txBytes;
}
return tx;
}
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 143a3ab4..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,12 +14,14 @@ 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;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -83,17 +85,17 @@ public final class WgQuickBackend implements Backend {
final Statistics stats = new Statistics();
final Collection<String> output = new ArrayList<>();
try {
- if (rootShell.run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0)
+ if (rootShell.run(output, String.format("wg show '%s' dump", tunnel.getName())) != 0)
return stats;
} catch (final Exception ignored) {
return stats;
}
for (final String line : output) {
final String[] parts = line.split("\\t");
- if (parts.length != 3)
+ if (parts.length != 8)
continue;
try {
- stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]));
+ stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[5]), Long.parseLong(parts[6]), Long.parseLong(parts[4]) * 1000);
} catch (final Exception ignored) {
}
}
@@ -166,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/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..5cd03fc0 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 toWgUserspaceStringWithChangedEndpoints(Resolver resolver) {
+ final StringBuilder sb = new StringBuilder();
+ for (final Peer peer : peers)
+ sb.append(peer.toWgUserspaceStringWithChangedEndpoint(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..d45914f8
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/config/HttpProxy.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2022 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..dffd534b 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,34 @@ 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, false);
+ }
+
+ public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force) {
+ return getResolved(resolver, force, false);
+ }
+
+ public Optional<InetEndpoint> getResolvedIfChanged(Resolver resolver) {
+ return getResolved(resolver, true, true);
+ }
+
+ public Optional<InetEndpoint> getResolved(Resolver resolver, Boolean force, Boolean ifChanged) {
+ 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();
+
+ if (ifChanged && resolvedNow.equals(resolved)) {
+ return Optional.empty();
+ }
+
+ 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..d7b75f16 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 toWgUserspaceStringWithChangedEndpoint(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..654e01f5
--- /dev/null
+++ b/tunnel/src/main/java/com/wireguard/util/Resolver.java
@@ -0,0 +1,138 @@
+/*
+ * 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");
+ continue;
+ }
+
+ Log.w(TAG, "Local address: " + sock.getLocalAddress());
+
+ if (candidate instanceof Inet4Address) {
+ // Accept IPv4 as preferred address.
+ address = candidate;
+ 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..4fa4468b
--- /dev/null
+++ b/tunnel/src/main/proto/libwg.proto
@@ -0,0 +1,125 @@
+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);
+}
+
+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;
+}
diff --git a/tunnel/tools/CMakeLists.txt b/tunnel/tools/CMakeLists.txt
index 843374ef..490a1755 100644
--- a/tunnel/tools/CMakeLists.txt
+++ b/tunnel/tools/CMakeLists.txt
@@ -32,7 +32,7 @@ add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib
# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions
file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)
-add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND "${CMAKE_C_COMPILER}"
+add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc
-O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""
-o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}
)
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..4409b49e 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.20.1
+PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go
+PROTOC := $(GRADLE_USER_HOME)/caches/protoc-$(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..78ee7f54 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,40 @@ 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)
}
func main() {}
diff --git a/tunnel/tools/libwg-go/dhcp.go b/tunnel/tools/libwg-go/dhcp.go
new file mode 100644
index 00000000..b7919151
--- /dev/null
+++ b/tunnel/tools/libwg-go/dhcp.go
@@ -0,0 +1,192 @@
+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"
+)
+
+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 {
+ addr :=opt.(*dhcpv6.OptIAAddress).IPv6Addr
+ lease := &gen.Lease{
+ Address: &gen.InetAddress{
+ Address: addr,
+ },
+ // PreferredLifetime: ,
+ // 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..9c457286
--- /dev/null
+++ b/tunnel/tools/libwg-go/http-proxy.go
@@ -0,0 +1,716 @@
+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
+ 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
+ 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)
+
+ 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..20bfb332 100644
--- a/tunnel/tools/libwg-go/jni.c
+++ b/tunnel/tools/libwg-go/jni.c
@@ -14,6 +14,8 @@ extern int wgGetSocketV4(int handle);
extern int wgGetSocketV6(int handle);
extern char *wgGetConfig(int handle);
extern char *wgVersion();
+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 +71,20 @@ JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion
free(version);
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..f5460486
--- /dev/null
+++ b/tunnel/tools/libwg-go/service.go
@@ -0,0 +1,316 @@
+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
+}
diff --git a/tunnel/tools/ndk-compat/compat.c b/tunnel/tools/ndk-compat/compat.c
index 2c54c086..c5ce85ec 100644
--- a/tunnel/tools/ndk-compat/compat.c
+++ b/tunnel/tools/ndk-compat/compat.c
@@ -6,58 +6,6 @@
#define FILE_IS_EMPTY
-#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 18
-#undef FILE_IS_EMPTY
-#include <stdio.h>
-#include <stdlib.h>
-
-ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp)
-{
- char *ptr, *eptr;
-
- if (*buf == NULL || *bufsiz == 0) {
- *bufsiz = BUFSIZ;
- if ((*buf = malloc(*bufsiz)) == NULL)
- return -1;
- }
-
- for (ptr = *buf, eptr = *buf + *bufsiz;;) {
- int c = fgetc(fp);
- if (c == -1) {
- if (feof(fp)) {
- ssize_t diff = (ssize_t)(ptr - *buf);
- if (diff != 0) {
- *ptr = '\0';
- return diff;
- }
- }
- return -1;
- }
- *ptr++ = c;
- if (c == delimiter) {
- *ptr = '\0';
- return ptr - *buf;
- }
- if (ptr + 2 >= eptr) {
- char *nbuf;
- size_t nbufsiz = *bufsiz * 2;
- ssize_t d = ptr - *buf;
- if ((nbuf = realloc(*buf, nbufsiz)) == NULL)
- return -1;
- *buf = nbuf;
- *bufsiz = nbufsiz;
- eptr = nbuf + nbufsiz;
- ptr = nbuf + d;
- }
- }
-}
-
-ssize_t getline(char **buf, size_t *bufsiz, FILE *fp)
-{
- return getdelim(buf, bufsiz, '\n', fp);
-}
-#endif
-
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
#undef FILE_IS_EMPTY
#include <string.h>
diff --git a/tunnel/tools/ndk-compat/compat.h b/tunnel/tools/ndk-compat/compat.h
index 32f6d799..7dfd8e14 100644
--- a/tunnel/tools/ndk-compat/compat.h
+++ b/tunnel/tools/ndk-compat/compat.h
@@ -4,12 +4,6 @@
*
*/
-#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 18
-#include <stdio.h>
-ssize_t getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp);
-ssize_t getline(char **buf, size_t *bufsiz, FILE *fp);
-#endif
-
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
char *strchrnul(const char *s, int c);
#endif
diff --git a/ui/build.gradle b/ui/build.gradle
index 35abacf8..3661d24c 100644
--- a/ui/build.gradle
+++ b/ui/build.gradle
@@ -42,7 +42,7 @@ android {
release {
minifyEnabled true
shrinkResources true
- proguardFiles "proguard-android-optimize.txt", "proguard-rules.pro"
+ proguardFiles "proguard-android-optimize.txt"
packagingOptions {
exclude "DebugProbesKt.bin"
exclude "kotlin-tooling-metadata.json"
diff --git a/ui/proguard-android-optimize.txt b/ui/proguard-android-optimize.txt
index e2095ad6..7bbc2b88 100644
--- a/ui/proguard-android-optimize.txt
+++ b/ui/proguard-android-optimize.txt
@@ -1,7 +1,6 @@
-allowaccessmodification
--dontpreverify
-dontusemixedcaseclassnames
--dontskipnonpubliclibraryclasses
+-dontobfuscate
-verbose
-keepattributes *Annotation*
diff --git a/ui/proguard-rules.pro b/ui/proguard-rules.pro
deleted file mode 100644
index d7864f48..00000000
--- a/ui/proguard-rules.pro
+++ /dev/null
@@ -1,14 +0,0 @@
-# Squelch all warnings, they're harmless but ProGuard
-# escalates them as errors.
--dontwarn sun.misc.Unsafe
-
-# Fragment 1.2.4 allows Fragment classes to be obfuscated but
-# databinding references in XML seem to not be rewritten to
-# match, so we preserve the names as 1.2.3 did.
--if public class ** extends androidx.fragment.app.Fragment
--keep public class <1> {
- public <init>();
-}
-
-# Don't obfuscate
--dontobfuscate
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index a7538a6e..e1807285 100644
--- a/ui/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -3,9 +3,12 @@
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" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
@@ -103,6 +106,7 @@
<intent-filter>
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
@@ -142,5 +146,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/Application.kt b/ui/src/main/java/com/wireguard/android/Application.kt
index 06903af4..4dcec508 100644
--- a/ui/src/main/java/com/wireguard/android/Application.kt
+++ b/ui/src/main/java/com/wireguard/android/Application.kt
@@ -22,6 +22,7 @@ import com.wireguard.android.backend.GoBackend
import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.configStore.FileConfigStore
import com.wireguard.android.model.TunnelManager
+import com.wireguard.android.updater.Updater
import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller
import com.wireguard.android.util.UserKnobs
@@ -115,6 +116,7 @@ class Application : android.app.Application() {
Log.e(TAG, Log.getStackTraceString(e))
}
}
+ Updater.monitorForUpdates()
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
diff --git a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
index 23775f44..2b9312a0 100644
--- a/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
+++ b/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt
@@ -8,15 +8,27 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
+import com.wireguard.android.activity.MainActivity
import com.wireguard.android.backend.WgQuickBackend
+import com.wireguard.android.updater.Updater
import com.wireguard.android.util.applicationScope
import kotlinx.coroutines.launch
class BootShutdownReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action ?: return
+
+ if (Intent.ACTION_MY_PACKAGE_REPLACED == action && Updater.installer() == context.packageName) {
+ /* TODO: does not work because of restrictions placed on broadcast receivers. */
+ val start = Intent(context, MainActivity::class.java)
+ start.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ start.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(start)
+ return
+ }
+
applicationScope.launch {
if (Application.getBackend() !is WgQuickBackend) return@launch
- val action = intent.action ?: return@launch
val tunnelManager = Application.getTunnelManager()
if (Intent.ACTION_BOOT_COMPLETED == action) {
Log.i(TAG, "Broadcast receiver restoring state (boot)")
diff --git a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
index 36925bea..9deed440 100644
--- a/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
+++ b/ui/src/main/java/com/wireguard/android/activity/LogViewerActivity.kt
@@ -157,8 +157,7 @@ class LogViewerActivity : AppCompatActivity() {
builder.append(rawLogLines[i])
builder.append('\n')
}
- val ret = builder.toString().toByteArray(Charsets.UTF_8)
- return ret
+ return builder.toString().toByteArray(Charsets.UTF_8)
}
private suspend fun saveLog() {
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 8b155e24..53f79c1d 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
@@ -77,7 +79,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
}
@@ -112,16 +116,25 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
?: continue
val publicKey = peer.item!!.publicKey
- val rx = statistics.peerRx(publicKey)
- val tx = statistics.peerTx(publicKey)
- if (rx == 0L && tx == 0L) {
+ val peerStats = statistics.peer(publicKey)
+ if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
- continue
+ } else {
+ peer.transferText.text = getString(R.string.transfer_rx_tx,
+ QuantityFormatter.formatBytes(peerStats.rxBytes),
+ QuantityFormatter.formatBytes(peerStats.txBytes))
+ peer.transferLabel.visibility = View.VISIBLE
+ peer.transferText.visibility = View.VISIBLE
+ }
+ if (peerStats == null || peerStats.latestHandshakeEpochMillis == 0L) {
+ peer.latestHandshakeLabel.visibility = View.GONE
+ peer.latestHandshakeText.visibility = View.GONE
+ } else {
+ peer.latestHandshakeText.text = QuantityFormatter.formatEpochAgo(peerStats.latestHandshakeEpochMillis)
+ peer.latestHandshakeLabel.visibility = View.VISIBLE
+ peer.latestHandshakeText.visibility = View.VISIBLE
}
- peer.transferText.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
- peer.transferLabel.visibility = View.VISIBLE
- peer.transferText.visibility = View.VISIBLE
}
} catch (e: Throwable) {
for (i in 0 until binding.peersLayout.childCount) {
@@ -129,7 +142,13 @@ class TunnelDetailFragment : BaseFragment(), MenuProvider {
?: continue
peer.transferLabel.visibility = View.GONE
peer.transferText.visibility = View.GONE
+ peer.latestHandshakeLabel.visibility = View.GONE
+ peer.latestHandshakeText.visibility = View.GONE
}
}
}
+
+ 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 7a8b822e..9597fef6 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -17,13 +17,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
@@ -33,6 +37,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
@@ -44,6 +49,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)
}
@@ -83,6 +103,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/fragment/TunnelListFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
index 0b6493c9..8c5389d9 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelListFragment.kt
@@ -33,6 +33,7 @@ import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowC
import com.wireguard.android.databinding.TunnelListFragmentBinding
import com.wireguard.android.databinding.TunnelListItemBinding
import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.updater.SnackbarUpdateShower
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.QrCodeFromFileScanner
import com.wireguard.android.util.TunnelImporter
@@ -122,6 +123,8 @@ class TunnelListFragment : BaseFragment() {
backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
backPressedCallback?.isEnabled = false
+ SnackbarUpdateShower.attachToActivity(requireActivity(), binding?.mainContainer!!, binding?.createFab)
+
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 c3e3405e..490a2216 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
}
@@ -137,6 +162,106 @@ class ObservableTunnel internal constructor(
}
+ @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 e7bb751b..77d69f70 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) })!!
}
@@ -155,7 +156,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/preference/DonatePreference.kt b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
index bb49c7e1..bf0624bc 100644
--- a/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt
@@ -14,6 +14,7 @@ import android.widget.Toast
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.wireguard.android.R
+import com.wireguard.android.updater.Updater
import com.wireguard.android.util.ErrorMessages
class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
@@ -22,21 +23,8 @@ class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(cont
override fun getTitle() = context.getString(R.string.donate_title)
override fun onClick() {
- val installer = try {
- val packageName = context.packageName
- val pm = context.packageManager
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
- } else {
- @Suppress("DEPRECATION")
- pm.getInstallerPackageName(packageName) ?: ""
- }
- } catch (_: Throwable) {
- ""
- }
-
/* Google Play Store forbids links to our donation page. */
- if (installer == "com.android.vending") {
+ if (Updater.installerIsGooglePlay()) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.donate_title)
.setMessage(R.string.donate_google_play_disappointment)
diff --git a/ui/src/main/java/com/wireguard/android/updater/Ed25519.java b/ui/src/main/java/com/wireguard/android/updater/Ed25519.java
new file mode 100644
index 00000000..d27caf1c
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/updater/Ed25519.java
@@ -0,0 +1,2507 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * Copyright 2017 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.updater;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * Implementation of Ed25519 signature verification.
+ *
+ * <p>This implementation is based on the ed25519/ref10 implementation in NaCl.</p>
+ *
+ * <p>It implements this twisted Edwards curve:
+ *
+ * <pre>
+ * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
+ * </pre>
+ *
+ * @see <a href="https://eprint.iacr.org/2008/013.pdf">Bernstein D.J., Birkner P., Joye M., Lange
+ * T., Peters C. (2008) Twisted Edwards Curves</a>
+ * @see <a href="https://eprint.iacr.org/2008/522.pdf">Hisil H., Wong K.KH., Carter G., Dawson E.
+ * (2008) Twisted Edwards Curves Revisited</a>
+ */
+final class Ed25519 {
+
+ // d = -121665 / 121666 mod 2^255-19
+ private static final long[] D;
+ // 2d
+ private static final long[] D2;
+ // 2^((p-1)/4) mod p where p = 2^255-19
+ private static final long[] SQRTM1;
+
+ /**
+ * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] =
+ * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0]
+ */
+ private static final CachedXYT[][] B_TABLE;
+ private static final CachedXYT[] B2;
+
+ private static final BigInteger P_BI =
+ BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
+ private static final BigInteger D_BI =
+ BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI);
+ private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI);
+ private static final BigInteger SQRTM1_BI =
+ BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI);
+
+ private Ed25519() {
+ }
+
+ private static class Point {
+ private BigInteger x;
+ private BigInteger y;
+ }
+
+ private static BigInteger recoverX(BigInteger y) {
+ // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19
+ BigInteger xx =
+ y.pow(2)
+ .subtract(BigInteger.ONE)
+ .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI));
+ BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI);
+ if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) {
+ x = x.multiply(SQRTM1_BI).mod(P_BI);
+ }
+ if (x.testBit(0)) {
+ x = P_BI.subtract(x);
+ }
+ return x;
+ }
+
+ private static Point edwards(Point a, Point b) {
+ Point o = new Point();
+ BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI);
+ o.x =
+ (a.x.multiply(b.y).add(b.x.multiply(a.y)))
+ .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI))
+ .mod(P_BI);
+ o.y =
+ (a.y.multiply(b.y).add(a.x.multiply(b.x)))
+ .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI))
+ .mod(P_BI);
+ return o;
+ }
+
+ private static byte[] toLittleEndian(BigInteger n) {
+ byte[] b = new byte[32];
+ byte[] nBytes = n.toByteArray();
+ System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
+ for (int i = 0; i < b.length / 2; i++) {
+ byte t = b[i];
+ b[i] = b[b.length - i - 1];
+ b[b.length - i - 1] = t;
+ }
+ return b;
+ }
+
+ private static CachedXYT getCachedXYT(Point p) {
+ return new CachedXYT(
+ Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))),
+ Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))),
+ Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI))));
+ }
+
+ static {
+ Point b = new Point();
+ b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI);
+ b.x = recoverX(b.y);
+
+ D = Field25519.expand(toLittleEndian(D_BI));
+ D2 = Field25519.expand(toLittleEndian(D2_BI));
+ SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI));
+
+ Point bi = b;
+ B_TABLE = new CachedXYT[32][8];
+ for (int i = 0; i < 32; i++) {
+ Point bij = bi;
+ for (int j = 0; j < 8; j++) {
+ B_TABLE[i][j] = getCachedXYT(bij);
+ bij = edwards(bij, bi);
+ }
+ for (int j = 0; j < 8; j++) {
+ bi = edwards(bi, bi);
+ }
+ }
+ bi = b;
+ Point b2 = edwards(b, b);
+ B2 = new CachedXYT[8];
+ for (int i = 0; i < 8; i++) {
+ B2[i] = getCachedXYT(bi);
+ bi = edwards(bi, b2);
+ }
+ }
+
+ private static final int PUBLIC_KEY_LEN = Field25519.FIELD_LEN;
+ private static final int SIGNATURE_LEN = Field25519.FIELD_LEN * 2;
+
+ /**
+ * Defines field 25519 function based on <a
+ * href="https://github.com/agl/curve25519-donna/blob/master/curve25519-donna.c">curve25519-donna C
+ * implementation</a> (mostly identical).
+ *
+ * <p>Field elements are written as an array of signed, 64-bit limbs (an array of longs), least
+ * significant first. The value of the field element is:
+ *
+ * <pre>
+ * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
+ * 2^204·x[8] + 2^230·x[9],
+ * </pre>
+ *
+ * <p>i.e. the limbs are 26, 25, 26, 25, ... bits wide.
+ */
+ private static final class Field25519 {
+ /**
+ * During Field25519 computation, the mixed radix representation may be in different forms:
+ * <ul>
+ * <li> Reduced-size form: the array has size at most 10.
+ * <li> Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most
+ * 19.
+ * </ul>
+ * <p>
+ * TODO(quannguyen):
+ * <ul>
+ * <li> Clarify ill-defined terminologies.
+ * <li> The reduction procedure is different from DJB's paper
+ * (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree and
+ * reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should check to
+ * see what's going on.
+ * <li> Consider using method mult() everywhere and making product() private.
+ * </ul>
+ */
+
+ static final int FIELD_LEN = 32;
+ static final int LIMB_CNT = 10;
+ private static final long TWO_TO_25 = 1 << 25;
+ private static final long TWO_TO_26 = TWO_TO_25 << 1;
+
+ private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28};
+ private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6};
+ private static final int[] MASK = {0x3ffffff, 0x1ffffff};
+ private static final int[] SHIFT = {26, 25};
+
+ /**
+ * Sums two numbers: output = in1 + in2
+ * <p>
+ * On entry: in1, in2 are in reduced-size form.
+ */
+ static void sum(long[] output, long[] in1, long[] in2) {
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = in1[i] + in2[i];
+ }
+ }
+
+ /**
+ * Sums two numbers: output += in
+ * <p>
+ * On entry: in is in reduced-size form.
+ */
+ static void sum(long[] output, long[] in) {
+ sum(output, output, in);
+ }
+
+ /**
+ * Find the difference of two numbers: output = in1 - in2
+ * (note the order of the arguments!).
+ * <p>
+ * On entry: in1, in2 are in reduced-size form.
+ */
+ static void sub(long[] output, long[] in1, long[] in2) {
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = in1[i] - in2[i];
+ }
+ }
+
+ /**
+ * Find the difference of two numbers: output = in - output
+ * (note the order of the arguments!).
+ * <p>
+ * On entry: in, output are in reduced-size form.
+ */
+ static void sub(long[] output, long[] in) {
+ sub(output, in, output);
+ }
+
+ /**
+ * Multiply a number by a scalar: output = in * scalar
+ */
+ static void scalarProduct(long[] output, long[] in, long scalar) {
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = in[i] * scalar;
+ }
+ }
+
+ /**
+ * Multiply two numbers: out = in2 * in
+ * <p>
+ * output must be distinct to both inputs. The inputs are reduced coefficient form,
+ * the output is not.
+ * <p>
+ * out[x] <= 14 * the largest product of the input limbs.
+ */
+ static void product(long[] out, long[] in2, long[] in) {
+ out[0] = in2[0] * in[0];
+ out[1] = in2[0] * in[1]
+ + in2[1] * in[0];
+ out[2] = 2 * in2[1] * in[1]
+ + in2[0] * in[2]
+ + in2[2] * in[0];
+ out[3] = in2[1] * in[2]
+ + in2[2] * in[1]
+ + in2[0] * in[3]
+ + in2[3] * in[0];
+ out[4] = in2[2] * in[2]
+ + 2 * (in2[1] * in[3] + in2[3] * in[1])
+ + in2[0] * in[4]
+ + in2[4] * in[0];
+ out[5] = in2[2] * in[3]
+ + in2[3] * in[2]
+ + in2[1] * in[4]
+ + in2[4] * in[1]
+ + in2[0] * in[5]
+ + in2[5] * in[0];
+ out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1])
+ + in2[2] * in[4]
+ + in2[4] * in[2]
+ + in2[0] * in[6]
+ + in2[6] * in[0];
+ out[7] = in2[3] * in[4]
+ + in2[4] * in[3]
+ + in2[2] * in[5]
+ + in2[5] * in[2]
+ + in2[1] * in[6]
+ + in2[6] * in[1]
+ + in2[0] * in[7]
+ + in2[7] * in[0];
+ out[8] = in2[4] * in[4]
+ + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1])
+ + in2[2] * in[6]
+ + in2[6] * in[2]
+ + in2[0] * in[8]
+ + in2[8] * in[0];
+ out[9] = in2[4] * in[5]
+ + in2[5] * in[4]
+ + in2[3] * in[6]
+ + in2[6] * in[3]
+ + in2[2] * in[7]
+ + in2[7] * in[2]
+ + in2[1] * in[8]
+ + in2[8] * in[1]
+ + in2[0] * in[9]
+ + in2[9] * in[0];
+ out[10] =
+ 2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1])
+ + in2[4] * in[6]
+ + in2[6] * in[4]
+ + in2[2] * in[8]
+ + in2[8] * in[2];
+ out[11] = in2[5] * in[6]
+ + in2[6] * in[5]
+ + in2[4] * in[7]
+ + in2[7] * in[4]
+ + in2[3] * in[8]
+ + in2[8] * in[3]
+ + in2[2] * in[9]
+ + in2[9] * in[2];
+ out[12] = in2[6] * in[6]
+ + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3])
+ + in2[4] * in[8]
+ + in2[8] * in[4];
+ out[13] = in2[6] * in[7]
+ + in2[7] * in[6]
+ + in2[5] * in[8]
+ + in2[8] * in[5]
+ + in2[4] * in[9]
+ + in2[9] * in[4];
+ out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5])
+ + in2[6] * in[8]
+ + in2[8] * in[6];
+ out[15] = in2[7] * in[8]
+ + in2[8] * in[7]
+ + in2[6] * in[9]
+ + in2[9] * in[6];
+ out[16] = in2[8] * in[8]
+ + 2 * (in2[7] * in[9] + in2[9] * in[7]);
+ out[17] = in2[8] * in[9]
+ + in2[9] * in[8];
+ out[18] = 2 * in2[9] * in[9];
+ }
+
+ /**
+ * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients.
+ *
+ * @param input An input array of any length. If the array has 19 elements, it will be used as
+ * temporary buffer and its contents changed.
+ * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold.
+ */
+ static void reduce(long[] input, long[] output) {
+ long[] tmp;
+ if (input.length == 19) {
+ tmp = input;
+ } else {
+ tmp = new long[19];
+ System.arraycopy(input, 0, tmp, 0, input.length);
+ }
+ reduceSizeByModularReduction(tmp);
+ reduceCoefficients(tmp);
+ System.arraycopy(tmp, 0, output, 0, LIMB_CNT);
+ }
+
+ /**
+ * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19.
+ * <p>
+ * On entry: |output[i]| < 14*2^54
+ * On exit: |output[0..8]| < 280*2^54
+ */
+ static void reduceSizeByModularReduction(long[] output) {
+ // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19.
+ // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8].
+ //
+ // Each of these shifts and adds ends up multiplying the value by 19.
+ //
+ // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus,
+ // on exit, |output[0..8]| < 280*2^54.
+ output[8] += output[18] << 4;
+ output[8] += output[18] << 1;
+ output[8] += output[18];
+ output[7] += output[17] << 4;
+ output[7] += output[17] << 1;
+ output[7] += output[17];
+ output[6] += output[16] << 4;
+ output[6] += output[16] << 1;
+ output[6] += output[16];
+ output[5] += output[15] << 4;
+ output[5] += output[15] << 1;
+ output[5] += output[15];
+ output[4] += output[14] << 4;
+ output[4] += output[14] << 1;
+ output[4] += output[14];
+ output[3] += output[13] << 4;
+ output[3] += output[13] << 1;
+ output[3] += output[13];
+ output[2] += output[12] << 4;
+ output[2] += output[12] << 1;
+ output[2] += output[12];
+ output[1] += output[11] << 4;
+ output[1] += output[11] << 1;
+ output[1] += output[11];
+ output[0] += output[10] << 4;
+ output[0] += output[10] << 1;
+ output[0] += output[10];
+ }
+
+ /**
+ * Reduce all coefficients of the short form input so that |x| < 2^26.
+ * <p>
+ * On entry: |output[i]| < 280*2^54
+ */
+ static void reduceCoefficients(long[] output) {
+ output[10] = 0;
+
+ for (int i = 0; i < LIMB_CNT; i += 2) {
+ long over = output[i] / TWO_TO_26;
+ // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in
+ // the first iteration of this loop. This is added to the next limb and we can approximate the
+ // resulting bound of that limb by 281*2^54.
+ output[i] -= over << 26;
+ output[i + 1] += over;
+
+ // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is
+ // added to the next limb, the resulting bound can be approximated as 281*2^54.
+ //
+ // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no
+ // overflow occurs.
+ over = output[i + 1] / TWO_TO_25;
+ output[i + 1] -= over << 25;
+ output[i + 2] += over;
+ }
+ // Now |output[10]| < 281*2^29 and all other coefficients are reduced.
+ output[0] += output[10] << 4;
+ output[0] += output[10] << 1;
+ output[0] += output[10];
+
+ output[10] = 0;
+ // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more
+ // than 2^16.
+ long over = output[0] / TWO_TO_26;
+ output[0] -= over << 26;
+ output[1] += over;
+ // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on
+ // |output[1]| is sufficient to meet our needs.
+ }
+
+ /**
+ * A helpful wrapper around {@ref Field25519#product}: output = in * in2.
+ * <p>
+ * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27.
+ * <p>
+ * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and
+ * |output[i]| < 2^26.
+ */
+ static void mult(long[] output, long[] in, long[] in2) {
+ long[] t = new long[19];
+ product(t, in, in2);
+ // |t[i]| < 2^26
+ reduce(t, output);
+ }
+
+ /**
+ * Square a number: out = in**2
+ * <p>
+ * output must be distinct from the input. The inputs are reduced coefficient form, the output is
+ * not.
+ * <p>
+ * out[x] <= 14 * the largest product of the input limbs.
+ */
+ private static void squareInner(long[] out, long[] in) {
+ out[0] = in[0] * in[0];
+ out[1] = 2 * in[0] * in[1];
+ out[2] = 2 * (in[1] * in[1] + in[0] * in[2]);
+ out[3] = 2 * (in[1] * in[2] + in[0] * in[3]);
+ out[4] = in[2] * in[2]
+ + 4 * in[1] * in[3]
+ + 2 * in[0] * in[4];
+ out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]);
+ out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]);
+ out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]);
+ out[8] = in[4] * in[4]
+ + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5]));
+ out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]);
+ out[10] = 2 * (in[5] * in[5]
+ + in[4] * in[6]
+ + in[2] * in[8]
+ + 2 * (in[3] * in[7] + in[1] * in[9]));
+ out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]);
+ out[12] = in[6] * in[6]
+ + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9]));
+ out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]);
+ out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]);
+ out[15] = 2 * (in[7] * in[8] + in[6] * in[9]);
+ out[16] = in[8] * in[8] + 4 * in[7] * in[9];
+ out[17] = 2 * in[8] * in[9];
+ out[18] = 2 * in[9] * in[9];
+ }
+
+ /**
+ * Returns in^2.
+ * <p>
+ * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27.
+ * <p>
+ * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide
+ * storage for 10 limbs) and |out[i]| < 2^26.
+ */
+ static void square(long[] output, long[] in) {
+ long[] t = new long[19];
+ squareInner(t, in);
+ // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner
+ // adds together, at most, 14 of those products.
+ reduce(t, output);
+ }
+
+ /**
+ * Takes a little-endian, 32-byte number and expands it into mixed radix form.
+ */
+ static long[] expand(byte[] input) {
+ long[] output = new long[LIMB_CNT];
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[i] = ((((long) (input[EXPAND_START[i]] & 0xff))
+ | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8
+ | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16
+ | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1];
+ }
+ return output;
+ }
+
+ /**
+ * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte
+ * array.
+ * <p>
+ * On entry: |input_limbs[i]| < 2^26
+ */
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ static byte[] contract(long[] inputLimbs) {
+ long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT);
+ for (int j = 0; j < 2; j++) {
+ for (int i = 0; i < 9; i++) {
+ // This calculation is a time-invariant way to make input[i] non-negative by borrowing
+ // from the next-larger limb.
+ int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]);
+ input[i] = input[i] + (carry << SHIFT[i & 1]);
+ input[i + 1] -= carry;
+ }
+
+ // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow
+ // from input[0], which is valid mod 2^255-19.
+ {
+ int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25);
+ input[9] += (carry << 25);
+ input[0] -= (carry * 19);
+ }
+
+ // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits,
+ // depending on position. However, input[0] may be negative.
+ }
+
+ // The first borrow-propagation pass above ended with every limb except (possibly) input[0]
+ // non-negative.
+ //
+ // If input[0] was negative after the first pass, then it was because of a carry from input[9].
+ // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus
+ // input[0] >= -19.
+ //
+ // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation
+ // pass could only have wrapped around to decrease input[0] again if the first pass left
+ // input[0] negative *and* input[1] through input[9] were all zero. In that case, input[1] is
+ // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative.
+ {
+ int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26);
+ input[0] += (carry << 26);
+ input[1] -= carry;
+ }
+
+ // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a
+ // limb which is, nominally, 25 bits wide.
+ for (int j = 0; j < 2; j++) {
+ for (int i = 0; i < 9; i++) {
+ int carry = (int) (input[i] >> SHIFT[i & 1]);
+ input[i] &= MASK[i & 1];
+ input[i + 1] += carry;
+ }
+ }
+
+ {
+ int carry = (int) (input[9] >> 25);
+ input[9] &= 0x1ffffff;
+ input[0] += 19 * carry;
+ }
+
+ // If the first carry-chain pass, just above, ended up with a carry from input[9], and that
+ // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was,
+ // at most, two.
+ //
+ // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] ->
+ // input[0] carry didn't push input[0] out of bounds.
+
+ // It still remains the case that input might be between 2^255-19 and 2^255. In this case,
+ // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff,
+ // which is 0x3ffffed.
+ int mask = gte((int) input[0], 0x3ffffed);
+ for (int i = 1; i < LIMB_CNT; i++) {
+ mask &= eq((int) input[i], MASK[i & 1]);
+ }
+
+ // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally
+ // subtracts 2^255-19.
+ input[0] -= mask & 0x3ffffed;
+ input[1] -= mask & 0x1ffffff;
+ for (int i = 2; i < LIMB_CNT; i += 2) {
+ input[i] -= mask & 0x3ffffff;
+ input[i + 1] -= mask & 0x1ffffff;
+ }
+
+ for (int i = 0; i < LIMB_CNT; i++) {
+ input[i] <<= EXPAND_SHIFT[i];
+ }
+ byte[] output = new byte[FIELD_LEN];
+ for (int i = 0; i < LIMB_CNT; i++) {
+ output[EXPAND_START[i]] |= input[i] & 0xff;
+ output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff;
+ output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff;
+ output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff;
+ }
+ return output;
+ }
+
+ /**
+ * Computes inverse of z = z(2^255 - 21)
+ * <p>
+ * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the
+ * comment format and the variable namings are different from those.
+ */
+ static void inverse(long[] out, long[] z) {
+ long[] z2 = new long[Field25519.LIMB_CNT];
+ long[] z9 = new long[Field25519.LIMB_CNT];
+ long[] z11 = new long[Field25519.LIMB_CNT];
+ long[] z2To5Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To10Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To20Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To50Minus1 = new long[Field25519.LIMB_CNT];
+ long[] z2To100Minus1 = new long[Field25519.LIMB_CNT];
+ long[] t0 = new long[Field25519.LIMB_CNT];
+ long[] t1 = new long[Field25519.LIMB_CNT];
+
+ square(z2, z); // 2
+ square(t1, z2); // 4
+ square(t0, t1); // 8
+ mult(z9, t0, z); // 9
+ mult(z11, z9, z2); // 11
+ square(t0, z11); // 22
+ mult(z2To5Minus1, t0, z9); // 2^5 - 2^0 = 31
+
+ square(t0, z2To5Minus1); // 2^6 - 2^1
+ square(t1, t0); // 2^7 - 2^2
+ square(t0, t1); // 2^8 - 2^3
+ square(t1, t0); // 2^9 - 2^4
+ square(t0, t1); // 2^10 - 2^5
+ mult(z2To10Minus1, t0, z2To5Minus1); // 2^10 - 2^0
+
+ square(t0, z2To10Minus1); // 2^11 - 2^1
+ square(t1, t0); // 2^12 - 2^2
+ for (int i = 2; i < 10; i += 2) { // 2^20 - 2^10
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(z2To20Minus1, t1, z2To10Minus1); // 2^20 - 2^0
+
+ square(t0, z2To20Minus1); // 2^21 - 2^1
+ square(t1, t0); // 2^22 - 2^2
+ for (int i = 2; i < 20; i += 2) { // 2^40 - 2^20
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(t0, t1, z2To20Minus1); // 2^40 - 2^0
+
+ square(t1, t0); // 2^41 - 2^1
+ square(t0, t1); // 2^42 - 2^2
+ for (int i = 2; i < 10; i += 2) { // 2^50 - 2^10
+ square(t1, t0);
+ square(t0, t1);
+ }
+ mult(z2To50Minus1, t0, z2To10Minus1); // 2^50 - 2^0
+
+ square(t0, z2To50Minus1); // 2^51 - 2^1
+ square(t1, t0); // 2^52 - 2^2
+ for (int i = 2; i < 50; i += 2) { // 2^100 - 2^50
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(z2To100Minus1, t1, z2To50Minus1); // 2^100 - 2^0
+
+ square(t1, z2To100Minus1); // 2^101 - 2^1
+ square(t0, t1); // 2^102 - 2^2
+ for (int i = 2; i < 100; i += 2) { // 2^200 - 2^100
+ square(t1, t0);
+ square(t0, t1);
+ }
+ mult(t1, t0, z2To100Minus1); // 2^200 - 2^0
+
+ square(t0, t1); // 2^201 - 2^1
+ square(t1, t0); // 2^202 - 2^2
+ for (int i = 2; i < 50; i += 2) { // 2^250 - 2^50
+ square(t0, t1);
+ square(t1, t0);
+ }
+ mult(t0, t1, z2To50Minus1); // 2^250 - 2^0
+
+ square(t1, t0); // 2^251 - 2^1
+ square(t0, t1); // 2^252 - 2^2
+ square(t1, t0); // 2^253 - 2^3
+ square(t0, t1); // 2^254 - 2^4
+ square(t1, t0); // 2^255 - 2^5
+ mult(out, t1, z11); // 2^255 - 21
+ }
+
+
+ /**
+ * Returns 0xffffffff iff a == b and zero otherwise.
+ */
+ private static int eq(int a, int b) {
+ a = ~(a ^ b);
+ a &= a << 16;
+ a &= a << 8;
+ a &= a << 4;
+ a &= a << 2;
+ a &= a << 1;
+ return a >> 31;
+ }
+
+ /**
+ * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative.
+ */
+ private static int gte(int a, int b) {
+ a -= b;
+ // a >= 0 iff a >= b.
+ return ~(a >> 31);
+ }
+ }
+
+ // (x = 0, y = 1) point
+ private static final CachedXYT CACHED_NEUTRAL = new CachedXYT(
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+ private static final PartialXYZT NEUTRAL = new PartialXYZT(
+ new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
+ new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+
+ /**
+ * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z
+ * <p>
+ * Note that this is referred as ge_p2 in ref10 impl.
+ * Also note that x = X, y = Y and z = Z below following Java coding style.
+ * <p>
+ * See
+ * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary
+ * Window Method.
+ * <p>
+ * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html
+ */
+ private static class XYZ {
+
+ final long[] x;
+ final long[] y;
+ final long[] z;
+
+ XYZ() {
+ this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);
+ }
+
+ XYZ(long[] x, long[] y, long[] z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ XYZ(XYZ xyz) {
+ x = Arrays.copyOf(xyz.x, Field25519.LIMB_CNT);
+ y = Arrays.copyOf(xyz.y, Field25519.LIMB_CNT);
+ z = Arrays.copyOf(xyz.z, Field25519.LIMB_CNT);
+ }
+
+ XYZ(PartialXYZT partialXYZT) {
+ this();
+ fromPartialXYZT(this, partialXYZT);
+ }
+
+ /**
+ * ge_p1p1_to_p2.c
+ */
+ static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) {
+ Field25519.mult(out.x, in.xyz.x, in.t);
+ Field25519.mult(out.y, in.xyz.y, in.xyz.z);
+ Field25519.mult(out.z, in.xyz.z, in.t);
+ return out;
+ }
+
+ /**
+ * Encodes this point to bytes.
+ */
+ byte[] toBytes() {
+ long[] recip = new long[Field25519.LIMB_CNT];
+ long[] x = new long[Field25519.LIMB_CNT];
+ long[] y = new long[Field25519.LIMB_CNT];
+ Field25519.inverse(recip, z);
+ Field25519.mult(x, this.x, recip);
+ Field25519.mult(y, this.y, recip);
+ byte[] s = Field25519.contract(y);
+ s[31] = (byte) (s[31] ^ (getLsb(x) << 7));
+ return s;
+ }
+
+
+ /**
+ * Best effort fix-timing array comparison.
+ *
+ * @return true if two arrays are equal.
+ */
+ private static boolean bytesEqual(final byte[] x, final byte[] y) {
+ if (x == null || y == null) {
+ return false;
+ }
+ if (x.length != y.length) {
+ return false;
+ }
+ int res = 0;
+ for (int i = 0; i < x.length; i++) {
+ res |= x[i] ^ y[i];
+ }
+ return res == 0;
+ }
+
+ /**
+ * Checks that the point is on curve
+ */
+ boolean isOnCurve() {
+ long[] x2 = new long[Field25519.LIMB_CNT];
+ Field25519.square(x2, x);
+ long[] y2 = new long[Field25519.LIMB_CNT];
+ Field25519.square(y2, y);
+ long[] z2 = new long[Field25519.LIMB_CNT];
+ Field25519.square(z2, z);
+ long[] z4 = new long[Field25519.LIMB_CNT];
+ Field25519.square(z4, z2);
+ long[] lhs = new long[Field25519.LIMB_CNT];
+ // lhs = y^2 - x^2
+ Field25519.sub(lhs, y2, x2);
+ // lhs = z^2 * (y2 - x2)
+ Field25519.mult(lhs, lhs, z2);
+ long[] rhs = new long[Field25519.LIMB_CNT];
+ // rhs = x^2 * y^2
+ Field25519.mult(rhs, x2, y2);
+ // rhs = D * x^2 * y^2
+ Field25519.mult(rhs, rhs, D);
+ // rhs = z^4 + D * x^2 * y^2
+ Field25519.sum(rhs, z4);
+ // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually
+ // reduce it here.
+ Field25519.reduce(rhs, rhs);
+ // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2
+ return bytesEqual(Field25519.contract(lhs), Field25519.contract(rhs));
+ }
+ }
+
+ /**
+ * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z,
+ * XY = ZT
+ * <p>
+ * Note that this is referred as ge_p3 in ref10 impl.
+ * Also note that t = T below following Java coding style.
+ * <p>
+ * See
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
+ */
+ private static class XYZT {
+
+ final XYZ xyz;
+ final long[] t;
+
+ XYZT() {
+ this(new XYZ(), new long[Field25519.LIMB_CNT]);
+ }
+
+ XYZT(XYZ xyz, long[] t) {
+ this.xyz = xyz;
+ this.t = t;
+ }
+
+ XYZT(PartialXYZT partialXYZT) {
+ this();
+ fromPartialXYZT(this, partialXYZT);
+ }
+
+ /**
+ * ge_p1p1_to_p2.c
+ */
+ private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) {
+ Field25519.mult(out.xyz.x, in.xyz.x, in.t);
+ Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z);
+ Field25519.mult(out.xyz.z, in.xyz.z, in.t);
+ Field25519.mult(out.t, in.xyz.x, in.xyz.y);
+ return out;
+ }
+
+ /**
+ * Decodes {@code s} into an extented projective point.
+ * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3
+ */
+ private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException {
+ long[] x = new long[Field25519.LIMB_CNT];
+ long[] y = Field25519.expand(s);
+ long[] z = new long[Field25519.LIMB_CNT];
+ z[0] = 1;
+ long[] t = new long[Field25519.LIMB_CNT];
+ long[] u = new long[Field25519.LIMB_CNT];
+ long[] v = new long[Field25519.LIMB_CNT];
+ long[] vxx = new long[Field25519.LIMB_CNT];
+ long[] check = new long[Field25519.LIMB_CNT];
+ Field25519.square(u, y);
+ Field25519.mult(v, u, D);
+ Field25519.sub(u, u, z); // u = y^2 - 1
+ Field25519.sum(v, v, z); // v = dy^2 + 1
+
+ long[] v3 = new long[Field25519.LIMB_CNT];
+ Field25519.square(v3, v);
+ Field25519.mult(v3, v3, v); // v3 = v^3
+ Field25519.square(x, v3);
+ Field25519.mult(x, x, v);
+ Field25519.mult(x, x, u); // x = uv^7
+
+ pow2252m3(x, x); // x = (uv^7)^((q-5)/8)
+ Field25519.mult(x, x, v3);
+ Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8)
+
+ Field25519.square(vxx, x);
+ Field25519.mult(vxx, vxx, v);
+ Field25519.sub(check, vxx, u); // vx^2-u
+ if (isNonZeroVarTime(check)) {
+ Field25519.sum(check, vxx, u); // vx^2+u
+ if (isNonZeroVarTime(check)) {
+ throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
+ + "coordinates. No square root exists for modulo 2^255-19");
+ }
+ Field25519.mult(x, x, SQRTM1);
+ }
+
+ if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) {
+ throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
+ + "coordinates. Computed x is zero and encoded x's least significant bit is not zero");
+ }
+ if (getLsb(x) == ((s[31] & 0xff) >> 7)) {
+ neg(x, x);
+ }
+
+ Field25519.mult(t, x, y);
+ return new XYZT(new XYZ(x, y, z), t);
+ }
+ }
+
+ /**
+ * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
+ * <p>
+ * Note that this is referred as complete form in the original ref10 impl (ge_p1p1).
+ * Also note that t = T below following Java coding style.
+ * <p>
+ * Although this has the same types as XYZT, it is redefined to have its own type so that it is
+ * readable and 1:1 corresponds to ref10 impl.
+ * <p>
+ * Can be converted to XYZT as follows:
+ * X1 = X * T = x * Z * T = x * Z1
+ * Y1 = Y * Z = y * T * Z = y * Z1
+ * Z1 = Z * T = Z * T
+ * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1
+ */
+ private static class PartialXYZT {
+
+ final XYZ xyz;
+ final long[] t;
+
+ PartialXYZT() {
+ this(new XYZ(), new long[Field25519.LIMB_CNT]);
+ }
+
+ PartialXYZT(XYZ xyz, long[] t) {
+ this.xyz = xyz;
+ this.t = t;
+ }
+
+ PartialXYZT(PartialXYZT other) {
+ xyz = new XYZ(other.xyz);
+ t = Arrays.copyOf(other.t, Field25519.LIMB_CNT);
+ }
+ }
+
+ /**
+ * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * with Z = 1.
+ */
+ private static class CachedXYT {
+
+ final long[] yPlusX;
+ final long[] yMinusX;
+ final long[] t2d;
+
+ /**
+ * Creates a cached XYZT with Z = 1
+ *
+ * @param yPlusX y + x
+ * @param yMinusX y - x
+ * @param t2d 2d * xy
+ */
+ CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) {
+ this.yPlusX = yPlusX;
+ this.yMinusX = yMinusX;
+ this.t2d = t2d;
+ }
+
+ CachedXYT(CachedXYT other) {
+ yPlusX = Arrays.copyOf(other.yPlusX, Field25519.LIMB_CNT);
+ yMinusX = Arrays.copyOf(other.yMinusX, Field25519.LIMB_CNT);
+ t2d = Arrays.copyOf(other.t2d, Field25519.LIMB_CNT);
+ }
+
+ // z is one implicitly, so this just copies {@code in} to {@code output}.
+ void multByZ(long[] output, long[] in) {
+ System.arraycopy(in, 0, output, 0, Field25519.LIMB_CNT);
+ }
+
+ /**
+ * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value.
+ */
+ void copyConditional(CachedXYT other, int icopy) {
+ copyConditional(yPlusX, other.yPlusX, icopy);
+ copyConditional(yMinusX, other.yMinusX, icopy);
+ copyConditional(t2d, other.t2d, icopy);
+ }
+
+ /**
+ * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1,
+ * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid
+ * side-channel attacks.
+ *
+ * <p>NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong
+ * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the
+ * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must
+ * have magnitude less than Integer.MAX_VALUE.
+ */
+ static void copyConditional(long[] a, long[] b, int icopy) {
+ int copy = -icopy;
+ for (int i = 0; i < Field25519.LIMB_CNT; i++) {
+ int x = copy & (((int) a[i]) ^ ((int) b[i]));
+ a[i] = ((int) a[i]) ^ x;
+ }
+ }
+ }
+
+ private static class CachedXYZT extends CachedXYT {
+
+ private final long[] z;
+
+ CachedXYZT() {
+ this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);
+ }
+
+ /**
+ * ge_p3_to_cached.c
+ */
+ CachedXYZT(XYZT xyzt) {
+ this();
+ Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x);
+ Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x);
+ System.arraycopy(xyzt.xyz.z, 0, z, 0, Field25519.LIMB_CNT);
+ Field25519.mult(t2d, xyzt.t, D2);
+ }
+
+ /**
+ * Creates a cached XYZT
+ *
+ * @param yPlusX Y + X
+ * @param yMinusX Y - X
+ * @param z Z
+ * @param t2d 2d * (XY/Z)
+ */
+ CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) {
+ super(yPlusX, yMinusX, t2d);
+ this.z = z;
+ }
+
+ @Override
+ public void multByZ(long[] output, long[] in) {
+ Field25519.mult(output, in, z);
+ }
+ }
+
+ /**
+ * Addition defined in Section 3.1 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * Please note that this is a partial of the operation listed there leaving out the final
+ * conversion from PartialXYZT to XYZT.
+ *
+ * @param extended extended projective point input
+ * @param cached cached projective point input
+ */
+ private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
+ long[] t = new long[Field25519.LIMB_CNT];
+
+ // Y1 + X1
+ Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
+
+ // Y1 - X1
+ Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
+
+ // A = (Y1 - X1) * (Y2 - X2)
+ Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX);
+
+ // B = (Y1 + X1) * (Y2 + X2)
+ Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX);
+
+ // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
+ Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
+
+ // Z1 * Z2
+ cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
+
+ // D = 2 * Z1 * Z2
+ Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
+
+ // X3 = B - A
+ Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Y3 = B + A
+ Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Z3 = D + C
+ Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t);
+
+ // T3 = D - C
+ Field25519.sub(partialXYZT.t, t, partialXYZT.t);
+ }
+
+ /**
+ * Based on the addition defined in Section 3.1 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * Please note that this is a partial of the operation listed there leaving out the final
+ * conversion from PartialXYZT to XYZT.
+ *
+ * @param extended extended projective point input
+ * @param cached cached projective point input
+ */
+ private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
+ long[] t = new long[Field25519.LIMB_CNT];
+
+ // Y1 + X1
+ Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
+
+ // Y1 - X1
+ Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
+
+ // A = (Y1 - X1) * (Y2 + X2)
+ Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX);
+
+ // B = (Y1 + X1) * (Y2 - X2)
+ Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX);
+
+ // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
+ Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
+
+ // Z1 * Z2
+ cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
+
+ // D = 2 * Z1 * Z2
+ Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
+
+ // X3 = B - A
+ Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Y3 = B + A
+ Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
+
+ // Z3 = D - C
+ Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t);
+
+ // T3 = D + C
+ Field25519.sum(partialXYZT.t, t, partialXYZT.t);
+ }
+
+ /**
+ * Doubles {@code p} and puts the result into this PartialXYZT.
+ * <p>
+ * This is based on the addition defined in formula 7 in Section 3.3 of
+ * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
+ * <p>
+ * Please note that this is a partial of the operation listed there leaving out the final
+ * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in
+ * the paper, H should be replaced with A+B.
+ */
+ private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) {
+ long[] t0 = new long[Field25519.LIMB_CNT];
+
+ // XX = X1^2
+ Field25519.square(partialXYZT.xyz.x, p.x);
+
+ // YY = Y1^2
+ Field25519.square(partialXYZT.xyz.z, p.y);
+
+ // B' = Z1^2
+ Field25519.square(partialXYZT.t, p.z);
+
+ // B = 2 * B'
+ Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t);
+
+ // A = X1 + Y1
+ Field25519.sum(partialXYZT.xyz.y, p.x, p.y);
+
+ // AA = A^2
+ Field25519.square(t0, partialXYZT.xyz.y);
+
+ // Y3 = YY + XX
+ Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x);
+
+ // Z3 = YY - XX
+ Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x);
+
+ // X3 = AA - Y3
+ Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y);
+
+ // T3 = B - Z3
+ Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z);
+ }
+
+ /**
+ * Doubles {@code p} and puts the result into this PartialXYZT.
+ */
+ private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) {
+ doubleXYZ(partialXYZT, p.xyz);
+ }
+
+ /**
+ * Compares two byte values in constant time.
+ */
+ private static int eq(int a, int b) {
+ int r = ~(a ^ b) & 0xff;
+ r &= r << 4;
+ r &= r << 2;
+ r &= r << 1;
+ return (r >> 7) & 1;
+ }
+
+ /**
+ * This is a constant time operation where point b*B*256^pos is stored in {@code t}.
+ * When b is 0, t remains the same (i.e., neutral point).
+ * <p>
+ * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select
+ * method negates the corresponding point if b is negative (which is straight forward in elliptic
+ * curves by just negating y coordinate). Therefore we can get multiples of B with the half of
+ * memory requirements.
+ *
+ * @param t neutral element (i.e., point 0), also serves as output.
+ * @param pos in B[pos][j] = (j+1)*B*256^pos
+ * @param b value in [-8, 8] range.
+ */
+ private static void select(CachedXYT t, int pos, byte b) {
+ int bnegative = (b & 0xff) >> 7;
+ int babs = b - (((-bnegative) & b) << 1);
+
+ t.copyConditional(B_TABLE[pos][0], eq(babs, 1));
+ t.copyConditional(B_TABLE[pos][1], eq(babs, 2));
+ t.copyConditional(B_TABLE[pos][2], eq(babs, 3));
+ t.copyConditional(B_TABLE[pos][3], eq(babs, 4));
+ t.copyConditional(B_TABLE[pos][4], eq(babs, 5));
+ t.copyConditional(B_TABLE[pos][5], eq(babs, 6));
+ t.copyConditional(B_TABLE[pos][6], eq(babs, 7));
+ t.copyConditional(B_TABLE[pos][7], eq(babs, 8));
+
+ long[] yPlusX = Arrays.copyOf(t.yMinusX, Field25519.LIMB_CNT);
+ long[] yMinusX = Arrays.copyOf(t.yPlusX, Field25519.LIMB_CNT);
+ long[] t2d = Arrays.copyOf(t.t2d, Field25519.LIMB_CNT);
+ neg(t2d, t2d);
+ CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d);
+ t.copyConditional(minust, bnegative);
+ }
+
+ /**
+ * Computes {@code a}*B
+ * where a = a[0]+256*a[1]+...+256^31 a[31] and
+ * B is the Ed25519 base point (x,4/5) with x positive.
+ * <p>
+ * Preconditions:
+ * a[31] <= 127
+ *
+ * @throws IllegalStateException iff there is arithmetic error.
+ */
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ private static XYZ scalarMultWithBase(byte[] a) {
+ byte[] e = new byte[2 * Field25519.FIELD_LEN];
+ for (int i = 0; i < Field25519.FIELD_LEN; i++) {
+ e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf);
+ e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf);
+ }
+ // each e[i] is between 0 and 15
+ // e[63] is between 0 and 7
+
+ // Rewrite e in a way that each e[i] is in [-8, 8].
+ // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte
+ // a[63] can be at most 1.
+ int carry = 0;
+ for (int i = 0; i < e.length - 1; i++) {
+ e[i] += carry;
+ carry = e[i] + 8;
+ carry >>= 4;
+ e[i] -= carry << 4;
+ }
+ e[e.length - 1] += carry;
+
+ PartialXYZT ret = new PartialXYZT(NEUTRAL);
+ XYZT xyzt = new XYZT();
+ // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64
+ // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd
+ // indices. After the result, we can double the result point 4 times to shift the multiplication
+ // scalar by 4 bits.
+ for (int i = 1; i < e.length; i += 2) {
+ CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
+ select(t, i / 2, e[i]);
+ add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
+ }
+
+ // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result
+ // for the odd indices in e.
+ XYZ xyz = new XYZ();
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+ doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
+
+ // Add multiples of B for even indices of e.
+ for (int i = 0; i < e.length; i += 2) {
+ CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
+ select(t, i / 2, e[i]);
+ add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
+ }
+
+ // This check is to protect against flaws, i.e. if there is a computation error through a
+ // faulty CPU or if the implementation contains a bug.
+ XYZ result = new XYZ(ret);
+ if (!result.isOnCurve()) {
+ throw new IllegalStateException("arithmetic error in scalar multiplication");
+ }
+ return result;
+ }
+
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ private static byte[] slide(byte[] a) {
+ byte[] r = new byte[256];
+ // Writes each bit in a[0..31] into r[0..255]:
+ // a = a[0]+256*a[1]+...+256^31*a[31] is equal to
+ // r = r[0]+2*r[1]+...+2^255*r[255]
+ for (int i = 0; i < 256; i++) {
+ r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7)));
+ }
+
+ // Transforms r[i] as odd values in [-15, 15]
+ for (int i = 0; i < 256; i++) {
+ if (r[i] != 0) {
+ for (int b = 1; b <= 6 && i + b < 256; b++) {
+ if (r[i + b] != 0) {
+ if (r[i] + (r[i + b] << b) <= 15) {
+ r[i] += r[i + b] << b;
+ r[i + b] = 0;
+ } else if (r[i] - (r[i + b] << b) >= -15) {
+ r[i] -= r[i + b] << b;
+ for (int k = i + b; k < 256; k++) {
+ if (r[k] == 0) {
+ r[k] = 1;
+ break;
+ }
+ r[k] = 0;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ }
+ return r;
+ }
+
+ /**
+ * Computes {@code a}*{@code pointA}+{@code b}*B
+ * where a = a[0]+256*a[1]+...+256^31*a[31].
+ * and b = b[0]+256*b[1]+...+256^31*b[31].
+ * B is the Ed25519 base point (x,4/5) with x positive.
+ * <p>
+ * Note that execution time varies based on the input since this will only be used in verification
+ * of signatures.
+ */
+ private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) {
+ // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA
+ CachedXYZT[] pointAArray = new CachedXYZT[8];
+ pointAArray[0] = new CachedXYZT(pointA);
+ PartialXYZT t = new PartialXYZT();
+ doubleXYZT(t, pointA);
+ XYZT doubleA = new XYZT(t);
+ for (int i = 1; i < pointAArray.length; i++) {
+ add(t, doubleA, pointAArray[i - 1]);
+ pointAArray[i] = new CachedXYZT(new XYZT(t));
+ }
+
+ byte[] aSlide = slide(a);
+ byte[] bSlide = slide(b);
+ t = new PartialXYZT(NEUTRAL);
+ XYZT u = new XYZT();
+ int i = 255;
+ for (; i >= 0; i--) {
+ if (aSlide[i] != 0 || bSlide[i] != 0) {
+ break;
+ }
+ }
+ for (; i >= 0; i--) {
+ doubleXYZ(t, new XYZ(t));
+ if (aSlide[i] > 0) {
+ add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]);
+ } else if (aSlide[i] < 0) {
+ sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]);
+ }
+ if (bSlide[i] > 0) {
+ add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]);
+ } else if (bSlide[i] < 0) {
+ sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]);
+ }
+ }
+
+ return new XYZ(t);
+ }
+
+ /**
+ * Returns true if {@code in} is nonzero.
+ * <p>
+ * Note that execution time might depend on the input {@code in}.
+ */
+ private static boolean isNonZeroVarTime(long[] in) {
+ long[] inCopy = new long[in.length + 1];
+ System.arraycopy(in, 0, inCopy, 0, in.length);
+ Field25519.reduceCoefficients(inCopy);
+ byte[] bytes = Field25519.contract(inCopy);
+ for (byte b : bytes) {
+ if (b != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the least significant bit of {@code in}.
+ */
+ private static int getLsb(long[] in) {
+ return Field25519.contract(in)[0] & 1;
+ }
+
+ /**
+ * Negates all values in {@code in} and store it in {@code out}.
+ */
+ private static void neg(long[] out, long[] in) {
+ for (int i = 0; i < in.length; i++) {
+ out[i] = -in[i];
+ }
+ }
+
+ /**
+ * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}.
+ */
+ private static void pow2252m3(long[] out, long[] in) {
+ long[] t0 = new long[Field25519.LIMB_CNT];
+ long[] t1 = new long[Field25519.LIMB_CNT];
+ long[] t2 = new long[Field25519.LIMB_CNT];
+
+ // z2 = z1^2^1
+ Field25519.square(t0, in);
+
+ // z8 = z2^2^2
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 2; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z9 = z1*z8
+ Field25519.mult(t1, in, t1);
+
+ // z11 = z2*z9
+ Field25519.mult(t0, t0, t1);
+
+ // z22 = z11^2^1
+ Field25519.square(t0, t0);
+
+ // z_5_0 = z9*z22
+ Field25519.mult(t0, t1, t0);
+
+ // z_10_5 = z_5_0^2^5
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 5; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_10_0 = z_10_5*z_5_0
+ Field25519.mult(t0, t1, t0);
+
+ // z_20_10 = z_10_0^2^10
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 10; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_20_0 = z_20_10*z_10_0
+ Field25519.mult(t1, t1, t0);
+
+ // z_40_20 = z_20_0^2^20
+ Field25519.square(t2, t1);
+ for (int i = 1; i < 20; i++) {
+ Field25519.square(t2, t2);
+ }
+
+ // z_40_0 = z_40_20*z_20_0
+ Field25519.mult(t1, t2, t1);
+
+ // z_50_10 = z_40_0^2^10
+ Field25519.square(t1, t1);
+ for (int i = 1; i < 10; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_50_0 = z_50_10*z_10_0
+ Field25519.mult(t0, t1, t0);
+
+ // z_100_50 = z_50_0^2^50
+ Field25519.square(t1, t0);
+ for (int i = 1; i < 50; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_100_0 = z_100_50*z_50_0
+ Field25519.mult(t1, t1, t0);
+
+ // z_200_100 = z_100_0^2^100
+ Field25519.square(t2, t1);
+ for (int i = 1; i < 100; i++) {
+ Field25519.square(t2, t2);
+ }
+
+ // z_200_0 = z_200_100*z_100_0
+ Field25519.mult(t1, t2, t1);
+
+ // z_250_50 = z_200_0^2^50
+ Field25519.square(t1, t1);
+ for (int i = 1; i < 50; i++) {
+ Field25519.square(t1, t1);
+ }
+
+ // z_250_0 = z_250_50*z_50_0
+ Field25519.mult(t0, t1, t0);
+
+ // z_252_2 = z_250_0^2^2
+ Field25519.square(t0, t0);
+ for (int i = 1; i < 2; i++) {
+ Field25519.square(t0, t0);
+ }
+
+ // z_252_3 = z_252_2*z1
+ Field25519.mult(out, t0, in);
+ }
+
+ /**
+ * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format.
+ */
+ private static long load3(byte[] in, int idx) {
+ long result;
+ result = (long) in[idx] & 0xff;
+ result |= (long) (in[idx + 1] & 0xff) << 8;
+ result |= (long) (in[idx + 2] & 0xff) << 16;
+ return result;
+ }
+
+ /**
+ * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format.
+ */
+ private static long load4(byte[] in, int idx) {
+ long result = load3(in, idx);
+ result |= (long) (in[idx + 3] & 0xff) << 24;
+ return result;
+ }
+
+ /**
+ * Input:
+ * s[0]+256*s[1]+...+256^63*s[63] = s
+ * <p>
+ * Output:
+ * s[0]+256*s[1]+...+256^31*s[31] = s mod l
+ * where l = 2^252 + 27742317777372353535851937790883648493.
+ * Overwrites s in place.
+ */
+ private static void reduce(byte[] s) {
+ // Observation:
+ // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l
+ // Let m = -27742317777372353535851937790883648493
+ // Thus a*2^252+b mod l is equivalent to a*m+b mod l
+ //
+ // First s is divided into chunks of 21 bits as follows:
+ // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63]
+ long s0 = 2097151 & load3(s, 0);
+ long s1 = 2097151 & (load4(s, 2) >> 5);
+ long s2 = 2097151 & (load3(s, 5) >> 2);
+ long s3 = 2097151 & (load4(s, 7) >> 7);
+ long s4 = 2097151 & (load4(s, 10) >> 4);
+ long s5 = 2097151 & (load3(s, 13) >> 1);
+ long s6 = 2097151 & (load4(s, 15) >> 6);
+ long s7 = 2097151 & (load3(s, 18) >> 3);
+ long s8 = 2097151 & load3(s, 21);
+ long s9 = 2097151 & (load4(s, 23) >> 5);
+ long s10 = 2097151 & (load3(s, 26) >> 2);
+ long s11 = 2097151 & (load4(s, 28) >> 7);
+ long s12 = 2097151 & (load4(s, 31) >> 4);
+ long s13 = 2097151 & (load3(s, 34) >> 1);
+ long s14 = 2097151 & (load4(s, 36) >> 6);
+ long s15 = 2097151 & (load3(s, 39) >> 3);
+ long s16 = 2097151 & load3(s, 42);
+ long s17 = 2097151 & (load4(s, 44) >> 5);
+ long s18 = 2097151 & (load3(s, 47) >> 2);
+ long s19 = 2097151 & (load4(s, 49) >> 7);
+ long s20 = 2097151 & (load4(s, 52) >> 4);
+ long s21 = 2097151 & (load3(s, 55) >> 1);
+ long s22 = 2097151 & (load4(s, 57) >> 6);
+ long s23 = (load4(s, 60) >> 3);
+ long carry0;
+ long carry1;
+ long carry2;
+ long carry3;
+ long carry4;
+ long carry5;
+ long carry6;
+ long carry7;
+ long carry8;
+ long carry9;
+ long carry10;
+ long carry11;
+ long carry12;
+ long carry13;
+ long carry14;
+ long carry15;
+ long carry16;
+
+ // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l
+ // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6)
+ // starting from s11 (s11*2^210)
+ // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs
+ s11 += s23 * 666643;
+ s12 += s23 * 470296;
+ s13 += s23 * 654183;
+ s14 -= s23 * 997805;
+ s15 += s23 * 136657;
+ s16 -= s23 * 683901;
+ // s23 = 0;
+
+ s10 += s22 * 666643;
+ s11 += s22 * 470296;
+ s12 += s22 * 654183;
+ s13 -= s22 * 997805;
+ s14 += s22 * 136657;
+ s15 -= s22 * 683901;
+ // s22 = 0;
+
+ s9 += s21 * 666643;
+ s10 += s21 * 470296;
+ s11 += s21 * 654183;
+ s12 -= s21 * 997805;
+ s13 += s21 * 136657;
+ s14 -= s21 * 683901;
+ // s21 = 0;
+
+ s8 += s20 * 666643;
+ s9 += s20 * 470296;
+ s10 += s20 * 654183;
+ s11 -= s20 * 997805;
+ s12 += s20 * 136657;
+ s13 -= s20 * 683901;
+ // s20 = 0;
+
+ s7 += s19 * 666643;
+ s8 += s19 * 470296;
+ s9 += s19 * 654183;
+ s10 -= s19 * 997805;
+ s11 += s19 * 136657;
+ s12 -= s19 * 683901;
+ // s19 = 0;
+
+ s6 += s18 * 666643;
+ s7 += s18 * 470296;
+ s8 += s18 * 654183;
+ s9 -= s18 * 997805;
+ s10 += s18 * 136657;
+ s11 -= s18 * 683901;
+ // s18 = 0;
+
+ // Reduce the bit length of limbs from s6 to s15 to 21-bits.
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry12 = (s12 + (1 << 20)) >> 21;
+ s13 += carry12;
+ s12 -= carry12 << 21;
+ carry14 = (s14 + (1 << 20)) >> 21;
+ s15 += carry14;
+ s14 -= carry14 << 21;
+ carry16 = (s16 + (1 << 20)) >> 21;
+ s17 += carry16;
+ s16 -= carry16 << 21;
+
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+ carry13 = (s13 + (1 << 20)) >> 21;
+ s14 += carry13;
+ s13 -= carry13 << 21;
+ carry15 = (s15 + (1 << 20)) >> 21;
+ s16 += carry15;
+ s15 -= carry15 << 21;
+
+ // Resume reduction where we left off.
+ s5 += s17 * 666643;
+ s6 += s17 * 470296;
+ s7 += s17 * 654183;
+ s8 -= s17 * 997805;
+ s9 += s17 * 136657;
+ s10 -= s17 * 683901;
+ // s17 = 0;
+
+ s4 += s16 * 666643;
+ s5 += s16 * 470296;
+ s6 += s16 * 654183;
+ s7 -= s16 * 997805;
+ s8 += s16 * 136657;
+ s9 -= s16 * 683901;
+ // s16 = 0;
+
+ s3 += s15 * 666643;
+ s4 += s15 * 470296;
+ s5 += s15 * 654183;
+ s6 -= s15 * 997805;
+ s7 += s15 * 136657;
+ s8 -= s15 * 683901;
+ // s15 = 0;
+
+ s2 += s14 * 666643;
+ s3 += s14 * 470296;
+ s4 += s14 * 654183;
+ s5 -= s14 * 997805;
+ s6 += s14 * 136657;
+ s7 -= s14 * 683901;
+ // s14 = 0;
+
+ s1 += s13 * 666643;
+ s2 += s13 * 470296;
+ s3 += s13 * 654183;
+ s4 -= s13 * 997805;
+ s5 += s13 * 136657;
+ s6 -= s13 * 683901;
+ // s13 = 0;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ // Reduce the range of limbs from s0 to s11 to 21-bits.
+ carry0 = (s0 + (1 << 20)) >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry2 = (s2 + (1 << 20)) >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry4 = (s4 + (1 << 20)) >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ carry1 = (s1 + (1 << 20)) >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry3 = (s3 + (1 << 20)) >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry5 = (s5 + (1 << 20)) >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs.
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry11 = s11 >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ // Do one last reduction as s12 might be 1.
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ // s12 = 0;
+
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ // Serialize the result into the s.
+ s[0] = (byte) s0;
+ s[1] = (byte) (s0 >> 8);
+ s[2] = (byte) ((s0 >> 16) | (s1 << 5));
+ s[3] = (byte) (s1 >> 3);
+ s[4] = (byte) (s1 >> 11);
+ s[5] = (byte) ((s1 >> 19) | (s2 << 2));
+ s[6] = (byte) (s2 >> 6);
+ s[7] = (byte) ((s2 >> 14) | (s3 << 7));
+ s[8] = (byte) (s3 >> 1);
+ s[9] = (byte) (s3 >> 9);
+ s[10] = (byte) ((s3 >> 17) | (s4 << 4));
+ s[11] = (byte) (s4 >> 4);
+ s[12] = (byte) (s4 >> 12);
+ s[13] = (byte) ((s4 >> 20) | (s5 << 1));
+ s[14] = (byte) (s5 >> 7);
+ s[15] = (byte) ((s5 >> 15) | (s6 << 6));
+ s[16] = (byte) (s6 >> 2);
+ s[17] = (byte) (s6 >> 10);
+ s[18] = (byte) ((s6 >> 18) | (s7 << 3));
+ s[19] = (byte) (s7 >> 5);
+ s[20] = (byte) (s7 >> 13);
+ s[21] = (byte) s8;
+ s[22] = (byte) (s8 >> 8);
+ s[23] = (byte) ((s8 >> 16) | (s9 << 5));
+ s[24] = (byte) (s9 >> 3);
+ s[25] = (byte) (s9 >> 11);
+ s[26] = (byte) ((s9 >> 19) | (s10 << 2));
+ s[27] = (byte) (s10 >> 6);
+ s[28] = (byte) ((s10 >> 14) | (s11 << 7));
+ s[29] = (byte) (s11 >> 1);
+ s[30] = (byte) (s11 >> 9);
+ s[31] = (byte) (s11 >> 17);
+ }
+
+ /**
+ * Input:
+ * a[0]+256*a[1]+...+256^31*a[31] = a
+ * b[0]+256*b[1]+...+256^31*b[31] = b
+ * c[0]+256*c[1]+...+256^31*c[31] = c
+ * <p>
+ * Output:
+ * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l
+ * where l = 2^252 + 27742317777372353535851937790883648493.
+ */
+ private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) {
+ // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c
+ // See Ed25519.reduce for related comments.
+ long a0 = 2097151 & load3(a, 0);
+ long a1 = 2097151 & (load4(a, 2) >> 5);
+ long a2 = 2097151 & (load3(a, 5) >> 2);
+ long a3 = 2097151 & (load4(a, 7) >> 7);
+ long a4 = 2097151 & (load4(a, 10) >> 4);
+ long a5 = 2097151 & (load3(a, 13) >> 1);
+ long a6 = 2097151 & (load4(a, 15) >> 6);
+ long a7 = 2097151 & (load3(a, 18) >> 3);
+ long a8 = 2097151 & load3(a, 21);
+ long a9 = 2097151 & (load4(a, 23) >> 5);
+ long a10 = 2097151 & (load3(a, 26) >> 2);
+ long a11 = (load4(a, 28) >> 7);
+ long b0 = 2097151 & load3(b, 0);
+ long b1 = 2097151 & (load4(b, 2) >> 5);
+ long b2 = 2097151 & (load3(b, 5) >> 2);
+ long b3 = 2097151 & (load4(b, 7) >> 7);
+ long b4 = 2097151 & (load4(b, 10) >> 4);
+ long b5 = 2097151 & (load3(b, 13) >> 1);
+ long b6 = 2097151 & (load4(b, 15) >> 6);
+ long b7 = 2097151 & (load3(b, 18) >> 3);
+ long b8 = 2097151 & load3(b, 21);
+ long b9 = 2097151 & (load4(b, 23) >> 5);
+ long b10 = 2097151 & (load3(b, 26) >> 2);
+ long b11 = (load4(b, 28) >> 7);
+ long c0 = 2097151 & load3(c, 0);
+ long c1 = 2097151 & (load4(c, 2) >> 5);
+ long c2 = 2097151 & (load3(c, 5) >> 2);
+ long c3 = 2097151 & (load4(c, 7) >> 7);
+ long c4 = 2097151 & (load4(c, 10) >> 4);
+ long c5 = 2097151 & (load3(c, 13) >> 1);
+ long c6 = 2097151 & (load4(c, 15) >> 6);
+ long c7 = 2097151 & (load3(c, 18) >> 3);
+ long c8 = 2097151 & load3(c, 21);
+ long c9 = 2097151 & (load4(c, 23) >> 5);
+ long c10 = 2097151 & (load3(c, 26) >> 2);
+ long c11 = (load4(c, 28) >> 7);
+ long s0;
+ long s1;
+ long s2;
+ long s3;
+ long s4;
+ long s5;
+ long s6;
+ long s7;
+ long s8;
+ long s9;
+ long s10;
+ long s11;
+ long s12;
+ long s13;
+ long s14;
+ long s15;
+ long s16;
+ long s17;
+ long s18;
+ long s19;
+ long s20;
+ long s21;
+ long s22;
+ long s23;
+ long carry0;
+ long carry1;
+ long carry2;
+ long carry3;
+ long carry4;
+ long carry5;
+ long carry6;
+ long carry7;
+ long carry8;
+ long carry9;
+ long carry10;
+ long carry11;
+ long carry12;
+ long carry13;
+ long carry14;
+ long carry15;
+ long carry16;
+ long carry17;
+ long carry18;
+ long carry19;
+ long carry20;
+ long carry21;
+ long carry22;
+
+ s0 = c0 + a0 * b0;
+ s1 = c1 + a0 * b1 + a1 * b0;
+ s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
+ s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
+ s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
+ s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
+ s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
+ s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
+ s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1
+ + a8 * b0;
+ s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2
+ + a8 * b1 + a9 * b0;
+ s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3
+ + a8 * b2 + a9 * b1 + a10 * b0;
+ s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4
+ + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
+ s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3
+ + a10 * b2 + a11 * b1;
+ s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3
+ + a11 * b2;
+ s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4
+ + a11 * b3;
+ s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
+ s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
+ s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
+ s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
+ s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
+ s20 = a9 * b11 + a10 * b10 + a11 * b9;
+ s21 = a10 * b11 + a11 * b10;
+ s22 = a11 * b11;
+ s23 = 0;
+
+ carry0 = (s0 + (1 << 20)) >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry2 = (s2 + (1 << 20)) >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry4 = (s4 + (1 << 20)) >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry12 = (s12 + (1 << 20)) >> 21;
+ s13 += carry12;
+ s12 -= carry12 << 21;
+ carry14 = (s14 + (1 << 20)) >> 21;
+ s15 += carry14;
+ s14 -= carry14 << 21;
+ carry16 = (s16 + (1 << 20)) >> 21;
+ s17 += carry16;
+ s16 -= carry16 << 21;
+ carry18 = (s18 + (1 << 20)) >> 21;
+ s19 += carry18;
+ s18 -= carry18 << 21;
+ carry20 = (s20 + (1 << 20)) >> 21;
+ s21 += carry20;
+ s20 -= carry20 << 21;
+ carry22 = (s22 + (1 << 20)) >> 21;
+ s23 += carry22;
+ s22 -= carry22 << 21;
+
+ carry1 = (s1 + (1 << 20)) >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry3 = (s3 + (1 << 20)) >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry5 = (s5 + (1 << 20)) >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+ carry13 = (s13 + (1 << 20)) >> 21;
+ s14 += carry13;
+ s13 -= carry13 << 21;
+ carry15 = (s15 + (1 << 20)) >> 21;
+ s16 += carry15;
+ s15 -= carry15 << 21;
+ carry17 = (s17 + (1 << 20)) >> 21;
+ s18 += carry17;
+ s17 -= carry17 << 21;
+ carry19 = (s19 + (1 << 20)) >> 21;
+ s20 += carry19;
+ s19 -= carry19 << 21;
+ carry21 = (s21 + (1 << 20)) >> 21;
+ s22 += carry21;
+ s21 -= carry21 << 21;
+
+ s11 += s23 * 666643;
+ s12 += s23 * 470296;
+ s13 += s23 * 654183;
+ s14 -= s23 * 997805;
+ s15 += s23 * 136657;
+ s16 -= s23 * 683901;
+ // s23 = 0;
+
+ s10 += s22 * 666643;
+ s11 += s22 * 470296;
+ s12 += s22 * 654183;
+ s13 -= s22 * 997805;
+ s14 += s22 * 136657;
+ s15 -= s22 * 683901;
+ // s22 = 0;
+
+ s9 += s21 * 666643;
+ s10 += s21 * 470296;
+ s11 += s21 * 654183;
+ s12 -= s21 * 997805;
+ s13 += s21 * 136657;
+ s14 -= s21 * 683901;
+ // s21 = 0;
+
+ s8 += s20 * 666643;
+ s9 += s20 * 470296;
+ s10 += s20 * 654183;
+ s11 -= s20 * 997805;
+ s12 += s20 * 136657;
+ s13 -= s20 * 683901;
+ // s20 = 0;
+
+ s7 += s19 * 666643;
+ s8 += s19 * 470296;
+ s9 += s19 * 654183;
+ s10 -= s19 * 997805;
+ s11 += s19 * 136657;
+ s12 -= s19 * 683901;
+ // s19 = 0;
+
+ s6 += s18 * 666643;
+ s7 += s18 * 470296;
+ s8 += s18 * 654183;
+ s9 -= s18 * 997805;
+ s10 += s18 * 136657;
+ s11 -= s18 * 683901;
+ // s18 = 0;
+
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry12 = (s12 + (1 << 20)) >> 21;
+ s13 += carry12;
+ s12 -= carry12 << 21;
+ carry14 = (s14 + (1 << 20)) >> 21;
+ s15 += carry14;
+ s14 -= carry14 << 21;
+ carry16 = (s16 + (1 << 20)) >> 21;
+ s17 += carry16;
+ s16 -= carry16 << 21;
+
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+ carry13 = (s13 + (1 << 20)) >> 21;
+ s14 += carry13;
+ s13 -= carry13 << 21;
+ carry15 = (s15 + (1 << 20)) >> 21;
+ s16 += carry15;
+ s15 -= carry15 << 21;
+
+ s5 += s17 * 666643;
+ s6 += s17 * 470296;
+ s7 += s17 * 654183;
+ s8 -= s17 * 997805;
+ s9 += s17 * 136657;
+ s10 -= s17 * 683901;
+ // s17 = 0;
+
+ s4 += s16 * 666643;
+ s5 += s16 * 470296;
+ s6 += s16 * 654183;
+ s7 -= s16 * 997805;
+ s8 += s16 * 136657;
+ s9 -= s16 * 683901;
+ // s16 = 0;
+
+ s3 += s15 * 666643;
+ s4 += s15 * 470296;
+ s5 += s15 * 654183;
+ s6 -= s15 * 997805;
+ s7 += s15 * 136657;
+ s8 -= s15 * 683901;
+ // s15 = 0;
+
+ s2 += s14 * 666643;
+ s3 += s14 * 470296;
+ s4 += s14 * 654183;
+ s5 -= s14 * 997805;
+ s6 += s14 * 136657;
+ s7 -= s14 * 683901;
+ // s14 = 0;
+
+ s1 += s13 * 666643;
+ s2 += s13 * 470296;
+ s3 += s13 * 654183;
+ s4 -= s13 * 997805;
+ s5 += s13 * 136657;
+ s6 -= s13 * 683901;
+ // s13 = 0;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ carry0 = (s0 + (1 << 20)) >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry2 = (s2 + (1 << 20)) >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry4 = (s4 + (1 << 20)) >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry6 = (s6 + (1 << 20)) >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry8 = (s8 + (1 << 20)) >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry10 = (s10 + (1 << 20)) >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ carry1 = (s1 + (1 << 20)) >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry3 = (s3 + (1 << 20)) >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry5 = (s5 + (1 << 20)) >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry7 = (s7 + (1 << 20)) >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry9 = (s9 + (1 << 20)) >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry11 = (s11 + (1 << 20)) >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ s12 = 0;
+
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+ carry11 = s11 >> 21;
+ s12 += carry11;
+ s11 -= carry11 << 21;
+
+ s0 += s12 * 666643;
+ s1 += s12 * 470296;
+ s2 += s12 * 654183;
+ s3 -= s12 * 997805;
+ s4 += s12 * 136657;
+ s5 -= s12 * 683901;
+ // s12 = 0;
+
+ carry0 = s0 >> 21;
+ s1 += carry0;
+ s0 -= carry0 << 21;
+ carry1 = s1 >> 21;
+ s2 += carry1;
+ s1 -= carry1 << 21;
+ carry2 = s2 >> 21;
+ s3 += carry2;
+ s2 -= carry2 << 21;
+ carry3 = s3 >> 21;
+ s4 += carry3;
+ s3 -= carry3 << 21;
+ carry4 = s4 >> 21;
+ s5 += carry4;
+ s4 -= carry4 << 21;
+ carry5 = s5 >> 21;
+ s6 += carry5;
+ s5 -= carry5 << 21;
+ carry6 = s6 >> 21;
+ s7 += carry6;
+ s6 -= carry6 << 21;
+ carry7 = s7 >> 21;
+ s8 += carry7;
+ s7 -= carry7 << 21;
+ carry8 = s8 >> 21;
+ s9 += carry8;
+ s8 -= carry8 << 21;
+ carry9 = s9 >> 21;
+ s10 += carry9;
+ s9 -= carry9 << 21;
+ carry10 = s10 >> 21;
+ s11 += carry10;
+ s10 -= carry10 << 21;
+
+ s[0] = (byte) s0;
+ s[1] = (byte) (s0 >> 8);
+ s[2] = (byte) ((s0 >> 16) | (s1 << 5));
+ s[3] = (byte) (s1 >> 3);
+ s[4] = (byte) (s1 >> 11);
+ s[5] = (byte) ((s1 >> 19) | (s2 << 2));
+ s[6] = (byte) (s2 >> 6);
+ s[7] = (byte) ((s2 >> 14) | (s3 << 7));
+ s[8] = (byte) (s3 >> 1);
+ s[9] = (byte) (s3 >> 9);
+ s[10] = (byte) ((s3 >> 17) | (s4 << 4));
+ s[11] = (byte) (s4 >> 4);
+ s[12] = (byte) (s4 >> 12);
+ s[13] = (byte) ((s4 >> 20) | (s5 << 1));
+ s[14] = (byte) (s5 >> 7);
+ s[15] = (byte) ((s5 >> 15) | (s6 << 6));
+ s[16] = (byte) (s6 >> 2);
+ s[17] = (byte) (s6 >> 10);
+ s[18] = (byte) ((s6 >> 18) | (s7 << 3));
+ s[19] = (byte) (s7 >> 5);
+ s[20] = (byte) (s7 >> 13);
+ s[21] = (byte) s8;
+ s[22] = (byte) (s8 >> 8);
+ s[23] = (byte) ((s8 >> 16) | (s9 << 5));
+ s[24] = (byte) (s9 >> 3);
+ s[25] = (byte) (s9 >> 11);
+ s[26] = (byte) ((s9 >> 19) | (s10 << 2));
+ s[27] = (byte) (s10 >> 6);
+ s[28] = (byte) ((s10 >> 14) | (s11 << 7));
+ s[29] = (byte) (s11 >> 1);
+ s[30] = (byte) (s11 >> 9);
+ s[31] = (byte) (s11 >> 17);
+ }
+
+ // The order of the generator as unsigned bytes in little endian order.
+ // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748)
+ private static final byte[] GROUP_ORDER = {
+ (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c,
+ (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
+ (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2,
+ (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10};
+
+ // Checks whether s represents an integer smaller than the order of the group.
+ // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check
+ // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.)
+ // @param s an integer in little-endian order.
+ private static boolean isSmallerThanGroupOrder(byte[] s) {
+ for (int j = Field25519.FIELD_LEN - 1; j >= 0; j--) {
+ // compare unsigned bytes
+ int a = s[j] & 0xff;
+ int b = GROUP_ORDER[j] & 0xff;
+ if (a != b) {
+ return a < b;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with
+ * {@code publicKey}.
+ */
+ public static boolean verify(final byte[] message, final byte[] signature,
+ final byte[] publicKey) {
+ try {
+ if (signature.length != SIGNATURE_LEN) {
+ return false;
+ }
+ if (publicKey.length != PUBLIC_KEY_LEN) {
+ return false;
+ }
+ byte[] s = Arrays.copyOfRange(signature, Field25519.FIELD_LEN, SIGNATURE_LEN);
+ if (!isSmallerThanGroupOrder(s)) {
+ return false;
+ }
+ MessageDigest digest = MessageDigest.getInstance("SHA-512");
+ digest.update(signature, 0, Field25519.FIELD_LEN);
+ digest.update(publicKey);
+ digest.update(message);
+ byte[] h = digest.digest();
+ reduce(h);
+
+ XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey);
+ XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s);
+ byte[] expectedR = xyz.toBytes();
+ for (int i = 0; i < Field25519.FIELD_LEN; i++) {
+ if (expectedR[i] != signature[i]) {
+ return false;
+ }
+ }
+ return true;
+ } catch (final GeneralSecurityException ignored) {
+ return false;
+ }
+ }
+}
diff --git a/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt
new file mode 100644
index 00000000..f7d32abb
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/updater/SnackbarUpdateShower.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.updater
+
+import android.view.View
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.snackbar.BaseTransientBottomBar
+import com.google.android.material.snackbar.Snackbar
+import com.wireguard.android.R
+import com.wireguard.android.util.ErrorMessages
+import com.wireguard.android.util.QuantityFormatter
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.seconds
+
+object SnackbarUpdateShower {
+ private class SwapableSnackbar(activity: FragmentActivity, view: View, anchor: View?) {
+ val actionSnackbar = makeSnackbar(activity, view, anchor)
+ val statusSnackbar = makeSnackbar(activity, view, anchor)
+ var showingAction: Boolean = false
+ var showingStatus: Boolean = false
+
+ private fun makeSnackbar(activity: FragmentActivity, view: View, anchor: View?): Snackbar {
+ val snackbar = Snackbar.make(activity, view, "", Snackbar.LENGTH_INDEFINITE)
+ if (anchor != null)
+ snackbar.anchorView = anchor
+ snackbar.setTextMaxLines(6)
+ snackbar.behavior = object : BaseTransientBottomBar.Behavior() {
+ override fun canSwipeDismissView(child: View): Boolean {
+ return false
+ }
+ }
+ snackbar.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
+ override fun onDismissed(snackbar: Snackbar?, @DismissEvent event: Int) {
+ super.onDismissed(snackbar, event)
+ if (event == DISMISS_EVENT_MANUAL || event == DISMISS_EVENT_ACTION ||
+ (snackbar == actionSnackbar && !showingAction) ||
+ (snackbar == statusSnackbar && !showingStatus)
+ )
+ return
+ activity.lifecycleScope.launch {
+ delay(5.seconds)
+ snackbar?.show()
+ }
+ }
+ })
+ return snackbar
+ }
+
+ fun showAction(text: String, action: String, listener: View.OnClickListener) {
+ if (showingStatus) {
+ showingStatus = false
+ statusSnackbar.dismiss()
+ }
+ actionSnackbar.setText(text)
+ actionSnackbar.setAction(action, listener)
+ if (!showingAction) {
+ actionSnackbar.show()
+ showingAction = true
+ }
+ }
+
+ fun showText(text: String) {
+ if (showingAction) {
+ showingAction = false
+ actionSnackbar.dismiss()
+ }
+ statusSnackbar.setText(text)
+ if (!showingStatus) {
+ statusSnackbar.show()
+ showingStatus = true
+ }
+ }
+
+ fun dismiss() {
+ actionSnackbar.dismiss()
+ statusSnackbar.dismiss()
+ showingAction = false
+ showingStatus = false
+ }
+ }
+
+ fun attachToActivity(activity: FragmentActivity, view: View, anchor: View?) {
+ val snackbar = SwapableSnackbar(activity, view, anchor)
+ val context = activity.applicationContext
+
+ var lastUserIntervention: Updater.Progress.NeedsUserIntervention? = null
+ val intentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ lastUserIntervention?.markAsDone()
+ }
+
+ Updater.state.onEach { progress ->
+ when (progress) {
+ is Updater.Progress.Complete ->
+ snackbar.dismiss()
+
+ is Updater.Progress.Available ->
+ snackbar.showAction(
+ context.getString(R.string.updater_avalable),
+ context.getString(R.string.updater_action)
+ ) {
+ progress.update()
+ }
+
+ is Updater.Progress.NeedsUserIntervention -> {
+ lastUserIntervention = progress
+ intentLauncher.launch(progress.intent)
+ }
+
+ is Updater.Progress.Installing ->
+ snackbar.showText(context.getString(R.string.updater_installing))
+
+ is Updater.Progress.Rechecking ->
+ snackbar.showText(context.getString(R.string.updater_rechecking))
+
+ is Updater.Progress.Downloading -> {
+ if (progress.bytesTotal != 0UL) {
+ snackbar.showText(
+ context.getString(
+ R.string.updater_download_progress,
+ QuantityFormatter.formatBytes(progress.bytesDownloaded.toLong()),
+ QuantityFormatter.formatBytes(progress.bytesTotal.toLong()),
+ progress.bytesDownloaded.toFloat() * 100.0 / progress.bytesTotal.toFloat()
+ )
+ )
+ } else {
+ snackbar.showText(
+ context.getString(
+ R.string.updater_download_progress_nototal,
+ QuantityFormatter.formatBytes(progress.bytesDownloaded.toLong())
+ )
+ )
+ }
+ }
+
+ is Updater.Progress.Failure -> {
+ snackbar.showText(
+ context.getString(
+ R.string.updater_failure,
+ ErrorMessages[progress.error]
+ )
+ )
+ delay(5.seconds)
+ progress.retry()
+ }
+ }
+ }.launchIn(activity.lifecycleScope)
+ }
+} \ No newline at end of file
diff --git a/ui/src/main/java/com/wireguard/android/updater/Updater.kt b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
new file mode 100644
index 00000000..d7050090
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/updater/Updater.kt
@@ -0,0 +1,401 @@
+/*
+ * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.wireguard.android.updater
+
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageInstaller
+import android.os.Build
+import android.util.Base64
+import android.util.Log
+import androidx.core.content.ContextCompat
+import androidx.core.content.IntentCompat
+import com.wireguard.android.Application
+import com.wireguard.android.BuildConfig
+import com.wireguard.android.util.UserKnobs
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.charset.StandardCharsets
+import java.security.InvalidKeyException
+import java.security.InvalidParameterException
+import java.security.MessageDigest
+import java.util.UUID
+import kotlin.math.max
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+object Updater {
+ private const val TAG = "WireGuard/Updater"
+ private const val LATEST_VERSION_URL = "https://download.wireguard.com/android-client/latest.sig"
+ private const val APK_PATH_URL = "https://download.wireguard.com/android-client/%s"
+ private const val APK_NAME_PREFIX = BuildConfig.APPLICATION_ID + "-"
+ private const val APK_NAME_SUFFIX = ".apk"
+ private const val RELEASE_PUBLIC_KEY_BASE64 = "RWTAzwGRYr3EC9px0Ia3fbttz8WcVN6wrOwWp2delz4el6SI8XmkKSMp"
+ private val CURRENT_VERSION = BuildConfig.VERSION_NAME.removeSuffix("-debug")
+
+ sealed class Progress {
+ object Complete : Progress()
+ class Available(val version: String) : Progress() {
+ fun update() {
+ Application.getCoroutineScope().launch {
+ UserKnobs.setUpdaterNewerVersionConsented(version)
+ }
+ }
+ }
+
+ object Rechecking : Progress()
+ class Downloading(val bytesDownloaded: ULong, val bytesTotal: ULong) : Progress()
+ object Installing : Progress()
+ class NeedsUserIntervention(val intent: Intent, private val id: Int) : Progress() {
+
+ private suspend fun installerActive(): Boolean {
+ if (mutableState.firstOrNull() != this@NeedsUserIntervention)
+ return true
+ try {
+ if (Application.get().packageManager.packageInstaller.getSessionInfo(id)?.isActive == true)
+ return true
+ } catch (_: SecurityException) {
+ return true
+ }
+ return false
+ }
+
+ fun markAsDone() {
+ Application.getCoroutineScope().launch {
+ if (installerActive())
+ return@launch
+ delay(7.seconds)
+ if (installerActive())
+ return@launch
+ emitProgress(Failure(Exception("Ignored by user")))
+ }
+ }
+ }
+
+ class Failure(val error: Throwable) : Progress() {
+ fun retry() {
+ Application.getCoroutineScope().launch {
+ downloadAndUpdateWrapErrors()
+ }
+ }
+ }
+ }
+
+ private val mutableState = MutableStateFlow<Progress>(Progress.Complete)
+ val state = mutableState.asStateFlow()
+
+ private suspend fun emitProgress(progress: Progress, force: Boolean = false) {
+ if (force || mutableState.firstOrNull()?.javaClass != progress.javaClass)
+ mutableState.emit(progress)
+ }
+
+ private fun versionIsNewer(lhs: String, rhs: String): Boolean {
+ val lhsParts = lhs.split(".")
+ val rhsParts = rhs.split(".")
+ if (lhsParts.isEmpty() || rhsParts.isEmpty())
+ throw InvalidParameterException("Version is empty")
+
+ for (i in 0 until max(lhsParts.size, rhsParts.size)) {
+ val lhsPart = if (i < lhsParts.size) lhsParts[i].toULong() else 0UL
+ val rhsPart = if (i < rhsParts.size) rhsParts[i].toULong() else 0UL
+ if (lhsPart == rhsPart)
+ continue
+ return lhsPart > rhsPart
+ }
+ return false
+ }
+
+ private fun versionOfFile(name: String): String? {
+ if (!name.startsWith(APK_NAME_PREFIX) || !name.endsWith(APK_NAME_SUFFIX))
+ return null
+ return name.substring(APK_NAME_PREFIX.length, name.length - APK_NAME_SUFFIX.length)
+ }
+
+ private fun verifySignedFileList(signifyDigest: String): Map<String, Sha256Digest> {
+ val publicKeyBytes = Base64.decode(RELEASE_PUBLIC_KEY_BASE64, Base64.DEFAULT)
+ if (publicKeyBytes == null || publicKeyBytes.size != 32 + 10 || publicKeyBytes[0] != 'E'.code.toByte() || publicKeyBytes[1] != 'd'.code.toByte())
+ throw InvalidKeyException("Invalid public key")
+ val lines = signifyDigest.split("\n", limit = 3)
+ if (lines.size != 3)
+ throw InvalidParameterException("Invalid signature format: too few lines")
+ if (!lines[0].startsWith("untrusted comment: "))
+ throw InvalidParameterException("Invalid signature format: missing comment")
+ val signatureBytes = Base64.decode(lines[1], Base64.DEFAULT)
+ if (signatureBytes == null || signatureBytes.size != 64 + 10)
+ throw InvalidParameterException("Invalid signature format: wrong sized or missing signature")
+ for (i in 0..9) {
+ if (signatureBytes[i] != publicKeyBytes[i])
+ throw InvalidParameterException("Invalid signature format: wrong signer")
+ }
+ if (!Ed25519.verify(
+ lines[2].toByteArray(StandardCharsets.UTF_8),
+ signatureBytes.sliceArray(10 until 10 + 64),
+ publicKeyBytes.sliceArray(10 until 10 + 32)
+ )
+ )
+ throw SecurityException("Invalid signature")
+ val hashes: MutableMap<String, Sha256Digest> = HashMap()
+ for (line in lines[2].split("\n").dropLastWhile { it.isEmpty() }) {
+ val components = line.split(" ", limit = 2)
+ if (components.size != 2)
+ throw InvalidParameterException("Invalid file list format: too few components")
+ hashes[components[1]] = Sha256Digest(components[0])
+ }
+ return hashes
+ }
+
+ private class Sha256Digest(hex: String) {
+ val bytes: ByteArray
+
+ init {
+ if (hex.length != 64)
+ throw InvalidParameterException("SHA256 hashes must be 32 bytes long")
+ bytes = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
+ }
+ }
+
+ private fun checkForUpdates(): Pair<String, Sha256Digest> {
+ val connection = URL(LATEST_VERSION_URL).openConnection() as HttpURLConnection
+ connection.setRequestProperty("User-Agent", Application.USER_AGENT)
+ connection.connect()
+ if (connection.responseCode != HttpURLConnection.HTTP_OK)
+ throw IOException("File list could not be fetched: ${connection.responseCode}")
+ var fileListBytes = ByteArray(1024 * 512 /* 512 KiB */)
+ connection.inputStream.use {
+ val len = it.read(fileListBytes)
+ if (len <= 0)
+ throw IOException("File list is empty")
+ fileListBytes = fileListBytes.sliceArray(0 until len)
+ }
+ val fileList = verifySignedFileList(fileListBytes.decodeToString())
+ if (fileList.isEmpty())
+ throw InvalidParameterException("File list is empty")
+ var newestFile: String? = null
+ var newestVersion: String? = null
+ var newestFileHash: Sha256Digest? = null
+ for (file in fileList) {
+ val fileVersion = versionOfFile(file.key)
+ try {
+ if (fileVersion != null && (newestVersion == null || versionIsNewer(fileVersion, newestVersion))) {
+ newestVersion = fileVersion
+ newestFile = file.key
+ newestFileHash = file.value
+ }
+ } catch (_: Throwable) {
+ }
+ }
+ if (newestFile == null || newestFileHash == null)
+ throw InvalidParameterException("File list is empty")
+ return Pair(newestFile, newestFileHash)
+ }
+
+ private suspend fun downloadAndUpdate() = withContext(Dispatchers.IO) {
+ val receiver = InstallReceiver()
+ val context = Application.get().applicationContext
+ val pendingIntent = withContext(Dispatchers.Main) {
+ ContextCompat.registerReceiver(
+ context,
+ receiver,
+ IntentFilter(receiver.sessionId),
+ ContextCompat.RECEIVER_NOT_EXPORTED
+ )
+ PendingIntent.getBroadcast(
+ context,
+ 0,
+ Intent(receiver.sessionId).setPackage(context.packageName),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ }
+
+ emitProgress(Progress.Rechecking)
+ val update = checkForUpdates()
+ val updateVersion = versionOfFile(checkForUpdates().first) ?: throw Exception("No versions returned")
+ if (!versionIsNewer(updateVersion, CURRENT_VERSION)) {
+ emitProgress(Progress.Complete)
+ return@withContext
+ }
+
+ emitProgress(Progress.Downloading(0UL, 0UL), true)
+ val connection = URL(APK_PATH_URL.format(update.first)).openConnection() as HttpURLConnection
+ connection.setRequestProperty("User-Agent", Application.USER_AGENT)
+ connection.connect()
+ if (connection.responseCode != HttpURLConnection.HTTP_OK)
+ throw IOException("Update could not be fetched: ${connection.responseCode}")
+
+ var downloadedByteLen: ULong = 0UL
+ val totalByteLen =
+ (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) connection.contentLengthLong else connection.contentLength).toLong()
+ .toULong()
+ val fileBytes = ByteArray(1024 * 32 /* 32 KiB */)
+ val digest = MessageDigest.getInstance("SHA-256")
+ emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
+
+ val installer = context.packageManager.packageInstaller
+ val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
+ params.setAppPackageName(context.packageName) /* Enforces updates; disallows new apps. */
+ val session = installer.openSession(installer.createSession(params))
+ var sessionFailure = true
+ try {
+ val installDest = session.openWrite(receiver.sessionId, 0, -1)
+
+ installDest.use { dest ->
+ connection.inputStream.use { src ->
+ while (true) {
+ val readLen = src.read(fileBytes)
+ if (readLen <= 0)
+ break
+
+ digest.update(fileBytes, 0, readLen)
+ dest.write(fileBytes, 0, readLen)
+
+ downloadedByteLen += readLen.toUInt()
+ emitProgress(Progress.Downloading(downloadedByteLen, totalByteLen), true)
+
+ if (downloadedByteLen >= 1024UL * 1024UL * 100UL /* 100 MiB */)
+ throw IOException("File too large")
+ }
+ }
+ }
+
+ emitProgress(Progress.Installing)
+ if (!digest.digest().contentEquals(update.second.bytes))
+ throw SecurityException("Update has invalid hash")
+ sessionFailure = false
+ } finally {
+ if (sessionFailure) {
+ session.abandon()
+ session.close()
+ }
+ }
+ session.commit(pendingIntent.intentSender)
+ session.close()
+ }
+
+ private suspend fun downloadAndUpdateWrapErrors() {
+ try {
+ downloadAndUpdate()
+ } catch (e: Throwable) {
+ Log.e(TAG, "Update failure", e)
+ emitProgress(Progress.Failure(e))
+ }
+ }
+
+ private class InstallReceiver : BroadcastReceiver() {
+ val sessionId = UUID.randomUUID().toString()
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (sessionId != intent.action)
+ return
+
+ when (val status =
+ intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE_INVALID)) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
+ val userIntervention = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)!!
+ Application.getCoroutineScope().launch {
+ emitProgress(Progress.NeedsUserIntervention(userIntervention, id))
+ }
+ }
+
+ PackageInstaller.STATUS_SUCCESS -> {
+ Application.getCoroutineScope().launch {
+ emitProgress(Progress.Complete)
+ }
+ context.applicationContext.unregisterReceiver(this)
+ }
+
+ else -> {
+ val id = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, 0)
+ try {
+ context.applicationContext.packageManager.packageInstaller.abandonSession(id)
+ } catch (_: SecurityException) {
+ }
+ val message =
+ intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "Installation error $status"
+ Application.getCoroutineScope().launch {
+ val e = Exception(message)
+ Log.e(TAG, "Update failure", e)
+ emitProgress(Progress.Failure(e))
+ }
+ context.applicationContext.unregisterReceiver(this)
+ }
+ }
+ }
+ }
+
+ fun monitorForUpdates() {
+ if (installerIsGooglePlay())
+ return
+
+ Application.getCoroutineScope().launch(Dispatchers.IO) {
+ if (UserKnobs.updaterNewerVersionSeen.firstOrNull()?.let { versionIsNewer(it, CURRENT_VERSION) } == true)
+ return@launch
+
+ var waitTime = 15
+ while (true) {
+ try {
+ val updateVersion = versionOfFile(checkForUpdates().first) ?: throw IllegalStateException("No versions returned")
+ if (versionIsNewer(updateVersion, CURRENT_VERSION)) {
+ Log.i(TAG, "Update available: $updateVersion")
+ UserKnobs.setUpdaterNewerVersionSeen(updateVersion)
+ return@launch
+ }
+ } catch (e: Throwable) {
+ Log.e(TAG, "Failed to check for updates", e)
+ }
+ delay(waitTime.minutes)
+ waitTime = 45
+ }
+ }
+
+ UserKnobs.updaterNewerVersionSeen.onEach { ver ->
+ if (ver != null && versionIsNewer(
+ ver,
+ CURRENT_VERSION
+ ) && UserKnobs.updaterNewerVersionConsented.firstOrNull()
+ ?.let { versionIsNewer(it, CURRENT_VERSION) } != true
+ )
+ emitProgress(Progress.Available(ver))
+ }.launchIn(Application.getCoroutineScope())
+
+ UserKnobs.updaterNewerVersionConsented.onEach { ver ->
+ if (ver != null && versionIsNewer(ver, CURRENT_VERSION))
+ downloadAndUpdateWrapErrors()
+ }.launchIn(Application.getCoroutineScope())
+ }
+
+ fun installer(): String {
+ val context = Application.get().applicationContext
+ return try {
+ val packageName = context.packageName
+ val pm = context.packageManager
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ pm.getInstallSourceInfo(packageName).installingPackageName ?: ""
+ } else {
+ @Suppress("DEPRECATION")
+ pm.getInstallerPackageName(packageName) ?: ""
+ }
+ } catch (_: Throwable) {
+ ""
+ }
+ }
+
+ fun installerIsGooglePlay(): Boolean = installer() == "com.android.vending"
+} \ No newline at end of file
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 66027d95..8df5eb0d 100644
--- a/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
+++ b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt
@@ -114,6 +114,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/util/QuantityFormatter.kt b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
index 974fe530..bc059e1b 100644
--- a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
+++ b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt
@@ -5,8 +5,17 @@
package com.wireguard.android.util
+import android.icu.text.ListFormatter
+import android.icu.text.MeasureFormat
+import android.icu.text.RelativeDateTimeFormatter
+import android.icu.util.Measure
+import android.icu.util.MeasureUnit
+import android.os.Build
import com.wireguard.android.Application
import com.wireguard.android.R
+import java.util.Locale
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
object QuantityFormatter {
fun formatBytes(bytes: Long): String {
@@ -19,4 +28,40 @@ object QuantityFormatter {
else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
}
}
+
+ fun formatEpochAgo(epochMillis: Long): String {
+ var span = (System.currentTimeMillis() - epochMillis) / 1000
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ return Application.get().applicationContext.getString(R.string.latest_handshake_ago, span.seconds.toString())
+
+ if (span <= 0L)
+ return RelativeDateTimeFormatter.getInstance().format(RelativeDateTimeFormatter.Direction.PLAIN, RelativeDateTimeFormatter.AbsoluteUnit.NOW)
+ val measureFormat = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+ val parts = ArrayList<CharSequence>(4)
+ if (span >= 24 * 60 * 60L) {
+ val v = span / (24 * 60 * 60L)
+ parts.add(measureFormat.format(Measure(v, MeasureUnit.DAY)))
+ span -= v * (24 * 60 * 60L)
+ }
+ if (span >= 60 * 60L) {
+ val v = span / (60 * 60L)
+ parts.add(measureFormat.format(Measure(v, MeasureUnit.HOUR)))
+ span -= v * (60 * 60L)
+ }
+ if (span >= 60L) {
+ val v = span / 60L
+ parts.add(measureFormat.format(Measure(v, MeasureUnit.MINUTE)))
+ span -= v * 60L
+ }
+ if (span > 0L)
+ parts.add(measureFormat.format(Measure(span, MeasureUnit.SECOND)))
+
+ val joined = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
+ parts.joinToString()
+ else
+ ListFormatter.getInstance(Locale.getDefault(), ListFormatter.Type.UNITS, ListFormatter.Width.SHORT).format(parts)
+
+ return Application.get().applicationContext.getString(R.string.latest_handshake_ago, joined)
+ }
} \ No newline at end of file
diff --git a/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
index e0144b63..2c6ca8c3 100644
--- a/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
+++ b/ui/src/main/java/com/wireguard/android/util/UserKnobs.kt
@@ -88,4 +88,34 @@ object UserKnobs {
it[RUNNING_TUNNELS] = runningTunnels
}
}
+
+ private val UPDATER_NEWER_VERSION_SEEN = stringPreferencesKey("updater_newer_version_seen")
+ val updaterNewerVersionSeen: Flow<String?>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[UPDATER_NEWER_VERSION_SEEN]
+ }
+
+ suspend fun setUpdaterNewerVersionSeen(newerVersionSeen: String?) {
+ Application.getPreferencesDataStore().edit {
+ if (newerVersionSeen == null)
+ it.remove(UPDATER_NEWER_VERSION_SEEN)
+ else
+ it[UPDATER_NEWER_VERSION_SEEN] = newerVersionSeen
+ }
+ }
+
+ private val UPDATER_NEWER_VERSION_CONSENTED = stringPreferencesKey("updater_newer_version_consented")
+ val updaterNewerVersionConsented: Flow<String?>
+ get() = Application.getPreferencesDataStore().data.map {
+ it[UPDATER_NEWER_VERSION_CONSENTED]
+ }
+
+ suspend fun setUpdaterNewerVersionConsented(newerVersionConsented: String?) {
+ Application.getPreferencesDataStore().edit {
+ if (newerVersionConsented == null)
+ it.remove(UPDATER_NEWER_VERSION_CONSENTED)
+ else
+ it[UPDATER_NEWER_VERSION_CONSENTED] = newerVersionConsented
+ }
+ }
}
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..16c3e6a3 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() != null && 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 3ebe24e8..425b364d 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
@@ -71,7 +73,7 @@
<TextView
android:id="@+id/interface_name_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/interface_name_text"
@@ -81,7 +83,7 @@
<TextView
android:id="@+id/interface_name_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/name"
android:nextFocusUp="@id/tunnel_switch"
@@ -96,7 +98,7 @@
<TextView
android:id="@+id/public_key_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
@@ -106,7 +108,7 @@
<TextView
android:id="@+id/public_key_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/public_key"
android:ellipsize="end"
@@ -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"
@@ -124,81 +126,108 @@
<TextView
android:id="@+id/addresses_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
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" />
<TextView
android:id="@+id/addresses_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
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.addresses}"
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
android:id="@+id/dns_servers_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
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="match_parent"
+ 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" />
<TextView
android:id="@+id/dns_search_domains_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
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" />
<TextView
android:id="@+id/dns_search_domains_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/dns_search_domains"
android:nextFocusUp="@id/dns_servers_text"
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,28 +305,55 @@
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="match_parent"
+ 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"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/applications"
android:nextFocusUp="@id/mtu_text"
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 5aeca08b..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
@@ -23,7 +23,7 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/peer_title"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/peer"
android:textAppearance="?attr/textAppearanceTitleMedium"
@@ -32,7 +32,7 @@
<TextView
android:id="@+id/public_key_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/public_key_text"
@@ -42,7 +42,7 @@
<TextView
android:id="@+id/public_key_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/public_key"
android:ellipsize="end"
@@ -59,18 +59,18 @@
<TextView
android:id="@+id/pre_shared_key_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
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" />
<TextView
android:id="@+id/pre_shared_key_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/pre_shared_key"
android:ellipsize="end"
@@ -81,14 +81,14 @@
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=" />
<TextView
android:id="@+id/allowed_ips_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/allowed_ips_text"
@@ -99,7 +99,7 @@
<TextView
android:id="@+id/allowed_ips_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/allowed_ips"
android:nextFocusUp="@id/pre_shared_key_text"
@@ -115,7 +115,7 @@
<TextView
android:id="@+id/endpoint_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/endpoint_text"
@@ -126,7 +126,7 @@
<TextView
android:id="@+id/endpoint_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/endpoint"
android:nextFocusUp="@id/allowed_ips_text"
@@ -142,7 +142,7 @@
<TextView
android:id="@+id/persistent_keepalive_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:labelFor="@+id/persistent_keepalive_text"
@@ -153,7 +153,7 @@
<TextView
android:id="@+id/persistent_keepalive_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/persistent_keepalive"
android:nextFocusUp="@id/endpoint_text"
@@ -169,9 +169,9 @@
<TextView
android:id="@+id/transfer_label"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@+id/endpoint_text"
+ android:layout_below="@+id/persistent_keepalive_text"
android:layout_marginTop="8dp"
android:labelFor="@+id/transfer_text"
android:text="@string/transfer"
@@ -182,7 +182,7 @@
<TextView
android:id="@+id/transfer_text"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/transfer_label"
android:contentDescription="@string/transfer"
@@ -194,6 +194,34 @@
app:layout_constraintTop_toBottomOf="@+id/transfer_label"
tools:text="1024 MB"
tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/latest_handshake_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/transfer_text"
+ android:layout_marginTop="8dp"
+ android:labelFor="@+id/latest_handshake_text"
+ android:text="@string/latest_handshake"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/transfer_text"
+ tools:visibility="visible" />
+
+ <TextView
+ android:id="@+id/latest_handshake_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/latest_handshake_label"
+ android:contentDescription="@string/latest_handshake"
+ android:nextFocusUp="@id/transfer_text"
+ android:onClick="@{ClipboardUtils::copyTextView}"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/latest_handshake_label"
+ tools:text="4 minutes, 27 seconds ago"
+ tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
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-ar-rSA/strings.xml b/ui/src/main/res/values-ar-rSA/strings.xml
index 755fd117..32bf386f 100644
--- a/ui/src/main/res/values-ar-rSA/strings.xml
+++ b/ui/src/main/res/values-ar-rSA/strings.xml
@@ -111,6 +111,8 @@
<string name="tv_select_a_storage_drive">تحديد محرك تخزين</string>
<string name="tv_no_file_picker">الرجاء تثبيت مدير ملفات لتتمكن من تصفح الملفات</string>
<string name="tv_add_tunnel_get_started">أضف نفقاً لتبدأ</string>
+ <string name="donate_title">♥️ تبرع لمشروع وايرجارد</string>
+ <string name="donate_summary">كل مساهمة تساعد</string>
<string name="disable_config_export_title">تعطيل تصدير التكوين</string>
<string name="disable_config_export_description">تعطيل تصدير الإعدادات يجعل المفاتيح الخاصة غير متاحة</string>
<string name="dns_servers">خوادم DNS</string>
@@ -120,6 +122,7 @@
<string name="error_down">خطأ في فصل النفق:%s</string>
<string name="error_fetching_apps">خطأ في جلب قائمة التطبيقات: %s</string>
<string name="error_root">الرجاء الحصول على صلاحيات الروت وحاول مرة أخرى</string>
+ <string name="error_prepare">خطأ في تحضير النفق: %s</string>
<string name="error_up">خطأ خلال إنشاء النفق: %s</string>
<string name="exclude_private_ips">استبعاد عناوين بروتوكول الإنترنت (IP) الخاصة</string>
<string name="generate_new_private_key">توليد مفتاح خاص جديد</string>
@@ -215,6 +218,7 @@
<string name="tunnel_create_success">تم إنشاء نفق \"%s\" بنجاح</string>
<string name="tunnel_error_already_exists">النفق \"%s\" موجود بالفعل</string>
<string name="tunnel_error_invalid_name">إسم غير صالح</string>
+ <string name="tunnel_list_placeholder">أضف نفق باستخدام الزر أدناه</string>
<string name="tunnel_name">إسم النفق</string>
<string name="tunnel_on_error">غير قادر على تشغيل النفق (wgTurnOn أعاد %d)</string>
<string name="tunnel_dns_failure">غير قادر على حل اسم مضيف نظام أسماء النطاقات (DNS hostname): \"%s\"</string>
diff --git a/ui/src/main/res/values-da-rDK/strings.xml b/ui/src/main/res/values-da-rDK/strings.xml
index 371c23fe..98f8de6e 100644
--- a/ui/src/main/res/values-da-rDK/strings.xml
+++ b/ui/src/main/res/values-da-rDK/strings.xml
@@ -72,6 +72,8 @@
<string name="config_file_exists_error">Konfigurationsfilen \"%s\" findes allerede</string>
<string name="config_not_found_error">Konfigurationsfilen \"%s\" blev ikke fundet</string>
<string name="config_rename_error">Kan ikke omdøbe konfigurationsfilen \"%s\"</string>
+ <string name="config_save_error">Kan ikke gemme konfigurationen for \"%1$s\": %2$s</string>
+ <string name="config_save_success">Konfiguration for \"%s\" blev gemt</string>
<string name="create_activity_title">Opret WireGuard tunnel</string>
<string name="create_empty">Opret fra ny</string>
<string name="create_from_file">Importér fra fil eller arkiv</string>
@@ -88,6 +90,7 @@
<string name="dns_search_domains">DNS-søgedomæner</string>
<string name="edit">Redigér</string>
<string name="endpoint">Slutpunkt</string>
+ <string name="error_prepare">Fejl ved forberedelse af tunnel: %s</string>
<string name="error_up">Fejl under aktivering af tunnel: %s</string>
<string name="exclude_private_ips">Eksludér private IP-adresser</string>
<string name="generate_new_private_key">Generér ny privat nøgle</string>
@@ -152,6 +155,7 @@
<string name="tunnel_create_success">Tunnelen blev succesfuldt oprettet \"%s\"</string>
<string name="tunnel_error_already_exists">Tunnel \"%s\" eksisterer allerede</string>
<string name="tunnel_error_invalid_name">Ugyldigt navn</string>
+ <string name="tunnel_list_placeholder">Tilføj en tunnel ved hjælp af knappen nedenfor</string>
<string name="tunnel_name">Tunnel Navn</string>
<string name="tunnel_dns_failure">Kunne ikke opslå DNS adresse: \"%s\"</string>
<string name="tunnel_rename_error">Kan ikke omdøbe tunnel: %s</string>
diff --git a/ui/src/main/res/values-no-rNO/strings.xml b/ui/src/main/res/values-no-rNO/strings.xml
index 8596017e..ca51975e 100644
--- a/ui/src/main/res/values-no-rNO/strings.xml
+++ b/ui/src/main/res/values-no-rNO/strings.xml
@@ -21,7 +21,7 @@
<item quantity="other">Importerte %d tunneler</item>
</plurals>
<plurals name="set_excluded_applications">
- <item quantity="one">%d ekskludert program</item>
+ <item quantity="one">%d ekskludert app</item>
<item quantity="other">%d ekskluderte apper</item>
</plurals>
<plurals name="set_included_applications">
@@ -79,6 +79,8 @@
<string name="bad_config_reason_unknown_section">Ukjent seksjon</string>
<string name="bad_config_reason_value_out_of_range">Verdien er utenfor gyldig område</string>
<string name="bad_extension_error">Filen må være .conf eller .zip</string>
+ <string name="error_no_qr_found">Ingen QR-kode funnet i bildet</string>
+ <string name="error_qr_checksum">Feil ved sjekksumverifisering av QR-kode</string>
<string name="cancel">Avbryt</string>
<string name="config_delete_error">Kan ikke slette konfigurasjonsfilen %s</string>
<string name="config_exists_error">Konfigurasjon for «%s» finnes allerede</string>
@@ -105,14 +107,19 @@
<string name="tv_select_a_storage_drive">Velg en lagringsenhet</string>
<string name="tv_no_file_picker">Vennligst installer et filhåndteringsverktøy for å bla i filer</string>
<string name="tv_add_tunnel_get_started">Opprett en ny tunnel for å komme i gang</string>
+ <string name="donate_title">♥ Donér til WireGuard-prosjektet</string>
+ <string name="donate_summary">Hvert bidrag hjelper</string>
+ <string name="donate_google_play_disappointment">Takk for at du støtter WireGuard-prosjektet!\n\nPå grunn av Googles retningslinjer, kan vi dessverre ikke linke til den delen av prosjektets nettside der du kan donere. Forhåpentligvis klarer du å finne denne selv!\n\nVi takker igjen for ditt bidrag.</string>
<string name="disable_config_export_title">Deaktiver eksport av konfigurasjon</string>
<string name="disable_config_export_description">Deaktivering av konfigurasjonseksport gjør private nøkler mindre tilgjengelig</string>
<string name="dns_servers">DNS tjenere</string>
+ <string name="dns_search_domains">Søk gjennom domener</string>
<string name="edit">Rediger</string>
<string name="endpoint">Endepunkt</string>
<string name="error_down">Feil når tunnel skulle tas ned: %s</string>
<string name="error_fetching_apps">Feil ved henting av applikasjonsliste: %s</string>
<string name="error_root">Vennligst få root-tilgang og prøv igjen</string>
+ <string name="error_prepare">Feil ved klargjøring av tunnel: %s</string>
<string name="error_up">Feil når tunnel skulle tas opp: %s</string>
<string name="exclude_private_ips">Utelukk private IP-adresser</string>
<string name="generate_new_private_key">Lag ny privat nøkkel</string>
@@ -210,13 +217,16 @@
<string name="tunnel_create_success">Opprettet tunnelen «%s»</string>
<string name="tunnel_error_already_exists">Tunnel «%s» finnes allerede</string>
<string name="tunnel_error_invalid_name">Ugyldig navn</string>
+ <string name="tunnel_list_placeholder">Legg til en tunnel ved å bruke knappen under</string>
<string name="tunnel_name">Tunnelnavn</string>
<string name="tunnel_on_error">Kan ikke slå på tunnel (wgTurnOn returnerte %d)</string>
+ <string name="tunnel_dns_failure">Kan ikke slå opp DNS-vertsnavn: “%s\"</string>
<string name="tunnel_rename_error">Kan ikke endre navn på tunnel: %s</string>
<string name="tunnel_rename_success">Endret navn på tunnelen til «%s»</string>
<string name="type_name_go_userspace">Bruk userspace</string>
<string name="type_name_kernel_module">Kjernemodul</string>
<string name="unknown_error">Ukjent feil</string>
+ <string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Sjekker %s backend versjon</string>
<string name="version_summary_unknown">Ukjent %s versjon</string>
<string name="version_title">WireGuard for Android v%s</string>
diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml
index ba7d4557..fd747768 100644
--- a/ui/src/main/res/values-ru/strings.xml
+++ b/ui/src/main/res/values-ru/strings.xml
@@ -133,6 +133,9 @@
<string name="tv_select_a_storage_drive">Выберите накопитель</string>
<string name="tv_no_file_picker">Пожалуйста, установите утилиту управления файлами для их просмотра</string>
<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>
@@ -142,6 +145,7 @@
<string name="error_down">Ошибка при выходе из туннеля: %s</string>
<string name="error_fetching_apps">Ошибка при получении списка приложений: %s</string>
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string>
+ <string name="error_prepare">Ошибка при подготовке туннеля: %s</string>
<string name="error_up">Ошибка при запуске туннеля: %s</string>
<string name="exclude_private_ips">Исключить частные IP-адреса</string>
<string name="generate_new_private_key">Сгенерировать новый приватный ключ</string>
@@ -161,6 +165,8 @@
<string name="key_length_explanation_base64">: ключи WireGuard base64 должны содержать 44 символа (32 байта)</string>
<string name="key_length_explanation_binary">: ключи WireGuard должны быть 32 байта</string>
<string name="key_length_explanation_hex">: HEX-ключи WireGuard должны содержать 64 символа (32 байта)</string>
+ <string name="latest_handshake">Последнее рукопожатие</string>
+ <string name="latest_handshake_ago">%s назад</string>
<string name="listen_port">Порт</string>
<string name="log_export_error">Не удалось экспортировать журнал: %s</string>
<string name="log_export_subject">Файл журнала WireGuard Android</string>
@@ -239,6 +245,7 @@
<string name="tunnel_create_success">Успешно создан туннель “%s”</string>
<string name="tunnel_error_already_exists">Туннель “%s” уже существует</string>
<string name="tunnel_error_invalid_name">Неправильное имя</string>
+ <string name="tunnel_list_placeholder">Добавьте туннель с помощью кнопки ниже</string>
<string name="tunnel_name">Название туннеля</string>
<string name="tunnel_on_error">Не удалось включить туннель (wgTurnOn вернул %d)</string>
<string name="tunnel_dns_failure">Не удалось определить DNS имя: “%s”</string>
diff --git a/ui/src/main/res/values-zh-rCN/strings.xml b/ui/src/main/res/values-zh-rCN/strings.xml
index 88dedc74..4b30afca 100644
--- a/ui/src/main/res/values-zh-rCN/strings.xml
+++ b/ui/src/main/res/values-zh-rCN/strings.xml
@@ -106,6 +106,7 @@
<string name="error_down">断开连接时出错:%s</string>
<string name="error_fetching_apps">获取应用列表时出错:%s</string>
<string name="error_root">请获取 root 权限并重试</string>
+ <string name="error_prepare">准备连接时出错:%s</string>
<string name="error_up">建立连接时出错:%s</string>
<string name="exclude_private_ips">排除局域网</string>
<string name="generate_new_private_key">生成新的私钥</string>
@@ -125,6 +126,8 @@
<string name="key_length_explanation_base64">:WireGuard 的 Base64 密钥长度必须为 44 个字符(32 字节)</string>
<string name="key_length_explanation_binary">:WireGuard 密钥大小必须为 32 字节</string>
<string name="key_length_explanation_hex">:WireGuard 的十六进制密钥长度必须为 64 个字符(32 字节)</string>
+ <string name="latest_handshake">上次握手时间</string>
+ <string name="latest_handshake_ago">%s之前</string>
<string name="listen_port">监听端口</string>
<string name="log_export_error">无法导出日志:%s</string>
<string name="log_export_subject">WireGuard 日志文件</string>
diff --git a/ui/src/main/res/values/dimens.xml b/ui/src/main/res/values/dimens.xml
index ddb4deac..8ea07dfb 100644
--- a/ui/src/main/res/values/dimens.xml
+++ b/ui/src/main/res/values/dimens.xml
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fab_margin">16dp</dimen>
- <dimen name="extra_margin">12dp</dimen>
<dimen name="bottom_sheet_item_height">56dp</dimen>
<dimen name="normal_margin">8dp</dimen>
<dimen name="bottom_sheet_top_padding">8dp</dimen>
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 7710a879..a1364123 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -70,6 +70,7 @@
<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>
+ <string name="bad_config_explanation_http_proxy">: Must be valid proxy hostname and port</string>
<string name="bad_config_reason_invalid_key">Invalid key</string>
<string name="bad_config_reason_invalid_number">Invalid number</string>
<string name="bad_config_reason_invalid_value">Invalid value</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>
@@ -140,6 +146,8 @@
<string name="key_length_explanation_base64">: WireGuard base64 keys must be 44 characters (32 bytes)</string>
<string name="key_length_explanation_binary">: WireGuard keys must be 32 bytes</string>
<string name="key_length_explanation_hex">: WireGuard hex keys must be 64 characters (32 bytes)</string>
+ <string name="latest_handshake">Latest handshake</string>
+ <string name="latest_handshake_ago">%s ago</string>
<string name="listen_port">Listen port</string>
<string name="log_export_error">Unable to export log: %s</string>
<string name="log_export_subject">WireGuard Android Log File</string>
@@ -227,6 +235,13 @@
<string name="type_name_go_userspace">Go userspace</string>
<string name="type_name_kernel_module">Kernel module</string>
<string name="unknown_error">Unknown error</string>
+ <string name="updater_avalable">An application update is available. Please update now.</string>
+ <string name="updater_action">Download &amp; Update</string>
+ <string name="updater_rechecking">Fetching update metadata…</string>
+ <string name="updater_download_progress">Downloading update: %1$s / %2$s (%3$.2f%%)</string>
+ <string name="updater_download_progress_nototal">Downloading update: %s</string>
+ <string name="updater_installing">Installing update…</string>
+ <string name="updater_failure">Update failure: %s. Will retry momentarily…</string>
<string name="version_summary">%1$s backend %2$s</string>
<string name="version_summary_checking">Checking %s backend version</string>
<string name="version_summary_unknown">Unknown %s version</string>
diff --git a/version.gradle b/version.gradle
index bae75693..51eb2aad 100644
--- a/version.gradle
+++ b/version.gradle
@@ -1,6 +1,7 @@
+
buildscript {
ext {
- wireguardVersionCode = 499
- wireguardVersionName = '1.0.20230412'
+ wireguardVersionCode = 501
+ wireguardVersionName = 'git describe --tags HEAD'.execute().text.trim()
}
}