mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 697d131397 | |||
| 89d9e30025 | |||
| dfde86df76 | |||
| d55fb25a40 | |||
| 492fcce053 | |||
| d61f17dbd3 | |||
| 8996979dc2 | |||
| 31ddb242a6 | |||
| b7b5d96b3b | |||
| 2e55e5fd05 | |||
| 40ebf8006e | |||
| 8e8643122e | |||
| c00a0b12e4 | |||
| 0e21520fd1 | |||
| 0db233e5c7 | |||
| b9948085a4 | |||
| d62526fde6 | |||
| 134f9c014e | |||
| 056cf472d9 | |||
| 0c161cc0c2 | |||
| 7894894610 | |||
| 7d48bef70a | |||
| 6bc3e257f8 | |||
| adc613d801 | |||
| fd573f6c1c | |||
| 1235e966d2 | |||
| e6a2f049ea | |||
| d9e9dd04af | |||
| 3c2fa15dc2 | |||
| e8c9c20570 | |||
| 0db2578ca0 | |||
| e71d6157f1 | |||
| 7ca2a0df18 | |||
| 7a4af834c2 | |||
| 314a0d124d | |||
| 6f1e86e8a7 | |||
| 840c7ea560 | |||
| 1839730663 | |||
| afd75cc4cf | |||
| 453a1aaa65 | |||
| 4905185e61 | |||
| 0990430513 | |||
| 7df13a044f | |||
| 6135a1f60a | |||
| 5e94adc73a | |||
| 44fc0228a9 | |||
| 4d2bfe3ef3 | |||
| 7f37ff032f | |||
| 30d508464f | |||
| 75b0fed00d | |||
| 927cc1fcf5 | |||
| b3090e277a | |||
| 0b45151a3d | |||
| eb5bfa9b5d | |||
| 4267e94dcd | |||
| fac9e7612f | |||
| 01e8e535f4 | |||
| e5e2e7571f | |||
| c889a8c8de | |||
| cbf2ea7b48 | |||
| 8078347880 | |||
| dd8a802bec | |||
| 6f6602ddd1 | |||
| 33e69db436 | |||
| 13cbec28d6 | |||
| 826083adf6 | |||
| a62bd28e1f | |||
| da1188c6ee | |||
| e22cefbfe3 | |||
| 02ea696070 | |||
| d25702d99d | |||
| c554413327 | |||
| f8c5f238ea | |||
| 66b46c8618 | |||
| 04689d37b7 | |||
| e041cacf4b | |||
| f54fc92b14 | |||
| 6e7b6bcc35 | |||
| e70d5be535 | |||
| 2ce51c8c2e | |||
| c621ec0c50 | |||
| 2ed2a1431d | |||
| 3a425394ca | |||
| d98ba463ad | |||
| c3d97acb31 | |||
| 8dbd464fa4 | |||
| 63a5bb1bbf | |||
| 8c03878808 | |||
| d29e50e50b | |||
| 687bf8b208 | |||
| 10d0807395 | |||
| 0a89c87190 | |||
| a7df92a64c | |||
| 4d3043c041 | |||
| 8261a18472 | |||
| a9f04c0bf4 | |||
| 84334a6bc9 | |||
| 1e5596f977 | |||
| b67fa3a38c | |||
| 8b0123042f | |||
| 16890a659e | |||
| d40ac7f89d | |||
| 2694f48b87 | |||
| 0f91aeb2d3 | |||
| bc0111f895 | |||
| f8a3e9b332 | |||
| db9397fd3e | |||
| 20717ff128 | |||
| a532a88585 | |||
| 0b077bd523 | |||
| e008efcf97 | |||
| 7cf676f9bd | |||
| b83538d08d | |||
| 3c31c340d8 | |||
| 59620456ee | |||
| e42bd29382 | |||
| 18dbc21f96 | |||
| 68d871c47c | |||
| a45a219e5f | |||
| 49788240aa | |||
| 52166500fd | |||
| 3c8fef2655 | |||
| 21af2f2f62 | |||
| 6d01296e8b | |||
| 749efcde21 | |||
| 3af2420da9 | |||
| d0d24f4554 | |||
| 96b44c1771 | |||
| 4a1d07b364 | |||
| 7fbe5349a2 | |||
| d8bad72fd6 | |||
| 927b32c99f | |||
| 27b691bef6 | |||
| d2b9de740d | |||
| eb45b4b1bd | |||
| c545b5e65f | |||
| 992b6486a1 | |||
| 421b1f889b | |||
| 1ee1368e18 | |||
| 1a6a8789c1 | |||
| b5c155db1b | |||
| 6c5b46eadd | |||
| 5abbab2635 | |||
| e5766094f4 | |||
| 96d77988d3 | |||
| e9e4fd4e8b | |||
| b5d9fbf1f4 | |||
| 81dc89f85b | |||
| 76305045db | |||
| c4ba48d7a9 | |||
| d5dcdf13bf | |||
| 7feb3dccbf | |||
| 2251d74fce | |||
| 2c89d3fa7f | |||
| 49cc634678 | |||
| c93e81c632 | |||
| 050e202291 | |||
| efe04d6602 | |||
| 823b8324c7 | |||
| ae2f88a9ee | |||
| 32287c60c4 | |||
| 69c6fa0a24 | |||
| c3e63df7b5 | |||
| fb4f1e30d5 | |||
| 6d0fde218b | |||
| 6d02b0a26d | |||
| 495bf9c954 | |||
| cb1bc95e6b | |||
| 6fdf0266cf | |||
| ee26198e2c | |||
| fe424197da | |||
| 053ca232aa | |||
| 2e8d566bd4 | |||
| dcb0e9b3e8 | |||
| 3497882ea6 | |||
| dc2463a0ab | |||
| b514717076 | |||
| 23932952d8 | |||
| 266ee7626c | |||
| c1ba1f409c | |||
| 0f669e8ca3 | |||
| 9de711a4f5 | |||
| e1965f121c | |||
| b2c9b3500c | |||
| f60d26c4bf | |||
| b43044fee9 | |||
| 2bc66e4574 | |||
| a641a093ad | |||
| 704369d431 | |||
| 5aa8191cd3 | |||
| bb43804d58 | |||
| 7a8d14c85c | |||
| d1e85633fb | |||
| a264f7ab36 | |||
| 4e134772d7 | |||
| 3eb6c91c9e | |||
| e7fd53b809 | |||
| 164ec1e31d | |||
| 373a5f18d6 | |||
| 36058ead7d | |||
| 33fd5b4634 |
+1
-2
@@ -6,7 +6,7 @@
|
||||
/captures/
|
||||
/local.properties
|
||||
.DS_Store
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
Thumbs.db
|
||||
build/
|
||||
*.apk
|
||||
@@ -15,4 +15,3 @@ build/
|
||||
*.iml
|
||||
*.jks
|
||||
keystore.properties
|
||||
package-info.java
|
||||
|
||||
+3
-9
@@ -1,9 +1,3 @@
|
||||
[submodule "app/tools/libmnl"]
|
||||
path = app/tools/libmnl
|
||||
url = https://git.netfilter.org/libmnl/
|
||||
[submodule "app/tools/wireguard"]
|
||||
path = app/tools/wireguard
|
||||
url = https://git.zx2c4.com/WireGuard
|
||||
[submodule "app/tools/wireguard-go"]
|
||||
path = app/tools/wireguard-go
|
||||
url = https://git.zx2c4.com/wireguard-go
|
||||
[submodule "tunnel/tools/wireguard-tools"]
|
||||
path = tunnel/tools/wireguard-tools
|
||||
url = https://git.zx2c4.com/wireguard-tools
|
||||
|
||||
Generated
+14
-93
@@ -29,16 +29,10 @@
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
<option name="RIGHT_MARGIN" value="100" />
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="USE_CUSTOM_SETTINGS" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
||||
<value />
|
||||
</option>
|
||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
||||
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
@@ -62,35 +56,7 @@
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<Objective-C-extensions>
|
||||
<file>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
||||
</file>
|
||||
<class>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
||||
</class>
|
||||
<extensions>
|
||||
<pair source="cpp" header="h" fileNamingConvention="NONE" />
|
||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
||||
</extensions>
|
||||
</Objective-C-extensions>
|
||||
<XML>
|
||||
<option name="XML_KEEP_LINE_BREAKS" value="false" />
|
||||
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
|
||||
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
</XML>
|
||||
<codeStyleSettings language="JAVA">
|
||||
@@ -382,7 +348,6 @@
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
@@ -393,6 +358,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -403,6 +369,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -414,6 +381,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -424,6 +392,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -434,6 +403,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -444,6 +414,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -454,6 +425,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@@ -464,64 +436,12 @@
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_width</NAME>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_height</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:width</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:height</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
@@ -529,6 +449,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright © &#36;today.year Firstname Lastname <email@example.org> SPDX-License-Identifier: Apache-2.0
|
||||
<option name="notice" value="Copyright © &#36;today.year WireGuard LLC. All Rights Reserved. SPDX-License-Identifier: Apache-2.0" />
|
||||
<option name="myName" value="Default" />
|
||||
</copyright>
|
||||
</component>
|
||||
|
||||
Generated
+2
-2
@@ -1,3 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="GPL-2.0-or-later" />
|
||||
</component>
|
||||
<settings default="Default" />
|
||||
</component>
|
||||
|
||||
Generated
-1
@@ -385,7 +385,6 @@
|
||||
<option name="ignorePrivateMethods" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ReturnOfDateField" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ReturnOfInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ScalarTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="SerializableClassInSecureContext" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SetReplaceableByEnumSet" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
|
||||
@@ -7,9 +7,7 @@ This is an Android GUI for [WireGuard](https://www.wireguard.com/). It [opportun
|
||||
## Building
|
||||
|
||||
```
|
||||
$ git clone https://git.zx2c4.com/wireguard-android
|
||||
$ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
|
||||
$ cd wireguard-android
|
||||
$ git submodule init
|
||||
$ git submodule update
|
||||
$ ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
task generateNonNullJavaFiles(dependsOn: "assembleDebug", type: Copy) {
|
||||
group = "Copying"
|
||||
description = "Generate package-info.java classes"
|
||||
|
||||
def basePackage = "com" + File.separatorChar + "wireguard"
|
||||
def mainSrcPhrase = "src" + File.separatorChar + "main" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
def mainTestSrcPhrase = "src" + File.separatorChar + "test" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
def mainAndroidTestSrcPhrase = "src" + File.separatorChar + "androidTest" + File.separatorChar +
|
||||
"java" + File.separatorChar
|
||||
|
||||
def sourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
|
||||
"main" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage )
|
||||
def testSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File.separatorChar +
|
||||
"test" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage)
|
||||
def androidTestSourceDir = file( "${projectDir}" + File.separatorChar + "src" + File
|
||||
.separatorChar +
|
||||
"androidTest" + File.separatorChar + "java" + File.separatorChar +
|
||||
basePackage )
|
||||
|
||||
generateInfoFiles(sourceDir, mainSrcPhrase);
|
||||
sourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainSrcPhrase)
|
||||
}
|
||||
if (file(testSourceDir).exists()) {
|
||||
generateInfoFiles(testSourceDir, mainTestSrcPhrase);
|
||||
testSourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainTestSrcPhrase)
|
||||
}
|
||||
}
|
||||
if (file(androidTestSourceDir).exists()) {
|
||||
generateInfoFiles(androidTestSourceDir, mainAndroidTestSrcPhrase);
|
||||
androidTestSourceDir.eachDirRecurse { dir ->
|
||||
generateInfoFiles(dir, mainAndroidTestSrcPhrase)
|
||||
}
|
||||
}
|
||||
println "[SUCCESS] NonNull generator: package-info.java files checked"
|
||||
}
|
||||
|
||||
private void generateInfoFiles(File dir, String mainSrcPhrase) {
|
||||
def infoFileContentHeader = getFileContentHeader();
|
||||
def infoFileContentFooter = getFileContentFooter();
|
||||
def infoFilePath = dir.getAbsolutePath() + File.separatorChar + "package-info.java"
|
||||
|
||||
//file(infoFilePath).delete(); //do not use in production code
|
||||
if (!file(infoFilePath).exists()) {
|
||||
def infoFileContentPackage = getFileContentPackage(dir.getAbsolutePath(), mainSrcPhrase);
|
||||
new File(infoFilePath).write(infoFileContentHeader +
|
||||
infoFileContentPackage + infoFileContentFooter)
|
||||
println "[dir] " + infoFilePath + " created";
|
||||
}
|
||||
}
|
||||
|
||||
def getFileContentPackage(String path, String mainSrcPhrase) {
|
||||
def mainSrcPhraseIndex = path.indexOf(mainSrcPhrase)
|
||||
def output = path.substring(mainSrcPhraseIndex)
|
||||
|
||||
// Win hotfix
|
||||
if (System.properties['os.name'].toLowerCase().contains('windows')) {
|
||||
output = output.replace("\\", "/")
|
||||
mainSrcPhrase = mainSrcPhrase.replace("\\", "/")
|
||||
}
|
||||
|
||||
return "package " + output.replaceAll(mainSrcPhrase, "").replaceAll(
|
||||
"/", ".") + ";\n"
|
||||
}
|
||||
|
||||
def getFileContentHeader() {
|
||||
return "/**\n" +
|
||||
" * Make all method parameters @NonNull by default.\n" +
|
||||
" */\n" +
|
||||
"@NonNullForAll\n"
|
||||
}
|
||||
|
||||
def getFileContentFooter() {
|
||||
return "\n" +
|
||||
"import com.wireguard.util.NonNullForAll;\n"
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public abstract class ThemeChangeAwareActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "WireGuard/" + ThemeChangeAwareActivity.class.getSimpleName();
|
||||
|
||||
@Nullable private static Resources lastResources;
|
||||
private static boolean lastDarkMode;
|
||||
private static synchronized void invalidateDrawableCache(final Resources resources, final boolean darkMode) {
|
||||
if (resources == lastResources && darkMode == lastDarkMode)
|
||||
return;
|
||||
|
||||
try {
|
||||
Field f;
|
||||
Object o = resources;
|
||||
try {
|
||||
f = o.getClass().getDeclaredField("mResourcesImpl");
|
||||
f.setAccessible(true);
|
||||
o = f.get(o);
|
||||
} catch (final Exception ignored) { }
|
||||
f = o.getClass().getDeclaredField("mDrawableCache");
|
||||
f.setAccessible(true);
|
||||
o = f.get(o);
|
||||
try {
|
||||
o.getClass().getMethod("onConfigurationChange", int.class).invoke(o, -1);
|
||||
} catch (final Exception ignored) {
|
||||
o.getClass().getMethod("clear").invoke(o);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "Failed to flush drawable cache", e);
|
||||
}
|
||||
|
||||
lastResources = resources;
|
||||
lastDarkMode = darkMode;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Application.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Application.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
|
||||
if ("dark_theme".equals(key)) {
|
||||
final boolean darkMode = sharedPreferences.getBoolean(key, false);
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
sharedPreferences.getBoolean(key, false) ?
|
||||
AppCompatDelegate.MODE_NIGHT_YES :
|
||||
AppCompatDelegate.MODE_NIGHT_NO);
|
||||
invalidateDrawableCache(getResources(), darkMode);
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
|
||||
*/
|
||||
|
||||
public final class WgQuickBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
|
||||
|
||||
private final File localTemporaryDir;
|
||||
|
||||
public WgQuickBackend(final Context context) {
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() throws Exception {
|
||||
final List<String> output = new ArrayList<>();
|
||||
if (Application.getRootShell()
|
||||
.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
|
||||
throw new Exception("Unable to determine kernel module version");
|
||||
return output.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() { return "Kernel module"; }
|
||||
|
||||
@Override
|
||||
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
|
||||
if (tunnel.getState() == State.UP) {
|
||||
// Restart the tunnel to apply the new config.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
// The new configuration didn't work, so try to go back to the old one.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
final List<String> output = new ArrayList<>();
|
||||
// Don't throw an exception here or nothing will show up in the UI.
|
||||
try {
|
||||
Application.getToolsInstaller().ensureToolsAvailable();
|
||||
if (Application.getRootShell().run(output, "wg show interfaces") != 0 || output.isEmpty())
|
||||
return Collections.emptySet();
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Unable to enumerate running tunnels", e);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return enumerate().contains(tunnel.getName()) ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
return new Statistics();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if (state == originalState)
|
||||
return originalState;
|
||||
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
|
||||
Application.getToolsInstaller().ensureToolsAvailable();
|
||||
setStateInternal(tunnel, tunnel.getConfig(), state);
|
||||
return getState(tunnel);
|
||||
}
|
||||
|
||||
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
|
||||
Objects.requireNonNull(config, "Trying to set state with a null config");
|
||||
|
||||
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
|
||||
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
|
||||
stream.write(config.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String command = String.format("wg-quick %s '%s'",
|
||||
state.toString().toLowerCase(), tempFile.getAbsolutePath());
|
||||
if (state == State.UP)
|
||||
command = "cat /sys/module/wireguard/version && " + command;
|
||||
final int result = Application.getRootShell().run(null, command);
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
tempFile.delete();
|
||||
if (result != 0)
|
||||
throw new Exception("Unable to configure tunnel (wg-quick returned " + result + ')');
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.databinding.TunnelDetailFragmentBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
/**
|
||||
* Fragment that shows details about a specific tunnel.
|
||||
*/
|
||||
|
||||
public class TunnelDetailFragment extends BaseFragment {
|
||||
@Nullable private TunnelDetailFragmentBinding binding;
|
||||
|
||||
private void onConfigLoaded(final String name, final Config config) {
|
||||
if (binding != null) {
|
||||
binding.setConfig(new Config.Observable(config, name));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.tunnel_detail, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false);
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
binding = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel, @Nullable final Tunnel newTunnel) {
|
||||
if (binding == null)
|
||||
return;
|
||||
binding.setTunnel(newTunnel);
|
||||
if (newTunnel == null)
|
||||
binding.setConfig(null);
|
||||
else
|
||||
newTunnel.getConfigAsync().thenAccept(a -> onConfigLoaded(newTunnel.getName(), a));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable final Bundle savedInstanceState) {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
binding.setFragment(this);
|
||||
onSelectedTunnelChanged(null, getSelectedTunnel());
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public final class SharedLibraryLoader {
|
||||
private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
|
||||
|
||||
private SharedLibraryLoader() { }
|
||||
|
||||
public static void loadSharedLibrary(final Context context, final String libName) {
|
||||
Throwable noAbiException;
|
||||
try {
|
||||
System.loadLibrary(libName);
|
||||
return;
|
||||
} catch (final UnsatisfiedLinkError e) {
|
||||
Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e);
|
||||
noAbiException = e;
|
||||
}
|
||||
|
||||
final ZipFile zipFile;
|
||||
try {
|
||||
zipFile = new ZipFile(new File(context.getApplicationInfo().sourceDir), ZipFile.OPEN_READ);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
final String mappedLibName = System.mapLibraryName(libName);
|
||||
final byte[] buffer = new byte[1024 * 32];
|
||||
for (final String abi : Build.SUPPORTED_ABIS) {
|
||||
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
|
||||
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
|
||||
if (zipEntry == null)
|
||||
continue;
|
||||
File f = null;
|
||||
try {
|
||||
f = File.createTempFile("lib", ".so", context.getCacheDir());
|
||||
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + f.getAbsolutePath() + " and loading");
|
||||
try (final FileOutputStream out = new FileOutputStream(f);
|
||||
final InputStream in = zipFile.getInputStream(zipEntry)) {
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
System.load(f.getAbsolutePath());
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
Log.d(TAG, "Failed to load library apk:/" + libZipPath, e);
|
||||
noAbiException = e;
|
||||
} finally {
|
||||
if (f != null)
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
if (noAbiException instanceof RuntimeException)
|
||||
throw (RuntimeException) noAbiException;
|
||||
throw new RuntimeException(noAbiException);
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BuildConfig;
|
||||
import com.wireguard.android.util.RootShell.NoRootException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper to install WireGuard tools to the system partition.
|
||||
*/
|
||||
|
||||
public final class ToolsInstaller {
|
||||
public static final int ERROR = 0x0;
|
||||
public static final int YES = 0x1;
|
||||
public static final int NO = 0x2;
|
||||
public static final int MAGISK = 0x4;
|
||||
public static final int SYSTEM = 0x8;
|
||||
|
||||
private static final String[][] EXECUTABLES = {
|
||||
{"libwg.so", "wg"},
|
||||
{"libwg-quick.so", "wg-quick"},
|
||||
};
|
||||
private static final File[] INSTALL_DIRS = {
|
||||
new File("/system/xbin"),
|
||||
new File("/system/bin"),
|
||||
};
|
||||
@Nullable private static final File INSTALL_DIR = getInstallDir();
|
||||
private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName();
|
||||
|
||||
private final File localBinaryDir;
|
||||
private final Object lock = new Object();
|
||||
private final File nativeLibraryDir;
|
||||
@Nullable private Boolean areToolsAvailable;
|
||||
@Nullable private Boolean installAsMagiskModule;
|
||||
|
||||
public ToolsInstaller(final Context context) {
|
||||
localBinaryDir = new File(context.getCacheDir(), "bin");
|
||||
nativeLibraryDir = new File(context.getApplicationInfo().nativeLibraryDir);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static File getInstallDir() {
|
||||
final String path = System.getenv("PATH");
|
||||
if (path == null)
|
||||
return INSTALL_DIRS[0];
|
||||
final List<String> paths = Arrays.asList(path.split(":"));
|
||||
for (final File dir : INSTALL_DIRS) {
|
||||
if (paths.contains(dir.getPath()) && dir.isDirectory())
|
||||
return dir;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int areInstalled() throws NoRootException {
|
||||
if (INSTALL_DIR == null)
|
||||
return ERROR;
|
||||
final StringBuilder script = new StringBuilder();
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
script.append(String.format("cmp -s '%s' '%s' && ",
|
||||
new File(nativeLibraryDir, names[0]),
|
||||
new File(INSTALL_DIR, names[1])));
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EALREADY).append(';');
|
||||
try {
|
||||
final int ret = Application.getRootShell().run(null, script.toString());
|
||||
if (ret == OsConstants.EALREADY)
|
||||
return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
|
||||
else
|
||||
return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureToolsAvailable() throws FileNotFoundException, NoRootException {
|
||||
synchronized (lock) {
|
||||
if (areToolsAvailable == null) {
|
||||
final int ret = symlink();
|
||||
if (ret == OsConstants.EALREADY) {
|
||||
Log.d(TAG, "Tools were already symlinked into our private binary dir");
|
||||
areToolsAvailable = true;
|
||||
} else if (ret == OsConstants.EXIT_SUCCESS) {
|
||||
Log.d(TAG, "Tools are now symlinked into our private binary dir");
|
||||
areToolsAvailable = true;
|
||||
} else {
|
||||
Log.e(TAG, "For some reason, wg and wg-quick are not available at all");
|
||||
areToolsAvailable = false;
|
||||
}
|
||||
}
|
||||
if (!areToolsAvailable)
|
||||
throw new FileNotFoundException("Required tools unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean willInstallAsMagiskModule() {
|
||||
synchronized (lock) {
|
||||
if (installAsMagiskModule == null) {
|
||||
try {
|
||||
installAsMagiskModule = Application.getRootShell().run(null, "[ -d /sbin/.core/mirror -a -d /sbin/.core/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
} catch (final Exception ignored) {
|
||||
installAsMagiskModule = false;
|
||||
}
|
||||
}
|
||||
return installAsMagiskModule;
|
||||
}
|
||||
}
|
||||
|
||||
private int installSystem() throws NoRootException {
|
||||
if (INSTALL_DIR == null)
|
||||
return OsConstants.ENOENT;
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
final File destination = new File(INSTALL_DIR, names[1]);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
|
||||
new File(nativeLibraryDir, names[0]), destination, destination, destination));
|
||||
}
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private int installMagisk() throws NoRootException {
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /sbin/.core/img/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /sbin/.core/img/wireguard/; mkdir -p /sbin/.core/img/wireguard%s; ", INSTALL_DIR));
|
||||
script.append(String.format("printf 'name=WireGuard Command Line Tools\nversion=%s\nversionCode=%s\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.core/img/wireguard/module.prop; ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
script.append("touch /sbin/.core/img/wireguard/auto_mount; ");
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
final File destination = new File("/sbin/.core/img/wireguard" + INSTALL_DIR, names[1]);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
|
||||
new File(nativeLibraryDir, names[0]), destination, destination, destination));
|
||||
}
|
||||
script.append("trap - INT TERM EXIT;");
|
||||
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
public int install() throws NoRootException {
|
||||
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
|
||||
}
|
||||
|
||||
public int symlink() throws NoRootException {
|
||||
final StringBuilder script = new StringBuilder("set -x; ");
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
script.append(String.format("test '%s' -ef '%s' && ",
|
||||
new File(nativeLibraryDir, names[0]),
|
||||
new File(localBinaryDir, names[1])));
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EALREADY).append("; set -e; ");
|
||||
|
||||
for (final String[] names : EXECUTABLES) {
|
||||
script.append(String.format("ln -fns '%s' '%s'; ",
|
||||
new File(nativeLibraryDir, names[0]),
|
||||
new File(localBinaryDir, names[1])));
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EXIT_SUCCESS).append(';');
|
||||
|
||||
try {
|
||||
return Application.getRootShell().run(null, script.toString());
|
||||
} catch (final IOException ignored) {
|
||||
return OsConstants.EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.support.design.widget.CoordinatorLayout;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
|
||||
|
||||
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
return dependency instanceof Snackbar.SnackbarLayout;
|
||||
}
|
||||
|
||||
private static final long ANIMATION_DURATION = 250;
|
||||
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
|
||||
|
||||
private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
|
||||
final float origin = child.getBehaviorYTranslation();
|
||||
if (Math.abs(destination - origin) < fullSpan / 2) {
|
||||
child.setBehaviorYTranslation(destination);
|
||||
return;
|
||||
}
|
||||
final ValueAnimator animator = new ValueAnimator();
|
||||
animator.setFloatValues(origin, destination);
|
||||
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
|
||||
animator.setDuration((long)(ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator a) {
|
||||
child.setBehaviorYTranslation(destination);
|
||||
}
|
||||
});
|
||||
animator.addUpdateListener(a -> child.setBehaviorYTranslation((float)a.getAnimatedValue()));
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
animateChange(child, 0, dependency.getMeasuredHeight());
|
||||
}
|
||||
}
|
||||
@@ -1,632 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Keep;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.content.res.ResourcesCompat;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class FloatingActionsMenu extends ViewGroup {
|
||||
public static final int EXPAND_UP = 0;
|
||||
public static final int EXPAND_DOWN = 1;
|
||||
public static final int EXPAND_LEFT = 2;
|
||||
public static final int EXPAND_RIGHT = 3;
|
||||
|
||||
public static final int LABELS_ON_LEFT_SIDE = 0;
|
||||
public static final int LABELS_ON_RIGHT_SIDE = 1;
|
||||
|
||||
private static final int ANIMATION_DURATION = 300;
|
||||
private static final float COLLAPSED_PLUS_ROTATION = 0f;
|
||||
private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
|
||||
private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator();
|
||||
private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
|
||||
private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
|
||||
private int mExpandDirection;
|
||||
private int mButtonSpacing;
|
||||
private int mLabelsMargin;
|
||||
private int mLabelsVerticalOffset;
|
||||
private boolean mExpanded;
|
||||
private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
@Nullable private FloatingActionButton mAddButton;
|
||||
@Nullable private RotatingDrawable mRotatingDrawable;
|
||||
private int mMaxButtonWidth;
|
||||
private int mMaxButtonHeight;
|
||||
private int mLabelsStyle;
|
||||
private int mLabelsPosition;
|
||||
private int mButtonsCount;
|
||||
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
|
||||
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
|
||||
private final Rect touchArea = new Rect(0, 0, 0, 0);
|
||||
private float scrollYTranslation;
|
||||
private float behaviorYTranslation;
|
||||
|
||||
public FloatingActionsMenu(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(final Context context, @Nullable final AttributeSet attributeSet) {
|
||||
mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing));
|
||||
mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin);
|
||||
mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset);
|
||||
|
||||
mTouchDelegateGroup = new TouchDelegateGroup(this);
|
||||
setTouchDelegate(mTouchDelegateGroup);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0);
|
||||
mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP);
|
||||
mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0);
|
||||
mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE);
|
||||
attr.recycle();
|
||||
|
||||
if (mLabelsStyle != 0 && expandsHorizontally()) {
|
||||
throw new IllegalStateException("Action labels in horizontal expand orientation is not supported.");
|
||||
}
|
||||
|
||||
createAddButton(context);
|
||||
}
|
||||
|
||||
public float getScrollYTranslation() {
|
||||
return scrollYTranslation;
|
||||
}
|
||||
|
||||
public void setScrollYTranslation(final float scrollYTranslation) {
|
||||
this.scrollYTranslation = scrollYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
public float getBehaviorYTranslation() {
|
||||
return behaviorYTranslation;
|
||||
}
|
||||
|
||||
public void setBehaviorYTranslation(final float behaviorYTranslation) {
|
||||
this.behaviorYTranslation = behaviorYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
private boolean expandsHorizontally() {
|
||||
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
|
||||
}
|
||||
|
||||
private void createAddButton(final Context context) {
|
||||
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
|
||||
mRotatingDrawable = rotatingDrawable;
|
||||
|
||||
final TimeInterpolator interpolator = new OvershootInterpolator();
|
||||
|
||||
final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION);
|
||||
final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION);
|
||||
|
||||
collapseAnimator.setInterpolator(interpolator);
|
||||
expandAnimator.setInterpolator(interpolator);
|
||||
|
||||
mExpandAnimation.play(expandAnimator);
|
||||
mCollapseAnimation.play(collapseAnimator);
|
||||
|
||||
mAddButton = new FloatingActionButton(context);
|
||||
mAddButton.setImageDrawable(rotatingDrawable);
|
||||
mAddButton.setId(R.id.fab_expand_menu_button);
|
||||
mAddButton.setOnClickListener(v -> toggle());
|
||||
|
||||
addView(mAddButton, super.generateDefaultLayoutParams());
|
||||
mButtonsCount++;
|
||||
}
|
||||
|
||||
public void addButton(final LabeledFloatingActionButton button) {
|
||||
addView(button, mButtonsCount - 1);
|
||||
mButtonsCount++;
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeButton(final LabeledFloatingActionButton button) {
|
||||
removeView(button.getLabelView());
|
||||
removeView(button);
|
||||
button.setTag(R.id.fab_label, null);
|
||||
mButtonsCount--;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
measureChildren(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
mMaxButtonWidth = 0;
|
||||
mMaxButtonHeight = 0;
|
||||
int maxLabelWidth = 0;
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child.getVisibility() == GONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
|
||||
height += child.getMeasuredHeight();
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += child.getMeasuredWidth();
|
||||
mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
|
||||
break;
|
||||
}
|
||||
|
||||
if (!expandsHorizontally()) {
|
||||
final TextView label = (TextView) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expandsHorizontally()) {
|
||||
height = mMaxButtonHeight;
|
||||
} else {
|
||||
width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
height += mButtonSpacing * (mButtonsCount - 1);
|
||||
height = adjustForOvershoot(height);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += mButtonSpacing * (mButtonsCount - 1);
|
||||
width = adjustForOvershoot(width);
|
||||
break;
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
private static int adjustForOvershoot(final int dimension) {
|
||||
return dimension * 12 / 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
final boolean expandUp = mExpandDirection == EXPAND_UP;
|
||||
|
||||
if (changed) {
|
||||
mTouchDelegateGroup.clearTouchDelegates();
|
||||
}
|
||||
|
||||
final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? r - l - mMaxButtonWidth / 2
|
||||
: mMaxButtonWidth / 2);
|
||||
final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2;
|
||||
mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight());
|
||||
|
||||
final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin;
|
||||
final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? buttonsHorizontalCenter - labelsOffset
|
||||
: buttonsHorizontalCenter + labelsOffset;
|
||||
|
||||
int nextY = expandUp ?
|
||||
addButtonY - mButtonSpacing :
|
||||
addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2;
|
||||
final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonY - childY;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
final View label = (View) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton - label.getMeasuredWidth()
|
||||
: labelsXNearButton + label.getMeasuredWidth();
|
||||
|
||||
final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelXAwayFromButton
|
||||
: labelsXNearButton;
|
||||
|
||||
final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton
|
||||
: labelXAwayFromButton;
|
||||
|
||||
final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2;
|
||||
|
||||
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight());
|
||||
|
||||
touchArea.set(Math.min(childX, labelLeft),
|
||||
childY - mButtonSpacing / 2,
|
||||
Math.max(childX + child.getMeasuredWidth(), labelRight),
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing / 2);
|
||||
mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child));
|
||||
|
||||
label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
label.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
|
||||
labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
labelParams.setAnimationsTarget(label);
|
||||
}
|
||||
|
||||
nextY = expandUp ?
|
||||
childY - mButtonSpacing :
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing;
|
||||
}
|
||||
break;
|
||||
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
|
||||
|
||||
final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
|
||||
mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
|
||||
|
||||
int nextX = expandLeft ?
|
||||
addButtonX - mButtonSpacing :
|
||||
addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
|
||||
final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonX - childX;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
nextX = expandLeft ?
|
||||
childX - mButtonSpacing :
|
||||
childX + child.getMeasuredWidth() + mButtonSpacing;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(super.generateDefaultLayoutParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
|
||||
return new LayoutParams(super.generateLayoutParams(attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
|
||||
return new LayoutParams(super.generateLayoutParams(p));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
bringChildToFront(mAddButton);
|
||||
mButtonsCount = getChildCount();
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equals("ASUS");
|
||||
|
||||
private void createLabels() {
|
||||
final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle);
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
|
||||
|
||||
if (button instanceof LabeledFloatingActionButton) {
|
||||
final String title = ((LabeledFloatingActionButton) button).getTitle();
|
||||
|
||||
final AppCompatTextView label = new AppCompatTextView(context);
|
||||
if (!BROKEN_LABEL_STYLE)
|
||||
label.setTextAppearance(context, mLabelsStyle);
|
||||
label.setText(title);
|
||||
addView(label);
|
||||
|
||||
button.setTag(R.id.fab_label, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void collapse() {
|
||||
collapse(false);
|
||||
}
|
||||
|
||||
public void collapseImmediately() {
|
||||
collapse(true);
|
||||
}
|
||||
|
||||
private void collapse(final boolean immediately) {
|
||||
if (mExpanded) {
|
||||
mExpanded = false;
|
||||
mTouchDelegateGroup.setEnabled(false);
|
||||
mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION);
|
||||
mCollapseAnimation.start();
|
||||
mExpandAnimation.cancel();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuCollapsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
if (mExpanded) {
|
||||
collapse();
|
||||
} else {
|
||||
expand();
|
||||
}
|
||||
}
|
||||
|
||||
public void expand() {
|
||||
if (!mExpanded) {
|
||||
mExpanded = true;
|
||||
mTouchDelegateGroup.setEnabled(true);
|
||||
mCollapseAnimation.cancel();
|
||||
mExpandAnimation.start();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuExpanded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return mExpanded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(final boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
|
||||
mAddButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState savedState = new SavedState(superState);
|
||||
savedState.mExpanded = mExpanded;
|
||||
|
||||
return savedState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
if (state instanceof SavedState) {
|
||||
final SavedState savedState = (SavedState) state;
|
||||
mExpanded = savedState.mExpanded;
|
||||
mTouchDelegateGroup.setEnabled(mExpanded);
|
||||
|
||||
if (mRotatingDrawable != null) {
|
||||
mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
|
||||
}
|
||||
|
||||
super.onRestoreInstanceState(savedState.getSuperState());
|
||||
} else {
|
||||
super.onRestoreInstanceState(state);
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnFloatingActionsMenuUpdateListener {
|
||||
void onMenuExpanded();
|
||||
|
||||
void onMenuCollapsed();
|
||||
}
|
||||
|
||||
private static class RotatingDrawable extends LayerDrawable {
|
||||
private float mRotation;
|
||||
|
||||
RotatingDrawable(final Drawable drawable) {
|
||||
super(new Drawable[]{drawable});
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public float getRotation() {
|
||||
return mRotation;
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public void setRotation(final float rotation) {
|
||||
mRotation = rotation;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final Canvas canvas) {
|
||||
canvas.save();
|
||||
canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
|
||||
super.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SavedState extends BaseSavedState {
|
||||
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
|
||||
|
||||
@Override
|
||||
public SavedState createFromParcel(final Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(final int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
public boolean mExpanded;
|
||||
|
||||
public SavedState(final Parcelable parcel) {
|
||||
super(parcel);
|
||||
}
|
||||
|
||||
private SavedState(final Parcel in) {
|
||||
super(in);
|
||||
mExpanded = in.readInt() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel out, final int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt(mExpanded ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class LayoutParams extends ViewGroup.LayoutParams {
|
||||
|
||||
private final ObjectAnimator mExpandDir = new ObjectAnimator();
|
||||
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
|
||||
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
|
||||
private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
|
||||
private boolean animationsSetToPlay;
|
||||
|
||||
LayoutParams(final ViewGroup.LayoutParams source) {
|
||||
super(source);
|
||||
|
||||
mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
|
||||
mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
|
||||
mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
|
||||
mCollapseAlpha.setProperty(View.ALPHA);
|
||||
mCollapseAlpha.setFloatValues(1f, 0f);
|
||||
|
||||
mExpandAlpha.setProperty(View.ALPHA);
|
||||
mExpandAlpha.setFloatValues(0f, 1f);
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_Y);
|
||||
mExpandDir.setProperty(View.TRANSLATION_Y);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_X);
|
||||
mExpandDir.setProperty(View.TRANSLATION_X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setAnimationsTarget(final View view) {
|
||||
mCollapseAlpha.setTarget(view);
|
||||
mCollapseDir.setTarget(view);
|
||||
mExpandAlpha.setTarget(view);
|
||||
mExpandDir.setTarget(view);
|
||||
|
||||
// Now that the animations have targets, set them to be played
|
||||
if (!animationsSetToPlay) {
|
||||
addLayerTypeListener(mExpandDir, view);
|
||||
addLayerTypeListener(mCollapseDir, view);
|
||||
|
||||
mCollapseAnimation.play(mCollapseAlpha);
|
||||
mCollapseAnimation.play(mCollapseDir);
|
||||
mExpandAnimation.play(mExpandAlpha);
|
||||
mExpandAnimation.play(mExpandDir);
|
||||
animationsSetToPlay = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void addLayerTypeListener(final Animator animator, final View view) {
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_NONE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
|
||||
private static final float SCALE_FACTOR = 1.5f;
|
||||
private final FloatingActionsMenu menu;
|
||||
|
||||
public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
|
||||
this.menu = menu;
|
||||
}
|
||||
|
||||
private static float bound(final float min, final float proposal, final float max) {
|
||||
return Math.min(max, Math.max(min, proposal));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class LabeledFloatingActionButton extends FloatingActionButton {
|
||||
|
||||
@Nullable private final String title;
|
||||
|
||||
public LabeledFloatingActionButton(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
|
||||
title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
|
||||
attr.recycle();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
TextView getLabelView() {
|
||||
return (TextView) getTag(R.id.fab_label);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(final int visibility) {
|
||||
final TextView label = getLabelView();
|
||||
if (label != null) {
|
||||
label.setVisibility(visibility);
|
||||
}
|
||||
|
||||
super.setVisibility(visibility);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class TouchDelegateGroup extends TouchDelegate {
|
||||
private static final Rect USELESS_HACKY_RECT = new Rect();
|
||||
private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
|
||||
@Nullable private TouchDelegate mCurrentTouchDelegate;
|
||||
private boolean mEnabled;
|
||||
|
||||
public TouchDelegateGroup(final View uselessHackyView) {
|
||||
super(USELESS_HACKY_RECT, uselessHackyView);
|
||||
}
|
||||
|
||||
public void addTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.add(touchDelegate);
|
||||
}
|
||||
|
||||
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.remove(touchDelegate);
|
||||
if (mCurrentTouchDelegate == touchDelegate) {
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearTouchDelegates() {
|
||||
mTouchDelegates.clear();
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
if (!mEnabled)
|
||||
return false;
|
||||
|
||||
TouchDelegate delegate = null;
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
for (final TouchDelegate touchDelegate : mTouchDelegates) {
|
||||
if (touchDelegate.onTouchEvent(event)) {
|
||||
mCurrentTouchDelegate = touchDelegate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
mCurrentTouchDelegate = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return delegate != null && delegate.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setEnabled(final boolean enabled) {
|
||||
mEnabled = enabled;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* The set of valid attributes for an interface or peer in a WireGuard configuration file.
|
||||
*/
|
||||
|
||||
public enum Attribute {
|
||||
ADDRESS("Address"),
|
||||
ALLOWED_IPS("AllowedIPs"),
|
||||
DNS("DNS"),
|
||||
EXCLUDED_APPLICATIONS("ExcludedApplications"),
|
||||
ENDPOINT("Endpoint"),
|
||||
LISTEN_PORT("ListenPort"),
|
||||
MTU("MTU"),
|
||||
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
|
||||
PRESHARED_KEY("PresharedKey"),
|
||||
PRIVATE_KEY("PrivateKey"),
|
||||
PUBLIC_KEY("PublicKey");
|
||||
|
||||
private static final String[] EMPTY_LIST = new String[0];
|
||||
private static final Map<String, Attribute> KEY_MAP;
|
||||
private static final Pattern LIST_SEPARATOR_PATTERN = Pattern.compile("\\s*,\\s*");
|
||||
private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\\s|=");
|
||||
|
||||
static {
|
||||
KEY_MAP = new HashMap<>(Attribute.values().length);
|
||||
for (final Attribute key : Attribute.values()) {
|
||||
KEY_MAP.put(key.token.toLowerCase(), key);
|
||||
}
|
||||
}
|
||||
|
||||
private final Pattern pattern;
|
||||
private final String token;
|
||||
|
||||
Attribute(final String token) {
|
||||
pattern = Pattern.compile(token + "\\s*=\\s*(\\S.*)");
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public static <T> String iterableToString(final Iterable<T> iterable) {
|
||||
return TextUtils.join(", ", iterable);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Attribute match(final CharSequence line) {
|
||||
return KEY_MAP.get(SEPARATOR_PATTERN.split(line)[0].toLowerCase());
|
||||
}
|
||||
|
||||
public static String[] stringToList(@Nullable final String string) {
|
||||
if (TextUtils.isEmpty(string))
|
||||
return EMPTY_LIST;
|
||||
return LIST_SEPARATOR_PATTERN.split(string.trim());
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String composeWith(@Nullable final Object value) {
|
||||
return String.format("%s = %s%n", token, value);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String composeWith(final int value) {
|
||||
return String.format("%s = %d%n", token, value);
|
||||
}
|
||||
|
||||
public <T> String composeWith(final Iterable<T> value) {
|
||||
return String.format("%s = %s%n", token, iterableToString(value));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String parse(final CharSequence line) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
return matcher.matches() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String[] parseList(final CharSequence line) {
|
||||
final Matcher matcher = pattern.matcher(line);
|
||||
return matcher.matches() ? stringToList(matcher.group(1)) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.content.Context;
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.databinding.ObservableArrayList;
|
||||
import android.databinding.ObservableList;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.databinding.library.baseAdapters.BR;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a wg-quick configuration file, its name, and its connection state.
|
||||
*/
|
||||
|
||||
public class Config {
|
||||
private final Interface interfaceSection = new Interface();
|
||||
private List<Peer> peers = new ArrayList<>();
|
||||
|
||||
public static Config from(final String string) throws IOException {
|
||||
return from(new BufferedReader(new StringReader(string)));
|
||||
}
|
||||
|
||||
public static Config from(final InputStream stream) throws IOException {
|
||||
return from(new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
public static Config from(final BufferedReader reader) throws IOException {
|
||||
final Config config = new Config();
|
||||
final Context context = Application.get();
|
||||
Peer currentPeer = null;
|
||||
String line;
|
||||
boolean inInterfaceSection = false;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final int commentIndex = line.indexOf('#');
|
||||
if (commentIndex != -1)
|
||||
line = line.substring(0, commentIndex);
|
||||
line = line.trim();
|
||||
if (line.isEmpty())
|
||||
continue;
|
||||
if ("[Interface]".toLowerCase().equals(line.toLowerCase())) {
|
||||
currentPeer = null;
|
||||
inInterfaceSection = true;
|
||||
} else if ("[Peer]".toLowerCase().equals(line.toLowerCase())) {
|
||||
currentPeer = new Peer();
|
||||
config.peers.add(currentPeer);
|
||||
inInterfaceSection = false;
|
||||
} else if (inInterfaceSection) {
|
||||
config.interfaceSection.parse(line);
|
||||
} else if (currentPeer != null) {
|
||||
currentPeer.parse(line);
|
||||
} else {
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_config_line, line));
|
||||
}
|
||||
}
|
||||
if (!inInterfaceSection && currentPeer == null) {
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_no_config_information));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public Interface getInterface() {
|
||||
return interfaceSection;
|
||||
}
|
||||
|
||||
public List<Peer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder().append(interfaceSection);
|
||||
for (final Peer peer : peers)
|
||||
sb.append('\n').append(peer);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
@Nullable private String name;
|
||||
private final Interface.Observable observableInterface;
|
||||
private final ObservableList<Peer.Observable> observablePeers;
|
||||
|
||||
public Observable(@Nullable final Config parent, @Nullable final String name) {
|
||||
this.name = name;
|
||||
|
||||
observableInterface = new Interface.Observable(parent == null ? null : parent.interfaceSection);
|
||||
observablePeers = new ObservableArrayList<>();
|
||||
if (parent != null) {
|
||||
for (final Peer peer : parent.getPeers())
|
||||
observablePeers.add(new Peer.Observable(peer));
|
||||
}
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
name = in.readString();
|
||||
observableInterface = in.readParcelable(Interface.Observable.class.getClassLoader());
|
||||
observablePeers = new ObservableArrayList<>();
|
||||
in.readTypedList(observablePeers, Peer.Observable.CREATOR);
|
||||
}
|
||||
|
||||
public void commitData(final Config parent) {
|
||||
observableInterface.commitData(parent.interfaceSection);
|
||||
final List<Peer> newPeers = new ArrayList<>(observablePeers.size());
|
||||
for (final Peer.Observable observablePeer : observablePeers) {
|
||||
final Peer peer = new Peer();
|
||||
observablePeer.commitData(peer);
|
||||
newPeers.add(peer);
|
||||
}
|
||||
parent.peers = newPeers;
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public Interface.Observable getInterfaceSection() {
|
||||
return observableInterface;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public String getName() {
|
||||
return name == null ? "" : name;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public ObservableList<Peer.Observable> getPeers() {
|
||||
return observablePeers;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
notifyPropertyChanged(BR.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(observableInterface, flags);
|
||||
dest.writeTypedList(observablePeers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public final class InetAddresses {
|
||||
private static final Method PARSER_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
// This method is only present on Android.
|
||||
PARSER_METHOD = InetAddress.class.getMethod("parseNumericAddress", String.class);
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private InetAddresses() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
public static InetAddress parse(@Nullable final String address) {
|
||||
if (address == null || address.isEmpty())
|
||||
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_inetaddress));
|
||||
try {
|
||||
return (InetAddress) PARSER_METHOD.invoke(null, address);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() == null ? e : e.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class InetEndpoint {
|
||||
private final String host;
|
||||
private final int port;
|
||||
@Nullable private InetAddress resolvedHost;
|
||||
|
||||
public InetEndpoint(@Nullable final String endpoint) {
|
||||
if (endpoint.indexOf('/') != -1 || endpoint.indexOf('?') != -1 || endpoint.indexOf('#') != -1)
|
||||
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_forbidden_endpoint_chars));
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI("wg://" + endpoint);
|
||||
} catch (final URISyntaxException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
host = uri.getHost();
|
||||
port = uri.getPort();
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String getResolvedEndpoint() throws UnknownHostException {
|
||||
if (resolvedHost == null) {
|
||||
final InetAddress[] candidates = InetAddress.getAllByName(host);
|
||||
if (candidates.length == 0)
|
||||
throw new UnknownHostException(host);
|
||||
for (final InetAddress addr : candidates) {
|
||||
if (addr instanceof Inet4Address) {
|
||||
resolvedHost = addr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resolvedHost == null)
|
||||
resolvedHost = candidates[0];
|
||||
}
|
||||
return String.format(resolvedHost instanceof Inet6Address ?
|
||||
"[%s]:%d" : "%s:%d", resolvedHost.getHostAddress(), port);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public String getEndpoint() {
|
||||
return String.format(host.contains(":") && !host.contains("[") ?
|
||||
"[%s]:%d" : "%s:%d", host, port);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
public class InetNetwork {
|
||||
private final InetAddress address;
|
||||
private final int mask;
|
||||
|
||||
public InetNetwork(final String input) {
|
||||
final int slash = input.lastIndexOf('/');
|
||||
final int rawMask;
|
||||
final String rawAddress;
|
||||
if (slash >= 0) {
|
||||
rawMask = Integer.parseInt(input.substring(slash + 1), 10);
|
||||
rawAddress = input.substring(0, slash);
|
||||
} else {
|
||||
rawMask = -1;
|
||||
rawAddress = input;
|
||||
}
|
||||
address = InetAddresses.parse(rawAddress);
|
||||
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
|
||||
mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof InetNetwork))
|
||||
return false;
|
||||
final InetNetwork other = (InetNetwork) obj;
|
||||
return Objects.equals(address, other.address) && mask == other.mask;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getMask() {
|
||||
return mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode() ^ mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return address.getHostAddress() + '/' + mask;
|
||||
}
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.content.Context;
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.BR;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.crypto.Keypair;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block).
|
||||
*/
|
||||
|
||||
public class Interface {
|
||||
private final List<InetNetwork> addressList;
|
||||
private final List<InetAddress> dnsList;
|
||||
private final List<String> excludedApplications;
|
||||
@Nullable private Keypair keypair;
|
||||
private int listenPort;
|
||||
private int mtu;
|
||||
private final Context context = Application.get();
|
||||
|
||||
public Interface() {
|
||||
addressList = new ArrayList<>();
|
||||
dnsList = new ArrayList<>();
|
||||
excludedApplications = new ArrayList<>();
|
||||
}
|
||||
|
||||
private void addAddresses(@Nullable final String[] addresses) {
|
||||
if (addresses != null && addresses.length > 0) {
|
||||
for (final String addr : addresses) {
|
||||
if (addr.isEmpty())
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_empty_interface_address));
|
||||
addressList.add(new InetNetwork(addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addDnses(@Nullable final String[] dnses) {
|
||||
if (dnses != null && dnses.length > 0) {
|
||||
for (final String dns : dnses) {
|
||||
dnsList.add(InetAddresses.parse(dns));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addExcludedApplications(@Nullable final String[] applications) {
|
||||
if (applications != null && applications.length > 0) {
|
||||
excludedApplications.addAll(Arrays.asList(applications));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getAddressString() {
|
||||
if (addressList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(addressList);
|
||||
}
|
||||
|
||||
public InetNetwork[] getAddresses() {
|
||||
return addressList.toArray(new InetNetwork[addressList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getDnsString() {
|
||||
if (dnsList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(getDnsStrings());
|
||||
}
|
||||
|
||||
private List<String> getDnsStrings() {
|
||||
final List<String> strings = new ArrayList<>();
|
||||
for (final InetAddress addr : dnsList)
|
||||
strings.add(addr.getHostAddress());
|
||||
return strings;
|
||||
}
|
||||
|
||||
public InetAddress[] getDnses() {
|
||||
return dnsList.toArray(new InetAddress[dnsList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getExcludedApplicationsString() {
|
||||
if (excludedApplications.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(excludedApplications);
|
||||
}
|
||||
|
||||
public String[] getExcludedApplications() {
|
||||
return excludedApplications.toArray(new String[excludedApplications.size()]);
|
||||
}
|
||||
|
||||
public int getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getListenPortString() {
|
||||
if (listenPort == 0)
|
||||
return null;
|
||||
return Integer.valueOf(listenPort).toString();
|
||||
}
|
||||
|
||||
public int getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getMtuString() {
|
||||
if (mtu == 0)
|
||||
return null;
|
||||
return Integer.toString(mtu);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPrivateKey() {
|
||||
if (keypair == null)
|
||||
return null;
|
||||
return keypair.getPrivateKey();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPublicKey() {
|
||||
if (keypair == null)
|
||||
return null;
|
||||
return keypair.getPublicKey();
|
||||
}
|
||||
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException(String.format(context.getString(R.string.tunnel_error_interface_parse_failed), line));
|
||||
switch (key) {
|
||||
case ADDRESS:
|
||||
addAddresses(key.parseList(line));
|
||||
break;
|
||||
case DNS:
|
||||
addDnses(key.parseList(line));
|
||||
break;
|
||||
case EXCLUDED_APPLICATIONS:
|
||||
addExcludedApplications(key.parseList(line));
|
||||
break;
|
||||
case LISTEN_PORT:
|
||||
setListenPortString(key.parse(line));
|
||||
break;
|
||||
case MTU:
|
||||
setMtuString(key.parse(line));
|
||||
break;
|
||||
case PRIVATE_KEY:
|
||||
setPrivateKey(key.parse(line));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAddressString(@Nullable final String addressString) {
|
||||
addressList.clear();
|
||||
addAddresses(Attribute.stringToList(addressString));
|
||||
}
|
||||
|
||||
private void setDnsString(@Nullable final String dnsString) {
|
||||
dnsList.clear();
|
||||
addDnses(Attribute.stringToList(dnsString));
|
||||
}
|
||||
|
||||
private void setExcludedApplicationsString(@Nullable final String applicationsString) {
|
||||
excludedApplications.clear();
|
||||
addExcludedApplications(Attribute.stringToList(applicationsString));
|
||||
}
|
||||
|
||||
private void setListenPort(final int listenPort) {
|
||||
this.listenPort = listenPort;
|
||||
}
|
||||
|
||||
private void setListenPortString(@Nullable final String port) {
|
||||
if (port != null && !port.isEmpty())
|
||||
setListenPort(Integer.parseInt(port, 10));
|
||||
else
|
||||
setListenPort(0);
|
||||
}
|
||||
|
||||
private void setMtu(final int mtu) {
|
||||
this.mtu = mtu;
|
||||
}
|
||||
|
||||
private void setMtuString(@Nullable final String mtu) {
|
||||
if (mtu != null && !mtu.isEmpty())
|
||||
setMtu(Integer.parseInt(mtu, 10));
|
||||
else
|
||||
setMtu(0);
|
||||
}
|
||||
|
||||
private void setPrivateKey(@Nullable String privateKey) {
|
||||
if (privateKey != null && privateKey.isEmpty())
|
||||
privateKey = null;
|
||||
keypair = privateKey == null ? null : new Keypair(privateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder().append("[Interface]\n");
|
||||
if (!addressList.isEmpty())
|
||||
sb.append(Attribute.ADDRESS.composeWith(addressList));
|
||||
if (!dnsList.isEmpty())
|
||||
sb.append(Attribute.DNS.composeWith(getDnsStrings()));
|
||||
if (!excludedApplications.isEmpty())
|
||||
sb.append(Attribute.EXCLUDED_APPLICATIONS.composeWith(excludedApplications));
|
||||
if (listenPort != 0)
|
||||
sb.append(Attribute.LISTEN_PORT.composeWith(listenPort));
|
||||
if (mtu != 0)
|
||||
sb.append(Attribute.MTU.composeWith(mtu));
|
||||
if (keypair != null)
|
||||
sb.append(Attribute.PRIVATE_KEY.composeWith(keypair.getPrivateKey()));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
@Nullable private String addresses;
|
||||
@Nullable private String dnses;
|
||||
@Nullable private String excludedApplications;
|
||||
@Nullable private String listenPort;
|
||||
@Nullable private String mtu;
|
||||
@Nullable private String privateKey;
|
||||
@Nullable private String publicKey;
|
||||
|
||||
public Observable(@Nullable final Interface parent) {
|
||||
if (parent != null)
|
||||
loadData(parent);
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
addresses = in.readString();
|
||||
dnses = in.readString();
|
||||
publicKey = in.readString();
|
||||
privateKey = in.readString();
|
||||
listenPort = in.readString();
|
||||
mtu = in.readString();
|
||||
excludedApplications = in.readString();
|
||||
}
|
||||
|
||||
public void commitData(final Interface parent) {
|
||||
parent.setAddressString(addresses);
|
||||
parent.setDnsString(dnses);
|
||||
parent.setExcludedApplicationsString(excludedApplications);
|
||||
parent.setPrivateKey(privateKey);
|
||||
parent.setListenPortString(listenPort);
|
||||
parent.setMtuString(mtu);
|
||||
loadData(parent);
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void generateKeypair() {
|
||||
final Keypair keypair = new Keypair();
|
||||
privateKey = keypair.getPrivateKey();
|
||||
publicKey = keypair.getPublicKey();
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getAddresses() {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getDnses() {
|
||||
return dnses;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getExcludedApplications() {
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public int getExcludedApplicationsCount() {
|
||||
return Attribute.stringToList(excludedApplications).length;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Bindable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private void loadData(final Interface parent) {
|
||||
addresses = parent.getAddressString();
|
||||
dnses = parent.getDnsString();
|
||||
excludedApplications = parent.getExcludedApplicationsString();
|
||||
publicKey = parent.getPublicKey();
|
||||
privateKey = parent.getPrivateKey();
|
||||
listenPort = parent.getListenPortString();
|
||||
mtu = parent.getMtuString();
|
||||
}
|
||||
|
||||
public void setAddresses(final String addresses) {
|
||||
this.addresses = addresses;
|
||||
notifyPropertyChanged(BR.addresses);
|
||||
}
|
||||
|
||||
public void setDnses(final String dnses) {
|
||||
this.dnses = dnses;
|
||||
notifyPropertyChanged(BR.dnses);
|
||||
}
|
||||
|
||||
public void setExcludedApplications(final String excludedApplications) {
|
||||
this.excludedApplications = excludedApplications;
|
||||
notifyPropertyChanged(BR.excludedApplications);
|
||||
notifyPropertyChanged(BR.excludedApplicationsCount);
|
||||
}
|
||||
|
||||
public void setListenPort(final String listenPort) {
|
||||
this.listenPort = listenPort;
|
||||
notifyPropertyChanged(BR.listenPort);
|
||||
}
|
||||
|
||||
public void setMtu(final String mtu) {
|
||||
this.mtu = mtu;
|
||||
notifyPropertyChanged(BR.mtu);
|
||||
}
|
||||
|
||||
public void setPrivateKey(final String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
|
||||
try {
|
||||
publicKey = new Keypair(privateKey).getPublicKey();
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
publicKey = "";
|
||||
}
|
||||
|
||||
notifyPropertyChanged(BR.privateKey);
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(addresses);
|
||||
dest.writeString(dnses);
|
||||
dest.writeString(publicKey);
|
||||
dest.writeString(privateKey);
|
||||
dest.writeString(listenPort);
|
||||
dest.writeString(mtu);
|
||||
dest.writeString(excludedApplications);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.databinding.BaseObservable;
|
||||
import android.databinding.Bindable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.databinding.library.baseAdapters.BR;
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.crypto.KeyEncoding;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import java9.lang.Iterables;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block).
|
||||
*/
|
||||
|
||||
public class Peer {
|
||||
private final List<InetNetwork> allowedIPsList;
|
||||
@Nullable private InetEndpoint endpoint;
|
||||
private int persistentKeepalive;
|
||||
@Nullable private String preSharedKey;
|
||||
@Nullable private String publicKey;
|
||||
private final Context context = Application.get();
|
||||
|
||||
public Peer() {
|
||||
allowedIPsList = new ArrayList<>();
|
||||
}
|
||||
|
||||
private void addAllowedIPs(@Nullable final String[] allowedIPs) {
|
||||
if (allowedIPs != null && allowedIPs.length > 0) {
|
||||
for (final String allowedIP : allowedIPs) {
|
||||
allowedIPsList.add(new InetNetwork(allowedIP));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InetNetwork[] getAllowedIPs() {
|
||||
return allowedIPsList.toArray(new InetNetwork[allowedIPsList.size()]);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getAllowedIPsString() {
|
||||
if (allowedIPsList.isEmpty())
|
||||
return null;
|
||||
return Attribute.iterableToString(allowedIPsList);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InetEndpoint getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getEndpointString() {
|
||||
if (endpoint == null)
|
||||
return null;
|
||||
return endpoint.getEndpoint();
|
||||
}
|
||||
|
||||
public int getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getPersistentKeepaliveString() {
|
||||
if (persistentKeepalive == 0)
|
||||
return null;
|
||||
return Integer.valueOf(persistentKeepalive).toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getResolvedEndpointString() throws UnknownHostException {
|
||||
if (endpoint == null)
|
||||
throw new UnknownHostException("{empty}");
|
||||
return endpoint.getResolvedEndpoint();
|
||||
}
|
||||
|
||||
public void parse(final String line) {
|
||||
final Attribute key = Attribute.match(line);
|
||||
if (key == null)
|
||||
throw new IllegalArgumentException(context.getString(R.string.tunnel_error_interface_parse_failed, line));
|
||||
switch (key) {
|
||||
case ALLOWED_IPS:
|
||||
addAllowedIPs(key.parseList(line));
|
||||
break;
|
||||
case ENDPOINT:
|
||||
setEndpointString(key.parse(line));
|
||||
break;
|
||||
case PERSISTENT_KEEPALIVE:
|
||||
setPersistentKeepaliveString(key.parse(line));
|
||||
break;
|
||||
case PRESHARED_KEY:
|
||||
setPreSharedKey(key.parse(line));
|
||||
break;
|
||||
case PUBLIC_KEY:
|
||||
setPublicKey(key.parse(line));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void setAllowedIPsString(@Nullable final String allowedIPsString) {
|
||||
allowedIPsList.clear();
|
||||
addAllowedIPs(Attribute.stringToList(allowedIPsString));
|
||||
}
|
||||
|
||||
private void setEndpoint(@Nullable final InetEndpoint endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
private void setEndpointString(@Nullable final String endpoint) {
|
||||
if (endpoint != null && !endpoint.isEmpty())
|
||||
setEndpoint(new InetEndpoint(endpoint));
|
||||
else
|
||||
setEndpoint(null);
|
||||
}
|
||||
|
||||
private void setPersistentKeepalive(final int persistentKeepalive) {
|
||||
this.persistentKeepalive = persistentKeepalive;
|
||||
}
|
||||
|
||||
private void setPersistentKeepaliveString(@Nullable final String persistentKeepalive) {
|
||||
if (persistentKeepalive != null && !persistentKeepalive.isEmpty())
|
||||
setPersistentKeepalive(Integer.parseInt(persistentKeepalive, 10));
|
||||
else
|
||||
setPersistentKeepalive(0);
|
||||
}
|
||||
|
||||
private void setPreSharedKey(@Nullable String preSharedKey) {
|
||||
if (preSharedKey != null && preSharedKey.isEmpty())
|
||||
preSharedKey = null;
|
||||
if (preSharedKey != null)
|
||||
KeyEncoding.keyFromBase64(preSharedKey);
|
||||
this.preSharedKey = preSharedKey;
|
||||
}
|
||||
|
||||
private void setPublicKey(@Nullable String publicKey) {
|
||||
if (publicKey != null && publicKey.isEmpty())
|
||||
publicKey = null;
|
||||
if (publicKey != null)
|
||||
KeyEncoding.keyFromBase64(publicKey);
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder().append("[Peer]\n");
|
||||
if (!allowedIPsList.isEmpty())
|
||||
sb.append(Attribute.ALLOWED_IPS.composeWith(allowedIPsList));
|
||||
if (endpoint != null)
|
||||
sb.append(Attribute.ENDPOINT.composeWith(getEndpointString()));
|
||||
if (persistentKeepalive != 0)
|
||||
sb.append(Attribute.PERSISTENT_KEEPALIVE.composeWith(persistentKeepalive));
|
||||
if (preSharedKey != null)
|
||||
sb.append(Attribute.PRESHARED_KEY.composeWith(preSharedKey));
|
||||
if (publicKey != null)
|
||||
sb.append(Attribute.PUBLIC_KEY.composeWith(publicKey));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Observable extends BaseObservable implements Parcelable {
|
||||
public static final Creator<Observable> CREATOR = new Creator<Observable>() {
|
||||
@Override
|
||||
public Observable createFromParcel(final Parcel in) {
|
||||
return new Observable(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Observable[] newArray(final int size) {
|
||||
return new Observable[size];
|
||||
}
|
||||
};
|
||||
@Nullable private String allowedIPs;
|
||||
@Nullable private String endpoint;
|
||||
@Nullable private String persistentKeepalive;
|
||||
@Nullable private String preSharedKey;
|
||||
@Nullable private String publicKey;
|
||||
private final List<String> interfaceDNSRoutes = new ArrayList<>();
|
||||
private int numSiblings;
|
||||
|
||||
public Observable(final Peer parent) {
|
||||
loadData(parent);
|
||||
}
|
||||
|
||||
private Observable(final Parcel in) {
|
||||
allowedIPs = in.readString();
|
||||
endpoint = in.readString();
|
||||
persistentKeepalive = in.readString();
|
||||
preSharedKey = in.readString();
|
||||
publicKey = in.readString();
|
||||
numSiblings = in.readInt();
|
||||
in.readStringList(interfaceDNSRoutes);
|
||||
}
|
||||
|
||||
public static Observable newInstance() {
|
||||
return new Observable(new Peer());
|
||||
}
|
||||
|
||||
public void commitData(final Peer parent) {
|
||||
parent.setAllowedIPsString(allowedIPs);
|
||||
parent.setEndpointString(endpoint);
|
||||
parent.setPersistentKeepaliveString(persistentKeepalive);
|
||||
parent.setPreSharedKey(preSharedKey);
|
||||
parent.setPublicKey(publicKey);
|
||||
if (parent.getPublicKey() == null)
|
||||
throw new IllegalArgumentException(Application.get().getString(R.string.tunnel_error_empty_peer_public_key));
|
||||
loadData(parent);
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static final String DEFAULT_ROUTE_V4 = "0.0.0.0/0";
|
||||
private static final List<String> DEFAULT_ROUTE_MOD_RFC1918_V4 = Arrays.asList("0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4");
|
||||
|
||||
public void toggleExcludePrivateIPs() {
|
||||
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
|
||||
final boolean hasDefaultRoute = ips.contains(DEFAULT_ROUTE_V4);
|
||||
final boolean hasDefaultRouteModRFC1918 = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
if ((!hasDefaultRoute && !hasDefaultRouteModRFC1918) || numSiblings > 0)
|
||||
return;
|
||||
Iterables.removeIf(ips, ip -> !ip.contains(":"));
|
||||
if (hasDefaultRoute) {
|
||||
ips.addAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
ips.addAll(interfaceDNSRoutes);
|
||||
} else if (hasDefaultRouteModRFC1918)
|
||||
ips.add(DEFAULT_ROUTE_V4);
|
||||
setAllowedIPs(Attribute.iterableToString(ips));
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean getCanToggleExcludePrivateIPs() {
|
||||
final Collection<String> ips = Arrays.asList(Attribute.stringToList(allowedIPs));
|
||||
return numSiblings == 0 && (ips.contains(DEFAULT_ROUTE_V4) || ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4));
|
||||
}
|
||||
|
||||
@Bindable
|
||||
public boolean getIsExcludePrivateIPsOn() {
|
||||
return numSiblings == 0 && Arrays.asList(Attribute.stringToList(allowedIPs)).containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getAllowedIPs() {
|
||||
return allowedIPs;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
@Bindable @Nullable
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private void loadData(final Peer parent) {
|
||||
allowedIPs = parent.getAllowedIPsString();
|
||||
endpoint = parent.getEndpointString();
|
||||
persistentKeepalive = parent.getPersistentKeepaliveString();
|
||||
preSharedKey = parent.getPreSharedKey();
|
||||
publicKey = parent.getPublicKey();
|
||||
}
|
||||
|
||||
public void setAllowedIPs(final String allowedIPs) {
|
||||
this.allowedIPs = allowedIPs;
|
||||
notifyPropertyChanged(BR.allowedIPs);
|
||||
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
|
||||
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
|
||||
}
|
||||
|
||||
public void setEndpoint(final String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
notifyPropertyChanged(BR.endpoint);
|
||||
}
|
||||
|
||||
public void setPersistentKeepalive(final String persistentKeepalive) {
|
||||
this.persistentKeepalive = persistentKeepalive;
|
||||
notifyPropertyChanged(BR.persistentKeepalive);
|
||||
}
|
||||
|
||||
public void setPreSharedKey(final String preSharedKey) {
|
||||
this.preSharedKey = preSharedKey;
|
||||
notifyPropertyChanged(BR.preSharedKey);
|
||||
}
|
||||
|
||||
public void setPublicKey(final String publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
notifyPropertyChanged(BR.publicKey);
|
||||
}
|
||||
|
||||
public void setInterfaceDNSRoutes(@Nullable final String dnsServers) {
|
||||
final Collection<String> ips = new HashSet<>(Arrays.asList(Attribute.stringToList(allowedIPs)));
|
||||
final boolean modifyAllowedIPs = ips.containsAll(DEFAULT_ROUTE_MOD_RFC1918_V4);
|
||||
|
||||
ips.removeAll(interfaceDNSRoutes);
|
||||
interfaceDNSRoutes.clear();
|
||||
for (final String dnsServer : Attribute.stringToList(dnsServers)) {
|
||||
if (!dnsServer.contains(":"))
|
||||
interfaceDNSRoutes.add(dnsServer + "/32");
|
||||
}
|
||||
ips.addAll(interfaceDNSRoutes);
|
||||
if (modifyAllowedIPs)
|
||||
setAllowedIPs(Attribute.iterableToString(ips));
|
||||
}
|
||||
|
||||
public void setNumSiblings(final int num) {
|
||||
numSiblings = num;
|
||||
notifyPropertyChanged(BR.canToggleExcludePrivateIPs);
|
||||
notifyPropertyChanged(BR.isExcludePrivateIPsOn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(allowedIPs);
|
||||
dest.writeString(endpoint);
|
||||
dest.writeString(persistentKeepalive);
|
||||
dest.writeString(preSharedKey);
|
||||
dest.writeString(publicKey);
|
||||
dest.writeInt(numSiblings);
|
||||
dest.writeStringList(interfaceDNSRoutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
|
||||
/**
|
||||
* This is a specialized constant-time base64 and hex implementation that resists side-channel attacks.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("MagicNumber")
|
||||
public final class KeyEncoding {
|
||||
public static final int KEY_LENGTH = 32;
|
||||
public static final int KEY_LENGTH_BASE64 = 44;
|
||||
public static final int KEY_LENGTH_HEX = 64;
|
||||
private static final String KEY_LENGTH_BASE64_EXCEPTION_MESSAGE =
|
||||
Application.get().getString(R.string.key_length_base64_exception_message);
|
||||
private static final String KEY_LENGTH_EXCEPTION_MESSAGE =
|
||||
Application.get().getString(R.string.key_length_exception_message);
|
||||
private static final String KEY_LENGTH_HEX_EXCEPTION_MESSAGE =
|
||||
Application.get().getString(R.string.key_length_hex_exception_message);
|
||||
|
||||
private KeyEncoding() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
|
||||
private static int decodeBase64(final char[] src, final int srcOffset) {
|
||||
int val = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
final char c = src[i + srcOffset];
|
||||
val |= (-1
|
||||
+ ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
|
||||
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
|
||||
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
|
||||
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
|
||||
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
|
||||
) << (18 - 6 * i);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
private static void encodeBase64(final byte[] src, final int srcOffset,
|
||||
final char[] dest, final int destOffset) {
|
||||
final byte[] input = {
|
||||
(byte) ((src[srcOffset] >>> 2) & 63),
|
||||
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
|
||||
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
|
||||
(byte) ((src[2 + srcOffset]) & 63),
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
dest[i + destOffset] = (char) (input[i] + 'A'
|
||||
+ (((25 - input[i]) >>> 8) & 6)
|
||||
- (((51 - input[i]) >>> 8) & 75)
|
||||
- (((61 - input[i]) >>> 8) & 15)
|
||||
+ (((62 - input[i]) >>> 8) & 3));
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] keyFromBase64(final String str) {
|
||||
final char[] input = str.toCharArray();
|
||||
final byte[] key = new byte[KEY_LENGTH];
|
||||
if (input.length != KEY_LENGTH_BASE64 || input[KEY_LENGTH_BASE64 - 1] != '=')
|
||||
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
|
||||
int i;
|
||||
int ret = 0;
|
||||
for (i = 0; i < KEY_LENGTH / 3; ++i) {
|
||||
final int val = decodeBase64(input, i * 4);
|
||||
ret |= val >>> 31;
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
key[i * 3 + 2] = (byte) (val & 0xff);
|
||||
}
|
||||
final char[] endSegment = {
|
||||
input[i * 4],
|
||||
input[i * 4 + 1],
|
||||
input[i * 4 + 2],
|
||||
'A',
|
||||
};
|
||||
final int val = decodeBase64(endSegment, 0);
|
||||
ret |= (val >>> 31) | (val & 0xff);
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
|
||||
if (ret != 0)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_BASE64_EXCEPTION_MESSAGE);
|
||||
return key;
|
||||
}
|
||||
|
||||
public static byte[] keyFromHex(final String str) {
|
||||
final char[] input = str.toCharArray();
|
||||
final byte[] key = new byte[KEY_LENGTH];
|
||||
if (input.length != KEY_LENGTH_HEX)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
|
||||
int ret = 0;
|
||||
|
||||
for (int i = 0; i < KEY_LENGTH_HEX; i += 2) {
|
||||
int c;
|
||||
int cNum;
|
||||
int cNum0;
|
||||
int cAlpha;
|
||||
int cAlpha0;
|
||||
int cVal;
|
||||
final int cAcc;
|
||||
|
||||
c = input[i];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
cAcc = cVal * 16;
|
||||
|
||||
c = input[i + 1];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
key[i / 2] = (byte) (cAcc | cVal);
|
||||
}
|
||||
if (ret != 0)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_HEX_EXCEPTION_MESSAGE);
|
||||
return key;
|
||||
}
|
||||
|
||||
public static String keyToBase64(final byte[] key) {
|
||||
final char[] output = new char[KEY_LENGTH_BASE64];
|
||||
if (key.length != KEY_LENGTH)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
|
||||
int i;
|
||||
for (i = 0; i < KEY_LENGTH / 3; ++i)
|
||||
encodeBase64(key, i * 3, output, i * 4);
|
||||
final byte[] endSegment = {
|
||||
key[i * 3],
|
||||
key[i * 3 + 1],
|
||||
0,
|
||||
};
|
||||
encodeBase64(endSegment, 0, output, i * 4);
|
||||
output[KEY_LENGTH_BASE64 - 1] = '=';
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
public static String keyToHex(final byte[] key) {
|
||||
final char[] output = new char[KEY_LENGTH_HEX];
|
||||
if (key.length != KEY_LENGTH)
|
||||
throw new IllegalArgumentException(KEY_LENGTH_EXCEPTION_MESSAGE);
|
||||
for (int i = 0; i < KEY_LENGTH; ++i) {
|
||||
output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
|
||||
+ ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
|
||||
output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
|
||||
+ ((((key[i] & 0xf) - 10) >> 8) & ~38));
|
||||
}
|
||||
return new String(output);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* Represents a Curve25519 keypair as used by WireGuard.
|
||||
*/
|
||||
|
||||
public class Keypair {
|
||||
private final byte[] privateKey;
|
||||
private final byte[] publicKey;
|
||||
|
||||
public Keypair() {
|
||||
this(generatePrivateKey());
|
||||
}
|
||||
|
||||
private Keypair(final byte[] privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
publicKey = generatePublicKey(privateKey);
|
||||
}
|
||||
|
||||
public Keypair(final String privateKey) {
|
||||
this(KeyEncoding.keyFromBase64(privateKey));
|
||||
}
|
||||
|
||||
@SuppressWarnings("MagicNumber")
|
||||
private static byte[] generatePrivateKey() {
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
final byte[] privateKey = new byte[KeyEncoding.KEY_LENGTH];
|
||||
secureRandom.nextBytes(privateKey);
|
||||
privateKey[0] &= 248;
|
||||
privateKey[31] &= 127;
|
||||
privateKey[31] |= 64;
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
private static byte[] generatePublicKey(final byte[] privateKey) {
|
||||
final byte[] publicKey = new byte[KeyEncoding.KEY_LENGTH];
|
||||
Curve25519.eval(publicKey, 0, privateKey, null);
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return KeyEncoding.keyToBase64(privateKey);
|
||||
}
|
||||
|
||||
public String getPublicKey() {
|
||||
return KeyEncoding.keyToBase64(publicKey);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="4dp" />
|
||||
<padding
|
||||
android:bottom="4dp"
|
||||
android:left="8dp"
|
||||
android:right="8dp"
|
||||
android:top="4dp" />
|
||||
<solid android:color="@color/fab_label_background_color" />
|
||||
</shape>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/colorForeground"
|
||||
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>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/master_detail_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false"
|
||||
android:divider="?attr/dividerHorizontal"
|
||||
android:orientation="horizontal"
|
||||
android:showDividers="middle"
|
||||
tools:context=".activity.MainActivity">
|
||||
|
||||
<fragment
|
||||
android:name="com.wireguard.android.fragment.TunnelListFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2"
|
||||
android:tag="LIST" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/detail_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="3" />
|
||||
</LinearLayout>
|
||||
@@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.wireguard.android.model.Tunnel" />
|
||||
|
||||
<variable
|
||||
name="fragment"
|
||||
type="com.wireguard.android.fragment.TunnelListFragment" />
|
||||
|
||||
<variable
|
||||
name="rowConfigurationHandler"
|
||||
type="com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler" />
|
||||
|
||||
<variable
|
||||
name="tunnels"
|
||||
type="com.wireguard.android.util.ObservableKeyedList<String, Tunnel>" />
|
||||
</data>
|
||||
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/main_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:clipChildren="false">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/tunnel_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@{@dimen/design_fab_size_normal * 1.1f}"
|
||||
android:clipToPadding="false"
|
||||
android:choiceMode="multipleChoiceModal"
|
||||
android:visibility="@{tunnels.size() > 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
|
||||
app:items="@{tunnels}"
|
||||
app:layout="@{@layout/tunnel_list_item}"
|
||||
app:configurationHandler="@{rowConfigurationHandler}" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="@{tunnels.size() == 0 ? android.view.View.VISIBLE : android.view.View.GONE}"
|
||||
android:layout_gravity="center">
|
||||
<android.support.v7.widget.AppCompatImageView
|
||||
android:id="@+id/logo_placeholder"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0.3333333"
|
||||
android:layout_marginTop="-70dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:text="@string/tunnel_list_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.wireguard.android.widget.fab.FloatingActionsMenu
|
||||
android:id="@+id/create_menu"
|
||||
android:clipChildren="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
app:fab_labelStyle="@style/fab_label"
|
||||
app:fab_labelsPosition="@integer/label_position"
|
||||
app:layout_behavior="com.wireguard.android.widget.fab.FloatingActionButtonBehavior" >
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/create_from_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestImportConfig}"
|
||||
app:srcCompat="@drawable/ic_action_open_white"
|
||||
app:fabSize="mini"
|
||||
app:fab_title="@string/create_from_file" />
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/scan_qr_code"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestScanQRCode}"
|
||||
app:srcCompat="@drawable/ic_action_scan_qr_code_white"
|
||||
app:fabSize="mini"
|
||||
app:fab_title="@string/create_from_qrcode" />
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/create_empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestCreateConfig}"
|
||||
app:fabSize="mini"
|
||||
app:srcCompat="@drawable/ic_action_edit_white"
|
||||
app:fab_title="@string/create_empty" />
|
||||
</com.wireguard.android.widget.fab.FloatingActionsMenu>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="label_position">1</integer>
|
||||
</resources>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
|
||||
<color name="fab_label_text_color">#000000</color>
|
||||
<color name="fab_label_background_color">#bbbbbb</color>
|
||||
|
||||
<color name="accent">#5e97f6</color>
|
||||
</resources>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="Multiselected">
|
||||
<attr name="state_multiselected" format="boolean"/>
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
|
||||
<color name="fab_label_text_color">#ffffff</color>
|
||||
<color name="fab_label_background_color">#444444</color>
|
||||
|
||||
<color name="accent">#2196F3</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
</resources>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="fab_expand_menu_button" type="id"/>
|
||||
<item name="fab_label" type="id"/>
|
||||
|
||||
<dimen name="fab_shadow_offset">3dp</dimen>
|
||||
<dimen name="fab_shadow_radius">9dp</dimen>
|
||||
|
||||
<dimen name="fab_stroke_width">1dp</dimen>
|
||||
|
||||
<dimen name="fab_actions_spacing">24dp</dimen>
|
||||
<dimen name="fab_labels_margin">8dp</dimen>
|
||||
|
||||
<declare-styleable name="LabeledFloatingActionButton">
|
||||
<attr name="fab_title" format="string"/>
|
||||
</declare-styleable>
|
||||
<declare-styleable name="FloatingActionsMenu">
|
||||
<attr name="fab_labelStyle" format="reference"/>
|
||||
<attr name="fab_labelsPosition" format="enum">
|
||||
<enum name="left" value="0"/>
|
||||
<enum name="right" value="1"/>
|
||||
</attr>
|
||||
<attr name="fab_expandDirection" format="enum">
|
||||
<enum name="up" value="0"/>
|
||||
<enum name="down" value="1"/>
|
||||
<enum name="left" value="2"/>
|
||||
<enum name="right" value="3"/>
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
<integer name="label_position">0</integer>
|
||||
</resources>
|
||||
@@ -1,120 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="delete_error">
|
||||
<item quantity="one">Unable to delete %d tunnel: %s</item>
|
||||
<item quantity="other">Unable to delete %d tunnels: %s</item>
|
||||
</plurals>
|
||||
<plurals name="delete_success">
|
||||
<item quantity="one">Successfully deleted %d tunnel</item>
|
||||
<item quantity="other">Successfully deleted %d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="delete_title">
|
||||
<item quantity="one">%d tunnel selected</item>
|
||||
<item quantity="other">%d tunnels selected</item>
|
||||
</plurals>
|
||||
<plurals name="import_partial_success">
|
||||
<item quantity="one">Imported %d of %d tunnels</item>
|
||||
<item quantity="other">Imported %d of %d tunnels</item>
|
||||
</plurals>
|
||||
<plurals name="import_total_success">
|
||||
<item quantity="one">Imported %d tunnel</item>
|
||||
<item quantity="other">Imported %d tunnels</item>
|
||||
</plurals>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="allowed_ips">Allowed IPs</string>
|
||||
<string name="app_name">WireGuard</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="config_save_error">Unable to save configuration for “%1$s”: %2$s</string>
|
||||
<string name="config_save_success">Successfully saved configuration for “%s”</string>
|
||||
<string name="create_activity_title">Create WireGuard Tunnel</string>
|
||||
<string name="create_empty">Create from scratch</string>
|
||||
<string name="create_from_file">Create from file or archive</string>
|
||||
<string name="create_from_qrcode">Create from QR code</string>
|
||||
<string name="create_tunnel">Create Tunnel</string>
|
||||
<string name="dark_theme_title">Use dark theme</string>
|
||||
<string name="dark_theme_summary_on">Currently using dark night theme</string>
|
||||
<string name="dark_theme_summary_off">Currently using light day theme</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="deselect_all">Deselect All</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="dns_servers">DNS servers</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="endpoint">Endpoint</string>
|
||||
<string name="error_fetching_apps">Error fetching apps list: %s</string>
|
||||
<string name="error_down">Error bringing down tunnel: %s</string>
|
||||
<string name="error_root">Please obtain root access and try again</string>
|
||||
<string name="error_up">Error bringing up tunnel: %s</string>
|
||||
<string name="excluded_applications">Excluded Applications</string>
|
||||
<string name="exclude_private_ips">Exclude private IPs</string>
|
||||
<string name="generate">Generate</string>
|
||||
<string name="hint_automatic">(auto)</string>
|
||||
<string name="hint_generated">(generated)</string>
|
||||
<string name="hint_optional">(optional)</string>
|
||||
<string name="hint_random">(random)</string>
|
||||
<string name="import_from_qrcode">Import Tunnel from QR Code</string>
|
||||
<string name="import_error">Unable to import tunnel: %s</string>
|
||||
<string name="import_success">Imported “%s”</string>
|
||||
<string name="interface_title">Interface</string>
|
||||
<string name="listen_port">Listen port</string>
|
||||
<string name="log_exporter_title">Export log file</string>
|
||||
<string name="log_export_error">Unable to export log: %s</string>
|
||||
<string name="log_export_success">Saved to %s</string>
|
||||
<string name="log_export_summary">Log file will be saved to downloads folder</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="permission_label">control WireGuard tunnels</string>
|
||||
<string name="permission_description">Allows an app to control WireGuard tunnels. Apps with this permission may enable and disable WireGuard tunnels at will, potentially misdirecting Internet traffic.</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="pre_shared_key">Pre-shared key</string>
|
||||
<string name="private_key">Private key</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="public_key_description">Public key</string>
|
||||
<string name="qrcode_hint">Tip: generate with `qrencode -t ansiutf8 < tunnel.conf`.</string>
|
||||
<string name="restore_on_boot_summary">Bring up previously-enabled tunnels on boot</string>
|
||||
<string name="restore_on_boot_title">Restore on boot</string>
|
||||
<string name="save">Save</string>
|
||||
<plurals name="set_excluded_applications">
|
||||
<item quantity="one">%d Excluded Application</item>
|
||||
<item quantity="other">%d Excluded Applications</item>
|
||||
</plurals>
|
||||
<string name="set_exclusions">Set Exclusions</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="toggle_error">Error toggling WireGuard tunnel: %s</string>
|
||||
<string name="tools_installer_already">wg and wg-quick are already installed</string>
|
||||
<string name="tools_installer_failure">Unable to install command-line tools (no root?)</string>
|
||||
<string name="tools_installer_initial">Install optional tools for scripting</string>
|
||||
<string name="tools_installer_working">Installing wg and wg-quick</string>
|
||||
<string name="tools_installer_initial_system">Install optional tools for scripting into the system partition</string>
|
||||
<string name="tools_installer_initial_magisk">Install optional tools for scripting as Magisk module</string>
|
||||
<string name="tools_installer_success_system">wg and wg-quick installed into the system partition</string>
|
||||
<string name="tools_installer_success_magisk">wg and wg-quick installed as a Magisk module (reboot required)</string>
|
||||
<string name="tools_installer_title">Install command line tools</string>
|
||||
<string name="tunnel_create_error">Unable to create tunnel: %s</string>
|
||||
<string name="tunnel_create_success">Successfully created tunnel “%s”</string>
|
||||
<string name="tunnel_list_placeholder">Add a tunnel using the blue button</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="tunnel_rename_error">Unable to rename tunnel: %s</string>
|
||||
<string name="tunnel_rename_success">Successfully renamed tunnel to “%s”</string>
|
||||
<string name="tunnel_error_invalid_name">Invalid name</string>
|
||||
<string name="tunnel_error_already_exists">Tunnel %s already exists</string>
|
||||
<string name="tunnel_error_empty_inetaddress">Empty address</string>
|
||||
<string name="tunnel_error_empty_interface_address">Address is empty</string>
|
||||
<string name="tunnel_error_interface_parse_failed">Unable to parse line: “%s”</string>
|
||||
<string name="tunnel_error_forbidden_endpoint_chars">Forbidden characters in endpoint</string>
|
||||
<string name="tunnel_error_empty_peer_public_key">Peer public key may not be empty</string>
|
||||
<string name="tunnel_error_invalid_config_line">Invalid configuration line: %s</string>
|
||||
<string name="tunnel_error_no_config_information">Could not find any config information</string>
|
||||
<string name="key_length_base64_exception_message">WireGuard base64 keys must be 44 characters encoding 32 bytes</string>
|
||||
<string name="key_length_exception_message">WireGuard keys must be 32 bytes</string>
|
||||
<string name="key_length_hex_exception_message">WireGuard hex keys must be 64 characters encoding 32 bytes</string>
|
||||
<string name="version_title">WireGuard for Android v%s"</string>
|
||||
<string name="version_summary">%1$s backend v%2$s</string>
|
||||
<string name="version_summary_checking">Checking %s backend version</string>
|
||||
<string name="version_summary_unknown">Unknown %s version</string>
|
||||
<string name="zip_exporter_title">Export tunnels to zip file</string>
|
||||
<string name="zip_export_error">Unable to export tunnels: %s</string>
|
||||
<string name="zip_export_success">Saved to %s</string>
|
||||
<string name="zip_export_summary">Zip file will be saved to downloads folder</string>
|
||||
</resources>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
</style>
|
||||
|
||||
<style name="SettingsTheme" parent="AppTheme">
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
<style name="fab_label" parent="TextAppearance.AppCompat.Inverse">
|
||||
<item name="android:background">@drawable/fab_label_background</item>
|
||||
<item name="android:textColor">@color/fab_label_text_color</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
Submodule app/tools/libmnl deleted from 0930a63252
@@ -1,2 +0,0 @@
|
||||
src/
|
||||
.gobuildversion
|
||||
@@ -1,58 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
FILES := $(filter-out %/main.go %/queueconstants.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go))
|
||||
|
||||
NDK_GO_ARCH_MAP_x86 := 386
|
||||
NDK_GO_ARCH_MAP_x86_64 := amd64
|
||||
NDK_GO_ARCH_MAP_arm := arm
|
||||
NDK_GO_ARCH_MAP_arm64 := arm64
|
||||
NDK_GO_ARCH_MAP_mips := mipsx
|
||||
NDK_GO_ARCH_MAP_mips64 := mips64x
|
||||
|
||||
CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
|
||||
export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
|
||||
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
|
||||
export CC := $(ANDROID_C_COMPILER)
|
||||
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
GOBUILDARCH := $(NDK_GO_ARCH_MAP_$(shell uname -m))
|
||||
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
|
||||
GOBUILDVERSION := 1.11
|
||||
GOBUILDTARBALL := https://dl.google.com/go/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
|
||||
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
|
||||
|
||||
GOBUILDVERSION_CURRENT := $(shell go version 2>/dev/null)
|
||||
ifeq ($(GOBUILDVERSION_NEEDED),$(GOBUILDVERSION_CURRENT))
|
||||
$(info Using system Go)
|
||||
else
|
||||
export GOROOT := $(CURDIR)/goroot
|
||||
export PATH := $(GOROOT)/bin:$(PATH)
|
||||
GOBUILDVERSION_CURRENT := $(shell $(GOROOT)/bin/go version 2>/dev/null)
|
||||
ifneq ($(GOBUILDVERSION_NEEDED),$(GOBUILDVERSION_CURRENT))
|
||||
$(shell rm -f $(GOROOT)/bin/go)
|
||||
endif
|
||||
$(GOROOT)/bin/go:
|
||||
rm -rf "$(GOROOT)"
|
||||
mkdir -p "$(GOROOT)"
|
||||
curl "$(GOBUILDTARBALL)" | tar -C "$(GOROOT)" --strip-components=1 -xzf - || { rm -rf "$(GOROOT)"; exit 1; }
|
||||
patch -p1 -f -N -r- -d "$(GOROOT)" < goruntime-boottime-over-monotonic.diff || { rm -rf "$(GOROOT)"; exit 1; }
|
||||
$(DESTDIR)/libwg-go.so: $(GOROOT)/bin/go
|
||||
endif
|
||||
|
||||
$(shell test "$$(cat .gobuildversion 2>/dev/null)" = "$(GOBUILDVERSION_CURRENT)" || rm -f "$(DESTDIR)/libwg-go.so")
|
||||
|
||||
$(DESTDIR)/libwg-go.so: $(FILES) src/git.zx2c4.com/wireguard-go/api-android.go src/git.zx2c4.com/wireguard-go/tun/api-android.go src/git.zx2c4.com/wireguard-go/jni.c
|
||||
@goroot="$$(go env GOROOT)" || exit 1; patch -p1 -f -R -s --dry-run -d "$$goroot" < goruntime-boottime-over-monotonic.diff >/dev/null 2>&1 || { echo "ERROR: Your system's Go is not patched to use the suspend-aware monotonic clock. Please try again after having run: \`patch -p1 -d \"$$goroot\" < \"$(CURDIR)/goruntime-boottime-over-monotonic.diff\"\`."; exit 1; }
|
||||
find . -name '*.go' -type l -delete
|
||||
find . -type d -empty -delete
|
||||
mkdir -p $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILES)))
|
||||
$(foreach FILE,$(FILES),ln -sfrt $(subst ../wireguard-go/,./src/git.zx2c4.com/wireguard-go/,$(dir $(FILE))) $(FILE);)
|
||||
GOPATH=$(PWD) go get -v -d git.zx2c4.com/wireguard-go
|
||||
GOPATH=$(PWD) go build -ldflags="-X main.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -o $(DESTDIR)/libwg-go.so -buildmode c-shared git.zx2c4.com/wireguard-go
|
||||
go version > .gobuildversion
|
||||
@@ -1,16 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Copyright (C) 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
/* Reduce memory consumption for Android */
|
||||
|
||||
const (
|
||||
QueueOutboundSize = 1024
|
||||
QueueInboundSize = 1024
|
||||
QueueHandshakeSize = 1024
|
||||
MaxSegmentSize = 2200
|
||||
PreallocatedBuffersPerPool = 4096
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Copyright (C) 2017-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"git.zx2c4.com/wireguard-go/rwcancel"
|
||||
"os"
|
||||
)
|
||||
|
||||
func CreateTUNFromFD(tun_fd int) (TUNDevice, string, error) {
|
||||
tun := &nativeTun{
|
||||
fd: os.NewFile(uintptr(tun_fd), "/dev/tun"),
|
||||
events: make(chan TUNEvent, 5),
|
||||
errors: make(chan error, 5),
|
||||
nopi: true,
|
||||
}
|
||||
var err error
|
||||
tun.fdCancel, err = rwcancel.NewRWCancel(tun_fd)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
name, err := tun.Name()
|
||||
if err != nil {
|
||||
tun.fdCancel.Cancel()
|
||||
return nil, "", err
|
||||
}
|
||||
return tun, name, nil
|
||||
}
|
||||
Submodule app/tools/wireguard deleted from 36c4f1047e
Submodule app/tools/wireguard-go deleted from dbb72402f2
+33
-1
@@ -6,8 +6,31 @@ allprojects {
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
annotationsVersion = '1.1.0'
|
||||
appcompatVersion = '1.1.0'
|
||||
cardviewVersion = '1.0.0'
|
||||
collectionVersion = '1.1.0'
|
||||
coreKtxVersion = '1.2.0'
|
||||
constraintLayoutVersion = '1.1.3'
|
||||
coordinatorLayoutVersion = '1.1.0'
|
||||
agpVersion = '3.6.1'
|
||||
fragmentVersion = '1.2.2'
|
||||
materialComponentsVersion = '1.1.0'
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinVersion = '1.3.70'
|
||||
preferenceVersion = '1.1.0'
|
||||
streamsupportVersion = '1.7.1'
|
||||
threetenabpVersion = '1.2.2'
|
||||
// ZXING switched minSdk to 24 so we cannot upgrade to 4.0.2 without following suit.
|
||||
// If you choose to upgrade to minSDK 24 then you should also disable Jetifier from
|
||||
// gradle.properties.
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
eddsaVersion = '0.3.0'
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.0'
|
||||
classpath "com.android.tools.build:gradle:$agpVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
@@ -18,3 +41,12 @@ buildscript {
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
tasks {
|
||||
wrapper {
|
||||
gradleVersion = "6.2.1"
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "version.gradle"
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-2
@@ -1,6 +1,5 @@
|
||||
#Mon Oct 01 15:59:48 CEST 2018
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
@@ -109,8 +125,8 @@ if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
@@ -138,19 +154,19 @@ if $cygwin ; then
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -159,14 +175,9 @@ save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
include ':app'
|
||||
include ':tunnel'
|
||||
include ':ui'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 29
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode wireguardVersionCode
|
||||
versionName wireguardVersionName
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'tools/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||
api "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.collection:collection:$collectionVersion"
|
||||
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
||||
implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
|
||||
implementation "net.i2p.crypto:eddsa:$eddsaVersion"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!--
|
||||
~ Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.wireguard.android.tunnel">
|
||||
<application>
|
||||
<service
|
||||
android:name="com.wireguard.android.backend.GoBackend$VpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
+17
-32
@@ -1,47 +1,37 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Interface for implementations of the WireGuard secure network tunnel.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface Backend {
|
||||
/**
|
||||
* Update the volatile configuration of a running tunnel and return the resulting configuration.
|
||||
* If the tunnel is not up, return the configuration that would result (if known), or else
|
||||
* simply return the given configuration.
|
||||
*
|
||||
* @param tunnel The tunnel to apply the configuration to.
|
||||
* @param config The new configuration for this tunnel.
|
||||
* @return The updated configuration of the tunnel.
|
||||
*/
|
||||
Config applyConfig(Tunnel tunnel, Config config) throws Exception;
|
||||
|
||||
/**
|
||||
* Enumerate the names of currently-running tunnels.
|
||||
* Enumerate names of currently-running tunnels.
|
||||
*
|
||||
* @return The set of running tunnel names.
|
||||
*/
|
||||
Set<String> enumerate();
|
||||
Set<String> getRunningTunnelNames();
|
||||
|
||||
/**
|
||||
* Get the actual state of a tunnel.
|
||||
* Get the state of a tunnel.
|
||||
*
|
||||
* @param tunnel The tunnel to examine the state of.
|
||||
* @return The state of the tunnel.
|
||||
*/
|
||||
State getState(Tunnel tunnel) throws Exception;
|
||||
Tunnel.State getState(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
|
||||
@@ -52,16 +42,6 @@ public interface Backend {
|
||||
*/
|
||||
Statistics getStatistics(Tunnel tunnel) throws Exception;
|
||||
|
||||
/**
|
||||
* Set the state of a tunnel.
|
||||
*
|
||||
* @param tunnel The tunnel to control the state of.
|
||||
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
|
||||
* {@code TOGGLE}.
|
||||
* @return The updated state of the tunnel.
|
||||
*/
|
||||
State setState(Tunnel tunnel, State state) throws Exception;
|
||||
|
||||
/**
|
||||
* Determine version of underlying backend.
|
||||
*
|
||||
@@ -71,9 +51,14 @@ public interface Backend {
|
||||
String getVersion() throws Exception;
|
||||
|
||||
/**
|
||||
* Determine type name of underlying backend.
|
||||
* Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config
|
||||
* may update the running configuration; config may be null when setting the tunnel down.
|
||||
*
|
||||
* @return Type name
|
||||
* @param tunnel The tunnel to control the state of.
|
||||
* @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
|
||||
* {@code TOGGLE}.
|
||||
* @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
|
||||
* @return The updated state of the tunnel.
|
||||
*/
|
||||
String getTypeName();
|
||||
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
@NonNullForAll
|
||||
public final class BackendException extends Exception {
|
||||
private final Object[] format;
|
||||
private final Reason reason;
|
||||
public BackendException(final Reason reason, final Object... format) {
|
||||
this.reason = reason;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public Object[] getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
UNKNOWN_KERNEL_MODULE_NAME,
|
||||
WG_QUICK_CONFIG_ERROR_CODE,
|
||||
TUNNEL_MISSING_CONFIG,
|
||||
VPN_NOT_AUTHORIZED,
|
||||
UNABLE_TO_START_VPN,
|
||||
TUN_CREATION_ERROR,
|
||||
GO_ACTIVATION_ERROR_CODE
|
||||
}
|
||||
}
|
||||
+125
-95
@@ -1,54 +1,56 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.activity.MainActivity;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.model.Tunnel.Statistics;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.backend.BackendException.Reason;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.util.SharedLibraryLoader;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.config.InetNetwork;
|
||||
import com.wireguard.config.Interface;
|
||||
import com.wireguard.config.Peer;
|
||||
import com.wireguard.crypto.KeyEncoding;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.Formatter;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArraySet;
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
|
||||
@NonNullForAll
|
||||
public final class GoBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + GoBackend.class.getSimpleName();
|
||||
@Nullable private static AlwaysOnCallback alwaysOnCallback;
|
||||
private static CompletableFuture<VpnService> vpnService = new CompletableFuture<>();
|
||||
|
||||
private final Context context;
|
||||
@Nullable private Config currentConfig;
|
||||
@Nullable private Tunnel currentTunnel;
|
||||
private int currentTunnelHandle = -1;
|
||||
|
||||
public GoBackend(final Context context) {
|
||||
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static void setAlwaysOnCallback(AlwaysOnCallback cb) {
|
||||
alwaysOnCallback = cb;
|
||||
}
|
||||
|
||||
private static native String wgGetConfig(int handle);
|
||||
|
||||
private static native int wgGetSocketV4(int handle);
|
||||
|
||||
private static native int wgGetSocketV6(int handle);
|
||||
@@ -60,29 +62,7 @@ public final class GoBackend implements Backend {
|
||||
private static native String wgVersion();
|
||||
|
||||
@Override
|
||||
public String getVersion() { return wgVersion(); }
|
||||
|
||||
@Override
|
||||
public String getTypeName() { return "Go userspace"; }
|
||||
|
||||
@Override
|
||||
public Config applyConfig(final Tunnel tunnel, final Config config) throws Exception {
|
||||
if (tunnel.getState() == State.UP) {
|
||||
// Restart the tunnel to apply the new config.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
// The new configuration didn't work, so try to go back to the old one.
|
||||
setStateInternal(tunnel, tunnel.getConfig(), State.UP);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> enumerate() {
|
||||
public Set<String> getRunningTunnelNames() {
|
||||
if (currentTunnel != null) {
|
||||
final Set<String> runningTunnels = new ArraySet<>();
|
||||
runningTunnels.add(currentTunnel.getName());
|
||||
@@ -98,33 +78,88 @@ public final class GoBackend implements Backend {
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
return new Statistics();
|
||||
final Statistics stats = new Statistics();
|
||||
if (tunnel != currentTunnel) {
|
||||
return stats;
|
||||
}
|
||||
final String config = wgGetConfig(currentTunnelHandle);
|
||||
Key key = null;
|
||||
long rx = 0, tx = 0;
|
||||
for (final String line : config.split("\\n")) {
|
||||
if (line.startsWith("public_key=")) {
|
||||
if (key != null)
|
||||
stats.add(key, rx, tx);
|
||||
rx = 0;
|
||||
tx = 0;
|
||||
try {
|
||||
key = Key.fromHex(line.substring(11));
|
||||
} catch (final KeyFormatException ignored) {
|
||||
key = null;
|
||||
}
|
||||
} else if (line.startsWith("rx_bytes=")) {
|
||||
if (key == null)
|
||||
continue;
|
||||
try {
|
||||
rx = Long.parseLong(line.substring(9));
|
||||
} catch (final NumberFormatException ignored) {
|
||||
rx = 0;
|
||||
}
|
||||
} else if (line.startsWith("tx_bytes=")) {
|
||||
if (key == null)
|
||||
continue;
|
||||
try {
|
||||
tx = Long.parseLong(line.substring(9));
|
||||
} catch (final NumberFormatException ignored) {
|
||||
tx = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key != null)
|
||||
stats.add(key, rx, tx);
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state) throws Exception {
|
||||
public String getVersion() {
|
||||
return wgVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if (state == originalState)
|
||||
if (state == originalState && tunnel == currentTunnel && config == currentConfig)
|
||||
return originalState;
|
||||
if (state == State.UP && currentTunnel != null)
|
||||
throw new IllegalStateException("Only one userspace tunnel can run at a time");
|
||||
Log.d(TAG, "Changing tunnel " + tunnel.getName() + " to state " + state);
|
||||
setStateInternal(tunnel, tunnel.getConfig(), state);
|
||||
if (state == State.UP) {
|
||||
final Config originalConfig = currentConfig;
|
||||
final Tunnel originalTunnel = currentTunnel;
|
||||
if (currentTunnel != null)
|
||||
setStateInternal(currentTunnel, null, State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, state);
|
||||
} catch (final Exception e) {
|
||||
if (originalTunnel != null)
|
||||
setStateInternal(originalTunnel, originalConfig, State.UP);
|
||||
throw e;
|
||||
}
|
||||
} else if (state == State.DOWN && tunnel == currentTunnel) {
|
||||
setStateInternal(tunnel, null, State.DOWN);
|
||||
}
|
||||
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);
|
||||
|
||||
if (state == State.UP) {
|
||||
Log.i(TAG, "Bringing tunnel up");
|
||||
|
||||
Objects.requireNonNull(config, "Trying to bring up a tunnel with no config");
|
||||
if (config == null)
|
||||
throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
|
||||
|
||||
if (VpnService.prepare(context) != null)
|
||||
throw new Exception("VPN service not authorized by user");
|
||||
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
|
||||
|
||||
final VpnService service;
|
||||
if (!vpnService.isDone())
|
||||
@@ -133,8 +168,11 @@ public final class GoBackend implements Backend {
|
||||
try {
|
||||
service = vpnService.get(2, TimeUnit.SECONDS);
|
||||
} catch (final TimeoutException e) {
|
||||
throw new Exception("Unable to start Android VPN service", 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");
|
||||
@@ -142,74 +180,49 @@ public final class GoBackend implements Backend {
|
||||
}
|
||||
|
||||
// Build config
|
||||
final Interface iface = config.getInterface();
|
||||
final String goConfig;
|
||||
try (final Formatter fmt = new Formatter(new StringBuilder())) {
|
||||
fmt.format("replace_peers=true\n");
|
||||
if (iface.getPrivateKey() != null)
|
||||
fmt.format("private_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(iface.getPrivateKey())));
|
||||
if (iface.getListenPort() != 0)
|
||||
fmt.format("listen_port=%d\n", config.getInterface().getListenPort());
|
||||
for (final Peer peer : config.getPeers()) {
|
||||
if (peer.getPublicKey() != null)
|
||||
fmt.format("public_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPublicKey())));
|
||||
if (peer.getPreSharedKey() != null)
|
||||
fmt.format("preshared_key=%s\n", KeyEncoding.keyToHex(KeyEncoding.keyFromBase64(peer.getPreSharedKey())));
|
||||
if (peer.getEndpoint() != null)
|
||||
fmt.format("endpoint=%s\n", peer.getResolvedEndpointString());
|
||||
if (peer.getPersistentKeepalive() != 0)
|
||||
fmt.format("persistent_keepalive_interval=%d\n", peer.getPersistentKeepalive());
|
||||
for (final InetNetwork addr : peer.getAllowedIPs()) {
|
||||
fmt.format("allowed_ip=%s\n", addr.toString());
|
||||
}
|
||||
}
|
||||
goConfig = fmt.toString();
|
||||
}
|
||||
final String goConfig = config.toWgUserspaceString();
|
||||
|
||||
// Create the vpn tunnel with android API
|
||||
final VpnService.Builder builder = service.getBuilder();
|
||||
builder.setSession(tunnel.getName());
|
||||
|
||||
final Intent configureIntent = new Intent(context, MainActivity.class);
|
||||
configureIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
builder.setConfigureIntent(PendingIntent.getActivity(context, 0, configureIntent, 0));
|
||||
|
||||
for (final String excludedApplication : config.getInterface().getExcludedApplications())
|
||||
builder.addDisallowedApplication(excludedApplication);
|
||||
|
||||
for (final InetNetwork addr : config.getInterface().getAddresses())
|
||||
builder.addAddress(addr.getAddress(), addr.getMask());
|
||||
|
||||
for (final InetAddress addr : config.getInterface().getDnses())
|
||||
for (final InetAddress addr : config.getInterface().getDnsServers())
|
||||
builder.addDnsServer(addr.getHostAddress());
|
||||
|
||||
for (final Peer peer : config.getPeers()) {
|
||||
for (final InetNetwork addr : peer.getAllowedIPs())
|
||||
for (final InetNetwork addr : peer.getAllowedIps())
|
||||
builder.addRoute(addr.getAddress(), addr.getMask());
|
||||
}
|
||||
|
||||
int mtu = config.getInterface().getMtu();
|
||||
if (mtu == 0)
|
||||
mtu = 1280;
|
||||
builder.setMtu(mtu);
|
||||
builder.setMtu(config.getInterface().getMtu().orElse(1280));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
builder.setMetered(false);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
service.setUnderlyingNetworks(null);
|
||||
|
||||
builder.setBlocking(true);
|
||||
try (final ParcelFileDescriptor tun = builder.establish()) {
|
||||
if (tun == null)
|
||||
throw new Exception("Unable to create tun device");
|
||||
throw new BackendException(Reason.TUN_CREATION_ERROR);
|
||||
Log.d(TAG, "Go backend v" + wgVersion());
|
||||
currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
|
||||
}
|
||||
if (currentTunnelHandle < 0)
|
||||
throw new Exception("Unable to turn tunnel on (wgTurnOn return " + currentTunnelHandle + ')');
|
||||
throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
|
||||
|
||||
currentTunnel = tunnel;
|
||||
currentConfig = config;
|
||||
|
||||
service.protect(wgGetSocketV4(currentTunnelHandle));
|
||||
service.protect(wgGetSocketV6(currentTunnelHandle));
|
||||
} else {
|
||||
Log.i(TAG, "Bringing tunnel down");
|
||||
|
||||
if (currentTunnelHandle == -1) {
|
||||
Log.w(TAG, "Tunnel already down");
|
||||
return;
|
||||
@@ -218,7 +231,10 @@ public final class GoBackend implements Backend {
|
||||
wgTurnOff(currentTunnelHandle);
|
||||
currentTunnel = null;
|
||||
currentTunnelHandle = -1;
|
||||
currentConfig = null;
|
||||
}
|
||||
|
||||
tunnel.onStateChange(state);
|
||||
}
|
||||
|
||||
private void startVpnService() {
|
||||
@@ -226,7 +242,13 @@ public final class GoBackend implements Backend {
|
||||
context.startService(new Intent(context, VpnService.class));
|
||||
}
|
||||
|
||||
public interface AlwaysOnCallback {
|
||||
void alwaysOnTriggered();
|
||||
}
|
||||
|
||||
public static class VpnService extends android.net.VpnService {
|
||||
@Nullable private GoBackend owner;
|
||||
|
||||
public Builder getBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
@@ -239,13 +261,17 @@ public final class GoBackend implements Backend {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Application.getTunnelManager().getTunnels().thenAccept(tunnels -> {
|
||||
for (final Tunnel tunnel : tunnels) {
|
||||
if (tunnel != null && tunnel.getState() != State.DOWN)
|
||||
tunnel.setState(State.DOWN);
|
||||
if (owner != null) {
|
||||
final Tunnel tunnel = owner.currentTunnel;
|
||||
if (tunnel != null) {
|
||||
if (owner.currentTunnelHandle != -1)
|
||||
wgTurnOff(owner.currentTunnelHandle);
|
||||
owner.currentTunnel = null;
|
||||
owner.currentTunnelHandle = -1;
|
||||
owner.currentConfig = null;
|
||||
tunnel.onStateChange(State.DOWN);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
vpnService = vpnService.newIncompleteFuture();
|
||||
super.onDestroy();
|
||||
}
|
||||
@@ -255,10 +281,14 @@ public final class GoBackend implements Backend {
|
||||
vpnService.complete(this);
|
||||
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
|
||||
Log.d(TAG, "Service started by Always-on VPN feature");
|
||||
Application.getTunnelManager().restoreState(true).whenComplete(ExceptionLoggers.D);
|
||||
if (alwaysOnCallback != null)
|
||||
alwaysOnCallback.alwaysOnTriggered();
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
public void setOwner(final GoBackend owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@NonNullForAll
|
||||
public class Statistics {
|
||||
private final Map<Key, Pair<Long, Long>> peerBytes = new HashMap<>();
|
||||
private long lastTouched = SystemClock.elapsedRealtime();
|
||||
|
||||
Statistics() {
|
||||
}
|
||||
|
||||
void add(final Key key, final long rx, final long tx) {
|
||||
peerBytes.put(key, Pair.create(rx, tx));
|
||||
lastTouched = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
public boolean isStale() {
|
||||
return SystemClock.elapsedRealtime() - lastTouched > 900;
|
||||
}
|
||||
|
||||
public long peerRx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
return 0;
|
||||
return peerBytes.get(peer).first;
|
||||
}
|
||||
|
||||
public long peerTx(final Key peer) {
|
||||
if (!peerBytes.containsKey(peer))
|
||||
return 0;
|
||||
return peerBytes.get(peer).second;
|
||||
}
|
||||
|
||||
public Key[] peers() {
|
||||
return peerBytes.keySet().toArray(new Key[0]);
|
||||
}
|
||||
|
||||
public long totalRx() {
|
||||
long rx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
rx += val.first;
|
||||
}
|
||||
return rx;
|
||||
}
|
||||
|
||||
public long totalTx() {
|
||||
long tx = 0;
|
||||
for (final Pair<Long, Long> val : peerBytes.values()) {
|
||||
tx += val.second;
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Represents a WireGuard tunnel.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public interface Tunnel {
|
||||
int NAME_MAX_LENGTH = 15;
|
||||
Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
|
||||
|
||||
static boolean isNameInvalid(final CharSequence name) {
|
||||
return !NAME_PATTERN.matcher(name).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the tunnel, which should always pass the !isNameInvalid test.
|
||||
*
|
||||
* @return The name of the tunnel.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* React to a change in state of the tunnel. Should only be directly called by Backend.
|
||||
*
|
||||
* @param newState The new state of the tunnel.
|
||||
* @return The new state of the tunnel.
|
||||
*/
|
||||
void onStateChange(State newState);
|
||||
|
||||
enum State {
|
||||
DOWN,
|
||||
TOGGLE,
|
||||
UP;
|
||||
|
||||
public static State of(final boolean running) {
|
||||
return running ? UP : DOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.backend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.wireguard.android.backend.BackendException.Reason;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
import com.wireguard.config.Config;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* WireGuard backend that uses {@code wg-quick} to implement tunnel configuration.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public final class WgQuickBackend implements Backend {
|
||||
private static final String TAG = "WireGuard/" + WgQuickBackend.class.getSimpleName();
|
||||
private final File localTemporaryDir;
|
||||
private final RootShell rootShell;
|
||||
private final Map<Tunnel, Config> runningConfigs = new HashMap<>();
|
||||
private final ToolsInstaller toolsInstaller;
|
||||
private boolean multipleTunnels;
|
||||
|
||||
public WgQuickBackend(final Context context, final RootShell rootShell, final ToolsInstaller toolsInstaller) {
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
this.rootShell = rootShell;
|
||||
this.toolsInstaller = toolsInstaller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRunningTunnelNames() {
|
||||
final List<String> output = new ArrayList<>();
|
||||
// Don't throw an exception here or nothing will show up in the UI.
|
||||
try {
|
||||
toolsInstaller.ensureToolsAvailable();
|
||||
if (rootShell.run(output, "wg show interfaces") != 0 || output.isEmpty())
|
||||
return Collections.emptySet();
|
||||
} catch (final Exception e) {
|
||||
Log.w(TAG, "Unable to enumerate running tunnels", e);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
// wg puts all interface names on the same line. Split them into separate elements.
|
||||
return Stream.of(output.get(0).split(" ")).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState(final Tunnel tunnel) {
|
||||
return getRunningTunnelNames().contains(tunnel.getName()) ? State.UP : State.DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statistics getStatistics(final Tunnel tunnel) {
|
||||
final Statistics stats = new Statistics();
|
||||
final Collection<String> output = new ArrayList<>();
|
||||
try {
|
||||
if (rootShell.run(output, String.format("wg show '%s' transfer", tunnel.getName())) != 0)
|
||||
return stats;
|
||||
} catch (final Exception ignored) {
|
||||
return stats;
|
||||
}
|
||||
for (final String line : output) {
|
||||
final String[] parts = line.split("\\t");
|
||||
if (parts.length != 3)
|
||||
continue;
|
||||
try {
|
||||
stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[1]), Long.parseLong(parts[2]));
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() throws Exception {
|
||||
final List<String> output = new ArrayList<>();
|
||||
if (rootShell.run(output, "cat /sys/module/wireguard/version") != 0 || output.isEmpty())
|
||||
throw new BackendException(Reason.UNKNOWN_KERNEL_MODULE_NAME);
|
||||
return output.get(0);
|
||||
}
|
||||
|
||||
public void setMultipleTunnels(boolean on) {
|
||||
multipleTunnels = on;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State setState(final Tunnel tunnel, State state, @Nullable final Config config) throws Exception {
|
||||
final State originalState = getState(tunnel);
|
||||
final Config originalConfig = runningConfigs.get(tunnel);
|
||||
final Map<Tunnel, Config> runningConfigsSnapshot = new HashMap<>(runningConfigs);
|
||||
|
||||
if (state == State.TOGGLE)
|
||||
state = originalState == State.UP ? State.DOWN : State.UP;
|
||||
if ((state == State.UP && originalState == State.UP && originalConfig != null && originalConfig == config) ||
|
||||
(state == State.DOWN && originalState == State.DOWN))
|
||||
return originalState;
|
||||
if (state == State.UP) {
|
||||
toolsInstaller.ensureToolsAvailable();
|
||||
if (!multipleTunnels && originalState == State.DOWN) {
|
||||
final List<Pair<Tunnel, Config>> rewind = new LinkedList<>();
|
||||
try {
|
||||
for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
|
||||
setStateInternal(entry.getKey(), entry.getValue(), State.DOWN);
|
||||
rewind.add(Pair.create(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
for (final Pair<Tunnel, Config> entry : rewind) {
|
||||
setStateInternal(entry.first, entry.second, State.UP);
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (originalState == State.UP)
|
||||
setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
|
||||
try {
|
||||
setStateInternal(tunnel, config, State.UP);
|
||||
} catch (final Exception e) {
|
||||
try {
|
||||
if (originalState == State.UP && originalConfig != null) {
|
||||
setStateInternal(tunnel, originalConfig, State.UP);
|
||||
}
|
||||
if (!multipleTunnels && originalState == State.DOWN) {
|
||||
for (final Map.Entry<Tunnel, Config> entry : runningConfigsSnapshot.entrySet()) {
|
||||
setStateInternal(entry.getKey(), entry.getValue(), State.UP);
|
||||
}
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
} else if (state == State.DOWN) {
|
||||
setStateInternal(tunnel, originalConfig == null ? config : originalConfig, State.DOWN);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private void setStateInternal(final Tunnel tunnel, @Nullable final Config config, final State state) throws Exception {
|
||||
Log.i(TAG, "Bringing tunnel " + tunnel.getName() + " " + state);
|
||||
|
||||
Objects.requireNonNull(config, "Trying to set state up with a null config");
|
||||
|
||||
final File tempFile = new File(localTemporaryDir, tunnel.getName() + ".conf");
|
||||
try (final FileOutputStream stream = new FileOutputStream(tempFile, false)) {
|
||||
stream.write(config.toWgQuickString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String command = String.format("wg-quick %s '%s'",
|
||||
state.toString().toLowerCase(Locale.ENGLISH), tempFile.getAbsolutePath());
|
||||
if (state == State.UP)
|
||||
command = "cat /sys/module/wireguard/version && " + command;
|
||||
final int result = rootShell.run(null, command);
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
tempFile.delete();
|
||||
if (result != 0)
|
||||
throw new BackendException(Reason.WG_QUICK_CONFIG_ERROR_CODE, result);
|
||||
|
||||
if (state == State.UP)
|
||||
runningConfigs.put(tunnel, config);
|
||||
else
|
||||
runningConfigs.remove(tunnel);
|
||||
|
||||
tunnel.onStateChange(state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright © 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.wireguard.android.util.RootShell.RootShellException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine;
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidParameterException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Signature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@NonNullForAll
|
||||
public class ModuleLoader {
|
||||
private static final String MODULE_LIST_URL = "https://download.wireguard.com/android-module/modules.txt.sig";
|
||||
private static final String MODULE_NAME = "wireguard-%s.ko";
|
||||
private static final String MODULE_PUBLIC_KEY_BASE64 = "RWRmHuT9PSqtwfsLtEx+QS06BJtLgFYteL9WCNjH7yuyu5Y1DieSN7If";
|
||||
private static final String MODULE_URL = "https://download.wireguard.com/android-module/%s";
|
||||
private final File moduleDir;
|
||||
private final RootShell rootShell;
|
||||
private final File tmpDir;
|
||||
private final String userAgent;
|
||||
|
||||
public ModuleLoader(final Context context, final RootShell rootShell, final String userAgent) {
|
||||
moduleDir = new File(context.getCacheDir(), "kmod");
|
||||
tmpDir = new File(context.getCacheDir(), "tmp");
|
||||
this.rootShell = rootShell;
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public static boolean isModuleLoaded() {
|
||||
return new File("/sys/module/wireguard").exists();
|
||||
}
|
||||
|
||||
public Integer download() throws IOException, RootShellException, NoSuchAlgorithmException {
|
||||
final List<String> output = new ArrayList<>();
|
||||
rootShell.run(output, "sha256sum /proc/version|cut -d ' ' -f 1");
|
||||
if (output.size() != 1 || output.get(0).length() != 64)
|
||||
throw new InvalidParameterException("Invalid sha256 of /proc/version");
|
||||
final String moduleName = String.format(MODULE_NAME, output.get(0));
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(MODULE_LIST_URL).openConnection();
|
||||
connection.setRequestProperty("User-Agent", userAgent);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
|
||||
throw new IOException("Hash list could not be found");
|
||||
byte[] input = new byte[1024 * 1024 * 3 /* 3MiB */];
|
||||
int len;
|
||||
try (final InputStream inputStream = connection.getInputStream()) {
|
||||
len = inputStream.read(input);
|
||||
}
|
||||
if (len <= 0)
|
||||
throw new IOException("Hash list was empty");
|
||||
final Map<String, Sha256Digest> modules = verifySignedHashes(new String(input, 0, len, StandardCharsets.UTF_8));
|
||||
if (modules == null)
|
||||
throw new InvalidParameterException("The signature did not verify or invalid hash list format");
|
||||
if (!modules.containsKey(moduleName))
|
||||
return OsConstants.ENOENT;
|
||||
connection = (HttpURLConnection) new URL(String.format(MODULE_URL, moduleName)).openConnection();
|
||||
connection.setRequestProperty("User-Agent", userAgent);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
|
||||
throw new IOException("Module file could not be found, despite being on hash list");
|
||||
|
||||
tmpDir.mkdirs();
|
||||
moduleDir.mkdir();
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("UNVERIFIED-", null, tmpDir);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try (final InputStream inputStream = connection.getInputStream();
|
||||
final FileOutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||
int total = 0;
|
||||
while ((len = inputStream.read(input)) > 0) {
|
||||
total += len;
|
||||
if (total > 1024 * 1024 * 15 /* 15 MiB */)
|
||||
throw new IOException("File too big");
|
||||
outputStream.write(input, 0, len);
|
||||
digest.update(input, 0, len);
|
||||
}
|
||||
outputStream.getFD().sync();
|
||||
}
|
||||
if (!Arrays.equals(digest.digest(), modules.get(moduleName).bytes))
|
||||
throw new IOException("Incorrect file hash");
|
||||
|
||||
if (!tempFile.renameTo(new File(moduleDir, moduleName)))
|
||||
throw new IOException("Unable to rename to final destination");
|
||||
} finally {
|
||||
if (tempFile != null)
|
||||
tempFile.delete();
|
||||
}
|
||||
return OsConstants.EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
public void loadModule() throws IOException, RootShellException {
|
||||
rootShell.run(null, String.format("insmod \"%s/wireguard-$(sha256sum /proc/version|cut -d ' ' -f 1).ko\"", moduleDir.getAbsolutePath()));
|
||||
}
|
||||
|
||||
public boolean moduleMightExist() {
|
||||
return moduleDir.exists() && moduleDir.isDirectory();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map<String, Sha256Digest> verifySignedHashes(final String signifyDigest) {
|
||||
final byte[] publicKeyBytes = Base64.decode(MODULE_PUBLIC_KEY_BASE64, Base64.DEFAULT);
|
||||
|
||||
if (publicKeyBytes == null || publicKeyBytes.length != 32 + 10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd')
|
||||
return null;
|
||||
|
||||
final String[] lines = signifyDigest.split("\n", 3);
|
||||
if (lines.length != 3)
|
||||
return null;
|
||||
if (!lines[0].startsWith("untrusted comment: "))
|
||||
return null;
|
||||
|
||||
final byte[] signatureBytes = Base64.decode(lines[1], Base64.DEFAULT);
|
||||
if (signatureBytes == null || signatureBytes.length != 64 + 10)
|
||||
return null;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (signatureBytes[i] != publicKeyBytes[i])
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
EdDSAParameterSpec parameterSpec = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
|
||||
Signature signature = new EdDSAEngine(MessageDigest.getInstance(parameterSpec.getHashAlgorithm()));
|
||||
byte[] rawPublicKeyBytes = new byte[32];
|
||||
System.arraycopy(publicKeyBytes, 10, rawPublicKeyBytes, 0, 32);
|
||||
signature.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(rawPublicKeyBytes, parameterSpec)));
|
||||
signature.update(lines[2].getBytes(StandardCharsets.UTF_8));
|
||||
if (!signature.verify(signatureBytes, 10, 64))
|
||||
return null;
|
||||
} catch (final Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Sha256Digest> hashes = new HashMap<>();
|
||||
for (final String line : lines[2].split("\n")) {
|
||||
final String[] components = line.split(" ", 2);
|
||||
if (components.length != 2)
|
||||
return null;
|
||||
try {
|
||||
hashes.put(components[1], new Sha256Digest(components[0]));
|
||||
} catch (final Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return hashes;
|
||||
}
|
||||
|
||||
private static final class Sha256Digest {
|
||||
private byte[] bytes;
|
||||
|
||||
private Sha256Digest(final String hex) {
|
||||
if (hex.length() != 64)
|
||||
throw new InvalidParameterException("SHA256 hashes must be 32 bytes long");
|
||||
bytes = new byte[32];
|
||||
for (int i = 0; i < 32; ++i)
|
||||
bytes[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
-28
@@ -1,20 +1,18 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.BuildConfig;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.util.RootShell.RootShellException.Reason;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
@@ -22,15 +20,17 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Helper class for running commands as root.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class RootShell {
|
||||
private static final String SU = "su";
|
||||
private static final String TAG = "WireGuard/" + RootShell.class.getSimpleName();
|
||||
|
||||
private final String deviceNotRootedMessage;
|
||||
private final File localBinaryDir;
|
||||
private final File localTemporaryDir;
|
||||
private final Object lock = new Object();
|
||||
@@ -41,12 +41,10 @@ public class RootShell {
|
||||
@Nullable private BufferedReader stdout;
|
||||
|
||||
public RootShell(final Context context) {
|
||||
deviceNotRootedMessage = context.getString(R.string.error_root);
|
||||
final File cacheDir = context.getCacheDir();
|
||||
localBinaryDir = new File(cacheDir, "bin");
|
||||
localTemporaryDir = new File(cacheDir, "tmp");
|
||||
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
|
||||
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||
preamble = String.format("export CALLING_PACKAGE=%s PATH=\"%s:$PATH\" TMPDIR='%s'; id -u\n",
|
||||
BuildConfig.APPLICATION_ID, localBinaryDir, localTemporaryDir);
|
||||
context.getPackageName(), localBinaryDir, localTemporaryDir);
|
||||
}
|
||||
|
||||
private static boolean isExecutableInPath(final String name) {
|
||||
@@ -82,7 +80,7 @@ public class RootShell {
|
||||
* @return The exit value of the command.
|
||||
*/
|
||||
public int run(@Nullable final Collection<String> output, final String command)
|
||||
throws IOException, NoRootException {
|
||||
throws IOException, RootShellException {
|
||||
synchronized (lock) {
|
||||
/* Start inside synchronized block to prevent a concurrent call to stop(). */
|
||||
start();
|
||||
@@ -121,24 +119,24 @@ public class RootShell {
|
||||
}
|
||||
}
|
||||
if (markersSeen != 4)
|
||||
throw new IOException("Expected 4 markers, received " + markersSeen);
|
||||
throw new RootShellException(Reason.SHELL_MARKER_COUNT_ERROR, markersSeen);
|
||||
if (errnoStdout != errnoStderr)
|
||||
throw new IOException("Unable to read exit status");
|
||||
throw new RootShellException(Reason.SHELL_EXIT_STATUS_READ_ERROR);
|
||||
Log.v(TAG, "exit: " + errnoStdout);
|
||||
return errnoStdout;
|
||||
}
|
||||
}
|
||||
|
||||
public void start() throws IOException, NoRootException {
|
||||
public void start() throws IOException, RootShellException {
|
||||
if (!isExecutableInPath(SU))
|
||||
throw new NoRootException(deviceNotRootedMessage);
|
||||
throw new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
synchronized (lock) {
|
||||
if (isRunning())
|
||||
return;
|
||||
if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
|
||||
throw new FileNotFoundException("Could not create local binary directory");
|
||||
throw new RootShellException(Reason.CREATE_BIN_DIR_ERROR);
|
||||
if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
|
||||
throw new FileNotFoundException("Could not create local temporary directory");
|
||||
throw new RootShellException(Reason.CREATE_TEMP_DIR_ERROR);
|
||||
try {
|
||||
final ProcessBuilder builder = new ProcessBuilder().command(SU);
|
||||
builder.environment().put("LC_ALL", "C");
|
||||
@@ -146,7 +144,9 @@ public class RootShell {
|
||||
process = builder.start();
|
||||
} catch (final IOException e) {
|
||||
// A failure at this stage means the device isn't rooted.
|
||||
throw new NoRootException(deviceNotRootedMessage, e);
|
||||
final RootShellException rse = new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
rse.initCause(e);
|
||||
throw rse;
|
||||
}
|
||||
stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8);
|
||||
stdout = new BufferedReader(new InputStreamReader(process.getInputStream(),
|
||||
@@ -159,18 +159,18 @@ public class RootShell {
|
||||
final String uid = stdout.readLine();
|
||||
if (!"0".equals(uid)) {
|
||||
Log.w(TAG, "Root check did not return correct UID: " + uid);
|
||||
throw new NoRootException(deviceNotRootedMessage);
|
||||
throw new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
}
|
||||
if (!isRunning()) {
|
||||
String line;
|
||||
while ((line = stderr.readLine()) != null) {
|
||||
Log.w(TAG, "Root check returned an error: " + line);
|
||||
if (line.contains("Permission denied"))
|
||||
throw new NoRootException(deviceNotRootedMessage);
|
||||
throw new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||
}
|
||||
throw new IOException("Shell failed to start: " + process.exitValue());
|
||||
throw new RootShellException(Reason.SHELL_START_ERROR, process.exitValue());
|
||||
}
|
||||
} catch (final IOException | NoRootException e) {
|
||||
} catch (final IOException | RootShellException e) {
|
||||
stop();
|
||||
throw e;
|
||||
}
|
||||
@@ -186,13 +186,33 @@ public class RootShell {
|
||||
}
|
||||
}
|
||||
|
||||
public static class NoRootException extends Exception {
|
||||
public NoRootException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
public static class RootShellException extends Exception {
|
||||
private final Object[] format;
|
||||
private final Reason reason;
|
||||
public RootShellException(final Reason reason, final Object... format) {
|
||||
this.reason = reason;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public NoRootException(final String message) {
|
||||
super(message);
|
||||
public Object[] getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public boolean isIORelated() {
|
||||
return reason != Reason.NO_ROOT_ACCESS;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
NO_ROOT_ACCESS,
|
||||
SHELL_MARKER_COUNT_ERROR,
|
||||
SHELL_EXIT_STATUS_READ_ERROR,
|
||||
SHELL_START_ERROR,
|
||||
CREATE_BIN_DIR_ERROR,
|
||||
CREATE_TEMP_DIR_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
@NonNullForAll
|
||||
public final class SharedLibraryLoader {
|
||||
private static final String TAG = "WireGuard/" + SharedLibraryLoader.class.getSimpleName();
|
||||
|
||||
private SharedLibraryLoader() {
|
||||
}
|
||||
|
||||
public static boolean extractLibrary(final Context context, final String libName, final File destination) throws IOException {
|
||||
final Collection<String> apks = new HashSet<>();
|
||||
if (context.getApplicationInfo().sourceDir != null)
|
||||
apks.add(context.getApplicationInfo().sourceDir);
|
||||
if (context.getApplicationInfo().splitSourceDirs != null)
|
||||
apks.addAll(Arrays.asList(context.getApplicationInfo().splitSourceDirs));
|
||||
|
||||
for (final String abi : Build.SUPPORTED_ABIS) {
|
||||
for (final String apk : apks) {
|
||||
final ZipFile zipFile;
|
||||
try {
|
||||
zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
final String mappedLibName = System.mapLibraryName(libName);
|
||||
final byte[] buffer = new byte[1024 * 32];
|
||||
final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
|
||||
final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
|
||||
if (zipEntry == null)
|
||||
continue;
|
||||
Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
|
||||
try (final FileOutputStream out = new FileOutputStream(destination);
|
||||
final InputStream in = zipFile.getInputStream(zipEntry)) {
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
out.getFD().sync();
|
||||
}
|
||||
zipFile.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void loadSharedLibrary(final Context context, final String libName) {
|
||||
Throwable noAbiException;
|
||||
try {
|
||||
System.loadLibrary(libName);
|
||||
return;
|
||||
} catch (final UnsatisfiedLinkError e) {
|
||||
Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e);
|
||||
noAbiException = e;
|
||||
}
|
||||
File f = null;
|
||||
try {
|
||||
f = File.createTempFile("lib", ".so", context.getCodeCacheDir());
|
||||
if (extractLibrary(context, libName, f)) {
|
||||
System.load(f.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.d(TAG, "Failed to load library apk:/" + libName, e);
|
||||
noAbiException = e;
|
||||
} finally {
|
||||
if (f != null)
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
f.delete();
|
||||
}
|
||||
if (noAbiException instanceof RuntimeException)
|
||||
throw (RuntimeException) noAbiException;
|
||||
throw new RuntimeException(noAbiException);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.util.RootShell.RootShellException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Helper to install WireGuard tools to the system partition.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public final class ToolsInstaller {
|
||||
public static final int ERROR = 0x0;
|
||||
public static final int MAGISK = 0x4;
|
||||
public static final int NO = 0x2;
|
||||
public static final int SYSTEM = 0x8;
|
||||
public static final int YES = 0x1;
|
||||
private static final String[] EXECUTABLES = {"wg", "wg-quick"};
|
||||
private static final File[] INSTALL_DIRS = {
|
||||
new File("/system/xbin"),
|
||||
new File("/system/bin"),
|
||||
};
|
||||
@Nullable private static final File INSTALL_DIR = getInstallDir();
|
||||
private static final String TAG = "WireGuard/" + ToolsInstaller.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final File localBinaryDir;
|
||||
private final Object lock = new Object();
|
||||
private final RootShell rootShell;
|
||||
@Nullable private Boolean areToolsAvailable;
|
||||
@Nullable private Boolean installAsMagiskModule;
|
||||
|
||||
public ToolsInstaller(final Context context, final RootShell rootShell) {
|
||||
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
|
||||
this.context = context;
|
||||
this.rootShell = rootShell;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static File getInstallDir() {
|
||||
final String path = System.getenv("PATH");
|
||||
if (path == null)
|
||||
return INSTALL_DIRS[0];
|
||||
final List<String> paths = Arrays.asList(path.split(":"));
|
||||
for (final File dir : INSTALL_DIRS) {
|
||||
if (paths.contains(dir.getPath()) && dir.isDirectory())
|
||||
return dir;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int areInstalled() throws RootShellException {
|
||||
if (INSTALL_DIR == null)
|
||||
return ERROR;
|
||||
final StringBuilder script = new StringBuilder();
|
||||
for (final String name : EXECUTABLES) {
|
||||
script.append(String.format("cmp -s '%s' '%s' && ",
|
||||
new File(localBinaryDir, name).getAbsolutePath(),
|
||||
new File(INSTALL_DIR, name).getAbsolutePath()));
|
||||
}
|
||||
script.append("exit ").append(OsConstants.EALREADY).append(';');
|
||||
try {
|
||||
final int ret = rootShell.run(null, script.toString());
|
||||
if (ret == OsConstants.EALREADY)
|
||||
return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
|
||||
else
|
||||
return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
} catch (final RootShellException e) {
|
||||
if (e.isIORelated())
|
||||
return ERROR;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureToolsAvailable() throws FileNotFoundException {
|
||||
synchronized (lock) {
|
||||
if (areToolsAvailable == null) {
|
||||
try {
|
||||
Log.d(TAG, extract() ? "Tools are now extracted into our private binary dir" :
|
||||
"Tools were already extracted into our private binary dir");
|
||||
areToolsAvailable = true;
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "The wg and wg-quick tools are not available", e);
|
||||
areToolsAvailable = false;
|
||||
}
|
||||
}
|
||||
if (!areToolsAvailable)
|
||||
throw new FileNotFoundException("Required tools unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean extract() throws IOException {
|
||||
localBinaryDir.mkdirs();
|
||||
final File files[] = new File[EXECUTABLES.length];
|
||||
final File tempFiles[] = new File[EXECUTABLES.length];
|
||||
boolean allExist = true;
|
||||
for (int i = 0; i < files.length; ++i) {
|
||||
files[i] = new File(localBinaryDir, EXECUTABLES[i]);
|
||||
tempFiles[i] = new File(localBinaryDir, EXECUTABLES[i] + ".tmp");
|
||||
allExist &= files[i].exists();
|
||||
}
|
||||
if (allExist)
|
||||
return false;
|
||||
for (int i = 0; i < files.length; ++i) {
|
||||
if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], tempFiles[i]))
|
||||
throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]);
|
||||
if (!tempFiles[i].setExecutable(true, false))
|
||||
throw new IOException("Unable to mark " + tempFiles[i].getAbsolutePath() + " as executable");
|
||||
if (!tempFiles[i].renameTo(files[i]))
|
||||
throw new IOException("Unable to rename " + tempFiles[i].getAbsolutePath() + " to " + files[i].getAbsolutePath());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int install() throws RootShellException, IOException {
|
||||
if (!context.getPackageName().startsWith("com.wireguard."))
|
||||
throw new SecurityException("The tools may only be installed system-wide from the main WireGuard app.");
|
||||
return willInstallAsMagiskModule() ? installMagisk() : installSystem();
|
||||
}
|
||||
|
||||
private int installMagisk() throws RootShellException, IOException {
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
|
||||
script.append("trap 'rm -rf /sbin/.magisk/img/wireguard' INT TERM EXIT; ");
|
||||
script.append(String.format("rm -rf /sbin/.magisk/img/wireguard/; mkdir -p /sbin/.magisk/img/wireguard%s; ", INSTALL_DIR));
|
||||
script.append("printf 'name=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /sbin/.magisk/img/wireguard/module.prop; ");
|
||||
script.append("touch /sbin/.magisk/img/wireguard/auto_mount; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File("/sbin/.magisk/img/wireguard" + INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
script.append("trap - INT TERM EXIT;");
|
||||
|
||||
try {
|
||||
return rootShell.run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
} catch (final RootShellException e) {
|
||||
if (e.isIORelated())
|
||||
return ERROR;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private int installSystem() throws RootShellException, IOException {
|
||||
if (INSTALL_DIR == null)
|
||||
return OsConstants.ENOENT;
|
||||
extract();
|
||||
final StringBuilder script = new StringBuilder("set -ex; ");
|
||||
script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
|
||||
for (final String name : EXECUTABLES) {
|
||||
final File destination = new File(INSTALL_DIR, name);
|
||||
script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
|
||||
new File(localBinaryDir, name), destination, destination, destination));
|
||||
}
|
||||
try {
|
||||
return rootShell.run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
|
||||
} catch (final IOException ignored) {
|
||||
return ERROR;
|
||||
} catch (final RootShellException e) {
|
||||
if (e.isIORelated())
|
||||
return ERROR;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean willInstallAsMagiskModule() {
|
||||
synchronized (lock) {
|
||||
if (installAsMagiskModule == null) {
|
||||
try {
|
||||
installAsMagiskModule = rootShell.run(null, "[ -d /sbin/.magisk/mirror -a -d /sbin/.magisk/img -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||
} catch (final Exception ignored) {
|
||||
installAsMagiskModule = false;
|
||||
}
|
||||
}
|
||||
return installAsMagiskModule;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import java9.util.Optional;
|
||||
|
||||
@NonNullForAll
|
||||
public final class Attribute {
|
||||
private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
|
||||
private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
|
||||
|
||||
private final String key;
|
||||
private final String value;
|
||||
|
||||
private Attribute(final String key, final String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static String join(final Iterable<?> values) {
|
||||
final Iterator<?> it = values.iterator();
|
||||
if (!it.hasNext()) {
|
||||
return "";
|
||||
}
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(it.next());
|
||||
while (it.hasNext()) {
|
||||
sb.append(", ");
|
||||
sb.append(it.next());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static Optional<Attribute> parse(final CharSequence line) {
|
||||
final Matcher matcher = LINE_PATTERN.matcher(line);
|
||||
if (!matcher.matches())
|
||||
return Optional.empty();
|
||||
return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
|
||||
}
|
||||
|
||||
public static String[] split(final CharSequence value) {
|
||||
return LIST_SEPARATOR.split(value);
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@NonNullForAll
|
||||
public class BadConfigException extends Exception {
|
||||
private final Location location;
|
||||
private final Reason reason;
|
||||
private final Section section;
|
||||
@Nullable private final CharSequence text;
|
||||
|
||||
private BadConfigException(final Section section, final Location location,
|
||||
final Reason reason, @Nullable final CharSequence text,
|
||||
@Nullable final Throwable cause) {
|
||||
super(cause);
|
||||
this.section = section;
|
||||
this.location = location;
|
||||
this.reason = reason;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public BadConfigException(final Section section, final Location location,
|
||||
final Reason reason, @Nullable final CharSequence text) {
|
||||
this(section, location, reason, text, null);
|
||||
}
|
||||
|
||||
public BadConfigException(final Section section, final Location location,
|
||||
final KeyFormatException cause) {
|
||||
this(section, location, Reason.INVALID_KEY, null, cause);
|
||||
}
|
||||
|
||||
public BadConfigException(final Section section, final Location location,
|
||||
@Nullable final CharSequence text,
|
||||
final NumberFormatException cause) {
|
||||
this(section, location, Reason.INVALID_NUMBER, text, cause);
|
||||
}
|
||||
|
||||
public BadConfigException(final Section section, final Location location,
|
||||
final ParseException cause) {
|
||||
this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
|
||||
}
|
||||
|
||||
public Location getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public Section getSection() {
|
||||
return section;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CharSequence getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public enum Location {
|
||||
TOP_LEVEL(""),
|
||||
ADDRESS("Address"),
|
||||
ALLOWED_IPS("AllowedIPs"),
|
||||
DNS("DNS"),
|
||||
ENDPOINT("Endpoint"),
|
||||
EXCLUDED_APPLICATIONS("ExcludedApplications"),
|
||||
LISTEN_PORT("ListenPort"),
|
||||
MTU("MTU"),
|
||||
PERSISTENT_KEEPALIVE("PersistentKeepalive"),
|
||||
PRE_SHARED_KEY("PresharedKey"),
|
||||
PRIVATE_KEY("PrivateKey"),
|
||||
PUBLIC_KEY("PublicKey");
|
||||
|
||||
private final String name;
|
||||
|
||||
Location(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
INVALID_KEY,
|
||||
INVALID_NUMBER,
|
||||
INVALID_VALUE,
|
||||
MISSING_ATTRIBUTE,
|
||||
MISSING_SECTION,
|
||||
MISSING_VALUE,
|
||||
SYNTAX_ERROR,
|
||||
UNKNOWN_ATTRIBUTE,
|
||||
UNKNOWN_SECTION
|
||||
}
|
||||
|
||||
public enum Section {
|
||||
CONFIG("Config"),
|
||||
INTERFACE("Interface"),
|
||||
PEER("Peer");
|
||||
|
||||
private final String name;
|
||||
|
||||
Section(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 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.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Represents the contents of a wg-quick configuration file, made up of one or more "Interface"
|
||||
* sections (combined together), and zero or more "Peer" sections (treated individually).
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class Config {
|
||||
private final Interface interfaze;
|
||||
private final List<Peer> peers;
|
||||
|
||||
private Config(final Builder builder) {
|
||||
interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
|
||||
// Defensively copy to ensure immutability even if the Builder is reused.
|
||||
peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
|
||||
* {@link BadConfigException} if the input is not well-formed or contains data that cannot
|
||||
* be parsed.
|
||||
*
|
||||
* @param stream a stream of UTF-8 text that is interpreted as a WireGuard configuration
|
||||
* @return a {@code Config} instance representing the supplied configuration
|
||||
*/
|
||||
public static Config parse(final InputStream stream)
|
||||
throws IOException, BadConfigException {
|
||||
return parse(new BufferedReader(new InputStreamReader(stream)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "Interface" and "Peer" sections into a {@code Config}. Throws
|
||||
* {@link BadConfigException} if the input is not well-formed or contains data that cannot
|
||||
* be parsed.
|
||||
*
|
||||
* @param reader a BufferedReader of UTF-8 text that is interpreted as a WireGuard configuration
|
||||
* @return a {@code Config} instance representing the supplied configuration
|
||||
*/
|
||||
public static Config parse(final BufferedReader reader)
|
||||
throws IOException, BadConfigException {
|
||||
final Builder builder = new Builder();
|
||||
final Collection<String> interfaceLines = new ArrayList<>();
|
||||
final Collection<String> peerLines = new ArrayList<>();
|
||||
boolean inInterfaceSection = false;
|
||||
boolean inPeerSection = false;
|
||||
@Nullable String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final int commentIndex = line.indexOf('#');
|
||||
if (commentIndex != -1)
|
||||
line = line.substring(0, commentIndex);
|
||||
line = line.trim();
|
||||
if (line.isEmpty())
|
||||
continue;
|
||||
if (line.startsWith("[")) {
|
||||
// Consume all [Peer] lines read so far.
|
||||
if (inPeerSection) {
|
||||
builder.parsePeer(peerLines);
|
||||
peerLines.clear();
|
||||
}
|
||||
if ("[Interface]".equalsIgnoreCase(line)) {
|
||||
inInterfaceSection = true;
|
||||
inPeerSection = false;
|
||||
} else if ("[Peer]".equalsIgnoreCase(line)) {
|
||||
inInterfaceSection = false;
|
||||
inPeerSection = true;
|
||||
} else {
|
||||
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
|
||||
Reason.UNKNOWN_SECTION, line);
|
||||
}
|
||||
} else if (inInterfaceSection) {
|
||||
interfaceLines.add(line);
|
||||
} else if (inPeerSection) {
|
||||
peerLines.add(line);
|
||||
} else {
|
||||
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
|
||||
Reason.UNKNOWN_SECTION, line);
|
||||
}
|
||||
}
|
||||
if (inPeerSection)
|
||||
builder.parsePeer(peerLines);
|
||||
else if (!inInterfaceSection)
|
||||
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL,
|
||||
Reason.MISSING_SECTION, null);
|
||||
// Combine all [Interface] sections in the file.
|
||||
builder.parseInterface(interfaceLines);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof Config))
|
||||
return false;
|
||||
final Config other = (Config) obj;
|
||||
return interfaze.equals(other.interfaze) && peers.equals(other.peers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interface section of the configuration.
|
||||
*
|
||||
* @return the interface configuration
|
||||
*/
|
||||
public Interface getInterface() {
|
||||
return interfaze;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of the configuration's peer sections.
|
||||
*
|
||||
* @return a list of {@link Peer}s
|
||||
*/
|
||||
public List<Peer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * interfaze.hashCode() + peers.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Config} into a string suitable for debugging purposes. The {@code Config}
|
||||
* is identified by its interface's public key and the number of peers it has.
|
||||
*
|
||||
* @return a concise single-line identifier for the {@code Config}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(Config " + interfaze + " (" + peers.size() + " peers))";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Config} into a string suitable for use as a {@code wg-quick}
|
||||
* configuration file.
|
||||
*
|
||||
* @return the {@code Config} represented as one [Interface] and zero or more [Peer] sections
|
||||
*/
|
||||
public String toWgQuickString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("[Interface]\n").append(interfaze.toWgQuickString());
|
||||
for (final Peer peer : peers)
|
||||
sb.append("\n[Peer]\n").append(peer.toWgQuickString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the {@code Config} for use with the WireGuard cross-platform userspace API.
|
||||
*
|
||||
* @return the {@code Config} represented as a series of "key=value" lines
|
||||
*/
|
||||
public String toWgUserspaceString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(interfaze.toWgUserspaceString());
|
||||
sb.append("replace_peers=true\n");
|
||||
for (final Peer peer : peers)
|
||||
sb.append(peer.toWgUserspaceString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// Defaults to an empty set.
|
||||
private final Set<Peer> peers = new LinkedHashSet<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private Interface interfaze;
|
||||
|
||||
public Builder addPeer(final Peer peer) {
|
||||
peers.add(peer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addPeers(final Collection<Peer> peers) {
|
||||
this.peers.addAll(peers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Config build() {
|
||||
if (interfaze == null)
|
||||
throw new IllegalArgumentException("An [Interface] section is required");
|
||||
return new Config(this);
|
||||
}
|
||||
|
||||
public Builder parseInterface(final Iterable<? extends CharSequence> lines)
|
||||
throws BadConfigException {
|
||||
return setInterface(Interface.parse(lines));
|
||||
}
|
||||
|
||||
public Builder parsePeer(final Iterable<? extends CharSequence> lines)
|
||||
throws BadConfigException {
|
||||
return addPeer(Peer.parse(lines));
|
||||
}
|
||||
|
||||
public Builder setInterface(final Interface interfaze) {
|
||||
this.interfaze = interfaze;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utility methods for creating instances of {@link InetAddress}.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class InetAddresses {
|
||||
@Nullable private static final Method PARSER_METHOD;
|
||||
private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
|
||||
|
||||
static {
|
||||
Method m = null;
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
|
||||
// noinspection JavaReflectionMemberAccess
|
||||
m = InetAddress.class.getMethod("parseNumericAddress", String.class);
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
PARSER_METHOD = m;
|
||||
}
|
||||
|
||||
private InetAddresses() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
|
||||
*
|
||||
* @param address a string representing the IP address
|
||||
* @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
|
||||
*/
|
||||
public static InetAddress parse(final String address) throws ParseException {
|
||||
if (address.isEmpty())
|
||||
throw new ParseException(InetAddress.class, address, "Empty address");
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
|
||||
return android.net.InetAddresses.parseNumericAddress(address);
|
||||
else if (PARSER_METHOD != null)
|
||||
return (InetAddress) PARSER_METHOD.invoke(null, address);
|
||||
else
|
||||
throw new NoSuchMethodException("parseNumericAddress");
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new ParseException(InetAddress.class, address, e);
|
||||
} catch (final Exception e) {
|
||||
final Throwable cause = e.getCause();
|
||||
// Re-throw parsing exceptions with the original type, as callers might try to catch
|
||||
// them. On the other hand, callers cannot be expected to handle reflection failures.
|
||||
if (cause instanceof IllegalArgumentException)
|
||||
throw new ParseException(InetAddress.class, address, cause);
|
||||
try {
|
||||
if (WONT_TOUCH_RESOLVER.matcher(address).matches())
|
||||
return InetAddress.getByName(address);
|
||||
else
|
||||
throw new ParseException(InetAddress.class, address, "Not an IP address");
|
||||
} catch (final UnknownHostException f) {
|
||||
throw new ParseException(InetAddress.class, address, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import org.threeten.bp.Duration;
|
||||
import org.threeten.bp.Instant;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
* An external endpoint (host and port) used to connect to a WireGuard {@link Peer}.
|
||||
* <p>
|
||||
* Instances of this class are externally immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class InetEndpoint {
|
||||
private static final Pattern BARE_IPV6 = Pattern.compile("^[^\\[\\]]*:[^\\[\\]]*");
|
||||
private static final Pattern FORBIDDEN_CHARACTERS = Pattern.compile("[/?#]");
|
||||
|
||||
private final String host;
|
||||
private final boolean isResolved;
|
||||
private final Object lock = new Object();
|
||||
private final int port;
|
||||
private Instant lastResolution = Instant.EPOCH;
|
||||
@Nullable private InetEndpoint resolved;
|
||||
|
||||
private InetEndpoint(final String host, final boolean isResolved, final int port) {
|
||||
this.host = host;
|
||||
this.isResolved = isResolved;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public static InetEndpoint parse(final String endpoint) throws ParseException {
|
||||
if (FORBIDDEN_CHARACTERS.matcher(endpoint).find())
|
||||
throw new ParseException(InetEndpoint.class, endpoint, "Forbidden characters");
|
||||
final URI uri;
|
||||
try {
|
||||
uri = new URI("wg://" + endpoint);
|
||||
} catch (final URISyntaxException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
if (uri.getPort() < 0 || uri.getPort() > 65535)
|
||||
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
|
||||
try {
|
||||
InetAddresses.parse(uri.getHost());
|
||||
// Parsing ths host as a numeric address worked, so we don't need to do DNS lookups.
|
||||
return new InetEndpoint(uri.getHost(), true, uri.getPort());
|
||||
} catch (final ParseException ignored) {
|
||||
// Failed to parse the host as a numeric address, so it must be a DNS hostname/FQDN.
|
||||
return new InetEndpoint(uri.getHost(), false, uri.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof InetEndpoint))
|
||||
return false;
|
||||
final InetEndpoint other = (InetEndpoint) obj;
|
||||
return host.equals(other.host) && port == other.port;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Because this function may perform network I/O, it must not be called from the main thread.
|
||||
*
|
||||
* @return the resolved endpoint, or {@link Optional#empty()}
|
||||
*/
|
||||
public Optional<InetEndpoint> getResolved() {
|
||||
if (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) {
|
||||
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);
|
||||
lastResolution = Instant.now();
|
||||
} catch (final UnknownHostException e) {
|
||||
resolved = null;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return host.hashCode() ^ port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final boolean isBareIpv6 = isResolved && BARE_IPV6.matcher(host).matches();
|
||||
return (isBareIpv6 ? '[' + host + ']' : host) + ':' + port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
/**
|
||||
* An Internet network, denoted by its address and netmask
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class InetNetwork {
|
||||
private final InetAddress address;
|
||||
private final int mask;
|
||||
|
||||
private InetNetwork(final InetAddress address, final int mask) {
|
||||
this.address = address;
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
public static InetNetwork parse(final String network) throws ParseException {
|
||||
final int slash = network.lastIndexOf('/');
|
||||
final String maskString;
|
||||
final int rawMask;
|
||||
final String rawAddress;
|
||||
if (slash >= 0) {
|
||||
maskString = network.substring(slash + 1);
|
||||
try {
|
||||
rawMask = Integer.parseInt(maskString, 10);
|
||||
} catch (final NumberFormatException ignored) {
|
||||
throw new ParseException(Integer.class, maskString);
|
||||
}
|
||||
rawAddress = network.substring(0, slash);
|
||||
} else {
|
||||
maskString = "";
|
||||
rawMask = -1;
|
||||
rawAddress = network;
|
||||
}
|
||||
final InetAddress address = InetAddresses.parse(rawAddress);
|
||||
final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
|
||||
if (rawMask > maxMask)
|
||||
throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
|
||||
final int mask = rawMask >= 0 && rawMask <= maxMask ? rawMask : maxMask;
|
||||
return new InetNetwork(address, mask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof InetNetwork))
|
||||
return false;
|
||||
final InetNetwork other = (InetNetwork) obj;
|
||||
return address.equals(other.address) && mask == other.mask;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getMask() {
|
||||
return mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode() ^ mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return address.getHostAddress() + '/' + mask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 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.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.crypto.KeyPair;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Lists;
|
||||
import java9.util.Optional;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard interface (an [Interface] block). Interfaces must
|
||||
* have a private key (used to initialize a {@code KeyPair}), and may optionally have several other
|
||||
* attributes.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class Interface {
|
||||
private static final int MAX_UDP_PORT = 65535;
|
||||
private static final int MIN_UDP_PORT = 0;
|
||||
|
||||
private final Set<InetNetwork> addresses;
|
||||
private final Set<InetAddress> dnsServers;
|
||||
private final Set<String> excludedApplications;
|
||||
private final KeyPair keyPair;
|
||||
private final Optional<Integer> listenPort;
|
||||
private final Optional<Integer> mtu;
|
||||
|
||||
private Interface(final Builder builder) {
|
||||
// Defensively copy to ensure immutability even if the Builder is reused.
|
||||
addresses = Collections.unmodifiableSet(new LinkedHashSet<>(builder.addresses));
|
||||
dnsServers = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsServers));
|
||||
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
|
||||
keyPair = Objects.requireNonNull(builder.keyPair, "Interfaces must have a private key");
|
||||
listenPort = builder.listenPort;
|
||||
mtu = builder.mtu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "KEY = VALUE" lines into an {@code Interface}. Throws
|
||||
* {@link ParseException} if the input is not well-formed or contains unknown attributes.
|
||||
*
|
||||
* @param lines An iterable sequence of lines, containing at least a private key attribute
|
||||
* @return An {@code Interface} with all of the attributes from {@code lines} set
|
||||
*/
|
||||
public static Interface parse(final Iterable<? extends CharSequence> lines)
|
||||
throws BadConfigException {
|
||||
final Builder builder = new Builder();
|
||||
for (final CharSequence line : lines) {
|
||||
final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
|
||||
new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
|
||||
Reason.SYNTAX_ERROR, line));
|
||||
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
|
||||
case "address":
|
||||
builder.parseAddresses(attribute.getValue());
|
||||
break;
|
||||
case "dns":
|
||||
builder.parseDnsServers(attribute.getValue());
|
||||
break;
|
||||
case "excludedapplications":
|
||||
builder.parseExcludedApplications(attribute.getValue());
|
||||
break;
|
||||
case "listenport":
|
||||
builder.parseListenPort(attribute.getValue());
|
||||
break;
|
||||
case "mtu":
|
||||
builder.parseMtu(attribute.getValue());
|
||||
break;
|
||||
case "privatekey":
|
||||
builder.parsePrivateKey(attribute.getValue());
|
||||
break;
|
||||
default:
|
||||
throw new BadConfigException(Section.INTERFACE, Location.TOP_LEVEL,
|
||||
Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof Interface))
|
||||
return false;
|
||||
final Interface other = (Interface) obj;
|
||||
return addresses.equals(other.addresses)
|
||||
&& dnsServers.equals(other.dnsServers)
|
||||
&& excludedApplications.equals(other.excludedApplications)
|
||||
&& keyPair.equals(other.keyPair)
|
||||
&& listenPort.equals(other.listenPort)
|
||||
&& mtu.equals(other.mtu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of IP addresses assigned to the interface.
|
||||
*
|
||||
* @return a set of {@link InetNetwork}s
|
||||
*/
|
||||
public Set<InetNetwork> getAddresses() {
|
||||
// The collection is already immutable.
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of DNS servers associated with the interface.
|
||||
*
|
||||
* @return a set of {@link InetAddress}es
|
||||
*/
|
||||
public Set<InetAddress> getDnsServers() {
|
||||
// The collection is already immutable.
|
||||
return dnsServers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of applications excluded from using the interface.
|
||||
*
|
||||
* @return a set of package names
|
||||
*/
|
||||
public Set<String> getExcludedApplications() {
|
||||
// The collection is already immutable.
|
||||
return excludedApplications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public/private key pair used by the interface.
|
||||
*
|
||||
* @return a key pair
|
||||
*/
|
||||
public KeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UDP port number that the WireGuard interface will listen on.
|
||||
*
|
||||
* @return a UDP port number, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getListenPort() {
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MTU used for the WireGuard interface.
|
||||
*
|
||||
* @return the MTU, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 1;
|
||||
hash = 31 * hash + addresses.hashCode();
|
||||
hash = 31 * hash + dnsServers.hashCode();
|
||||
hash = 31 * hash + excludedApplications.hashCode();
|
||||
hash = 31 * hash + keyPair.hashCode();
|
||||
hash = 31 * hash + listenPort.hashCode();
|
||||
hash = 31 * hash + mtu.hashCode();
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Interface} into a string suitable for debugging purposes. The {@code
|
||||
* Interface} is identified by its public key and (if set) the port used for its UDP socket.
|
||||
*
|
||||
* @return A concise single-line identifier for the {@code Interface}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("(Interface ");
|
||||
sb.append(keyPair.getPublicKey().toBase64());
|
||||
listenPort.ifPresent(lp -> sb.append(" @").append(lp));
|
||||
sb.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Interface} into a string suitable for inclusion in a {@code wg-quick}
|
||||
* configuration file.
|
||||
*
|
||||
* @return The {@code Interface} represented as a series of "Key = Value" lines
|
||||
*/
|
||||
public String toWgQuickString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
if (!addresses.isEmpty())
|
||||
sb.append("Address = ").append(Attribute.join(addresses)).append('\n');
|
||||
if (!dnsServers.isEmpty()) {
|
||||
final List<String> dnsServerStrings = StreamSupport.stream(dnsServers)
|
||||
.map(InetAddress::getHostAddress)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
|
||||
}
|
||||
if (!excludedApplications.isEmpty())
|
||||
sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
|
||||
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
|
||||
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
|
||||
sb.append("PrivateKey = ").append(keyPair.getPrivateKey().toBase64()).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the {@code Interface} for use with the WireGuard cross-platform userspace API.
|
||||
* Note that not all attributes are included in this representation.
|
||||
*
|
||||
* @return the {@code Interface} represented as a series of "KEY=VALUE" lines
|
||||
*/
|
||||
public String toWgUserspaceString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("private_key=").append(keyPair.getPrivateKey().toHex()).append('\n');
|
||||
listenPort.ifPresent(lp -> sb.append("listen_port=").append(lp).append('\n'));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// Defaults to an empty set.
|
||||
private final Set<InetNetwork> addresses = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<InetAddress> dnsServers = new LinkedHashSet<>();
|
||||
// Defaults to an empty set.
|
||||
private final Set<String> excludedApplications = new LinkedHashSet<>();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private KeyPair keyPair;
|
||||
// Defaults to not present.
|
||||
private Optional<Integer> listenPort = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Integer> mtu = Optional.empty();
|
||||
|
||||
public Builder addAddress(final InetNetwork address) {
|
||||
addresses.add(address);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addAddresses(final Collection<InetNetwork> addresses) {
|
||||
this.addresses.addAll(addresses);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addDnsServer(final InetAddress dnsServer) {
|
||||
dnsServers.add(dnsServer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addDnsServers(final Collection<? extends InetAddress> dnsServers) {
|
||||
this.dnsServers.addAll(dnsServers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Interface build() throws BadConfigException {
|
||||
if (keyPair == null)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
|
||||
Reason.MISSING_ATTRIBUTE, null);
|
||||
return new Interface(this);
|
||||
}
|
||||
|
||||
public Builder excludeApplication(final String application) {
|
||||
excludedApplications.add(application);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder excludeApplications(final Collection<String> applications) {
|
||||
excludedApplications.addAll(applications);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parseAddresses(final CharSequence addresses) throws BadConfigException {
|
||||
try {
|
||||
for (final String address : Attribute.split(addresses))
|
||||
addAddress(InetNetwork.parse(address));
|
||||
return this;
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.ADDRESS, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseDnsServers(final CharSequence dnsServers) throws BadConfigException {
|
||||
try {
|
||||
for (final String dnsServer : Attribute.split(dnsServers))
|
||||
addDnsServer(InetAddresses.parse(dnsServer));
|
||||
return this;
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.DNS, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseExcludedApplications(final CharSequence apps) {
|
||||
return excludeApplications(Lists.of(Attribute.split(apps)));
|
||||
}
|
||||
|
||||
public Builder parseListenPort(final String listenPort) throws BadConfigException {
|
||||
try {
|
||||
return setListenPort(Integer.parseInt(listenPort));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT, listenPort, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseMtu(final String mtu) throws BadConfigException {
|
||||
try {
|
||||
return setMtu(Integer.parseInt(mtu));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.MTU, mtu, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePrivateKey(final String privateKey) throws BadConfigException {
|
||||
try {
|
||||
return setKeyPair(new KeyPair(Key.fromBase64(privateKey)));
|
||||
} catch (final KeyFormatException e) {
|
||||
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder setKeyPair(final KeyPair keyPair) {
|
||||
this.keyPair = keyPair;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setListenPort(final int listenPort) throws BadConfigException {
|
||||
if (listenPort < MIN_UDP_PORT || listenPort > MAX_UDP_PORT)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
|
||||
Reason.INVALID_VALUE, String.valueOf(listenPort));
|
||||
this.listenPort = listenPort == 0 ? Optional.empty() : Optional.of(listenPort);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMtu(final int mtu) throws BadConfigException {
|
||||
if (mtu < 0)
|
||||
throw new BadConfigException(Section.INTERFACE, Location.LISTEN_PORT,
|
||||
Reason.INVALID_VALUE, String.valueOf(mtu));
|
||||
this.mtu = mtu == 0 ? Optional.empty() : Optional.of(mtu);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.config;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@NonNullForAll
|
||||
public class ParseException extends Exception {
|
||||
private final Class<?> parsingClass;
|
||||
private final CharSequence text;
|
||||
|
||||
public ParseException(final Class<?> parsingClass, final CharSequence text,
|
||||
@Nullable final String message, @Nullable final Throwable cause) {
|
||||
super(message, cause);
|
||||
this.parsingClass = parsingClass;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public ParseException(final Class<?> parsingClass, final CharSequence text,
|
||||
@Nullable final String message) {
|
||||
this(parsingClass, text, message, null);
|
||||
}
|
||||
|
||||
public ParseException(final Class<?> parsingClass, final CharSequence text,
|
||||
@Nullable final Throwable cause) {
|
||||
this(parsingClass, text, null, cause);
|
||||
}
|
||||
|
||||
public ParseException(final Class<?> parsingClass, final CharSequence text) {
|
||||
this(parsingClass, text, null, null);
|
||||
}
|
||||
|
||||
public Class<?> getParsingClass() {
|
||||
return parsingClass;
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 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.Reason;
|
||||
import com.wireguard.config.BadConfigException.Section;
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.crypto.KeyFormatException;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java9.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents the configuration for a WireGuard peer (a [Peer] block). Peers must have a public key,
|
||||
* and may optionally have several other attributes.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class Peer {
|
||||
private final Set<InetNetwork> allowedIps;
|
||||
private final Optional<InetEndpoint> endpoint;
|
||||
private final Optional<Integer> persistentKeepalive;
|
||||
private final Optional<Key> preSharedKey;
|
||||
private final Key publicKey;
|
||||
|
||||
private Peer(final Builder builder) {
|
||||
// Defensively copy to ensure immutability even if the Builder is reused.
|
||||
allowedIps = Collections.unmodifiableSet(new LinkedHashSet<>(builder.allowedIps));
|
||||
endpoint = builder.endpoint;
|
||||
persistentKeepalive = builder.persistentKeepalive;
|
||||
preSharedKey = builder.preSharedKey;
|
||||
publicKey = Objects.requireNonNull(builder.publicKey, "Peers must have a public key");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an series of "KEY = VALUE" lines into a {@code Peer}. Throws {@link ParseException} if
|
||||
* the input is not well-formed or contains unknown attributes.
|
||||
*
|
||||
* @param lines an iterable sequence of lines, containing at least a public key attribute
|
||||
* @return a {@code Peer} with all of its attributes set from {@code lines}
|
||||
*/
|
||||
public static Peer parse(final Iterable<? extends CharSequence> lines)
|
||||
throws BadConfigException {
|
||||
final Builder builder = new Builder();
|
||||
for (final CharSequence line : lines) {
|
||||
final Attribute attribute = Attribute.parse(line).orElseThrow(() ->
|
||||
new BadConfigException(Section.PEER, Location.TOP_LEVEL,
|
||||
Reason.SYNTAX_ERROR, line));
|
||||
switch (attribute.getKey().toLowerCase(Locale.ENGLISH)) {
|
||||
case "allowedips":
|
||||
builder.parseAllowedIPs(attribute.getValue());
|
||||
break;
|
||||
case "endpoint":
|
||||
builder.parseEndpoint(attribute.getValue());
|
||||
break;
|
||||
case "persistentkeepalive":
|
||||
builder.parsePersistentKeepalive(attribute.getValue());
|
||||
break;
|
||||
case "presharedkey":
|
||||
builder.parsePreSharedKey(attribute.getValue());
|
||||
break;
|
||||
case "publickey":
|
||||
builder.parsePublicKey(attribute.getValue());
|
||||
break;
|
||||
default:
|
||||
throw new BadConfigException(Section.PEER, Location.TOP_LEVEL,
|
||||
Reason.UNKNOWN_ATTRIBUTE, attribute.getKey());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (!(obj instanceof Peer))
|
||||
return false;
|
||||
final Peer other = (Peer) obj;
|
||||
return allowedIps.equals(other.allowedIps)
|
||||
&& endpoint.equals(other.endpoint)
|
||||
&& persistentKeepalive.equals(other.persistentKeepalive)
|
||||
&& preSharedKey.equals(other.preSharedKey)
|
||||
&& publicKey.equals(other.publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's set of allowed IPs.
|
||||
*
|
||||
* @return the set of allowed IPs
|
||||
*/
|
||||
public Set<InetNetwork> getAllowedIps() {
|
||||
// The collection is already immutable.
|
||||
return allowedIps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's endpoint.
|
||||
*
|
||||
* @return the endpoint, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<InetEndpoint> getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's persistent keepalive.
|
||||
*
|
||||
* @return the persistent keepalive, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Integer> getPersistentKeepalive() {
|
||||
return persistentKeepalive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's pre-shared key.
|
||||
*
|
||||
* @return the pre-shared key, or {@code Optional.empty()} if none is configured
|
||||
*/
|
||||
public Optional<Key> getPreSharedKey() {
|
||||
return preSharedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the peer's public key.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public Key getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 1;
|
||||
hash = 31 * hash + allowedIps.hashCode();
|
||||
hash = 31 * hash + endpoint.hashCode();
|
||||
hash = 31 * hash + persistentKeepalive.hashCode();
|
||||
hash = 31 * hash + preSharedKey.hashCode();
|
||||
hash = 31 * hash + publicKey.hashCode();
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("(Peer ");
|
||||
sb.append(publicKey.toBase64());
|
||||
endpoint.ifPresent(ep -> sb.append(" @").append(ep));
|
||||
sb.append(')');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@code Peer} into a string suitable for inclusion in a {@code wg-quick}
|
||||
* configuration file.
|
||||
*
|
||||
* @return the {@code Peer} represented as a series of "Key = Value" lines
|
||||
*/
|
||||
public String toWgQuickString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
if (!allowedIps.isEmpty())
|
||||
sb.append("AllowedIPs = ").append(Attribute.join(allowedIps)).append('\n');
|
||||
endpoint.ifPresent(ep -> sb.append("Endpoint = ").append(ep).append('\n'));
|
||||
persistentKeepalive.ifPresent(pk -> sb.append("PersistentKeepalive = ").append(pk).append('\n'));
|
||||
preSharedKey.ifPresent(psk -> sb.append("PreSharedKey = ").append(psk.toBase64()).append('\n'));
|
||||
sb.append("PublicKey = ").append(publicKey.toBase64()).append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the {@code Peer} for use with the WireGuard cross-platform userspace API. Note
|
||||
* that not all attributes are included in this representation.
|
||||
*
|
||||
* @return the {@code Peer} represented as a series of "key=value" lines
|
||||
*/
|
||||
public String toWgUserspaceString() {
|
||||
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'));
|
||||
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();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static final class Builder {
|
||||
// See wg(8)
|
||||
private static final int MAX_PERSISTENT_KEEPALIVE = 65535;
|
||||
|
||||
// Defaults to an empty set.
|
||||
private final Set<InetNetwork> allowedIps = new LinkedHashSet<>();
|
||||
// Defaults to not present.
|
||||
private Optional<InetEndpoint> endpoint = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Integer> persistentKeepalive = Optional.empty();
|
||||
// Defaults to not present.
|
||||
private Optional<Key> preSharedKey = Optional.empty();
|
||||
// No default; must be provided before building.
|
||||
@Nullable private Key publicKey;
|
||||
|
||||
public Builder addAllowedIp(final InetNetwork allowedIp) {
|
||||
allowedIps.add(allowedIp);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addAllowedIps(final Collection<InetNetwork> allowedIps) {
|
||||
this.allowedIps.addAll(allowedIps);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Peer build() throws BadConfigException {
|
||||
if (publicKey == null)
|
||||
throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY,
|
||||
Reason.MISSING_ATTRIBUTE, null);
|
||||
return new Peer(this);
|
||||
}
|
||||
|
||||
public Builder parseAllowedIPs(final CharSequence allowedIps) throws BadConfigException {
|
||||
try {
|
||||
for (final String allowedIp : Attribute.split(allowedIps))
|
||||
addAllowedIp(InetNetwork.parse(allowedIp));
|
||||
return this;
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.ALLOWED_IPS, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parseEndpoint(final String endpoint) throws BadConfigException {
|
||||
try {
|
||||
return setEndpoint(InetEndpoint.parse(endpoint));
|
||||
} catch (final ParseException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.ENDPOINT, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePersistentKeepalive(final String persistentKeepalive)
|
||||
throws BadConfigException {
|
||||
try {
|
||||
return setPersistentKeepalive(Integer.parseInt(persistentKeepalive));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
|
||||
persistentKeepalive, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePreSharedKey(final String preSharedKey) throws BadConfigException {
|
||||
try {
|
||||
return setPreSharedKey(Key.fromBase64(preSharedKey));
|
||||
} catch (final KeyFormatException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.PRE_SHARED_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder parsePublicKey(final String publicKey) throws BadConfigException {
|
||||
try {
|
||||
return setPublicKey(Key.fromBase64(publicKey));
|
||||
} catch (final KeyFormatException e) {
|
||||
throw new BadConfigException(Section.PEER, Location.PUBLIC_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder setEndpoint(final InetEndpoint endpoint) {
|
||||
this.endpoint = Optional.of(endpoint);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPersistentKeepalive(final int persistentKeepalive)
|
||||
throws BadConfigException {
|
||||
if (persistentKeepalive < 0 || persistentKeepalive > MAX_PERSISTENT_KEEPALIVE)
|
||||
throw new BadConfigException(Section.PEER, Location.PERSISTENT_KEEPALIVE,
|
||||
Reason.INVALID_VALUE, String.valueOf(persistentKeepalive));
|
||||
this.persistentKeepalive = persistentKeepalive == 0 ?
|
||||
Optional.empty() : Optional.of(persistentKeepalive);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPreSharedKey(final Key preSharedKey) {
|
||||
this.preSharedKey = Optional.of(preSharedKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPublicKey(final Key publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -1,15 +1,17 @@
|
||||
/*
|
||||
* Copyright © 2016 Southern Storm Software, Pty Ltd.
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Implementation of the Curve25519 elliptic curve algorithm.
|
||||
* <p>
|
||||
@@ -25,6 +27,7 @@ import java.util.Arrays;
|
||||
* References: http://cr.yp.to/ecdh.html, RFC 7748
|
||||
*/
|
||||
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
|
||||
@NonNullForAll
|
||||
public final class Curve25519 {
|
||||
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
|
||||
private static final int NUM_LIMBS_255BIT = 10;
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.crypto.KeyFormatException.Type;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a WireGuard public or private key. This class uses specialized constant-time base64
|
||||
* and hexadecimal codec implementations that resist side-channel attacks.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@SuppressWarnings("MagicNumber")
|
||||
@NonNullForAll
|
||||
public final class Key {
|
||||
private final byte[] key;
|
||||
|
||||
/**
|
||||
* Constructs an object encapsulating the supplied key.
|
||||
*
|
||||
* @param key an array of bytes containing a binary key. Callers of this constructor are
|
||||
* responsible for ensuring that the array is of the correct length.
|
||||
*/
|
||||
private Key(final byte[] key) {
|
||||
// Defensively copy to ensure immutability.
|
||||
this.key = Arrays.copyOf(key, key.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a single 4-character base64 chunk to an integer in constant time.
|
||||
*
|
||||
* @param src an array of at least 4 characters in base64 format
|
||||
* @param srcOffset the offset of the beginning of the chunk in {@code src}
|
||||
* @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
|
||||
* valid base64
|
||||
*/
|
||||
private static int decodeBase64(final char[] src, final int srcOffset) {
|
||||
int val = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
final char c = src[i + srcOffset];
|
||||
val |= (-1
|
||||
+ ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
|
||||
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
|
||||
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
|
||||
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
|
||||
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
|
||||
) << (18 - 6 * i);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
|
||||
*
|
||||
* @param src an array of at least 3 bytes
|
||||
* @param srcOffset the offset of the beginning of the chunk in {@code src}
|
||||
* @param dest an array of at least 4 characters
|
||||
* @param destOffset the offset of the beginning of the chunk in {@code dest}
|
||||
*/
|
||||
private static void encodeBase64(final byte[] src, final int srcOffset,
|
||||
final char[] dest, final int destOffset) {
|
||||
final byte[] input = {
|
||||
(byte) ((src[srcOffset] >>> 2) & 63),
|
||||
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
|
||||
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
|
||||
(byte) ((src[2 + srcOffset]) & 63),
|
||||
};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
dest[i + destOffset] = (char) (input[i] + 'A'
|
||||
+ (((25 - input[i]) >>> 8) & 6)
|
||||
- (((51 - input[i]) >>> 8) & 75)
|
||||
- (((61 - input[i]) >>> 8) & 15)
|
||||
+ (((62 - input[i]) >>> 8) & 3));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a WireGuard public or private key from its base64 string representation. This
|
||||
* function throws a {@link KeyFormatException} if the source string is not well-formed.
|
||||
*
|
||||
* @param str the base64 string representation of a WireGuard key
|
||||
* @return the decoded key encapsulated in an immutable container
|
||||
*/
|
||||
public static Key fromBase64(final String str) throws KeyFormatException {
|
||||
final char[] input = str.toCharArray();
|
||||
if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
|
||||
throw new KeyFormatException(Format.BASE64, Type.LENGTH);
|
||||
final byte[] key = new byte[Format.BINARY.length];
|
||||
int i;
|
||||
int ret = 0;
|
||||
for (i = 0; i < key.length / 3; ++i) {
|
||||
final int val = decodeBase64(input, i * 4);
|
||||
ret |= val >>> 31;
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
key[i * 3 + 2] = (byte) (val & 0xff);
|
||||
}
|
||||
final char[] endSegment = {
|
||||
input[i * 4],
|
||||
input[i * 4 + 1],
|
||||
input[i * 4 + 2],
|
||||
'A',
|
||||
};
|
||||
final int val = decodeBase64(endSegment, 0);
|
||||
ret |= (val >>> 31) | (val & 0xff);
|
||||
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||
|
||||
if (ret != 0)
|
||||
throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
|
||||
return new Key(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a WireGuard public or private key in an immutable container. This function throws a
|
||||
* {@link KeyFormatException} if the source data is not the correct length.
|
||||
*
|
||||
* @param bytes an array of bytes containing a WireGuard key in binary format
|
||||
* @return the key encapsulated in an immutable container
|
||||
*/
|
||||
public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
|
||||
if (bytes.length != Format.BINARY.length)
|
||||
throw new KeyFormatException(Format.BINARY, Type.LENGTH);
|
||||
return new Key(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a WireGuard public or private key from its hexadecimal string representation. This
|
||||
* function throws a {@link KeyFormatException} if the source string is not well-formed.
|
||||
*
|
||||
* @param str the hexadecimal string representation of a WireGuard key
|
||||
* @return the decoded key encapsulated in an immutable container
|
||||
*/
|
||||
public static Key fromHex(final String str) throws KeyFormatException {
|
||||
final char[] input = str.toCharArray();
|
||||
if (input.length != Format.HEX.length)
|
||||
throw new KeyFormatException(Format.HEX, Type.LENGTH);
|
||||
final byte[] key = new byte[Format.BINARY.length];
|
||||
int ret = 0;
|
||||
for (int i = 0; i < key.length; ++i) {
|
||||
int c;
|
||||
int cNum;
|
||||
int cNum0;
|
||||
int cAlpha;
|
||||
int cAlpha0;
|
||||
int cVal;
|
||||
final int cAcc;
|
||||
|
||||
c = input[i * 2];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
cAcc = cVal * 16;
|
||||
|
||||
c = input[i * 2 + 1];
|
||||
cNum = c ^ 48;
|
||||
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||
cAlpha = (c & ~32) - 55;
|
||||
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||
key[i] = (byte) (cAcc | cVal);
|
||||
}
|
||||
if (ret != 0)
|
||||
throw new KeyFormatException(Format.HEX, Type.CONTENTS);
|
||||
return new Key(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a private key using the system's {@link SecureRandom} number generator.
|
||||
*
|
||||
* @return a well-formed random private key
|
||||
*/
|
||||
static Key generatePrivateKey() {
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
final byte[] privateKey = new byte[Format.BINARY.getLength()];
|
||||
secureRandom.nextBytes(privateKey);
|
||||
privateKey[0] &= 248;
|
||||
privateKey[31] &= 127;
|
||||
privateKey[31] |= 64;
|
||||
return new Key(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a public key from an existing private key.
|
||||
*
|
||||
* @param privateKey a private key
|
||||
* @return a well-formed public key that corresponds to the supplied private key
|
||||
*/
|
||||
static Key generatePublicKey(final Key privateKey) {
|
||||
final byte[] publicKey = new byte[Format.BINARY.getLength()];
|
||||
Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
|
||||
return new Key(publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (obj == this)
|
||||
return true;
|
||||
if (obj == null || obj.getClass() != getClass())
|
||||
return false;
|
||||
final Key other = (Key) obj;
|
||||
return MessageDigest.isEqual(key, other.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key as an array of bytes.
|
||||
*
|
||||
* @return an array of bytes containing the raw binary key
|
||||
*/
|
||||
public byte[] getBytes() {
|
||||
// Defensively copy to ensure immutability.
|
||||
return Arrays.copyOf(key, key.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int ret = 0;
|
||||
for (int i = 0; i < key.length / 4; ++i)
|
||||
ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the key to base64.
|
||||
*
|
||||
* @return a string containing the encoded key
|
||||
*/
|
||||
public String toBase64() {
|
||||
final char[] output = new char[Format.BASE64.length];
|
||||
int i;
|
||||
for (i = 0; i < key.length / 3; ++i)
|
||||
encodeBase64(key, i * 3, output, i * 4);
|
||||
final byte[] endSegment = {
|
||||
key[i * 3],
|
||||
key[i * 3 + 1],
|
||||
0,
|
||||
};
|
||||
encodeBase64(endSegment, 0, output, i * 4);
|
||||
output[Format.BASE64.length - 1] = '=';
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the key to hexadecimal ASCII characters.
|
||||
*
|
||||
* @return a string containing the encoded key
|
||||
*/
|
||||
public String toHex() {
|
||||
final char[] output = new char[Format.HEX.length];
|
||||
for (int i = 0; i < key.length; ++i) {
|
||||
output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
|
||||
+ ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
|
||||
output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
|
||||
+ ((((key[i] & 0xf) - 10) >> 8) & ~38));
|
||||
}
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported formats for encoding a WireGuard key.
|
||||
*/
|
||||
public enum Format {
|
||||
BASE64(44),
|
||||
BINARY(32),
|
||||
HEX(64);
|
||||
|
||||
private final int length;
|
||||
|
||||
Format(final int length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
/**
|
||||
* An exception thrown when attempting to parse an invalid key (too short, too long, or byte
|
||||
* data inappropriate for the format). The format being parsed can be accessed with the
|
||||
* {@link #getFormat} method.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public final class KeyFormatException extends Exception {
|
||||
private final Key.Format format;
|
||||
private final Type type;
|
||||
|
||||
KeyFormatException(final Key.Format format, final Type type) {
|
||||
this.format = format;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Key.Format getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
CONTENTS,
|
||||
LENGTH
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.crypto;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
/**
|
||||
* Represents a Curve25519 key pair as used by WireGuard.
|
||||
* <p>
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
@NonNullForAll
|
||||
public class KeyPair {
|
||||
private final Key privateKey;
|
||||
private final Key publicKey;
|
||||
|
||||
/**
|
||||
* Creates a key pair using a newly-generated private key.
|
||||
*/
|
||||
public KeyPair() {
|
||||
this(Key.generatePrivateKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key pair using an existing private key.
|
||||
*
|
||||
* @param privateKey a private key, used to derive the public key
|
||||
*/
|
||||
public KeyPair(final Key privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
publicKey = Key.generatePublicKey(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the private key from the key pair.
|
||||
*
|
||||
* @return the private key
|
||||
*/
|
||||
public Key getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key from the key pair.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public Key getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -22,4 +22,6 @@ import javax.annotation.meta.TypeQualifierDefault;
|
||||
@Nonnull
|
||||
@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NonNullForAll { }
|
||||
|
||||
public @interface NonNullForAll {
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright © 2018 WireGuard LLC. All Rights Reserved.
|
||||
# Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
@@ -8,12 +8,14 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
# Work around https://github.com/android-ndk/ndk/issues/602
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold")
|
||||
|
||||
add_executable(libwg-quick.so wireguard/src/tools/wg-quick/android.c ndk-compat/compat.c)
|
||||
target_compile_options(libwg-quick.so PUBLIC -O3 -std=gnu11 -Wall -pedantic -Wno-missing-field-initializers -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
|
||||
add_executable(libwg-quick.so wireguard-tools/src/wg-quick/android.c ndk-compat/compat.c)
|
||||
target_compile_options(libwg-quick.so PUBLIC -O3 -std=gnu11 -Wall -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
|
||||
target_link_libraries(libwg-quick.so -ldl)
|
||||
|
||||
file(GLOB WG_SOURCES wireguard/src/tools/*.c libmnl/src/*.c ndk-compat/compat.c)
|
||||
file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
|
||||
add_executable(libwg.so ${WG_SOURCES})
|
||||
target_compile_options(libwg.so PUBLIC -idirafter "${CMAKE_CURRENT_SOURCE_DIR}/libmnl/include/" -I "${CMAKE_CURRENT_SOURCE_DIR}/wireguard/src/tools/" -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\" -Wno-pointer-arith -Wno-unused-parameter)
|
||||
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
|
||||
target_compile_options(libwg.so PUBLIC -O3 -std=gnu11 -D_GNU_SOURCE -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DHAVE_VISIBILITY_HIDDEN -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
|
||||
|
||||
add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND make
|
||||
ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
|
||||
@@ -25,6 +27,7 @@ add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib
|
||||
CFLAGS=${CMAKE_C_FLAGS}\ -Wno-unused-command-line-argument
|
||||
LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}\ -fuse-ld=gold
|
||||
DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||
BUILDDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../generated-src
|
||||
)
|
||||
# Hack to make it actually build as part of the default target
|
||||
add_dependencies(libwg.so libwg-go.so)
|
||||
@@ -0,0 +1 @@
|
||||
build/
|
||||
@@ -0,0 +1,35 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
BUILDDIR ?= $(CURDIR)/build
|
||||
DESTDIR ?= $(CURDIR)/out
|
||||
|
||||
NDK_GO_ARCH_MAP_x86 := 386
|
||||
NDK_GO_ARCH_MAP_x86_64 := amd64
|
||||
NDK_GO_ARCH_MAP_arm := arm
|
||||
NDK_GO_ARCH_MAP_arm64 := arm64
|
||||
NDK_GO_ARCH_MAP_mips := mipsx
|
||||
NDK_GO_ARCH_MAP_mips64 := mips64x
|
||||
|
||||
CLANG_FLAGS := --target=$(ANDROID_LLVM_TRIPLE) --gcc-toolchain=$(ANDROID_TOOLCHAIN_ROOT) --sysroot=$(ANDROID_SYSROOT)
|
||||
export CGO_CFLAGS := $(CLANG_FLAGS) $(CFLAGS)
|
||||
export CGO_LDFLAGS := $(CLANG_FLAGS) $(LDFLAGS)
|
||||
export CC := $(ANDROID_C_COMPILER)
|
||||
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||
export GOOS := android
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
DESIRED_GO_VERSION := 1.13.7
|
||||
|
||||
default: $(DESTDIR)/libwg-go.so
|
||||
|
||||
$(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared:
|
||||
mkdir -p "$(dir $@)"
|
||||
curl "https://dl.google.com/go/go$(DESIRED_GO_VERSION).$(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)).tar.gz" | tar -C "$(dir $@)" --strip-components=1 -xzf -
|
||||
patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff
|
||||
touch "$@"
|
||||
|
||||
$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/bin/:$(PATH)
|
||||
$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(DESIRED_GO_VERSION)/.prepared go.mod
|
||||
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
|
||||
+40
-47
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Copyright (C) 2017-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package main
|
||||
@@ -11,9 +11,11 @@ import "C"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"git.zx2c4.com/wireguard-go/tun"
|
||||
"golang.org/x/sys/unix"
|
||||
"io/ioutil"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/ipc"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"bytes"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
@@ -35,14 +37,14 @@ func (l AndroidLogger) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
type TunnelHandle struct {
|
||||
device *Device
|
||||
device *device.Device
|
||||
uapi net.Listener
|
||||
}
|
||||
|
||||
var tunnelHandles map[int32]TunnelHandle
|
||||
|
||||
func init() {
|
||||
roamingDisabled = true
|
||||
device.RoamingDisabled = true
|
||||
tunnelHandles = make(map[int32]TunnelHandle)
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals, unix.SIGUSR2)
|
||||
@@ -53,17 +55,17 @@ func init() {
|
||||
case <-signals:
|
||||
n := runtime.Stack(buf, true)
|
||||
buf[n] = 0
|
||||
C.__android_log_write(C.ANDROID_LOG_ERROR, C.CString("WireGuard/GoBackend/Stacktrace"), (*_Ctype_char)(unsafe.Pointer(&buf[0])))
|
||||
C.__android_log_write(C.ANDROID_LOG_ERROR, C.CString("WireGuard/GoBackend/Stacktrace"), (*C.char)(unsafe.Pointer(&buf[0])))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//export wgTurnOn
|
||||
func wgTurnOn(ifnameRef string, tun_fd int32, settings string) int32 {
|
||||
func wgTurnOn(ifnameRef string, tunFd int32, settings string) int32 {
|
||||
interfaceName := string([]byte(ifnameRef))
|
||||
|
||||
logger := &Logger{
|
||||
logger := &device.Logger{
|
||||
Debug: log.New(&AndroidLogger{level: C.ANDROID_LOG_DEBUG, interfaceName: interfaceName}, "", 0),
|
||||
Info: log.New(&AndroidLogger{level: C.ANDROID_LOG_INFO, interfaceName: interfaceName}, "", 0),
|
||||
Error: log.New(&AndroidLogger{level: C.ANDROID_LOG_ERROR, interfaceName: interfaceName}, "", 0),
|
||||
@@ -71,33 +73,30 @@ func wgTurnOn(ifnameRef string, tun_fd int32, settings string) int32 {
|
||||
|
||||
logger.Debug.Println("Debug log enabled")
|
||||
|
||||
tun, name, err := tun.CreateTUNFromFD(int(tun_fd))
|
||||
tun, name, err := tun.CreateUnmonitoredTUNFromFD(int(tunFd))
|
||||
if err != nil {
|
||||
unix.Close(int(tun_fd))
|
||||
unix.Close(int(tunFd))
|
||||
logger.Error.Println(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
logger.Info.Println("Attaching to interface", name)
|
||||
device := NewDevice(tun, logger)
|
||||
device := device.NewDevice(tun, logger)
|
||||
|
||||
logger.Debug.Println("Interface has MTU", device.tun.mtu)
|
||||
|
||||
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(settings)), bufio.NewWriter(ioutil.Discard))
|
||||
setError := ipcSetOperation(device, bufferedSettings)
|
||||
setError := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings)))
|
||||
if setError != nil {
|
||||
unix.Close(int(tun_fd))
|
||||
unix.Close(int(tunFd))
|
||||
logger.Error.Println(setError)
|
||||
return -1
|
||||
}
|
||||
|
||||
var uapi net.Listener;
|
||||
var uapi net.Listener
|
||||
|
||||
uapiFile, err := UAPIOpen(name)
|
||||
uapiFile, err := ipc.UAPIOpen(name)
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
} else {
|
||||
uapi, err = UAPIListen(name, uapiFile)
|
||||
uapi, err = ipc.UAPIListen(name, uapiFile)
|
||||
if err != nil {
|
||||
uapiFile.Close()
|
||||
logger.Error.Println(err)
|
||||
@@ -108,7 +107,7 @@ func wgTurnOn(ifnameRef string, tun_fd int32, settings string) int32 {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go ipcHandle(device, conn)
|
||||
go device.IpcHandle(conn)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -124,7 +123,7 @@ func wgTurnOn(ifnameRef string, tun_fd int32, settings string) int32 {
|
||||
}
|
||||
}
|
||||
if i == math.MaxInt32 {
|
||||
unix.Close(int(tun_fd))
|
||||
unix.Close(int(tunFd))
|
||||
return -1
|
||||
}
|
||||
tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
|
||||
@@ -150,22 +149,11 @@ func wgGetSocketV4(tunnelHandle int32) int32 {
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
native, ok := handle.device.net.bind.(*NativeBind)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
fd := int32(-1)
|
||||
conn, err := native.ipv4.SyscallConn()
|
||||
fd, err := handle.device.PeekLookAtSocketFd4()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
err = conn.Control(func(f uintptr) {
|
||||
fd = int32(f)
|
||||
})
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return fd
|
||||
return int32(fd)
|
||||
}
|
||||
|
||||
//export wgGetSocketV6
|
||||
@@ -174,27 +162,32 @@ func wgGetSocketV6(tunnelHandle int32) int32 {
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
native, ok := handle.device.net.bind.(*NativeBind)
|
||||
fd, err := handle.device.PeekLookAtSocketFd6()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return int32(fd)
|
||||
}
|
||||
|
||||
//export wgGetConfig
|
||||
func wgGetConfig(tunnelHandle int32) *C.char {
|
||||
handle, ok := tunnelHandles[tunnelHandle]
|
||||
if !ok {
|
||||
return -1
|
||||
return nil
|
||||
}
|
||||
fd := int32(-1)
|
||||
conn, err := native.ipv6.SyscallConn()
|
||||
settings := new(bytes.Buffer)
|
||||
writer := bufio.NewWriter(settings)
|
||||
err := handle.device.IpcGetOperation(writer)
|
||||
if err != nil {
|
||||
return -1
|
||||
return nil
|
||||
}
|
||||
err = conn.Control(func(f uintptr) {
|
||||
fd = int32(f)
|
||||
})
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return fd
|
||||
writer.Flush()
|
||||
return C.CString(settings.String())
|
||||
}
|
||||
|
||||
//export wgVersion
|
||||
func wgVersion() *C.char {
|
||||
return C.CString(WireGuardGoVersion)
|
||||
return C.CString(device.WireGuardGoVersion)
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -0,0 +1,10 @@
|
||||
module golang.zx2c4.com/wireguard/android
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad // indirect
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82
|
||||
golang.zx2c4.com/wireguard v0.0.20200121
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
|
||||
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
|
||||
+37
-25
@@ -1,6 +1,6 @@
|
||||
From 6f345a38d9533db388009d91d29981673dcf95b4 Mon Sep 17 00:00:00 2001
|
||||
From: Jason A. Donenfeld <Jason@zx2c4.com>
|
||||
Date: Fri, 04 May 2018 03:48:23 +0200
|
||||
From b19623e7673a4d6743745382d5d38751b64e011d Mon Sep 17 00:00:00 2001
|
||||
From: "Jason A. Donenfeld" <Jason@zx2c4.com>
|
||||
Date: Wed, 27 Feb 2019 05:05:44 +0100
|
||||
Subject: [PATCH] runtime: use CLOCK_BOOTTIME in nanotime on Linux
|
||||
|
||||
This makes timers account for having expired while a computer was
|
||||
@@ -17,12 +17,21 @@ Fixes #24595
|
||||
|
||||
Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321
|
||||
---
|
||||
src/runtime/sys_linux_386.s | 4 ++--
|
||||
src/runtime/sys_linux_amd64.s | 2 +-
|
||||
src/runtime/sys_linux_arm.s | 4 ++--
|
||||
src/runtime/sys_linux_arm64.s | 4 ++--
|
||||
src/runtime/sys_linux_mips64x.s | 2 +-
|
||||
src/runtime/sys_linux_mipsx.s | 2 +-
|
||||
src/runtime/sys_linux_ppc64x.s | 2 +-
|
||||
src/runtime/sys_linux_s390x.s | 2 +-
|
||||
8 files changed, 11 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
|
||||
index 8d5a4ff..bfad064 100644
|
||||
index 72c43bd9da..daadfe32a9 100644
|
||||
--- a/src/runtime/sys_linux_386.s
|
||||
+++ b/src/runtime/sys_linux_386.s
|
||||
@@ -285,13 +285,13 @@
|
||||
@@ -288,13 +288,13 @@ noswitch:
|
||||
|
||||
LEAL 8(SP), BX // &ts (struct timespec)
|
||||
MOVL BX, 4(SP)
|
||||
@@ -39,10 +48,10 @@ index 8d5a4ff..bfad064 100644
|
||||
INVOKE_SYSCALL
|
||||
|
||||
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
|
||||
index 62d8024..8510be4 100644
|
||||
index 5c300f553d..e4a6f12ec6 100644
|
||||
--- a/src/runtime/sys_linux_amd64.s
|
||||
+++ b/src/runtime/sys_linux_amd64.s
|
||||
@@ -257,7 +257,7 @@
|
||||
@@ -261,7 +261,7 @@ noswitch:
|
||||
MOVQ runtime·vdsoClockgettimeSym(SB), AX
|
||||
CMPQ AX, $0
|
||||
JEQ fallback
|
||||
@@ -52,7 +61,7 @@ index 62d8024..8510be4 100644
|
||||
CALL AX
|
||||
MOVQ 0(SP), AX // sec
|
||||
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
|
||||
index aa39732..d29cb88 100644
|
||||
index 9c7398451c..61b6cd91f6 100644
|
||||
--- a/src/runtime/sys_linux_arm.s
|
||||
+++ b/src/runtime/sys_linux_arm.s
|
||||
@@ -11,7 +11,7 @@
|
||||
@@ -64,7 +73,7 @@ index aa39732..d29cb88 100644
|
||||
|
||||
// for EABI, as we don't support OABI
|
||||
#define SYS_BASE 0x0
|
||||
@@ -287,7 +287,7 @@
|
||||
@@ -291,7 +291,7 @@ noswitch:
|
||||
SUB $24, R13 // Space for results
|
||||
BIC $0x7, R13 // Align for C code
|
||||
|
||||
@@ -74,7 +83,7 @@ index aa39732..d29cb88 100644
|
||||
MOVW runtime·vdsoClockgettimeSym(SB), R11
|
||||
CMP $0, R11
|
||||
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
|
||||
index c6afd76..24294e8 100644
|
||||
index 2835b6ca1c..346ca9cfce 100644
|
||||
--- a/src/runtime/sys_linux_arm64.s
|
||||
+++ b/src/runtime/sys_linux_arm64.s
|
||||
@@ -13,7 +13,7 @@
|
||||
@@ -86,7 +95,7 @@ index c6afd76..24294e8 100644
|
||||
|
||||
#define SYS_exit 93
|
||||
#define SYS_read 63
|
||||
@@ -243,7 +243,7 @@
|
||||
@@ -247,7 +247,7 @@ noswitch:
|
||||
BIC $15, R1
|
||||
MOVD R1, RSP
|
||||
|
||||
@@ -96,10 +105,10 @@ index c6afd76..24294e8 100644
|
||||
CBZ R2, fallback
|
||||
BL (R2)
|
||||
diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
|
||||
index 8e64f1c..521062f 100644
|
||||
index 33ed1050c2..59a5be179c 100644
|
||||
--- a/src/runtime/sys_linux_mips64x.s
|
||||
+++ b/src/runtime/sys_linux_mips64x.s
|
||||
@@ -185,7 +185,7 @@
|
||||
@@ -189,7 +189,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$16
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
@@ -109,10 +118,10 @@ index 8e64f1c..521062f 100644
|
||||
MOVV $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
|
||||
index a6bca3b..920ed9a 100644
|
||||
index 6e539fbc6f..55b2bf7156 100644
|
||||
--- a/src/runtime/sys_linux_mipsx.s
|
||||
+++ b/src/runtime/sys_linux_mipsx.s
|
||||
@@ -189,7 +189,7 @@
|
||||
@@ -194,7 +194,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$8-12
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$8-8
|
||||
@@ -122,23 +131,23 @@ index a6bca3b..920ed9a 100644
|
||||
MOVW $SYS_clock_gettime, R2
|
||||
SYSCALL
|
||||
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
|
||||
index 483cb8e..11f3c49 100644
|
||||
index 13d23156bd..f67e5062aa 100644
|
||||
--- a/src/runtime/sys_linux_ppc64x.s
|
||||
+++ b/src/runtime/sys_linux_ppc64x.s
|
||||
@@ -164,7 +164,7 @@
|
||||
RET
|
||||
@@ -204,7 +204,7 @@ fallback:
|
||||
JMP finish
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
- MOVW $1, R3 // CLOCK_MONOTONIC
|
||||
+ MOVW $7, R3 // CLOCK_BOOTTIME
|
||||
MOVD $0(R1), R4
|
||||
SYSCALL $SYS_clock_gettime
|
||||
MOVD 0(R1), R3 // sec
|
||||
- MOVD $1, R3 // CLOCK_MONOTONIC
|
||||
+ MOVD $7, R3 // CLOCK_BOOTTIME
|
||||
|
||||
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 1ff110c..e9049e0 100644
|
||||
index 58b36dff0a..cb92e9a402 100644
|
||||
--- a/src/runtime/sys_linux_s390x.s
|
||||
+++ b/src/runtime/sys_linux_s390x.s
|
||||
@@ -176,7 +176,7 @@
|
||||
@@ -180,7 +180,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$16
|
||||
RET
|
||||
|
||||
TEXT runtime·nanotime(SB),NOSPLIT,$16
|
||||
@@ -147,3 +156,6 @@ index 1ff110c..e9049e0 100644
|
||||
MOVD $tp-16(SP), R3
|
||||
MOVW $SYS_clock_gettime, R1
|
||||
SYSCALL
|
||||
--
|
||||
2.23.0
|
||||
|
||||
+13
-1
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Copyright © 2017-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
* Copyright © 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
@@ -12,6 +12,7 @@ extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settin
|
||||
extern void wgTurnOff(int handle);
|
||||
extern int wgGetSocketV4(int handle);
|
||||
extern int wgGetSocketV6(int handle);
|
||||
extern char *wgGetConfig(int handle);
|
||||
extern char *wgVersion();
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
|
||||
@@ -47,6 +48,17 @@ JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV
|
||||
return wgGetSocketV6(handle);
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetConfig(JNIEnv *env, jclass c, jint handle)
|
||||
{
|
||||
jstring ret;
|
||||
char *config = wgGetConfig(handle);
|
||||
if (!config)
|
||||
return NULL;
|
||||
ret = (*env)->NewStringUTF(env, config);
|
||||
free(config);
|
||||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c)
|
||||
{
|
||||
jstring ret;
|
||||
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: BSD
|
||||
*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: BSD
|
||||
*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
*
|
||||
*/
|
||||
|
||||
Submodule
+1
Submodule tunnel/tools/wireguard-tools added at 828ffc88cd
@@ -1,24 +1,29 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: 'nonnull.gradle'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
// Create a variable called keystorePropertiesFile, and initialize it to your
|
||||
// keystore.properties file, in the rootProject folder.
|
||||
final def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
|
||||
android {
|
||||
buildToolsVersion '28.0.3'
|
||||
buildToolsVersion '29.0.3'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
compileSdkVersion 28
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
compileSdkVersion 29
|
||||
dataBinding.enabled true
|
||||
defaultConfig {
|
||||
applicationId 'com.wireguard.android'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 441
|
||||
versionName '0.0.20181001'
|
||||
targetSdkVersion 29
|
||||
versionCode wireguardVersionCode
|
||||
versionName wireguardVersionName
|
||||
buildConfigField 'int', 'MIN_SDK_VERSION', "$minSdkVersion.apiLevel"
|
||||
}
|
||||
// If the keystore file exists
|
||||
@@ -46,6 +51,8 @@ android {
|
||||
arguments "-DANDROID_PACKAGE_NAME=${android.defaultConfig.applicationId}"
|
||||
}
|
||||
}
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
@@ -57,34 +64,22 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'tools/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
databindingVersion = '3.1.3'
|
||||
supportLibsVersion = '27.1.1'
|
||||
streamsupportVersion = '1.6.0'
|
||||
jsr305Version = '3.0.2'
|
||||
zxingEmbeddedVersion = '3.6.0'
|
||||
acraVersion = '5.2.0-rc2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.android.databinding:library:$databindingVersion"
|
||||
implementation "com.android.support:appcompat-v7:$supportLibsVersion"
|
||||
implementation "com.android.support:cardview-v7:$supportLibsVersion"
|
||||
implementation "com.android.support:design:$supportLibsVersion"
|
||||
implementation "com.android.support:preference-v14:$supportLibsVersion"
|
||||
implementation "com.android.support:support-annotations:$supportLibsVersion"
|
||||
implementation "com.google.code.findbugs:jsr305:$jsr305Version"
|
||||
implementation project(":tunnel")
|
||||
implementation "androidx.annotation:annotation:$annotationsVersion"
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation "androidx.cardview:cardview:$cardviewVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-ktx:$coreKtxVersion"
|
||||
implementation "androidx.databinding:databinding-runtime:$agpVersion"
|
||||
implementation "androidx.fragment:fragment:$fragmentVersion"
|
||||
implementation "androidx.preference:preference:$preferenceVersion"
|
||||
implementation "com.google.android.material:material:$materialComponentsVersion"
|
||||
implementation "com.journeyapps:zxing-android-embedded:$zxingEmbeddedVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
implementation "ch.acra:acra-http:$acraVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# Squelch all warnings, they're harmless but ProGuard
|
||||
# escalates them as errors.
|
||||
-dontwarn sun.misc.Unsafe
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"comment": "Interface names",
|
||||
"names": [
|
||||
{
|
||||
"names": [
|
||||
{ "name": "wg0" },
|
||||
{ "name": "wg1" },
|
||||
{ "name": "wg2" },
|
||||
{ "name": "wg3" },
|
||||
{ "name": "wg4" },
|
||||
{ "name": "wg5" },
|
||||
{ "name": "wg6" },
|
||||
{ "name": "wg7" },
|
||||
{ "name": "wg8" },
|
||||
{ "name": "wg9" },
|
||||
{ "name": "wg10" },
|
||||
{ "name": "wg11" }
|
||||
],
|
||||
"checked": [
|
||||
{ "checked": true },
|
||||
{ "checked": false },
|
||||
{ "checked": true },
|
||||
{ "checked": false },
|
||||
{ "checked": true },
|
||||
{ "checked": false },
|
||||
{ "checked": true },
|
||||
{ "checked": false },
|
||||
{ "checked": true },
|
||||
{ "checked": false },
|
||||
{ "checked": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,17 +9,18 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.CONTROL_TUNNELS"
|
||||
android:protectionLevel="dangerous"
|
||||
android:description="@string/permission_description"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/permission_label"
|
||||
android:description="@string/permission_description" />
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
@@ -27,12 +28,12 @@
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<activity android:name=".activity.TunnelToggleActivity" android:theme="@style/NoBackgroundTheme"/>
|
||||
<activity android:name=".activity.MainActivity">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
@@ -43,8 +44,7 @@
|
||||
<activity
|
||||
android:name=".activity.SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName=".activity.MainActivity"
|
||||
android:theme="@style/SettingsTheme" />
|
||||
android:parentActivityName=".activity.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.TunnelCreatorActivity"
|
||||
@@ -73,14 +73,6 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".backend.GoBackend$VpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".QuickTileService"
|
||||
android:icon="@drawable/ic_tile"
|
||||
+83
-96
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -8,15 +8,12 @@ package com.wireguard.android;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wireguard.android.backend.Backend;
|
||||
import com.wireguard.android.backend.GoBackend;
|
||||
@@ -24,99 +21,38 @@ import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.configStore.FileConfigStore;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.AsyncWorker;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.util.ModuleLoader;
|
||||
import com.wireguard.android.util.RootShell;
|
||||
import com.wireguard.android.util.ToolsInstaller;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.annotation.AcraCore;
|
||||
import org.acra.annotation.AcraHttpSender;
|
||||
import org.acra.data.StringFormat;
|
||||
import org.acra.sender.HttpSender;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import java9.util.concurrent.CompletableFuture;
|
||||
|
||||
|
||||
@AcraCore(reportFormat = StringFormat.JSON,
|
||||
buildConfigClass = BuildConfig.class,
|
||||
logcatArguments = {"-b", "all", "-d", "-v", "threadtime", "*:V"},
|
||||
excludeMatchingSharedPreferencesKeys = {"last_used_tunnel", "enabled_configs"})
|
||||
@AcraHttpSender(uri = "https://crashreport.zx2c4.com/android/report",
|
||||
basicAuthLogin = "6RCovLxEVCTXGiW5",
|
||||
basicAuthPassword = "O7I3sVa5ULVdiC51",
|
||||
httpMethod = HttpSender.Method.POST,
|
||||
compress = true)
|
||||
public class Application extends android.app.Application {
|
||||
@NonNullForAll
|
||||
public class Application extends android.app.Application implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
public static final String USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, Build.SUPPORTED_ABIS.length > 0 ? Build.SUPPORTED_ABIS[0] : "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT);
|
||||
private static final String TAG = "WireGuard/" + Application.class.getSimpleName();
|
||||
@SuppressWarnings("NullableProblems") private static WeakReference<Application> weakSelf;
|
||||
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
|
||||
@SuppressWarnings("NullableProblems") private AsyncWorker asyncWorker;
|
||||
@Nullable private Backend backend;
|
||||
@SuppressWarnings("NullableProblems") private ModuleLoader moduleLoader;
|
||||
@SuppressWarnings("NullableProblems") private RootShell rootShell;
|
||||
@SuppressWarnings("NullableProblems") private SharedPreferences sharedPreferences;
|
||||
@SuppressWarnings("NullableProblems") private ToolsInstaller toolsInstaller;
|
||||
@SuppressWarnings("NullableProblems") private TunnelManager tunnelManager;
|
||||
@Nullable private Backend backend;
|
||||
private final CompletableFuture<Backend> futureBackend = new CompletableFuture<>();
|
||||
|
||||
public Application() {
|
||||
weakSelf = new WeakReference<>(this);
|
||||
}
|
||||
|
||||
/* The ACRA password can be trivially reverse engineered and is open source anyway,
|
||||
* so there's no point in trying to protect it. However, we do want to at least
|
||||
* prevent innocent self-builders from uploading stuff to our crash reporter. So, we
|
||||
* check the DN of the certs that signed the apk, without even bothering to try
|
||||
* validating that they're authentic. It's a good enough heuristic.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Nullable
|
||||
private static String getInstallSource(final Context context) {
|
||||
if (BuildConfig.DEBUG)
|
||||
return null;
|
||||
try {
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
for (final Signature sig : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures) {
|
||||
try {
|
||||
for (final String category : ((X509Certificate) cf.generateCertificate(new ByteArrayInputStream(sig.toByteArray()))).getSubjectDN().getName().split(", *")) {
|
||||
final String[] parts = category.split("=", 2);
|
||||
if (!"O".equals(parts[0]))
|
||||
continue;
|
||||
switch (parts[1]) {
|
||||
case "Google Inc.":
|
||||
return "Play Store";
|
||||
case "fdroid.org":
|
||||
return "F-Droid";
|
||||
}
|
||||
}
|
||||
} catch (final Exception ignored) { }
|
||||
}
|
||||
} catch (final Exception ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context context) {
|
||||
super.attachBaseContext(context);
|
||||
|
||||
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
|
||||
final Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_HOME);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
final String installSource = getInstallSource(context);
|
||||
if (installSource != null) {
|
||||
ACRA.init(this);
|
||||
ACRA.getErrorReporter().putCustomData("installSource", installSource);
|
||||
}
|
||||
}
|
||||
|
||||
public static Application get() {
|
||||
return weakSelf.get();
|
||||
}
|
||||
@@ -130,15 +66,31 @@ public class Application extends android.app.Application {
|
||||
synchronized (app.futureBackend) {
|
||||
if (app.backend == null) {
|
||||
Backend backend = null;
|
||||
if (new File("/sys/module/wireguard").exists()) {
|
||||
boolean didStartRootShell = false;
|
||||
if (!ModuleLoader.isModuleLoaded() && app.moduleLoader.moduleMightExist()) {
|
||||
try {
|
||||
app.rootShell.start();
|
||||
backend = new WgQuickBackend(app.getApplicationContext());
|
||||
didStartRootShell = true;
|
||||
app.moduleLoader.loadModule();
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
if (backend == null)
|
||||
if (!app.sharedPreferences.getBoolean("disable_kernel_module", false) && ModuleLoader.isModuleLoaded()) {
|
||||
try {
|
||||
if (!didStartRootShell)
|
||||
app.rootShell.start();
|
||||
WgQuickBackend wgQuickBackend = new WgQuickBackend(app.getApplicationContext(), app.rootShell, app.toolsInstaller);
|
||||
wgQuickBackend.setMultipleTunnels(app.sharedPreferences.getBoolean("multiple_tunnels", false));
|
||||
backend = wgQuickBackend;
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
if (backend == null) {
|
||||
backend = new GoBackend(app.getApplicationContext());
|
||||
GoBackend.setAlwaysOnCallback(() -> {
|
||||
get().tunnelManager.restoreState(true).whenComplete(ExceptionLoggers.D);
|
||||
});
|
||||
}
|
||||
app.backend = backend;
|
||||
}
|
||||
return app.backend;
|
||||
@@ -149,6 +101,10 @@ public class Application extends android.app.Application {
|
||||
return get().futureBackend;
|
||||
}
|
||||
|
||||
public static ModuleLoader getModuleLoader() {
|
||||
return get().moduleLoader;
|
||||
}
|
||||
|
||||
public static RootShell getRootShell() {
|
||||
return get().rootShell;
|
||||
}
|
||||
@@ -165,29 +121,60 @@ public class Application extends android.app.Application {
|
||||
return get().tunnelManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context context) {
|
||||
super.attachBaseContext(context);
|
||||
|
||||
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
|
||||
final Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_HOME);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.i(TAG, USER_AGENT);
|
||||
super.onCreate();
|
||||
|
||||
asyncWorker = new AsyncWorker(AsyncTask.SERIAL_EXECUTOR, new Handler(Looper.getMainLooper()));
|
||||
rootShell = new RootShell(getApplicationContext());
|
||||
toolsInstaller = new ToolsInstaller(getApplicationContext());
|
||||
toolsInstaller = new ToolsInstaller(getApplicationContext(), rootShell);
|
||||
moduleLoader = new ModuleLoader(getApplicationContext(), rootShell, USER_AGENT);
|
||||
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
sharedPreferences.getBoolean("dark_theme", false) ?
|
||||
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
sharedPreferences.getBoolean("dark_theme", false) ?
|
||||
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
}
|
||||
|
||||
tunnelManager = new TunnelManager(new FileConfigStore(getApplicationContext()));
|
||||
tunnelManager.onCreate();
|
||||
|
||||
asyncWorker.supplyAsync(Application::getBackend).thenAccept(backend -> {
|
||||
futureBackend.complete(backend);
|
||||
if (ACRA.isInitialised()) {
|
||||
ACRA.getErrorReporter().putCustomData("backend", backend.getClass().getSimpleName());
|
||||
asyncWorker.supplyAsync(backend::getVersion).thenAccept(version ->
|
||||
ACRA.getErrorReporter().putCustomData("backendVersion", version));
|
||||
}
|
||||
});
|
||||
asyncWorker.supplyAsync(Application::getBackend).thenAccept(futureBackend::complete);
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
|
||||
if ("multiple_tunnels".equals(key) && backend != null && backend instanceof WgQuickBackend)
|
||||
((WgQuickBackend) backend).setMultipleTunnels(sharedPreferences.getBoolean(key, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onTerminate();
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,9 @@ import android.util.Log;
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.model.TunnelManager;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
@NonNullForAll
|
||||
public class BootShutdownReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "WireGuard/" + BootShutdownReceiver.class.getSimpleName();
|
||||
|
||||
+42
-40
@@ -1,14 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.databinding.Observable;
|
||||
import android.databinding.Observable.OnPropertyChangedCallback;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Icon;
|
||||
@@ -16,33 +13,38 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.service.quicksettings.TileService;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wireguard.android.activity.MainActivity;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.Tunnel.State;
|
||||
import com.wireguard.android.util.ExceptionLoggers;
|
||||
import com.wireguard.android.activity.TunnelToggleActivity;
|
||||
import com.wireguard.android.backend.Tunnel.State;
|
||||
import com.wireguard.android.model.ObservableTunnel;
|
||||
import com.wireguard.android.widget.SlashDrawable;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.databinding.Observable;
|
||||
import androidx.databinding.Observable.OnPropertyChangedCallback;
|
||||
|
||||
/**
|
||||
* Service that maintains the application's custom Quick Settings tile. This service is bound by the
|
||||
* system framework as necessary to update the appearance of the tile in the system UI, and to
|
||||
* forward click events to the application.
|
||||
*/
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@NonNullForAll
|
||||
public class QuickTileService extends TileService {
|
||||
private static final String TAG = "WireGuard/" + QuickTileService.class.getSimpleName();
|
||||
|
||||
private final OnStateChangedCallback onStateChangedCallback = new OnStateChangedCallback();
|
||||
private final OnTunnelChangedCallback onTunnelChangedCallback = new OnTunnelChangedCallback();
|
||||
@Nullable private Tunnel tunnel;
|
||||
@Nullable private Icon iconOn;
|
||||
@Nullable private Icon iconOff;
|
||||
@Nullable private Icon iconOn;
|
||||
@Nullable private ObservableTunnel tunnel;
|
||||
|
||||
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||
@Override
|
||||
@@ -57,6 +59,32 @@ public class QuickTileService extends TileService {
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
if (tunnel != null) {
|
||||
unlockAndRun(() -> {
|
||||
final Tile tile = getQsTile();
|
||||
if (tile != null) {
|
||||
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
|
||||
tile.updateTile();
|
||||
}
|
||||
tunnel.setState(State.TOGGLE).whenComplete((v, t) -> {
|
||||
if (t == null) {
|
||||
updateTile();
|
||||
} else {
|
||||
final Intent toggleIntent = new Intent(this, TunnelToggleActivity.class);
|
||||
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(toggleIntent);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
final Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -79,22 +107,6 @@ public class QuickTileService extends TileService {
|
||||
iconOff = Icon.createWithBitmap(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
if (tunnel != null) {
|
||||
final Tile tile = getQsTile();
|
||||
if (tile != null) {
|
||||
tile.setIcon(tile.getIcon() == iconOn ? iconOff : iconOn);
|
||||
tile.updateTile();
|
||||
}
|
||||
tunnel.setState(State.TOGGLE).whenComplete(this::onToggleFinished);
|
||||
} else {
|
||||
final Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivityAndCollapse(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartListening() {
|
||||
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback);
|
||||
@@ -110,19 +122,9 @@ public class QuickTileService extends TileService {
|
||||
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback);
|
||||
}
|
||||
|
||||
private void onToggleFinished(@SuppressWarnings("unused") final State state,
|
||||
@Nullable final Throwable throwable) {
|
||||
if (throwable == null)
|
||||
return;
|
||||
final String error = ExceptionLoggers.unwrapMessage(throwable);
|
||||
final String message = getString(R.string.toggle_error, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void updateTile() {
|
||||
// Update the tunnel.
|
||||
final Tunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
|
||||
final ObservableTunnel newTunnel = Application.getTunnelManager().getLastUsedTunnel();
|
||||
if (newTunnel != tunnel) {
|
||||
if (tunnel != null)
|
||||
tunnel.removeOnPropertyChangedCallback(onStateChangedCallback);
|
||||
@@ -136,7 +138,7 @@ public class QuickTileService extends TileService {
|
||||
final Tile tile = getQsTile();
|
||||
if (tunnel != null) {
|
||||
label = tunnel.getName();
|
||||
state = tunnel.getState() == Tunnel.State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
|
||||
state = tunnel.getState() == State.UP ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
|
||||
} else {
|
||||
label = getString(R.string.app_name);
|
||||
state = Tile.STATE_INACTIVE;
|
||||
+18
-15
@@ -1,36 +1,39 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.databinding.CallbackRegistry;
|
||||
import android.databinding.CallbackRegistry.NotifierCallback;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.ObservableTunnel;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.databinding.CallbackRegistry;
|
||||
import androidx.databinding.CallbackRegistry.NotifierCallback;
|
||||
|
||||
/**
|
||||
* Base class for activities that need to remember the currently-selected tunnel.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public abstract class BaseActivity extends ThemeChangeAwareActivity {
|
||||
private static final String KEY_SELECTED_TUNNEL = "selected_tunnel";
|
||||
|
||||
private final SelectionChangeRegistry selectionChangeRegistry = new SelectionChangeRegistry();
|
||||
@Nullable private Tunnel selectedTunnel;
|
||||
@Nullable private ObservableTunnel selectedTunnel;
|
||||
|
||||
public void addOnSelectedTunnelChangedListener(final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.add(listener);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Tunnel getSelectedTunnel() {
|
||||
public ObservableTunnel getSelectedTunnel() {
|
||||
return selectedTunnel;
|
||||
}
|
||||
|
||||
@@ -60,15 +63,15 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
protected abstract void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
|
||||
protected abstract void onSelectedTunnelChanged(@Nullable ObservableTunnel oldTunnel, @Nullable ObservableTunnel newTunnel);
|
||||
|
||||
public void removeOnSelectedTunnelChangedListener(
|
||||
final OnSelectedTunnelChangedListener listener) {
|
||||
selectionChangeRegistry.remove(listener);
|
||||
}
|
||||
|
||||
public void setSelectedTunnel(@Nullable final Tunnel tunnel) {
|
||||
final Tunnel oldTunnel = selectedTunnel;
|
||||
public void setSelectedTunnel(@Nullable final ObservableTunnel tunnel) {
|
||||
final ObservableTunnel oldTunnel = selectedTunnel;
|
||||
if (Objects.equals(oldTunnel, tunnel))
|
||||
return;
|
||||
selectedTunnel = tunnel;
|
||||
@@ -77,21 +80,21 @@ public abstract class BaseActivity extends ThemeChangeAwareActivity {
|
||||
}
|
||||
|
||||
public interface OnSelectedTunnelChangedListener {
|
||||
void onSelectedTunnelChanged(@Nullable Tunnel oldTunnel, @Nullable Tunnel newTunnel);
|
||||
void onSelectedTunnelChanged(@Nullable ObservableTunnel oldTunnel, @Nullable ObservableTunnel newTunnel);
|
||||
}
|
||||
|
||||
private static final class SelectionChangeNotifier
|
||||
extends NotifierCallback<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
extends NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> {
|
||||
@Override
|
||||
public void onNotifyCallback(final OnSelectedTunnelChangedListener listener,
|
||||
final Tunnel oldTunnel, final int ignored,
|
||||
final Tunnel newTunnel) {
|
||||
final ObservableTunnel oldTunnel, final int ignored,
|
||||
final ObservableTunnel newTunnel) {
|
||||
listener.onSelectedTunnelChanged(oldTunnel, newTunnel);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SelectionChangeRegistry
|
||||
extends CallbackRegistry<OnSelectedTunnelChangedListener, Tunnel, Tunnel> {
|
||||
extends CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel> {
|
||||
private SelectionChangeRegistry() {
|
||||
super(new SelectionChangeNotifier());
|
||||
}
|
||||
+28
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -8,20 +8,24 @@ package com.wireguard.android.activity;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnApplyWindowInsetsListener;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.fragment.TunnelDetailFragment;
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment;
|
||||
import com.wireguard.android.fragment.TunnelListFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.model.ObservableTunnel;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
/**
|
||||
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
|
||||
@@ -29,20 +33,15 @@ import com.wireguard.android.model.Tunnel;
|
||||
* editing the configuration and interface state of WireGuard tunnels.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class MainActivity extends BaseActivity
|
||||
implements FragmentManager.OnBackStackChangedListener {
|
||||
@Nullable private ActionBar actionBar;
|
||||
private boolean isTwoPaneLayout;
|
||||
@Nullable private TunnelListFragment listFragment;
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
|
||||
// If the action menu is visible and expanded, collapse it instead of navigating back.
|
||||
if (isTwoPaneLayout || backStackEntries == 0) {
|
||||
if (listFragment != null && listFragment.collapseActionMenu())
|
||||
return;
|
||||
}
|
||||
// If the two-pane layout does not have an editor open, going back should exit the app.
|
||||
if (isTwoPaneLayout && backStackEntries <= 1) {
|
||||
finish();
|
||||
@@ -50,6 +49,7 @@ public class MainActivity extends BaseActivity
|
||||
}
|
||||
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
|
||||
if (!isTwoPaneLayout && backStackEntries == 1) {
|
||||
getSupportFragmentManager().popBackStack();
|
||||
setSelectedTunnel(null);
|
||||
return;
|
||||
}
|
||||
@@ -74,12 +74,20 @@ public class MainActivity extends BaseActivity
|
||||
setContentView(R.layout.main_activity);
|
||||
actionBar = getSupportActionBar();
|
||||
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
|
||||
listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
onBackStackChanged();
|
||||
final View actionBarView = findViewById(R.id.action_bar);
|
||||
if (actionBarView != null)
|
||||
actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
|
||||
// Dispatch insets on back stack change
|
||||
// This is required to ensure replaced fragments are also able to consume insets
|
||||
findViewById(R.id.master_detail_wrapper).setOnApplyWindowInsetsListener((OnApplyWindowInsetsListener) (v, insets) -> {
|
||||
final FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
fragmentManager.addOnBackStackChangedListener(() -> {
|
||||
final List<Fragment> fragments = fragmentManager.getFragments();
|
||||
for (int i = 0; i < fragments.size(); i++) {
|
||||
fragments.get(i).requireView().dispatchApplyWindowInsets(insets);
|
||||
}
|
||||
});
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,8 +122,8 @@ public class MainActivity extends BaseActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSelectedTunnelChanged(@Nullable final Tunnel oldTunnel,
|
||||
@Nullable final Tunnel newTunnel) {
|
||||
protected void onSelectedTunnelChanged(@Nullable final ObservableTunnel oldTunnel,
|
||||
@Nullable final ObservableTunnel newTunnel) {
|
||||
final FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
final int backStackEntries = fragmentManager.getBackStackEntryCount();
|
||||
if (newTunnel == null) {
|
||||
+34
-10
@@ -1,33 +1,38 @@
|
||||
/*
|
||||
* Copyright © 2017-2018 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.activity;
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.support.v7.preference.PreferenceFragmentCompat;
|
||||
import android.support.v7.preference.PreferenceScreen;
|
||||
import android.util.SparseArray;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.wireguard.android.Application;
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.backend.WgQuickBackend;
|
||||
import com.wireguard.android.util.ModuleLoader;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
/**
|
||||
* Interface for changing application-global persistent settings.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class SettingsActivity extends ThemeChangeAwareActivity {
|
||||
private final SparseArray<PermissionRequestCallback> permissionRequestCallbacks = new SparseArray<>();
|
||||
private int permissionRequestCounter;
|
||||
@@ -91,13 +96,17 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String key) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
final PreferenceScreen screen = getPreferenceScreen();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
screen.removePreference(getPreferenceManager().findPreference("dark_theme"));
|
||||
|
||||
final Preference wgQuickOnlyPrefs[] = {
|
||||
getPreferenceManager().findPreference("tools_installer"),
|
||||
getPreferenceManager().findPreference("restore_on_boot")
|
||||
getPreferenceManager().findPreference("tools_installer"),
|
||||
getPreferenceManager().findPreference("restore_on_boot"),
|
||||
getPreferenceManager().findPreference("multiple_tunnels")
|
||||
};
|
||||
for (final Preference pref : wgQuickOnlyPrefs)
|
||||
pref.setVisible(false);
|
||||
final PreferenceScreen screen = getPreferenceScreen();
|
||||
Application.getBackendAsync().thenAccept(backend -> {
|
||||
for (final Preference pref : wgQuickOnlyPrefs) {
|
||||
if (backend instanceof WgQuickBackend)
|
||||
@@ -106,6 +115,21 @@ public class SettingsActivity extends ThemeChangeAwareActivity {
|
||||
screen.removePreference(pref);
|
||||
}
|
||||
});
|
||||
|
||||
final Preference moduleInstaller = getPreferenceManager().findPreference("module_downloader");
|
||||
final Preference kernelModuleDisabler = getPreferenceManager().findPreference("kernel_module_disabler");
|
||||
moduleInstaller.setVisible(false);
|
||||
if (ModuleLoader.isModuleLoaded()) {
|
||||
screen.removePreference(moduleInstaller);
|
||||
} else {
|
||||
screen.removePreference(kernelModuleDisabler);
|
||||
Application.getAsyncWorker().runAsync(Application.getRootShell()::start).whenComplete((v, e) -> {
|
||||
if (e == null)
|
||||
moduleInstaller.setVisible(true);
|
||||
else
|
||||
screen.removePreference(moduleInstaller);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user