diff options
72 files changed, 4096 insertions, 282 deletions
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 3eb23b6b..d19645e3 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -66,9 +66,6 @@ <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> - <XML> - <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> - </XML> <codeStyleSettings language="JAVA"> <option name="METHOD_ANNOTATION_WRAP" value="0" /> <option name="FIELD_ANNOTATION_WRAP" value="0" /> diff --git a/build.gradle b/build.gradle index 02647c0c..fb1eed89 100644 --- a/build.gradle +++ b/build.gradle @@ -1,69 +1,39 @@ buildscript { ext { - activityVersion = '1.4.0' - annotationsVersion = '1.3.0' - appcompatVersion = '1.4.1' + activityVersion = '1.6.1' + annotationsVersion = '1.5.0' + appcompatVersion = '1.5.1' biometricVersion = '1.1.0' collectionVersion = '1.2.0' - constraintLayoutVersion = '2.1.3' + constraintLayoutVersion = '2.1.4' coordinatorLayoutVersion = '1.2.0' - coreKtxVersion = '1.7.0' - coroutinesVersion = '1.6.1' + coreKtxVersion = '1.9.0' + coroutinesVersion = '1.6.4' datastoreVersion = '1.0.0' desugarVersion = '1.1.5' - fragmentVersion = '1.4.1' + fragmentVersion = '1.5.4' + grpcVersion = '1.46.0' jsr305Version = '3.0.2' junitVersion = '4.13.2' - lifecycleRuntimeKtxVersion = '2.4.1' - materialComponentsVersion = '1.5.0' + lifecycleRuntimeKtxVersion = '2.5.1' + materialComponentsVersion = '1.7.0' preferenceVersion = '1.2.0' + protobufGradleVersion = '0.8.18' + protocVersion = '3.20.1' zxingEmbeddedVersion = '4.3.0' groupName = 'com.wireguard.android' } } -plugins { - id "de.undercouch.download" version "5.0.4" -} - -task downloadCrowdin(type: Download) { - src 'https://crowdin.com/backend/download/project/wireguard.zip' - dest file('build/translations.zip') - overwrite true -} - -task cleanCrowdin(type: Delete) { - delete 'ui/src/main/res/values-*/strings.xml' -} - -task extractCrowdin(type: Copy, dependsOn: ['downloadCrowdin', 'cleanCrowdin']) { - mustRunAfter 'downloadCrowdin' - from zipTree(file('build/translations.zip')) - into file('build/translations') - doFirst { - delete 'build/translations' - } -} - -task crowdin(type: Copy, dependsOn: ['extractCrowdin']) { - mustRunAfter 'extractCrowdin' - from 'build/translations/wireguard-android/ui/src/main/res' - into 'ui/src/main/res/' - doLast { - delete 'build/translations' - delete 'build/translations.zip' - } -} - task clean(type: Delete) { delete rootProject.buildDir } tasks { wrapper { - gradleVersion = "7.3.3" - distributionSha256Sum = "b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302" + gradleVersion = "7.6" + distributionSha256Sum = "7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b" } } diff --git a/gradle.properties b/gradle.properties index b91f5409..3efc9b38 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,9 +18,6 @@ org.gradle.jvmargs=-Xmx1536m # Turn off AP discovery in compile path to enable compile avoidance kapt.include.compile.classpath=false -# Enable AndroidX -android.useAndroidX=true - # Enable non-transitive R class namespacing where each library only contains # references to the resources it declares instead of declarations plus all # transitive dependency references. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differindex 7454180f..249e5832 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59250647..adb6acbd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 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"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,6 +209,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/keystore/debug.keystore b/keystore/debug.keystore Binary files differnew file mode 100644 index 00000000..c2655107 --- /dev/null +++ b/keystore/debug.keystore diff --git a/settings.gradle b/settings.gradle index 574688e0..c5d41a3b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ pluginManagement { - def agpVersion = "7.1.3" - def kotlinVersion = "1.6.20" + def agpVersion = "7.4.0" + def kotlinVersion = "1.8.0" repositories { gradlePluginPortal() google() @@ -25,4 +25,5 @@ dependencyResolutionManagement { rootProject.name = "wireguard-android" include ':tunnel' +include ':bgp-java' include ':ui' diff --git a/tunnel/build.gradle b/tunnel/build.gradle index ff1aad19..a0ea31ae 100644 --- a/tunnel/build.gradle +++ b/tunnel/build.gradle @@ -1,16 +1,25 @@ +buildscript { + dependencies { + classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufGradleVersion" + } +} + plugins { id 'com.android.library' + id 'com.google.protobuf' version "$protobufGradleVersion" } version wireguardVersionName group groupName android { - compileSdkVersion 31 + compileSdk 33 + ndkVersion '21.4.7075529' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + namespace 'com.wireguard.android.tunnel' defaultConfig { minSdkVersion 21 targetSdkVersion 31 @@ -47,13 +56,73 @@ android { disable('LongLogTag') disable('NewApi') // Desugaring! } + ndkVersion "21.3.6528147" + splits { + abi { + enable true + reset() + include "arm64-v8a", "armeabi-v7a" + universalApk false + } + } } dependencies { + implementation project(":bgp-java") implementation "androidx.annotation:annotation:$annotationsVersion" implementation "androidx.collection:collection:$collectionVersion" + 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:$grpcVersion" + } + } + 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/AndroidManifest.xml b/tunnel/src/main/AndroidManifest.xml index dc0d6c7f..0d97f321 100644 --- a/tunnel/src/main/AndroidManifest.xml +++ b/tunnel/src/main/AndroidManifest.xml @@ -3,14 +3,13 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.wireguard.android.tunnel"> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application> <service android:name="com.wireguard.android.backend.GoBackend$VpnService" android:permission="android.permission.BIND_VPN_SERVICE" - android:exported="true"> + android:exported="false"> <intent-filter> <action android:name="android.net.VpnService" /> </intent-filter> 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..12668621 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/Bgp.java @@ -0,0 +1,281 @@ +/* + * 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.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 ManagedChannel channel; + private Tunnel tunnel; + private int tunnelHandle; + private BGPServer server; + + public Bgp(ManagedChannel channel, Tunnel tunnel, int tunnelHandle) { + 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); + + } 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 3d0886cf..d32955ad 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.okhttp.OkHttpChannelBuilder; +import io.grpc.stub.StreamObserver; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +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.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,15 @@ 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)); + ManagedChannelBuilder<?> channelBuilder = ManagedChannelBuilder.forAddress("localhost", 10000).usePlaintext(); + LocalSocketAddress address = new LocalSocketAddress(socketName, LocalSocketAddress.Namespace.FILESYSTEM); + SocketFactory socketFactory = new UnixDomainSocketFactory(address); + ((OkHttpChannelBuilder) channelBuilder).socketFactory(socketFactory); + channel = channelBuilder.build(); } /** @@ -76,12 +153,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,10 +206,17 @@ public final class GoBackend implements Backend { Key key = null; long rx = 0; long tx = 0; + long handshakeSec = 0; + int handshakeNSec = 0; for (final String line : config.split("\\n")) { if (line.startsWith("public_key=")) { - if (key != null) - stats.add(key, rx, tx); + if (key != null) { + LocalDateTime handshake = null; + if (handshakeSec > 0) { + handshake = LocalDateTime.ofEpochSecond(handshakeSec, handshakeNSec, ZoneOffset.UTC); + } + stats.add(key, rx, tx, handshake); + } rx = 0; tx = 0; try { @@ -152,10 +240,31 @@ 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 { + handshakeSec = Long.parseLong(line.substring(24)); + } catch (final NumberFormatException ignored) { + handshakeSec = 0; + } + } else if (line.startsWith("last_handshake_time_nsec=")) { + if (key == null) + continue; + try { + handshakeNSec = Integer.parseInt(line.substring(25)); + } catch (final NumberFormatException ignored) { + handshakeNSec = 0; + } } } - if (key != null) - stats.add(key, rx, tx); + if (key != null) { + LocalDateTime handshake = null; + if (handshakeSec > 0) { + handshake = LocalDateTime.ofEpochSecond(handshakeSec, handshakeNSec, ZoneOffset.UTC); + } + stats.add(key, rx, tx, handshake); + } return stats; } @@ -166,7 +275,10 @@ public final class GoBackend implements Backend { */ @Override public String getVersion() { - return wgVersion(); + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + VersionRequest request = VersionRequest.newBuilder().build(); + VersionResponse resp = stub.version(request); + return resp.getVersion(); } /** @@ -205,78 +317,251 @@ 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); + 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(); + } + } - if (state == State.UP) { - if (config == null) - throw new BackendException(Reason.TUNNEL_MISSING_CONFIG); + return buf.toString(); + } - if (VpnService.prepare(context) != null) - throw new BackendException(Reason.VPN_NOT_AUTHORIZED); + 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); + } - final VpnService service; - if (!vpnService.isDone()) { - Log.d(TAG, "Requesting to start VpnService"); - context.startService(new Intent(context, VpnService.class)); + 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); + } } + }); - 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; + 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(); + } } - service.setOwner(this); + } - if (currentTunnelHandle != -1) { - Log.w(TAG, "Tunnel already up"); - return; + LibwgGrpc.LibwgBlockingStub stub = LibwgGrpc.newBlockingStub(channel); + DhcpRequest request = DhcpRequest.newBuilder().setSource(source).build(); + DhcpResponse resp = stub.dhcp(request); + Log.i(TAG, "Dhcp: " + resp.getError().getMessage()); + + 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 + } } + } + Dhcp dhcp = new Dhcp(addresses); - 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()); + // 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(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; } } - break; - } - // Build config - final String goConfig = config.toWgUserspaceString(); + @Override + public void onError(Throwable t) { + // failed = t; + Log.i(TAG, "streamReverse error: " + t); + finishLatch.countDown(); + } - // Create the vpn tunnel with android API + @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 +571,125 @@ 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(); + Log.i(TAG, "Builder 14.1"); + if (proxy.isPresent()) { + Log.i(TAG, "Builder 14.2"); + ProxyInfo pi = proxy.get().getProxyInfo(); + Log.i(TAG, "Builder 14.3"); + Uri pacFileUrl = pi.getPacFileUrl(); + Log.i(TAG, "Builder 14.4"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Log.i(TAG, "Builder 14.5"); + if (pacFileUrl != null && pacFileUrl != Uri.EMPTY) { + Log.i(TAG, "Builder 14.6"); + String pacFile = downloadPacFile(activeNetwork, pacFileUrl); + int listenPort = startHttpProxy(pacFile); + Log.i(TAG, "Builder 14.7"); + ProxyInfo localPi = ProxyInfo.buildDirectProxy("localhost", listenPort); + Log.i(TAG, "Builder 14.8"); + builder.setHttpProxy(localPi); + Log.i(TAG, "Builder 14.9"); + } else { + Log.i(TAG, "Builder 14.10"); + builder.setHttpProxy(pi); + Log.i(TAG, "Builder 14.11"); + } + Log.i(TAG, "Builder 14.12"); + } + Log.i(TAG, "Builder 14.13"); + } + Log.i(TAG, "Builder 14.14"); + + 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 +704,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 +722,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); } @@ -385,10 +794,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; @@ -414,4 +834,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/LocalSocketAdapter.java b/tunnel/src/main/java/com/wireguard/android/backend/LocalSocketAdapter.java new file mode 100644 index 00000000..bf027ae1 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/LocalSocketAdapter.java @@ -0,0 +1,325 @@ +/* + */ +package com.wireguard.android.backend; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketImplFactory; +import java.net.SocketOptions; +import java.nio.channels.SocketChannel; +import java.util.Vector; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.util.Log; + +/** + * Adaptor allows using a LocalSocket as a Socket. + */ +final class LocalSocketAdapter extends Socket { + private final LocalSocketAddress address; + private final LocalSocket unix; + private final SocketAddress localAddress; + private InetSocketAddress inetSocketAddress; + private InputStream is; + private OutputStream os; + + LocalSocketAdapter(LocalSocketAddress address) { + this.address = address; + this.localAddress = new InetSocketAddress(0); + unix = new LocalSocket(); + } + + LocalSocketAdapter(LocalSocketAddress address, InetSocketAddress inetAddress) { + this(address); + this.inetSocketAddress = inetAddress; + } + + private void throwUnsupportedOperationException() { + Log.i("helloworld", "Unsupported: " + Log.getStackTraceString(new Exception())); + throw new UnsupportedOperationException(); + } + + @Override + public void bind (SocketAddress bindpoint) { + throwUnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + unix.close(); + } + + @Override public void connect(SocketAddress endpoint) throws IOException { + this.inetSocketAddress = (InetSocketAddress) endpoint; + try { + unix.connect(address); + } catch (IOException e) { + Log.i("helloworld", "Error: " + e.toString()); + throw e; + } + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + this.inetSocketAddress = (InetSocketAddress) endpoint; + unix.connect(address, timeout); + } + + @Override + public SocketChannel getChannel() { + throwUnsupportedOperationException(); + return null; + } + + @Override public InetAddress getInetAddress() { + return inetSocketAddress.getAddress(); + } + + @Override + public InputStream getInputStream() throws IOException { + is = unix.getInputStream(); + return is; + } + + @Override + public boolean getKeepAlive() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public InetAddress getLocalAddress() { + throwUnsupportedOperationException(); + return null; + } + + @Override + public int getLocalPort() { + throwUnsupportedOperationException(); + return 0; + } + + @Override + public SocketAddress getLocalSocketAddress() { + //throwUnsupportedOperationException(); + return localAddress; + } + + @Override + public boolean getOOBInline() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public OutputStream getOutputStream() throws IOException { + if (os != null) + return os; + + OutputStream unixOs = unix.getOutputStream(); + os = new OutputStream() { + @Override + public void close() throws IOException { + // LocalSocket's default implementation closes the socket, + // which leaves readers of thes InputStream hanging. + // Instead shutdown input (and output) to release readers. + LocalSocketAdapter.this.shutdownInput(); + LocalSocketAdapter.this.shutdownOutput(); + } + + @Override + public void write (byte[] b) throws IOException { + unixOs.write(b); + } + + @Override + public void write (byte[] b, int off, int len) throws IOException { + unixOs.write(b, off, len); + } + + @Override + public void write (int b) throws IOException { + unixOs.write(b); + } + }; + return os; + } + + @Override + public int getPort() { + return inetSocketAddress.getPort(); + } + + @Override + public int getReceiveBufferSize() throws SocketException { + try { + return unix.getReceiveBufferSize(); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return inetSocketAddress; + } + + @Override + public boolean getReuseAddress() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public int getSendBufferSize() throws SocketException { + try { + return unix.getSendBufferSize(); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public int getSoLinger() { + throwUnsupportedOperationException(); + return 0; + } + + @Override + public int getSoTimeout() throws SocketException { + try { + return unix.getSoTimeout(); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public boolean getTcpNoDelay() { + throwUnsupportedOperationException(); + return false; + } + + @Override + public int getTrafficClass() { + throwUnsupportedOperationException(); + return 0; + } + + @Override + public boolean isBound() { + return unix.isBound(); + } + + @Override + public boolean isClosed() { + return unix.isClosed(); + } + + @Override + public boolean isConnected() { + return unix.isConnected(); + } + + @Override + public boolean isInputShutdown() { + return unix.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return unix.isOutputShutdown(); + } + + @Override + public void sendUrgentData (int data) { + throwUnsupportedOperationException(); + } + + @Override + public void setKeepAlive (boolean on) { + throwUnsupportedOperationException(); + } + + @Override + public void setOOBInline (boolean on) { + throwUnsupportedOperationException(); + } + + @Override + public void setPerformancePreferences (int connectionTime, + int latency, + int bandwidth) { + throwUnsupportedOperationException(); + } + + @Override + public void setReceiveBufferSize(int size) throws SocketException { + try { + unix.setReceiveBufferSize(size); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public void setReuseAddress (boolean on) { + throwUnsupportedOperationException(); + } + + @Override + public void setSendBufferSize(int size) throws SocketException { + try { + unix.setSendBufferSize(size); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public void setSoLinger (boolean on, + int linger) { + throwUnsupportedOperationException(); + } + + @Override + public void setSoTimeout(int timeout) throws SocketException { + try { + unix.setSoTimeout(timeout); + } catch (IOException e) { + throw new SocketException(e.getMessage()); + } + } + + @Override + public void setTcpNoDelay (boolean on) { + // Not relevant for local sockets. + } + + @Override + public void setTrafficClass (int tc) { + throwUnsupportedOperationException(); + } + + @Override + public void shutdownInput() throws IOException { + unix.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + unix.shutdownOutput(); + } + + @Override + public String toString() { + return unix.toString(); + } +} 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 5d658019..322e766f 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java @@ -11,6 +11,7 @@ import android.util.Pair; import com.wireguard.crypto.Key; import com.wireguard.util.NonNullForAll; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @@ -19,7 +20,19 @@ import java.util.Map; */ @NonNullForAll public class Statistics { - private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>(); + private static class Stat { + long rx; + long tx; + LocalDateTime lastHandshake; + + Stat(long rx, long tx, LocalDateTime lastHandshake) { + this.rx = rx; + this.tx = tx; + this.lastHandshake = lastHandshake; + } + } + + private final Map<Key, Stat> peerBytes = new HashMap<>(); private long lastTouched = SystemClock.elapsedRealtime(); Statistics() { @@ -34,8 +47,8 @@ public class Statistics { * @param tx The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by * the provided {@link Key}. This value is in bytes. */ - 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 rx, final long tx, final LocalDateTime lastHandshake) { + peerBytes.put(key, new Stat(rx, tx, lastHandshake)); lastTouched = SystemClock.elapsedRealtime(); } @@ -56,10 +69,10 @@ public class Statistics { * @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) + final Stat stat = peerBytes.get(peer); + if (stat == null) return 0; - return rxTx.first; + return stat.rx; } /** @@ -70,10 +83,17 @@ public class Statistics { * @return a long representing the number of bytes transmitted by this peer. */ public long peerTx(final Key peer) { - final Pair<Long, Long> rxTx = peerBytes.get(peer); - if (rxTx == null) + final Stat stat = peerBytes.get(peer); + if (stat == null) return 0; - return rxTx.second; + return stat.tx; + } + + public LocalDateTime peerLastHandshake(final Key peer) { + final Stat stat = peerBytes.get(peer); + if (stat == null) + return null; + return stat.lastHandshake; } /** @@ -93,8 +113,8 @@ public class Statistics { */ public long totalRx() { long rx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - rx += val.first; + for (final Stat val : peerBytes.values()) { + rx += val.rx; } return rx; } @@ -106,8 +126,8 @@ public class Statistics { */ public long totalTx() { long tx = 0; - for (final Pair<Long, Long> val : peerBytes.values()) { - tx += val.second; + for (final Stat val : peerBytes.values()) { + tx += val.tx; } 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 9564bbd1..4c435ca7 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/UnixDomainSocketFactory.java b/tunnel/src/main/java/com/wireguard/android/backend/UnixDomainSocketFactory.java new file mode 100644 index 00000000..427a19f1 --- /dev/null +++ b/tunnel/src/main/java/com/wireguard/android/backend/UnixDomainSocketFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.wireguard.android.backend; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import javax.net.SocketFactory; +import android.net.LocalSocketAddress; + +/** Impersonate TCP-style SocketFactory over UNIX domain sockets. */ +public final class UnixDomainSocketFactory extends SocketFactory { + private final LocalSocketAddress address; + + public UnixDomainSocketFactory(LocalSocketAddress address) { + this.address = address; + } + + @Override public Socket createSocket() throws IOException { + return new LocalSocketAdapter(address); + } + + @Override public Socket createSocket(String host, int port) throws IOException { + Socket result = createSocket(); + result.connect(new InetSocketAddress(host, port)); + return result; + } + + @Override public Socket createSocket( + String host, int port, InetAddress localHost, int localPort) throws IOException { + return createSocket(host, port); + } + + @Override public Socket createSocket(InetAddress host, int port) throws IOException { + Socket result = createSocket(); + result.connect(new InetSocketAddress(host, port)); + return result; + } + + @Override public Socket createSocket( + InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { + return createSocket(host, port); + } +} 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 3121c996..d0dc6a46 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/WgQuickBackend.java @@ -93,7 +93,7 @@ public final class WgQuickBackend implements Backend { if (parts.length != 3) 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[1]), Long.parseLong(parts[2]), null); } catch (final Exception ignored) { } } diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java index 8766ce51..e5a94e89 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 807ebec8..ea05e9c8 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 66855f11..62ac0152 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 8abf79aa..8c88556c 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 694f313a..a4fa2a19 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 9b87b397..3cf5dc15 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/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 a2aefef7..9ed24e1c 100644 --- a/tunnel/tools/libwg-go/Makefile +++ b/tunnel/tools/libwg-go/Makefile @@ -19,13 +19,21 @@ export CC := $(ANDROID_C_COMPILER) export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME)) export GOOS := android export CGO_ENABLED := 1 +export GOPATH ?= $(HOME)/go -GO_VERSION := 1.18.2 +GO_VERSION := 1.19.5 GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)) GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz -GO_HASH_darwin-amd64 := 1f5f539ce0baa8b65f196ee219abf73a7d9cf558ba9128cc0fe4833da18b04f2 -GO_HASH_darwin-arm64 := 6c7df9a2405f09aa9bab55c93c9c4ce41d3e58127d626bc1825ba5d0a0045d5c -GO_HASH_linux-amd64 := e54bec97a1a5d230fc2f9ad0880fcbabb5888f30ed9666eca4a91c5a32e86cbc +GO_HASH_darwin-amd64 := 03e8323f63043d13c309a4ebbfc0b0e6c65b835ad4384b445f1a0c39d2c335d7 +GO_HASH_darwin-arm64 := 4fbbf81242a1c6cdd7e505684c12422570b9c628108d021d4354fed07d79d443 +GO_HASH_linux-amd64 := 36519702ae2fd573c9869461990ae550c8c0d955cd28d2827a6b159fda81ff95 + +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 @@ -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" -v -trimpath -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 e64d2fb1..730b4691 100644 --- a/tunnel/tools/libwg-go/go.mod +++ b/tunnel/tools/libwg-go/go.mod @@ -1,14 +1,29 @@ module golang.zx2c4.com/wireguard/android -go 1.18 +go 1.19 require ( - golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a + 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.0.0-20220516162934-403b01795ae8 // indirect - golang.org/x/net v0.0.0-20220516155154-20f960328961 // indirect - golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // 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 633bbf7a..bae2bc33 100644 --- a/tunnel/tools/libwg-go/go.sum +++ b/tunnel/tools/libwg-go/go.sum @@ -1,10 +1,176 @@ -golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs= -golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/net v0.0.0-20220516155154-20f960328961 h1:+W/iTMPG0EL7aW+/atntZwZrvSRIj3m3yX414dSULUU= -golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= -golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0= -golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= +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/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +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/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +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= +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= +gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY= +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/goruntime-boottime-over-monotonic.diff b/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff index 5cbc2256..52369503 100644 --- a/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff +++ b/tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff @@ -144,18 +144,18 @@ index fd69ee70a5..ff6bc8355b 100644 MOVD R1, R15 // R15 is unchanged by C code MOVD g_m(g), R21 // R21 = m diff --git a/src/runtime/sys_linux_s390x.s b/src/runtime/sys_linux_s390x.s -index c15a1d5364..f52c4d5098 100644 +index 1448670b91..7d2ee3231c 100644 --- a/src/runtime/sys_linux_s390x.s +++ b/src/runtime/sys_linux_s390x.s -@@ -207,7 +207,7 @@ TEXT runtimeĀ·walltime1(SB),NOSPLIT,$16 +@@ -296,7 +296,7 @@ fallback: RET - TEXT runtimeĀ·nanotime1(SB),NOSPLIT,$16 -- MOVW $1, R2 // CLOCK_MONOTONIC -+ MOVW $7, R2 // CLOCK_BOOTTIME - MOVD $tp-16(SP), R3 - MOVW $SYS_clock_gettime, R1 - SYSCALL + TEXT runtimeĀ·nanotime1(SB),NOSPLIT,$32-8 +- MOVW $1, R2 // CLOCK_MONOTONIC ++ MOVW $7, R2 // CLOCK_BOOTTIME + + MOVD R15, R7 // Backup stack pointer + -- 2.30.1 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..38601f14 --- /dev/null +++ b/tunnel/tools/libwg-go/service.go @@ -0,0 +1,319 @@ +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) { + e.logger.Verbosef("StartHttpProxy 1") + var listenPort uint16 + if e.httpProxy == nil { + e.logger.Verbosef("StartHttpProxy 2") + e.httpProxy = NewHttpProxy(e.uidRequest, e.logger) + e.logger.Verbosef("StartHttpProxy 3") + var err error + listenPort, err = e.httpProxy.Start() + e.logger.Verbosef("StartHttpProxy 4") + + if err != nil { + e.httpProxy = nil + e.logger.Verbosef("StartHttpProxy 5") + return buildStartHttpProxyError(fmt.Sprintf("Http proxy start failed: %v", err)) + } + } else { + listenPort = e.httpProxy.GetAddrPort().Port() + } + + e.logger.Verbosef("StartHttpProxy 6") + // pacFileUrl, err := url.Parse(req.PacFileUrl) + // if err != nil { + // e.logger.Verbosef("StartHttpProxy 7") + // return buildStartHttpProxyError(fmt.Sprintf("Bad pacFileUrl: %v (%s)", err, req.PacFileUrl)) + // } + + e.logger.Verbosef("StartHttpProxy 8") + err := e.httpProxy.SetPacFileContent(req.GetPacFileContent()) + if err != nil { + e.logger.Verbosef("StartHttpProxy 9") + return buildStartHttpProxyError(fmt.Sprintf("Bad pacFile: %v (%s)", req.GetPacFileContent())) + } + + r := &gen.StartHttpProxyResponse{ + ListenPort: uint32(listenPort), + } + e.logger.Verbosef("StartHttpProxy 10") + 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/ui/build.gradle b/ui/build.gradle index 2bbbe539..91a6c461 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -12,14 +12,15 @@ group groupName final def keystorePropertiesFile = rootProject.file("keystore.properties") android { - compileSdkVersion 31 + compileSdk 33 buildFeatures { buildConfig = true dataBinding = true viewBinding = true } + namespace = 'com.wireguard.android' defaultConfig { - applicationId 'com.wireguard.android' + applicationId 'eu.m7n.wireguard.android' minSdkVersion 21 targetSdkVersion 30 versionCode wireguardVersionCode @@ -31,11 +32,25 @@ android { targetCompatibility JavaVersion.VERSION_1_8 coreLibraryDesugaringEnabled = true } + splits { + abi { + enable true + reset() + include "arm64-v8a", "armeabi-v7a" + universalApk false + } + } if (keystorePropertiesFile.exists()) { final def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) signingConfigs { + debug { + storeFile file('../keystore/debug.keystore') + storePassword "android" + keyAlias "androiddebugkey" + keyPassword "android" + } release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] @@ -60,6 +75,7 @@ android { warning('MissingTranslation') warning('ImpliedQuantity') } + ndkVersion "21.3.6528147" } dependencies { @@ -90,6 +106,5 @@ tasks.withType(JavaCompile) { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 - useIR = true } } diff --git a/ui/src/debug/res/values/strings.xml b/ui/src/debug/res/values/strings.xml index 947b7381..af58a39d 100644 --- a/ui/src/debug/res/values/strings.xml +++ b/ui/src/debug/res/values/strings.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="app_name" translatable="false">WireGuard Ī²</string> + <string name="app_name" translatable="false">(m7n.eu) WireGuard Ī²</string> </resources> diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index 4dd38cb2..405a3bed 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -1,10 +1,11 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - package="com.wireguard.android" 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 @@ -21,6 +22,9 @@ <uses-feature android:name="android.hardware.camera.any" android:required="false" /> + <uses-feature + android:name="android.hardware.camera" + android:required="false" /> <permission android:name="${applicationId}.permission.CONTROL_TUNNELS" @@ -44,7 +48,7 @@ android:theme="@style/NoBackgroundTheme" android:excludeFromRecents="true"/> - <activity android:name=".activity.MainActivity"> + <activity android:name=".activity.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -58,7 +62,8 @@ <activity android:name=".activity.TvMainActivity" - android:theme="@style/TvTheme"> + android:theme="@style/TvTheme" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> @@ -82,7 +87,8 @@ <activity android:name=".activity.LogViewerActivity" - android:label="@string/log_viewer_title"> + android:label="@string/log_viewer_title" + android:exported="false"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> @@ -94,7 +100,7 @@ android:exported="false" android:grantUriPermissions="true" /> - <receiver android:name=".BootShutdownReceiver"> + <receiver android:name=".BootShutdownReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.ACTION_SHUTDOWN" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> @@ -103,7 +109,8 @@ <receiver android:name=".model.TunnelManager$IntentReceiver" - android:permission="${applicationId}.permission.CONTROL_TUNNELS"> + android:permission="${applicationId}.permission.CONTROL_TUNNELS" + android:exported="true"> <intent-filter> <action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" /> <action android:name="com.wireguard.android.action.SET_TUNNEL_UP" /> @@ -114,7 +121,8 @@ <service android:name=".QuickTileService" android:icon="@drawable/ic_tile" - android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" + android:exported="true"> <intent-filter> <action android:name="android.service.quicksettings.action.QS_TILE" /> @@ -135,5 +143,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 d9dcb637..7573bdad 100644 --- a/ui/src/main/java/com/wireguard/android/Application.kt +++ b/ui/src/main/java/com/wireguard/android/Application.kt @@ -16,6 +16,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStoreFile +import com.google.android.material.color.DynamicColors import com.wireguard.android.backend.Backend import com.wireguard.android.backend.GoBackend import com.wireguard.android.backend.WgQuickBackend @@ -64,11 +65,9 @@ class Application : android.app.Application() { private suspend fun determineBackend(): Backend { var backend: Backend? = null - var didStartRootShell = false if (UserKnobs.enableKernelModule.first() && WgQuickBackend.hasKernelSupport()) { try { - if (!didStartRootShell) - rootShell.start() + rootShell.start() val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller) wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first()) backend = wgQuickBackend @@ -88,6 +87,7 @@ class Application : android.app.Application() { override fun onCreate() { Log.i(TAG, USER_AGENT) super.onCreate() + DynamicColors.applyToActivitiesIfAvailable(this) rootShell = RootShell(applicationContext) toolsInstaller = ToolsInstaller(applicationContext, rootShell) preferencesDataStore = PreferenceDataStoreFactory.create { applicationContext.preferencesDataStoreFile("settings") } diff --git a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt index f6f57ddb..1a40a1cd 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/AppListDialogFragment.kt @@ -15,6 +15,7 @@ import androidx.databinding.Observable import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayout import com.wireguard.android.BR import com.wireguard.android.R @@ -88,7 +89,7 @@ class AppListDialogFragment : DialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val alertDialogBuilder = AlertDialog.Builder(requireActivity()) + val alertDialogBuilder = MaterialAlertDialogBuilder(requireActivity()) val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false) binding.executePendingBindings() alertDialogBuilder.setView(binding.root) diff --git a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt index d20f21c6..0c4e9d20 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt @@ -12,6 +12,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText import com.wireguard.android.Application import com.wireguard.android.R @@ -64,7 +65,7 @@ class ConfigNamingDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val activity = requireActivity() imm = activity.getSystemService() - val alertDialogBuilder = AlertDialog.Builder(activity) + val alertDialogBuilder = MaterialAlertDialogBuilder(activity) alertDialogBuilder.setTitle(R.string.import_from_qr_code) binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false) binding?.apply { 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 7046cb96..50d43690 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 @@ -18,8 +19,12 @@ 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 +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId /** * Fragment that shows details about a specific tunnel. @@ -70,7 +75,9 @@ class TunnelDetailFragment : BaseFragment() { } 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 } @@ -99,6 +106,7 @@ class TunnelDetailFragment : BaseFragment() { val state = tunnel.state if (state != Tunnel.State.UP && lastState == state) return lastState = state + var now = LocalDateTime.now(ZoneId.of("UTC")) try { val statistics = tunnel.getStatisticsAsync() for (i in 0 until binding.peersLayout.childCount) { @@ -110,11 +118,20 @@ class TunnelDetailFragment : BaseFragment() { if (rx == 0L && tx == 0L) { peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE - continue + } else { + 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 + } + val lastHandshake:LocalDateTime? = statistics.peerLastHandshake(publicKey) + if (lastHandshake == null) { + peer.lastHandshakeLabel.visibility = View.GONE + peer.lastHandshakeText.visibility = View.GONE + } else { + peer.lastHandshakeText.text = getString(R.string.last_handshake_ago, QuantityFormatter.formatDuration(Duration.between(lastHandshake, now))) + peer.lastHandshakeLabel.visibility = View.VISIBLE + peer.lastHandshakeText.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) { @@ -122,7 +139,13 @@ class TunnelDetailFragment : BaseFragment() { ?: continue peer.transferLabel.visibility = View.GONE peer.transferText.visibility = View.GONE + peer.lastHandshakeLabel.visibility = View.GONE + peer.lastHandshakeText.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 6c6f53f9..1b7cf224 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt @@ -16,10 +16,14 @@ 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.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 @@ -29,6 +33,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 @@ -40,6 +45,21 @@ class TunnelEditorFragment : BaseFragment() { private var binding: TunnelEditorFragmentBinding? = null private var tunnel: ObservableTunnel? = null + private class MaterialSpinnerAdapter<T>(context: Context, resource: Int, private val objects: List<T>) : ArrayAdapter<T>(context, resource, objects) { + private val _filter: Filter by lazy { + object : Filter() { + override fun performFiltering(constraint: CharSequence?): FilterResults { + return FilterResults() + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults?) { + } + } + } + + override fun getFilter(): Filter = _filter + } + private fun onConfigLoaded(config: Config) { binding?.config = ConfigProxy(config) } @@ -80,6 +100,13 @@ class TunnelEditorFragment : BaseFragment() { executePendingBindings() privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() } } + + var httpProxyMenu = binding?.root?.findViewById<TextInputLayout>(R.id.http_proxy_menu) + var httpProxyItems = listOf(Constants.HTTP_PROXY_NONE, Constants.HTTP_PROXY_MANUAL, Constants.HTTP_PROXY_PAC) + var httpProxyAdapter = MaterialSpinnerAdapter(requireContext(), R.layout.http_proxy_menu_item, httpProxyItems) + var httpProxyMenuText = httpProxyMenu?.editText as? AutoCompleteTextView + httpProxyMenuText?.setAdapter(httpProxyAdapter) + return binding?.root } diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt index 252e8759..9684ffab 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()?.get()) + } + } 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 ec796164..5671aa7a 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/util/ErrorMessages.kt b/ui/src/main/java/com/wireguard/android/util/ErrorMessages.kt index 60c6b878..d42f2434 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 4a9ffed4..cd2adbcb 100644 --- a/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt +++ b/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt @@ -8,6 +8,8 @@ package com.wireguard.android.util import com.wireguard.android.Application import com.wireguard.android.R +import java.time.Duration + object QuantityFormatter { fun formatBytes(bytes: Long): String { val context = Application.get().applicationContext @@ -19,4 +21,58 @@ object QuantityFormatter { else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0) } } -}
\ No newline at end of file + + fun formatDuration(duration: Duration): String { + val context = Application.get().applicationContext + val str = formatHours(duration.getSeconds()) + return when { + str != "" -> str + else -> context.getString(R.string.duration_seconds, 0) + } + } + + fun formatHours(seconds: Long): String { + val context = Application.get().applicationContext + val hours = seconds / 3600 + val restSeconds = seconds % 3600 + val str = formatMinutes(restSeconds) + + return when { + hours > 0 -> context.getString(R.string.duration_hours, hours) + if (str != "") ", " + str else "" + else -> str + } + } + + fun formatDays(seconds: Long): String { + val context = Application.get().applicationContext + val days = seconds / 3600 / 24 + val restSeconds = seconds % (3600 * 24) + val str = formatHours(restSeconds) + + return when { + days > 0 -> context.getString(R.string.duration_days, days) + if (str != "") ", " + str else "" + else -> str + } + } + + fun formatMinutes(seconds: Long): String { + val context = Application.get().applicationContext + val minutes = seconds / 60 + val restSeconds = seconds % 60 + val str = formatSeconds(restSeconds) + + return when { + minutes > 0 -> context.getString(R.string.duration_minutes, minutes) + if (str != "") ", " + str else "" + else -> str + } + } + + fun formatSeconds(seconds: Long): String { + val context = Application.get().applicationContext + + return when { + seconds > 0 -> context.getString(R.string.duration_seconds, seconds) + else -> "" + } + } +} 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 2792f749..1833d5c8 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/drawable/ic_action_add_white.xml b/ui/src/main/res/drawable/ic_action_add_white.xml index 18fe19db..1ea440df 100644 --- a/ui/src/main/res/drawable/ic_action_add_white.xml +++ b/ui/src/main/res/drawable/ic_action_add_white.xml @@ -1,9 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:tint="?attr/colorControlNormal" + android:viewportWidth="24" + android:viewportHeight="24"> <path - android:fillColor="?attr/colorOnSecondary" + android:fillColor="#FFFFFFFF" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_delete.xml b/ui/src/main/res/drawable/ic_action_delete.xml index 51517c42..73eb7352 100644 --- a/ui/src/main/res/drawable/ic_action_delete.xml +++ b/ui/src/main/res/drawable/ic_action_delete.xml @@ -1,9 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?attr/colorOnPrimary" + android:fillColor="#FFFFFFFF" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_edit.xml b/ui/src/main/res/drawable/ic_action_edit.xml index 8d8acf85..d40c78d9 100644 --- a/ui/src/main/res/drawable/ic_action_edit.xml +++ b/ui/src/main/res/drawable/ic_action_edit.xml @@ -1,9 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?attr/colorOnPrimary" + android:fillColor="#FFFFFFFF" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_edit_white.xml b/ui/src/main/res/drawable/ic_action_edit_white.xml deleted file mode 100644 index 212fc72c..00000000 --- a/ui/src/main/res/drawable/ic_action_edit_white.xml +++ /dev/null @@ -1,9 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:fillColor="?attr/colorOnSecondary" - android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /> -</vector> diff --git a/ui/src/main/res/drawable/ic_action_generate.xml b/ui/src/main/res/drawable/ic_action_generate.xml index 7324eae4..51d26aed 100644 --- a/ui/src/main/res/drawable/ic_action_generate.xml +++ b/ui/src/main/res/drawable/ic_action_generate.xml @@ -1,9 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_open_white.xml b/ui/src/main/res/drawable/ic_action_open.xml index 91075680..c9fd6fba 100644 --- a/ui/src/main/res/drawable/ic_action_open_white.xml +++ b/ui/src/main/res/drawable/ic_action_open.xml @@ -1,9 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?attr/colorOnSecondary" + android:fillColor="#FFFFFFFF" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_save.xml b/ui/src/main/res/drawable/ic_action_save.xml index ed98e85a..6e618edb 100644 --- a/ui/src/main/res/drawable/ic_action_save.xml +++ b/ui/src/main/res/drawable/ic_action_save.xml @@ -2,9 +2,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?attr/colorOnPrimary" + android:fillColor="#FFFFFFFF" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_scan_qr_code_white.xml b/ui/src/main/res/drawable/ic_action_scan_qr_code.xml index a5ede695..4522ae46 100644 --- a/ui/src/main/res/drawable/ic_action_scan_qr_code_white.xml +++ b/ui/src/main/res/drawable/ic_action_scan_qr_code.xml @@ -1,9 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?attr/colorOnSecondary" + android:fillColor="#FFFFFFFF" android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_select_all.xml b/ui/src/main/res/drawable/ic_action_select_all.xml index 43f5f15e..490ca715 100644 --- a/ui/src/main/res/drawable/ic_action_select_all.xml +++ b/ui/src/main/res/drawable/ic_action_select_all.xml @@ -2,9 +2,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?attr/colorOnPrimary" + android:fillColor="#FFFFFFFF" android:pathData="M3 5L5 5 5 3C3.9 3 3 3.9 3 5Zm0 8l2 0 0 -2 -2 0 0 2zm4 8l2 0 0 -2 -2 0 0 2zM3 9L5 9 5 7 3 7 3 9Zm10 -6l-2 0 0 2 2 0 0 -2zm6 0l0 2 2 0C21 3.9 20.1 3 19 3ZM5 21L5 19 3 19c0 1.1 0.9 2 2 2zm-2 -4l2 0 0 -2 -2 0 0 2zM9 3L7 3 7 5 9 5 9 3Zm2 18l2 0 0 -2 -2 0 0 2zm8 -8l2 0 0 -2 -2 0 0 2zm0 8c1.1 0 2 -0.9 2 -2l-2 0 0 2zm0 -12l2 0 0 -2 -2 0 0 2zm0 8l2 0 0 -2 -2 0 0 2zm-4 4l2 0 0 -2 -2 0 0 2zm0 -16l2 0 0 -2 -2 0 0 2zM7 17L17 17 17 7 7 7 7 17Zm2 -8l6 0 0 6 -6 0 0 -6z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_action_share_white.xml b/ui/src/main/res/drawable/ic_action_share_white.xml index 70843cf3..04ee5b74 100644 --- a/ui/src/main/res/drawable/ic_action_share_white.xml +++ b/ui/src/main/res/drawable/ic_action_share_white.xml @@ -1,6 +1,7 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path diff --git a/ui/src/main/res/drawable/ic_arrow_back.xml b/ui/src/main/res/drawable/ic_arrow_back.xml index 0d549893..05d8f7d7 100644 --- a/ui/src/main/res/drawable/ic_arrow_back.xml +++ b/ui/src/main/res/drawable/ic_arrow_back.xml @@ -6,9 +6,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="#000000" + android:fillColor="#FFFFFFFF" android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42 -0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z" /> </vector> diff --git a/ui/src/main/res/drawable/ic_settings.xml b/ui/src/main/res/drawable/ic_settings.xml index 6d1cfa71..af9f2634 100644 --- a/ui/src/main/res/drawable/ic_settings.xml +++ b/ui/src/main/res/drawable/ic_settings.xml @@ -1,9 +1,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?android:attr/colorForeground" + android:fillColor="#FFFFFFFF" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" /> </vector> diff --git a/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml index 68f186b6..9ed57ac6 100644 --- a/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml +++ b/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml @@ -20,7 +20,7 @@ android:text="@string/create_from_file" android:textAlignment="viewStart" android:textColor="?attr/colorOnSurface" - app:icon="@drawable/ic_action_open_white" + app:icon="@drawable/ic_action_open" app:iconPadding="@dimen/bottom_sheet_icon_padding" app:iconTint="?attr/colorSecondary" app:layout_constraintBottom_toTopOf="@+id/create_from_qrcode" @@ -44,7 +44,7 @@ android:text="@string/create_from_qr_code" android:textAlignment="viewStart" android:textColor="?attr/colorOnSurface" - app:icon="@drawable/ic_action_scan_qr_code_white" + app:icon="@drawable/ic_action_scan_qr_code" app:iconPadding="@dimen/bottom_sheet_icon_padding" app:iconTint="?attr/colorSecondary" app:layout_constraintBottom_toBottomOf="@+id/create_empty" 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 8e34f082..59112d5e 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 @@ -117,7 +119,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}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/public_key_label" tools:text="wOs2eguFEohqIZxlSJ1CAT9584tc6ejj9hfGFsoBVkA=" /> @@ -129,7 +131,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/addresses_text" android:text="@string/addresses" - android:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -140,13 +142,40 @@ android:layout_height="wrap_content" android:contentDescription="@string/addresses" android:nextFocusUp="@id/public_key_text" + android:nextFocusDown="@id/dhcp_addresses_text" + android:nextFocusForward="@id/dhcp_addresses_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:text="@{config.config.interface.addresses}" + 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/dhcp_addresses_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:labelFor="@+id/dhcp_addresses_text" + android:text="@string/dhcp_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/dhcp_addresses_text" + style="@style/DetailText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/dhcp_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:visibility="@{config.interface.addresses.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{tunnel.dhcp.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_label" + app:layout_constraintTop_toBottomOf="@+id/dhcp_addresses_label" tools:text="fc00:bbbb:bbbb:bb11::3:368b/128" /> <TextView @@ -156,9 +185,9 @@ 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/dhcp_addresses_text" /> <TextView android:id="@+id/dns_servers_text" @@ -166,12 +195,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:contentDescription="@string/dns_servers" - android:nextFocusUp="@id/addresses_text" + android:nextFocusUp="@id/dhcp_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:visibility="@{config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.dnsServers}" + android:visibility="@{config.config.interface.dnsServers.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/dns_servers_label" tools:text="8.8.8.8, 8.8.4.4" /> @@ -183,7 +212,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/dns_search_domain_text" android:text="@string/dns_search_domains" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{config.config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/dns_servers_text" /> @@ -197,8 +226,8 @@ android:nextFocusDown="@id/listen_port_text" android:nextFocusForward="@id/listen_port_text" android:onClick="@{ClipboardUtils::copyTextView}" - android:text="@{config.interface.dnsSearchDomains}" - android:visibility="@{config.interface.dnsSearchDomains.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.dnsSearchDomains}" + 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" @@ -224,11 +253,11 @@ 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:visibility="@{!config.interface.listenPort.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.listenPort}" + 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" @@ -257,10 +286,10 @@ 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:visibility="@{!config.interface.mtu.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:text="@{config.config.interface.mtu}" + 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" @@ -268,6 +297,40 @@ app:layout_constraintTop_toBottomOf="@+id/mtu_label" tools:text="1500" /> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/listen_port_mtu_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + 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 < 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" + style="@style/DetailText" + 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:visibility="@{(Build.VERSION.SDK_INT < 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" @@ -275,9 +338,9 @@ android:layout_marginTop="8dp" android:labelFor="@+id/applications_text" android:text="@string/applications" - android:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/listen_port_text" /> + android:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" + app:layout_constraintTop_toBottomOf="@+id/http_proxy_text" + app:layout_constraintStart_toStartOf="parent" /> <TextView android:id="@+id/applications_text" @@ -289,10 +352,10 @@ 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:visibility="@{config.interface.includedApplications.isEmpty() && config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" - app:layout_constraintStart_toStartOf="parent" + 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:visibility="@{config.config.interface.includedApplications.isEmpty() && config.config.interface.excludedApplications.isEmpty() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintTop_toBottomOf="@+id/applications_label" + app:layout_constraintStart_toStartOf="parent" tools:text="8 excluded" /> </androidx.constraintlayout.widget.ConstraintLayout> </com.google.android.material.card.MaterialCardView> @@ -304,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" diff --git a/ui/src/main/res/layout/tunnel_detail_peer.xml b/ui/src/main/res/layout/tunnel_detail_peer.xml index 0fbee8f1..b4a07003 100644 --- a/ui/src/main/res/layout/tunnel_detail_peer.xml +++ b/ui/src/main/res/layout/tunnel_detail_peer.xml @@ -9,7 +9,7 @@ <variable name="item" - type="com.wireguard.config.Peer" /> + type="com.wireguard.android.viewmodel.PeerDetail" /> </data> <com.google.android.material.card.MaterialCardView @@ -64,7 +64,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/pre_shared_key_text" android:text="@string/pre_shared_key" - android:visibility="@{!item.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" + android:visibility="@{!item.peer.preSharedKey.isPresent() ? android.view.View.GONE : android.view.View.VISIBLE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -81,7 +81,7 @@ android:nextFocusForward="@id/allowed_ips_text" android:singleLine="true" android:text="@string/pre_shared_key_enabled" - 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=" /> @@ -194,6 +194,34 @@ app:layout_constraintTop_toBottomOf="@+id/transfer_label" tools:text="1024 MB" tools:visibility="visible" /> + + <TextView + android:id="@+id/last_handshake_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/persistent_keepalive_text" + android:layout_marginTop="8dp" + android:labelFor="@+id/last_handshake_text" + android:text="@string/last_handshake" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transfer_text" + tools:visibility="visible" /> + + <TextView + android:id="@+id/last_handshake_text" + style="@style/DetailText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/last_handshake_label" + android:contentDescription="@string/last_handshake" + android:nextFocusUp="@id/transfer_text" + android:onClick="@{ClipboardUtils::copyTextView}" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/last_handshake_label" + tools:text="29 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 59572b32..789c839b 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/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"> + + <com.google.android.material.textfield.MaterialAutoCompleteTextView + 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.MaterialComponents.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/strings.xml b/ui/src/main/res/values/strings.xml index 6c090199..320b29e2 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -63,13 +63,14 @@ <string name="allow_remote_control_intents_summary_on">External apps may toggle tunnels (advanced)</string> <string name="allow_remote_control_intents_title">Allow remote control apps</string> <string name="allowed_ips">Allowed IPs</string> - <string name="app_name" translatable="false">WireGuard</string> + <string name="app_name" translatable="false">(m7n.eu) WireGuard</string> <string name="bad_config_context">%1$s\'s %2$s</string> <string name="bad_config_context_top_level">%s</string> <string name="bad_config_error">%1$s in %2$s</string> <string name="bad_config_explanation_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="dhcp_addresses">DHCP 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> @@ -112,6 +114,10 @@ <string name="disable_config_export_description">Disabling config exporting makes private keys less accessible</string> <string name="dns_servers">DNS servers</string> <string name="dns_search_domains">Search domains</string> + <string name="duration_days">%d days</string> + <string name="duration_hours">%d hours</string> + <string name="duration_minutes">%d minutes</string> + <string name="duration_seconds">%d seconds</string> <string name="edit">Edit</string> <string name="endpoint">Endpoint</string> <string name="error_down">Error bringing down tunnel: %s</string> @@ -126,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> @@ -136,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="last_handshake">Latest handshake</string> + <string name="last_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> diff --git a/ui/src/main/res/values/styles.xml b/ui/src/main/res/values/styles.xml index b90f111f..da3c2cb7 100644 --- a/ui/src/main/res/values/styles.xml +++ b/ui/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <resources xmlns:android="http://schemas.android.com/apk/res/android"> - <style name="WireGuardTheme" parent="Theme.MaterialComponents.DayNight"> + <style name="WireGuardTheme" parent="Theme.Material3.DayNight"> <item name="colorPrimary">@color/primary_color</item> <item name="colorOnPrimary">@color/color_control_normal</item> <item name="colorPrimaryDark">@color/primary_color</item> @@ -17,9 +17,6 @@ <item name="elevationOverlayEnabled">true</item> <item name="android:statusBarColor">@color/status_bar_color</item> <item name="android:windowBackground">@color/primary_color</item> - <item name="alertDialogTheme">@style/AppTheme.Dialog</item> - <item name="materialAlertDialogTheme">@style/AppTheme.Dialog</item> - <item name="textInputStyle">@style/TextInputLayoutBase</item> <item name="materialCardViewStyle">@style/AppTheme.MaterialCardView</item> </style> @@ -32,12 +29,6 @@ <item name="cardBackgroundColor">?attr/elevationOverlayColor</item> </style> - <style name="AppTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert"> - <item name="colorPrimary">@color/secondary_color</item> - <item name="colorSecondary">@color/secondary_color</item> - <item name="android:windowBackground">?attr/colorBackground</item> - </style> - <style name="BottomSheetDialogTheme" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog"> <item name="android:windowIsFloating">false</item> <item name="android:navigationBarColor">?attr/colorBackground</item> @@ -71,11 +62,7 @@ <item name="colorControlActivated">@color/color_control_normal</item> </style> - <style name="TextInputLayoutBase" parent="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + <style name="ExposedDropDownMenu" parent="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"> <item name="boxStrokeColor">?attr/colorSecondary</item> - <item name="hintTextColor">?attr/colorOnPrimary</item> - <item name="materialThemeOverlay"> - @style/ThemeOverlay.AppTheme.TextInputEditText.OutlinedBox - </item> </style> </resources> diff --git a/ui/src/main/res/values/tv_styles.xml b/ui/src/main/res/values/tv_styles.xml index c5477f6a..536ca752 100644 --- a/ui/src/main/res/values/tv_styles.xml +++ b/ui/src/main/res/values/tv_styles.xml @@ -16,12 +16,26 @@ <item name="elevationOverlayEnabled">false</item> <item name="android:statusBarColor">@color/tv_primary_color</item> <item name="android:windowBackground">@color/tv_primary_color</item> - <item name="alertDialogTheme">@style/AppTheme.Dialog</item> - <item name="materialAlertDialogTheme">@style/AppTheme.Dialog</item> + <item name="alertDialogTheme">@style/TvTheme.Dialog</item> + <item name="materialAlertDialogTheme">@style/TvTheme.Dialog</item> <item name="textInputStyle">@style/TextInputLayoutBase</item> <item name="materialCardViewStyle">@style/TvTheme.MaterialCardView</item> </style> + <style name="TextInputLayoutBase" parent="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + <item name="boxStrokeColor">?attr/colorSecondary</item> + <item name="hintTextColor">?attr/colorOnPrimary</item> + <item name="materialThemeOverlay"> + @style/ThemeOverlay.AppTheme.TextInputEditText.OutlinedBox + </item> + </style> + + <style name="TvTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert"> + <item name="colorPrimary">@color/secondary_color</item> + <item name="colorSecondary">@color/secondary_color</item> + <item name="android:windowBackground">?attr/colorBackground</item> + </style> + <style name="TvTheme.MaterialCardView" parent="Widget.MaterialComponents.CardView"> <item name="cornerRadius">4dp</item> <item name="cardElevation">8dp</item> |