Repo cloned
This commit is contained in:
parent
b280361250
commit
db901828a8
235 changed files with 27925 additions and 2 deletions
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/.gradle/
|
||||||
|
/.idea/*.xml
|
||||||
|
/.idea/caches/
|
||||||
|
/.idea/dictionaries/
|
||||||
|
/.idea/libraries/
|
||||||
|
/captures/
|
||||||
|
/local.properties
|
||||||
|
.DS_Store
|
||||||
|
.cxx/
|
||||||
|
Thumbs.db
|
||||||
|
build/
|
||||||
|
*.apk
|
||||||
|
*.class
|
||||||
|
*.dex
|
||||||
|
*.iml
|
||||||
|
*.jks
|
||||||
|
gradlew.bat
|
||||||
|
maint/
|
||||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[submodule "tunnel/tools/wireguard-tools"]
|
||||||
|
path = tunnel/tools/wireguard-tools
|
||||||
|
url = https://git.zx2c4.com/wireguard-tools
|
||||||
|
[submodule "tunnel/tools/elf-cleaner"]
|
||||||
|
path = tunnel/tools/elf-cleaner
|
||||||
|
url = https://github.com/termux/termux-elf-cleaner
|
||||||
474
.idea/codeStyles/Project.xml
generated
Normal file
474
.idea/codeStyles/Project.xml
generated
Normal file
|
|
@ -0,0 +1,474 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="AUTODETECT_INDENTS" value="false" />
|
||||||
|
<option name="LINE_SEPARATOR" value=" " />
|
||||||
|
<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="IMPORT_LAYOUT_TABLE">
|
||||||
|
<value>
|
||||||
|
<package name="android" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="com" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="junit" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="net" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="org" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="java" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="javax" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
<emptyLine />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<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" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="com" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="junit" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="net" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="org" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="java" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="javax" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
<emptyLine />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
|
<value>
|
||||||
|
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="10" />
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="10" />
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="JAVA">
|
||||||
|
<option name="METHOD_ANNOTATION_WRAP" value="0" />
|
||||||
|
<option name="FIELD_ANNOTATION_WRAP" value="0" />
|
||||||
|
<arrangement>
|
||||||
|
<groups />
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PROTECTED>true</PROTECTED>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PRIVATE>true</PRIVATE>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PROTECTED>true</PROTECTED>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PRIVATE>true</PRIVATE>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<INITIALIZER_BLOCK>true</INITIALIZER_BLOCK>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PROTECTED>true</PROTECTED>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<FINAL>true</FINAL>
|
||||||
|
<PRIVATE>true</PRIVATE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PUBLIC>true</PUBLIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PROTECTED>true</PROTECTED>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PACKAGE_PRIVATE>true</PACKAGE_PRIVATE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
<PRIVATE>true</PRIVATE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<FIELD>true</FIELD>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<INITIALIZER_BLOCK>true</INITIALIZER_BLOCK>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<CONSTRUCTOR>true</CONSTRUCTOR>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<METHOD>true</METHOD>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<METHOD>true</METHOD>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<ENUM>true</ENUM>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<INTERFACE>true</INTERFACE>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<CLASS>true</CLASS>
|
||||||
|
<STATIC>true</STATIC>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<CLASS>true</CLASS>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<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>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<option name="RIGHT_MARGIN" value="160" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
6
.idea/copyright/Default.xml
generated
Normal file
6
.idea/copyright/Default.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="CopyrightManager">
|
||||||
|
<copyright>
|
||||||
|
<option name="notice" value="Copyright © 2017-&#36;today.year WireGuard LLC. All Rights Reserved. SPDX-License-Identifier: Apache-2.0" />
|
||||||
|
<option name="myName" value="Default" />
|
||||||
|
</copyright>
|
||||||
|
</component>
|
||||||
3
.idea/copyright/profiles_settings.xml
generated
Normal file
3
.idea/copyright/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<component name="CopyrightManager">
|
||||||
|
<settings default="Default" />
|
||||||
|
</component>
|
||||||
527
.idea/inspectionProfiles/Default.xml
generated
Normal file
527
.idea/inspectionProfiles/Default.xml
generated
Normal file
|
|
@ -0,0 +1,527 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0" is_locked="false">
|
||||||
|
<option name="myName" value="Default" />
|
||||||
|
<inspection_tool class="AbstractClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="AbstractClassWithoutAbstractMethods" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AccessToNonThreadSafeStaticFieldFromInstance" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="nonThreadSafeClasses">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="nonThreadSafeTypes" value="" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AndroidLintIconExpectedSize" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AndroidLintTypographyQuotes" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AnnotationNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AnonymousClassVariableHidesContainingMethodVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AnonymousInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AnonymousInnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ApiName" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ApiNamespace" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ApiParameter" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ArrayEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ArrayIssues" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentOrReturnOfFieldWithMutableTypeMerged" />
|
||||||
|
<inspection_tool class="AssignmentToCatchBlockParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentToCollectionFieldFromParameter" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="ignorePrivateMethods" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AssignmentToDateFieldFromParameter" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="ignorePrivateMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AssignmentToForLoopParameter" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_checkForeachParameters" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentToSuperclassField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.util.HashMap,put,java.util.Map,put" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="BadExceptionCaught" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="exceptionsString" value="" />
|
||||||
|
<option name="exceptions">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="BadOddness" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="BooleanExpressionMayBeConditional" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="BooleanParameter" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="onlyReportMultiple" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="CallToSimpleGetterInClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreGetterCallsOnOtherObjects" value="false" />
|
||||||
|
<option name="onlyReportPrivateGetter" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="CallToStringConcatCanBeReplacedByOperator" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CannotResolve" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CatchMayIgnoreException" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreCatchBlocksWithComments" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassInitializer" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ClassReferencesSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ComparableImplementedButEqualsNotOverridden" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CompareToUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConditionalExpression" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ConditionalExpressionWithIdenticalBranches" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConstExpressionRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConstantNamingConvention" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="onlyCheckImmutables" value="false" />
|
||||||
|
<option name="m_regex" value="[A-Z][A-Z_\d]*" />
|
||||||
|
<option name="m_minLength" value="0" />
|
||||||
|
<option name="m_maxLength" value="0" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ConstantValueVariableUse" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ConstructionIsNotAllowed" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="Constructor" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="Convert2streamapi" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="REPLACE_TRIVIAL_FOREACH" value="true" />
|
||||||
|
<option name="SUGGEST_FOREACH" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ConvertAnnotations" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="CovariantEquals" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DeclareCollectionAsInterface" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreLocalVariables" value="false" />
|
||||||
|
<option name="ignorePrivateMethodsAndFields" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DerivedFunctionsReturnTypeMismatch" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DeserializableClassInSecureContext" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="DoubleBraceInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DoubleCheckedLocking" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreOnVolatileVariables" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="DoubleLiteralMayBeFloatLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DuplicateAlternationBranch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DuplicateDeclarations" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="DynamicRegexReplaceableByCompiledPattern" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ElementOnlyUsedFromTestCode" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="EmptyClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignorableAnnotations">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="ignoreClassWithParameterization" value="true" />
|
||||||
|
<option name="ignoreThrowables" value="true" />
|
||||||
|
<option name="commentsAreContent" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="EmptySynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EnumSwitchStatementWhichMissesCases" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSwitchStatementsWithDefault" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="EnumeratedClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="EnumeratedConstantNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Z_\d]*" />
|
||||||
|
<option name="m_minLength" value="5" />
|
||||||
|
<option name="m_maxLength" value="32" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="EnumerationCanBeIteration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EqualityOperatorComparesObjects" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EqualsAndHashcode" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EqualsCalledOnEnumConstant" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EqualsUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="EscapedMetaCharacter" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ExceptionFromCatchWhichDoesntWrap" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreGetMessage" value="false" />
|
||||||
|
<option name="ignoreCantWrap" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ExtendsThrowable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ExtendsUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FallthruInSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FieldAccessedSynchronizedAndUnsynchronized" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="countGettersAndSetters" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FieldMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FieldMustBeInitialized" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FinalMethodInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FloatingPointEquality" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ForLoopReplaceableByWhile" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreLoopsWithoutConditions" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="FrequentlyUsedInheritorInspection" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="FullJavaName" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FullMethodName" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FunctionParameterCountMismatch" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="FuseStreamOperations" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="GrFieldAlreadyDefined" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="GroovyAnnotationNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GroovyClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GroovyEnumerationNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GroovyInterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="HashCodeUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HtmlTagCanBeJavadocTag" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IOResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredTypesString" value="java.io.ByteArrayOutputStream,java.io.ByteArrayInputStream,java.io.StringBufferInputStream,java.io.CharArrayWriter,java.io.CharArrayReader,java.io.StringWriter,java.io.StringReader" />
|
||||||
|
<option name="insideTryAllowed" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="IfMayBeConditional" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IfStatementWithIdenticalBranches" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IgnoreResultOfCall" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_reportAllNonLibraryCalls" value="false" />
|
||||||
|
<option name="callCheckString" value="java.io.File,.*,java.io.InputStream,read|skip|available|markSupported,java.io.Reader,read|skip|ready|markSupported,java.lang.Boolean,.*,java.lang.Byte,.*,java.lang.Character,.*,java.lang.Double,.*,java.lang.Float,.*,java.lang.Integer,.*,java.lang.Long,.*,java.lang.Math,.*,java.lang.Object,equals|hashCode|toString,java.lang.Short,.*,java.lang.StrictMath,.*,java.lang.String,.*,java.math.BigInteger,.*,java.math.BigDecimal,.*,java.net.InetAddress,.*,java.net.URI,.*,java.util.UUID,.*,java.util.regex.Matcher,pattern|toMatchResult|start|end|group|groupCount|matches|find|lookingAt|quoteReplacement|replaceAll|replaceFirst|regionStart|regionEnd|hasTransparantBounds|hasAnchoringBounds|hitEnd|requireEnd,java.util.regex.Pattern,.*,java.util.stream.BaseStream,.*" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ImplicitSubclassInspection" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="IncompatibleTypes" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InitializerIssues" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreInvisibleFields" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="InstanceMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="4" />
|
||||||
|
<option name="m_maxLength" value="32" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="InstanceVariableNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="m_[a-z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="5" />
|
||||||
|
<option name="m_maxLength" value="32" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="InstanceofCatchParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InstantiationOfUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IntLiteralMayBeLongLiteral" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IntegerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InterfaceNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="InvalidParameterAnnotations" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="IteratorNextDoesNotThrowNoSuchElementException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="JUnit3MethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="JUnit4MethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="JUnitAbstractTestClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*TestCase" />
|
||||||
|
<option name="m_minLength" value="12" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="JUnitTestClassNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*Test" />
|
||||||
|
<option name="m_minLength" value="8" />
|
||||||
|
<option name="m_maxLength" value="64" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="Java8ArraySetAll" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="Java9CollectionFactory" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="JavaRequiresAutoModule" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="JavadocHtmlLint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LambdaCanBeMethodCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LambdaParameterHidingMemberVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LengthOneStringsInConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LimitedScopeInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ListIndexOfReplaceableByContains" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LiteralAsArgToStringEquals" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="REPORT_VARIABLES" value="true" />
|
||||||
|
<option name="REPORT_PARAMETERS" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="LocalVariableHidingMemberVariable" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreInvisibleFields" value="true" />
|
||||||
|
<option name="m_ignoreStaticMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="LoopWithImplicitTerminationCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MagicNumber" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreInitialCapacity" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MapReplaceableByEnumMap" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MemberVisibility" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MemberVisibilityCanPrivate" enabled="true" level="INFO" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_onlyPrivateOrFinal" value="false" />
|
||||||
|
<option name="m_ignoreEmptyMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MethodMayBeSynchronized" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodName" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodOnlyUsedFromInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreMethodsAccessedFromAnonymousClass" value="false" />
|
||||||
|
<option name="ignoreStaticMethodsFromNonStaticInnerClass" value="false" />
|
||||||
|
<option name="onlyReportStaticMethods" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MethodOverloadsParentMethod" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="reportIncompatibleParameters" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MethodOverridesInaccessibleMethodOfSuper" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodOverridesStaticMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodParameterType" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodReturnType" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MissingDeprecatedAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreObjectMethods" value="false" />
|
||||||
|
<option name="ignoreAnonymousClassMethods" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MissortedModifiers" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_requireAnnotationsFirst" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MoveFieldAssignmentToInitializer" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="MultipleDeclaration" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreForLoopDeclarations" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MultipleVariablesInDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NamedResource" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NativeMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreNegatedNullComparison" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="NegatedConditionalExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NegatedEqualityExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NegatedIfElse" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreNegatedNullComparison" value="true" />
|
||||||
|
<option name="m_ignoreNegatedZeroComparison" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="NegativelyNamedBooleanVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NestedSynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NewClassNamingConventionMerged" />
|
||||||
|
<inspection_tool class="NewExceptionWithoutArguments" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NewGroovyClassNamingConventionMerged" />
|
||||||
|
<inspection_tool class="NewMethodNamingConventionMerged" />
|
||||||
|
<inspection_tool class="NoDefaultBaseConstructor" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonAsciiCharacters" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="CHECK_FOR_FILES_CONTAINING_BOM" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="NonExceptionNameEndsWithException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalFieldInEnum" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalFieldOfException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalStaticVariableUsedInClassInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonFinalUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonProtectedConstructorInAbstractClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreNonPublicClasses" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="NonShortCircuitBoolean" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonSynchronizedMethodOverridesSynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NonThreadSafeLazyInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NoopMethodInAbstractClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NotAssignable" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="NullThrown" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObjectEquality" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_ignoreEnums" value="true" />
|
||||||
|
<option name="m_ignoreClassObjects" value="false" />
|
||||||
|
<option name="m_ignorePrivateConstructors" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ObjectInstantiationInEqualsHashCode" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObjectToString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ObsoleteCollection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreRequiredObsoleteCollectionTypes" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="OctalEscape" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="OptionalContainsCollection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreInMatchingInstanceof" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="OverriddenMethodCallDuringObjectConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PackageInfoWithoutPackage" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PointerTypeRequired" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ProtectedField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ProtectedInnerClass" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreEnums" value="false" />
|
||||||
|
<option name="ignoreInterfaces" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PublicConstructorInNonPublicClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PublicField" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreEnums" value="false" />
|
||||||
|
<option name="ignorableAnnotations">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PublicFieldAccessedInSynchronizedContext" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RandomDoubleForRandomInteger" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSerializable" value="false" />
|
||||||
|
<option name="ignoreCloneable" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="RedundantMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RedundantThrowsDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RepeatedSpace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReplaceAssignmentWithOperatorAssignment" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreLazyOperators" value="true" />
|
||||||
|
<option name="ignoreObscureOperators" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ReplaceCallWithComparison" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RequireNonNull" enabled="false" level="INFORMATION" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ResourceParameter" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RestSignature" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ResultOfObjectAllocationIgnored" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ReturnOfCollectionField" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="ignorePrivateMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ReturnOfDateField" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<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" />
|
||||||
|
<inspection_tool class="SimplifiableAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SingleCharAlternation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StaticCallOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StaticFieldReferenceOnSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StaticImport" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="allowedClasses">
|
||||||
|
<set>
|
||||||
|
<option value="org.junit.Assert" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="StaticInheritance" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StaticMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[a-z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="4" />
|
||||||
|
<option name="m_maxLength" value="32" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="StaticNonFinalField" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StaticVariableNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="checkMutableFinals" value="false" />
|
||||||
|
<option name="m_regex" value="s_[a-z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="5" />
|
||||||
|
<option name="m_maxLength" value="32" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="StaticnessMismatch" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringBufferToStringInConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringConcatenationInFormatCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringConcatenationMissingWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringEqualsCharSequence" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="StringEqualsEmptyString" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="StringReplaceableByStringBuffer" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="onlyWarnOnLoop" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SubtractionInCompareTo" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SuperClassHasFrequentlyUsedInheritors" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="SuspiciousArrayCast" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SuspiciousIndentAfterControlStatement" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SwitchStatementWithConfusingDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SynchronizeOnLock" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SynchronizeOnThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="SynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_includeNativeMethods" value="true" />
|
||||||
|
<option name="ignoreSynchronizedSuperMethods" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SynchronizedOnLiteralObject" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TemplateArgumentsIssues" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TestMethodWithoutAssertion" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="assertionMethods" value="org.junit.Assert,assert.*|fail.*,junit.framework.Assert,assert.*|fail.*,org.junit.jupiter.api.Assertions,assert.*|fail.*,org.mockito.Mockito,verify.*,org.mockito.InOrder,verify,org.junit.rules.ExpectedException,expect.*,org.hamcrest.MatcherAssert,assertThat" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="TestNGMethodNamingConvention" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ThisEscapedInConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ThrowCaughtLocally" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreRethrownExceptions" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ThrowsRuntimeException" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TooBroadScope" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_allowConstructorAsInitializer" value="false" />
|
||||||
|
<option name="m_onlyLookAtBlocks" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="TrivialMethodReference" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TrivialStringConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TypeMayBeWeakened" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="useRighthandTypeAsWeakestTypeInAssignments" value="true" />
|
||||||
|
<option name="useParameterizedTypeForCollectionMethods" value="true" />
|
||||||
|
<option name="doNotWeakenToJavaLangObject" value="true" />
|
||||||
|
<option name="onlyWeakentoInterface" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TypeParameterNamingConvention" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="m_regex" value="[A-Z][A-Za-z\d]*" />
|
||||||
|
<option name="m_minLength" value="1" />
|
||||||
|
<option name="m_maxLength" value="1" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnaryPlus" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessarilyQualifiedInnerClassAccess" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreReferencesNeedingImport" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryBlockStatement" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreSwitchBranches" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryExplicitNumericCast" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreJavadoc" value="false" />
|
||||||
|
<option name="ignoreInModuleStatements" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryInheritDoc" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryJavaDocLink" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreInlineLinkToSuper" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessarySuperQualifier" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnnecessaryUnaryMinus" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnsecureRandomNumberGeneration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UnusedCatchParameter" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="m_ignoreCatchBlocksWithComments" value="false" />
|
||||||
|
<option name="m_ignoreTestCases" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="UnusedImport" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="UpperCaseFieldNameNotConstant" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfClone" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UseOfObsoleteDateTimeApi" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UtilityClassWithPublicConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="UtilityClassWithoutPrivateConstructor" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignorableAnnotations">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="ignoreClassesWithOnlyMain" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="WeakerAccess" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS" value="false" />
|
||||||
|
<option name="SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES" value="true" />
|
||||||
|
<option name="SUGGEST_PRIVATE_FOR_INNERS" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="WhileLoopSpinsOnField" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoreNonEmtpyLoops" value="false" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="unused" enabled="true" level="WARNING" enabled_by_default="true" klass="packageLocal" inner_class="protected" field="protected" method="protected" parameter="protected">
|
||||||
|
<option name="LOCAL_VARIABLE" value="true" />
|
||||||
|
<option name="FIELD" value="true" />
|
||||||
|
<option name="METHOD" value="true" />
|
||||||
|
<option name="CLASS" value="true" />
|
||||||
|
<option name="PARAMETER" value="true" />
|
||||||
|
<option name="REPORT_PARAMETER_FOR_PUBLIC_METHODS" value="false" />
|
||||||
|
<option name="ADD_MAINS_TO_ENTRIES" value="true" />
|
||||||
|
<option name="ADD_APPLET_TO_ENTRIES" value="true" />
|
||||||
|
<option name="ADD_SERVLET_TO_ENTRIES" value="true" />
|
||||||
|
<option name="ADD_NONJAVA_TO_ENTRIES" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="PROJECT_PROFILE" value="Default" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
202
COPYING
Normal file
202
COPYING
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
41
README.md
41
README.md
|
|
@ -1,3 +1,40 @@
|
||||||
# wireguard
|
# Android GUI for [WireGuard](https://www.wireguard.com/)
|
||||||
|
|
||||||
VPN for Android
|
**[Download from the Play Store](https://play.google.com/store/apps/details?id=com.wireguard.android)**
|
||||||
|
|
||||||
|
This is an Android GUI for [WireGuard](https://www.wireguard.com/). It [opportunistically uses the kernel implementation](https://git.zx2c4.com/android_kernel_wireguard/about/), and falls back to using the non-root [userspace implementation](https://git.zx2c4.com/wireguard-go/about/).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
|
||||||
|
$ cd wireguard-android
|
||||||
|
$ ./gradlew assembleRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
macOS users may need [flock(1)](https://github.com/discoteq/flock).
|
||||||
|
|
||||||
|
## Embedding
|
||||||
|
|
||||||
|
The tunnel library is [on Maven Central](https://search.maven.org/artifact/com.wireguard.android/tunnel), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
|
||||||
|
|
||||||
|
```
|
||||||
|
implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
|
||||||
|
```
|
||||||
|
|
||||||
|
The library makes use of Java 8 features, so be sure to support those in your gradle configuration with [desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring):
|
||||||
|
|
||||||
|
```
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
coreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translating
|
||||||
|
|
||||||
|
Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
|
||||||
|
|
|
||||||
13
build.gradle.kts
Normal file
13
build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.android.library) apply false
|
||||||
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
alias(libs.plugins.kotlin.kapt) apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
wrapper {
|
||||||
|
gradleVersion = "8.14"
|
||||||
|
distributionSha256Sum = "61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
gradle.properties
Normal file
49
gradle.properties
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
wireguardVersionCode=518
|
||||||
|
wireguardVersionName=1.0.20260102
|
||||||
|
wireguardPackageName=com.wireguard.android
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
|
||||||
|
# Enable Kotlin incremental compilation
|
||||||
|
kotlin.incremental=true
|
||||||
|
|
||||||
|
# Enable AndroidX support
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# Turn off AP discovery in compile path to enable compile avoidance
|
||||||
|
kapt.include.compile.classpath=false
|
||||||
|
|
||||||
|
# Experimental AGP flags
|
||||||
|
# Generate compile-time only R class for app modules.
|
||||||
|
android.enableAppCompileTimeRClass=true
|
||||||
|
# Keep AAPT2 daemons alive between incremental builds.
|
||||||
|
android.keepWorkerActionServicesBetweenBuilds=true
|
||||||
|
# Generate manifest class as a .class directly rather than a Java source file.
|
||||||
|
android.generateManifestClass=true
|
||||||
|
|
||||||
|
# Default Android build features
|
||||||
|
# Disable resource values generation by default in libraries
|
||||||
|
android.defaults.buildfeatures.resvalues=false
|
||||||
|
# Disable shader compilation by default
|
||||||
|
android.defaults.buildfeatures.shaders=false
|
||||||
|
# Disable Android resource processing by default
|
||||||
|
android.library.defaults.buildfeatures.androidresources=false
|
||||||
|
|
||||||
|
# Suppress warnings for some features that aren't yet stabilized
|
||||||
|
android.suppressUnsupportedOptionWarnings=android.keepWorkerActionServicesBetweenBuilds,\
|
||||||
|
android.enableAppCompileTimeRClass,\
|
||||||
|
android.suppressUnsupportedOptionWarnings
|
||||||
|
|
||||||
|
# OSSRH sometimes struggles with slow deployments, so this makes Gradle
|
||||||
|
# more tolerant to those delays.
|
||||||
|
systemProp.org.gradle.internal.http.connectionTimeout=500000
|
||||||
|
systemProp.org.gradle.internal.http.socketTimeout=500000
|
||||||
29
gradle/libs.versions.toml
Normal file
29
gradle/libs.versions.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
[versions]
|
||||||
|
agp = "8.13.2"
|
||||||
|
kotlin = "2.3.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-activity-ktx = "androidx.activity:activity-ktx:1.12.2"
|
||||||
|
androidx-annotation = "androidx.annotation:annotation:1.9.1"
|
||||||
|
androidx-appcompat = "androidx.appcompat:appcompat:1.7.1"
|
||||||
|
androidx-biometric = "androidx.biometric:biometric:1.1.0"
|
||||||
|
androidx-collection = "androidx.collection:collection:1.5.0"
|
||||||
|
androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.1"
|
||||||
|
androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.3.0"
|
||||||
|
androidx-core-ktx = "androidx.core:core-ktx:1.17.0"
|
||||||
|
androidx-datastore-preferences = "androidx.datastore:datastore-preferences:1.2.0"
|
||||||
|
androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.8.9"
|
||||||
|
androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.10.0"
|
||||||
|
androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.1"
|
||||||
|
desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.1.5"
|
||||||
|
google-material = "com.google.android.material:material:1.13.0"
|
||||||
|
jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
|
||||||
|
junit = "junit:junit:4.13.2"
|
||||||
|
kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||||
|
zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
248
gradlew
vendored
Executable file
248
gradlew
vendored
Executable file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original 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.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
22
settings.gradle.kts
Normal file
22
settings.gradle.kts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "wireguard-android"
|
||||||
|
|
||||||
|
include(":tunnel")
|
||||||
|
include(":ui")
|
||||||
10
sync-crowdin.sh
Executable file
10
sync-crowdin.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Download https://crowdin.com/backend/download/project/wireguard.zip and provide path as argument"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
bsdtar -C ui/src/main/res -x -f "$1" --strip-components 5 wireguard-android
|
||||||
|
find ui/src/main/res -name strings.xml -exec bash -c '[[ $(xmllint --xpath "count(//resources/*)" {}) -ne 0 ]] || rm -rf "$(dirname {})"' \;
|
||||||
143
tunnel/build.gradle.kts
Normal file
143
tunnel/build.gradle.kts
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
|
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||||
|
import org.gradle.api.tasks.bundling.Zip
|
||||||
|
|
||||||
|
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.library)
|
||||||
|
`maven-publish`
|
||||||
|
signing
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = 36
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
namespace = "${pkg}.tunnel"
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path("tools/CMakeLists.txt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testOptions.unitTests.all {
|
||||||
|
it.testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
all {
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
targets("libwg-go.so", "libwg.so", "libwg-quick.so")
|
||||||
|
arguments("-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}")
|
||||||
|
arguments("-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments("-DANDROID_PACKAGE_NAME=${pkg}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments("-DANDROID_PACKAGE_NAME=${pkg}.debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lint {
|
||||||
|
disable += "LongLogTag"
|
||||||
|
disable += "NewApi"
|
||||||
|
}
|
||||||
|
publishing {
|
||||||
|
singleVariant("release") {
|
||||||
|
withJavadocJar()
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.annotation)
|
||||||
|
implementation(libs.androidx.collection)
|
||||||
|
compileOnly(libs.jsr305)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
register<MavenPublication>("release") {
|
||||||
|
groupId = pkg
|
||||||
|
artifactId = "tunnel"
|
||||||
|
version = providers.gradleProperty("wireguardVersionName").get()
|
||||||
|
afterEvaluate {
|
||||||
|
from(components["release"])
|
||||||
|
}
|
||||||
|
pom {
|
||||||
|
name = "WireGuard Tunnel Library"
|
||||||
|
description = "Embeddable tunnel library for WireGuard for Android"
|
||||||
|
url = "https://www.wireguard.com/"
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = "The Apache Software License, Version 2.0"
|
||||||
|
url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||||
|
distribution = "repo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
connection = "scm:git:https://git.zx2c4.com/wireguard-android"
|
||||||
|
developerConnection = "scm:git:https://git.zx2c4.com/wireguard-android"
|
||||||
|
url = "https://git.zx2c4.com/wireguard-android"
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
organization {
|
||||||
|
name = "WireGuard"
|
||||||
|
url = "https://www.wireguard.com/"
|
||||||
|
}
|
||||||
|
developer {
|
||||||
|
name = "WireGuard"
|
||||||
|
email = "team@wireguard.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = "SonatypeUpload"
|
||||||
|
setUrl(layout.buildDirectory.dir("sonatype"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val releasePublication = publishing.publications.named("release").get() as MavenPublication
|
||||||
|
val mavenGroupPath = releasePublication.groupId.replace('.', '/')
|
||||||
|
val mavenArtifactId = releasePublication.artifactId
|
||||||
|
val mavenVersion = releasePublication.version
|
||||||
|
|
||||||
|
tasks.register<Zip>("zipReleasePublication") {
|
||||||
|
dependsOn(tasks.named("publishReleasePublicationToSonatypeUploadRepository"))
|
||||||
|
group = "distribution"
|
||||||
|
description = "Zips the release publication in Maven repository layout."
|
||||||
|
|
||||||
|
val sourceDir = layout.buildDirectory.dir("sonatype/${mavenGroupPath}/${mavenArtifactId}/${mavenVersion}")
|
||||||
|
from(sourceDir)
|
||||||
|
archiveFileName.set("${mavenArtifactId}-${mavenVersion}-maven.zip")
|
||||||
|
destinationDirectory.set(layout.buildDirectory.dir("distributions"))
|
||||||
|
into("${mavenGroupPath}/${mavenArtifactId}/${mavenVersion}")
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
useGpgCmd()
|
||||||
|
sign(publishing.publications)
|
||||||
|
}
|
||||||
18
tunnel/src/main/AndroidManifest.xml
Normal file
18
tunnel/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!--
|
||||||
|
~ Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<service
|
||||||
|
android:name="com.wireguard.android.backend.GoBackend$VpnService"
|
||||||
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.VpnService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.backend;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
/**
|
||||||
|
* Enumerate names of currently-running tunnels.
|
||||||
|
*
|
||||||
|
* @return The set of running tunnel names.
|
||||||
|
*/
|
||||||
|
Set<String> getRunningTunnelNames();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the state of a tunnel.
|
||||||
|
*
|
||||||
|
* @param tunnel The tunnel to examine the state of.
|
||||||
|
* @return The state of the tunnel.
|
||||||
|
* @throws Exception Exception raised when retrieving tunnel's state.
|
||||||
|
*/
|
||||||
|
Tunnel.State getState(Tunnel tunnel) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
|
||||||
|
* statistics object will be filled with zero values.
|
||||||
|
*
|
||||||
|
* @param tunnel The tunnel to retrieve statistics for.
|
||||||
|
* @return The statistics for the tunnel.
|
||||||
|
* @throws Exception Exception raised when retrieving statistics.
|
||||||
|
*/
|
||||||
|
Statistics getStatistics(Tunnel tunnel) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine version of underlying backend.
|
||||||
|
*
|
||||||
|
* @return The version of the backend.
|
||||||
|
* @throws Exception Exception raised while retrieving version.
|
||||||
|
*/
|
||||||
|
String getVersion() throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the service is running in always-on VPN mode.
|
||||||
|
* In this mode the system ensures that the service is always running by restarting it when necessary,
|
||||||
|
* e.g. after reboot.
|
||||||
|
*
|
||||||
|
* @return A boolean indicating whether the service is running in always-on VPN mode.
|
||||||
|
* @throws Exception Exception raised while retrieving the always-on status.
|
||||||
|
*/
|
||||||
|
|
||||||
|
boolean isAlwaysOn() throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the service is running in always-on VPN lockdown mode.
|
||||||
|
* In this mode the system ensures that the service is always running and that the apps
|
||||||
|
* aren't allowed to bypass the VPN.
|
||||||
|
*
|
||||||
|
* @return A boolean indicating whether the service is running in always-on VPN lockdown mode.
|
||||||
|
* @throws Exception Exception raised while retrieving the lockdown status.
|
||||||
|
*/
|
||||||
|
|
||||||
|
boolean isLockdownEnabled() throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
* @throws Exception Exception raised while changing state.
|
||||||
|
*/
|
||||||
|
Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.backend;
|
||||||
|
|
||||||
|
import com.wireguard.util.NonNullForAll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
|
||||||
|
* implementations of {@link Backend}.
|
||||||
|
*/
|
||||||
|
@NonNullForAll
|
||||||
|
public final class BackendException extends Exception {
|
||||||
|
private final Object[] format;
|
||||||
|
private final Reason reason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public constructor for BackendException.
|
||||||
|
*
|
||||||
|
* @param reason The {@link Reason} which caused this exception to be thrown
|
||||||
|
* @param format Format string values used when converting exceptions to user-facing strings.
|
||||||
|
*/
|
||||||
|
public BackendException(final Reason reason, final Object... format) {
|
||||||
|
this.reason = reason;
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the format string values associated with the instance.
|
||||||
|
*
|
||||||
|
* @return Array of {@link Object} for string formatting purposes
|
||||||
|
*/
|
||||||
|
public Object[] getFormat() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the reason for this exception.
|
||||||
|
*
|
||||||
|
* @return Associated {@link Reason} for this exception.
|
||||||
|
*/
|
||||||
|
public Reason getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum class containing all known reasons for why a {@link BackendException} might be thrown.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
DNS_RESOLUTION_FAILURE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,429 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.backend;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.system.OsConstants;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
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.InetEndpoint;
|
||||||
|
import com.wireguard.config.InetNetwork;
|
||||||
|
import com.wireguard.config.Peer;
|
||||||
|
import com.wireguard.crypto.Key;
|
||||||
|
import com.wireguard.crypto.KeyFormatException;
|
||||||
|
import com.wireguard.util.NonNullForAll;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.collection.ArraySet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link Backend} that uses the wireguard-go userspace implementation to provide
|
||||||
|
* WireGuard tunnels.
|
||||||
|
*/
|
||||||
|
@NonNullForAll
|
||||||
|
public final class GoBackend implements Backend {
|
||||||
|
private static final int DNS_RESOLUTION_RETRIES = 10;
|
||||||
|
private static final String TAG = "WireGuard/GoBackend";
|
||||||
|
@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 constructor for GoBackend.
|
||||||
|
*
|
||||||
|
* @param context An Android {@link Context}
|
||||||
|
*/
|
||||||
|
public GoBackend(final Context context) {
|
||||||
|
SharedLibraryLoader.loadSharedLibrary(context, "wg-go");
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a {@link AlwaysOnCallback} to be invoked when {@link VpnService} is started by the
|
||||||
|
* system's Always-On VPN mode.
|
||||||
|
*
|
||||||
|
* @param cb Callback to be invoked
|
||||||
|
*/
|
||||||
|
public static void setAlwaysOnCallback(final AlwaysOnCallback cb) {
|
||||||
|
alwaysOnCallback = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable private static native String wgGetConfig(int handle);
|
||||||
|
|
||||||
|
private static native int wgGetSocketV4(int handle);
|
||||||
|
|
||||||
|
private static native int wgGetSocketV6(int handle);
|
||||||
|
|
||||||
|
private static native void wgTurnOff(int handle);
|
||||||
|
|
||||||
|
private static native int wgTurnOn(String ifName, int tunFd, String settings);
|
||||||
|
|
||||||
|
private static native String wgVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to get the names of running tunnels.
|
||||||
|
*
|
||||||
|
* @return A set of string values denoting names of running tunnels.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Set<String> getRunningTunnelNames() {
|
||||||
|
if (currentTunnel != null) {
|
||||||
|
final Set<String> runningTunnels = new ArraySet<>();
|
||||||
|
runningTunnels.add(currentTunnel.getName());
|
||||||
|
return runningTunnels;
|
||||||
|
}
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the associated {@link State} for a given {@link Tunnel}.
|
||||||
|
*
|
||||||
|
* @param tunnel The tunnel to examine the state of.
|
||||||
|
* @return {@link State} associated with the given tunnel.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public State getState(final Tunnel tunnel) {
|
||||||
|
return currentTunnel == tunnel ? State.UP : State.DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the associated {@link Statistics} for a given {@link Tunnel}.
|
||||||
|
*
|
||||||
|
* @param tunnel The tunnel to retrieve statistics for.
|
||||||
|
* @return {@link Statistics} associated with the given tunnel.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Statistics getStatistics(final Tunnel tunnel) {
|
||||||
|
final Statistics stats = new Statistics();
|
||||||
|
if (tunnel != currentTunnel || currentTunnelHandle == -1)
|
||||||
|
return stats;
|
||||||
|
final String config = wgGetConfig(currentTunnelHandle);
|
||||||
|
if (config == null)
|
||||||
|
return stats;
|
||||||
|
Key key = null;
|
||||||
|
long rx = 0;
|
||||||
|
long tx = 0;
|
||||||
|
long latestHandshakeMSec = 0;
|
||||||
|
for (final String line : config.split("\\n")) {
|
||||||
|
if (line.startsWith("public_key=")) {
|
||||||
|
if (key != null)
|
||||||
|
stats.add(key, rx, tx, latestHandshakeMSec);
|
||||||
|
rx = 0;
|
||||||
|
tx = 0;
|
||||||
|
latestHandshakeMSec = 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;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("last_handshake_time_sec=")) {
|
||||||
|
if (key == null)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
latestHandshakeMSec += Long.parseLong(line.substring(24)) * 1000;
|
||||||
|
} catch (final NumberFormatException ignored) {
|
||||||
|
latestHandshakeMSec = 0;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith("last_handshake_time_nsec=")) {
|
||||||
|
if (key == null)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
latestHandshakeMSec += Long.parseLong(line.substring(25)) / 1000000;
|
||||||
|
} catch (final NumberFormatException ignored) {
|
||||||
|
latestHandshakeMSec = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key != null)
|
||||||
|
stats.add(key, rx, tx, latestHandshakeMSec);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version of the underlying wireguard-go library.
|
||||||
|
*
|
||||||
|
* @return {@link String} value of the version of the wireguard-go library.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getVersion() {
|
||||||
|
return wgVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the service is running in always-on VPN mode.
|
||||||
|
* @return {@link boolean} whether the service is running in always-on VPN mode.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isAlwaysOn() throws ExecutionException, InterruptedException, TimeoutException {
|
||||||
|
return vpnService.get(0, TimeUnit.NANOSECONDS).isAlwaysOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the service is running in always-on VPN lockdown mode.
|
||||||
|
* @return {@link boolean} whether the service is running in always-on VPN lockdown mode.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isLockdownEnabled() throws ExecutionException, InterruptedException, TimeoutException {
|
||||||
|
return vpnService.get(0, TimeUnit.NANOSECONDS).isLockdownEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the state of a given {@link Tunnel}, optionally applying a given {@link Config}.
|
||||||
|
*
|
||||||
|
* @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 {@link State} of the tunnel after state changes are applied.
|
||||||
|
* @throws Exception Exception raised while changing tunnel state.
|
||||||
|
*/
|
||||||
|
@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 && tunnel == currentTunnel && config == currentConfig)
|
||||||
|
return originalState;
|
||||||
|
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) {
|
||||||
|
if (config == null)
|
||||||
|
throw new BackendException(Reason.TUNNEL_MISSING_CONFIG);
|
||||||
|
|
||||||
|
if (VpnService.prepare(context) != null)
|
||||||
|
throw new BackendException(Reason.VPN_NOT_AUTHORIZED);
|
||||||
|
|
||||||
|
final VpnService service;
|
||||||
|
if (!vpnService.isDone()) {
|
||||||
|
Log.d(TAG, "Requesting to start VpnService");
|
||||||
|
context.startService(new Intent(context, VpnService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
service = vpnService.get(2, TimeUnit.SECONDS);
|
||||||
|
} catch (final TimeoutException e) {
|
||||||
|
final Exception be = new BackendException(Reason.UNABLE_TO_START_VPN);
|
||||||
|
be.initCause(e);
|
||||||
|
throw be;
|
||||||
|
}
|
||||||
|
service.setOwner(this);
|
||||||
|
|
||||||
|
if (currentTunnelHandle != -1) {
|
||||||
|
Log.w(TAG, "Tunnel already up");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dnsRetry: for (int i = 0; i < DNS_RESOLUTION_RETRIES; ++i) {
|
||||||
|
// Pre-resolve IPs so they're cached when building the userspace string
|
||||||
|
for (final Peer peer : config.getPeers()) {
|
||||||
|
final InetEndpoint ep = peer.getEndpoint().orElse(null);
|
||||||
|
if (ep == null)
|
||||||
|
continue;
|
||||||
|
if (ep.getResolved().orElse(null) == null) {
|
||||||
|
if (i < DNS_RESOLUTION_RETRIES - 1) {
|
||||||
|
Log.w(TAG, "DNS host \"" + ep.getHost() + "\" failed to resolve; trying again");
|
||||||
|
Thread.sleep(1000);
|
||||||
|
continue dnsRetry;
|
||||||
|
} else
|
||||||
|
throw new BackendException(Reason.DNS_RESOLUTION_FAILURE, ep.getHost());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build config
|
||||||
|
final String goConfig = config.toWgUserspaceString();
|
||||||
|
|
||||||
|
// Create the vpn tunnel with android API
|
||||||
|
final VpnService.Builder builder = service.getBuilder();
|
||||||
|
builder.setSession(tunnel.getName());
|
||||||
|
|
||||||
|
for (final String excludedApplication : config.getInterface().getExcludedApplications())
|
||||||
|
builder.addDisallowedApplication(excludedApplication);
|
||||||
|
|
||||||
|
for (final String includedApplication : config.getInterface().getIncludedApplications())
|
||||||
|
builder.addAllowedApplication(includedApplication);
|
||||||
|
|
||||||
|
for (final InetNetwork addr : config.getInterface().getAddresses())
|
||||||
|
builder.addAddress(addr.getAddress(), addr.getMask());
|
||||||
|
|
||||||
|
for (final InetAddress addr : config.getInterface().getDnsServers())
|
||||||
|
builder.addDnsServer(addr.getHostAddress());
|
||||||
|
|
||||||
|
for (final String dnsSearchDomain : config.getInterface().getDnsSearchDomains())
|
||||||
|
builder.addSearchDomain(dnsSearchDomain);
|
||||||
|
|
||||||
|
boolean sawDefaultRoute = false;
|
||||||
|
for (final Peer peer : config.getPeers()) {
|
||||||
|
for (final InetNetwork addr : peer.getAllowedIps()) {
|
||||||
|
if (addr.getMask() == 0)
|
||||||
|
sawDefaultRoute = true;
|
||||||
|
builder.addRoute(addr.getAddress(), addr.getMask());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Kill-switch" semantics
|
||||||
|
if (!(sawDefaultRoute && config.getPeers().size() == 1)) {
|
||||||
|
builder.allowFamily(OsConstants.AF_INET);
|
||||||
|
builder.allowFamily(OsConstants.AF_INET6);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 BackendException(Reason.TUN_CREATION_ERROR);
|
||||||
|
Log.d(TAG, "Go backend " + wgVersion());
|
||||||
|
currentTunnelHandle = wgTurnOn(tunnel.getName(), tun.detachFd(), goConfig);
|
||||||
|
}
|
||||||
|
if (currentTunnelHandle < 0)
|
||||||
|
throw new BackendException(Reason.GO_ACTIVATION_ERROR_CODE, currentTunnelHandle);
|
||||||
|
|
||||||
|
currentTunnel = tunnel;
|
||||||
|
currentConfig = config;
|
||||||
|
|
||||||
|
service.protect(wgGetSocketV4(currentTunnelHandle));
|
||||||
|
service.protect(wgGetSocketV6(currentTunnelHandle));
|
||||||
|
} else {
|
||||||
|
if (currentTunnelHandle == -1) {
|
||||||
|
Log.w(TAG, "Tunnel already down");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int handleToClose = currentTunnelHandle;
|
||||||
|
currentTunnel = null;
|
||||||
|
currentTunnelHandle = -1;
|
||||||
|
currentConfig = null;
|
||||||
|
wgTurnOff(handleToClose);
|
||||||
|
try {
|
||||||
|
vpnService.get(0, TimeUnit.NANOSECONDS).stopSelf();
|
||||||
|
} catch (final TimeoutException ignored) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.onStateChange(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for {@link GoBackend} that is invoked when {@link VpnService} is started by the
|
||||||
|
* system's Always-On VPN mode.
|
||||||
|
*/
|
||||||
|
public interface AlwaysOnCallback {
|
||||||
|
void alwaysOnTriggered();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link android.net.VpnService} implementation for {@link GoBackend}
|
||||||
|
*/
|
||||||
|
public static class VpnService extends android.net.VpnService {
|
||||||
|
@Nullable private GoBackend owner;
|
||||||
|
|
||||||
|
public Builder getBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
vpnService.complete(this);
|
||||||
|
super.onCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
|
vpnService = vpnService.newIncompleteFuture();
|
||||||
|
else
|
||||||
|
vpnService = new CompletableFuture<>();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
|
||||||
|
vpnService.complete(this);
|
||||||
|
if (intent == null || intent.getComponent() == null || !intent.getComponent().getPackageName().equals(getPackageName())) {
|
||||||
|
Log.d(TAG, "Service started by Always-on VPN feature");
|
||||||
|
if (alwaysOnCallback != null)
|
||||||
|
alwaysOnCallback.alwaysOnTriggered();
|
||||||
|
}
|
||||||
|
return super.onStartCommand(intent, flags, startId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOwner(final GoBackend owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.backend;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import com.wireguard.crypto.Key;
|
||||||
|
import com.wireguard.util.NonNullForAll;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing transfer statistics for a {@link Tunnel} instance.
|
||||||
|
*/
|
||||||
|
@NonNullForAll
|
||||||
|
public class Statistics {
|
||||||
|
public record PeerStats(long rxBytes, long txBytes, long latestHandshakeEpochMillis) { }
|
||||||
|
private final Map<Key, PeerStats> stats = new HashMap<>();
|
||||||
|
private long lastTouched = SystemClock.elapsedRealtime();
|
||||||
|
|
||||||
|
Statistics() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a peer and its current stats to the internal map.
|
||||||
|
*
|
||||||
|
* @param key A WireGuard public key bound to a particular peer
|
||||||
|
* @param rxBytes The received traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||||
|
* the provided {@link Key}. This value is in bytes
|
||||||
|
* @param txBytes The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
|
||||||
|
* the provided {@link Key}. This value is in bytes.
|
||||||
|
* @param latestHandshake The timestamp of the latest handshake for the {@link com.wireguard.config.Peer}
|
||||||
|
* referenced by the provided {@link Key}. The value is in epoch milliseconds.
|
||||||
|
*/
|
||||||
|
void add(final Key key, final long rxBytes, final long txBytes, final long latestHandshake) {
|
||||||
|
stats.put(key, new PeerStats(rxBytes, txBytes, latestHandshake));
|
||||||
|
lastTouched = SystemClock.elapsedRealtime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
|
||||||
|
*
|
||||||
|
* @return boolean indicating if the current statistics instance has stale values.
|
||||||
|
*/
|
||||||
|
public boolean isStale() {
|
||||||
|
return SystemClock.elapsedRealtime() - lastTouched > 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the statistics for the {@link com.wireguard.config.Peer} referenced by the provided {@link Key}
|
||||||
|
*
|
||||||
|
* @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
|
||||||
|
* @return a {@link PeerStats} representing various statistics about this peer.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public PeerStats peer(final Key peer) {
|
||||||
|
return stats.get(peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of peers being tracked by this instance.
|
||||||
|
*
|
||||||
|
* @return An array of {@link Key} instances representing WireGuard
|
||||||
|
* {@link com.wireguard.config.Peer}s
|
||||||
|
*/
|
||||||
|
public Key[] peers() {
|
||||||
|
return stats.keySet().toArray(new Key[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total received traffic by all the peers being tracked by this instance
|
||||||
|
*
|
||||||
|
* @return a long representing the number of bytes received by the peers being tracked.
|
||||||
|
*/
|
||||||
|
public long totalRx() {
|
||||||
|
long rx = 0;
|
||||||
|
for (final PeerStats val : stats.values()) {
|
||||||
|
rx += val.rxBytes;
|
||||||
|
}
|
||||||
|
return rx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total transmitted traffic by all the peers being tracked by this instance
|
||||||
|
*
|
||||||
|
* @return a long representing the number of bytes transmitted by the peers being tracked.
|
||||||
|
*/
|
||||||
|
public long totalTx() {
|
||||||
|
long tx = 0;
|
||||||
|
for (final PeerStats val : stats.values()) {
|
||||||
|
tx += val.txBytes;
|
||||||
|
}
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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.
|
||||||
|
*/
|
||||||
|
void onStateChange(State newState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum class to represent all possible states of a {@link Tunnel}.
|
||||||
|
*/
|
||||||
|
enum State {
|
||||||
|
DOWN,
|
||||||
|
TOGGLE,
|
||||||
|
UP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the state of a {@link Tunnel}
|
||||||
|
*
|
||||||
|
* @param running boolean indicating if the tunnel is running.
|
||||||
|
* @return State of the tunnel based on whether or not it is running.
|
||||||
|
*/
|
||||||
|
public static State of(final boolean running) {
|
||||||
|
return running ? UP : DOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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.time.Instant;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link Backend} that uses the kernel module and {@code wg-quick} to provide
|
||||||
|
* WireGuard tunnels.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@NonNullForAll
|
||||||
|
public final class WgQuickBackend implements Backend {
|
||||||
|
private static final String TAG = "WireGuard/WgQuickBackend";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasKernelSupport() {
|
||||||
|
return new File("/sys/module/wireguard").exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 Set.of(output.get(0).split(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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' dump", tunnel.getName())) != 0)
|
||||||
|
return stats;
|
||||||
|
} catch (final Exception ignored) {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
for (final String line : output) {
|
||||||
|
final String[] parts = line.split("\\t");
|
||||||
|
if (parts.length != 8)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
stats.add(Key.fromBase64(parts[0]), Long.parseLong(parts[5]), Long.parseLong(parts[6]), Long.parseLong(parts[4]) * 1000);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAlwaysOn() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLockdownEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMultipleTunnels(final 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
tunnel/src/main/java/com/wireguard/android/util/RootShell.java
Normal file
222
tunnel/src/main/java/com/wireguard/android/util/RootShell.java
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.wireguard.android.util.RootShell.RootShellException.Reason;
|
||||||
|
import com.wireguard.util.NonNullForAll;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
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";
|
||||||
|
|
||||||
|
private final File localBinaryDir;
|
||||||
|
private final File localTemporaryDir;
|
||||||
|
private final Object lock = new Object();
|
||||||
|
private final String preamble;
|
||||||
|
@Nullable private Process process;
|
||||||
|
@Nullable private BufferedReader stderr;
|
||||||
|
@Nullable private OutputStreamWriter stdin;
|
||||||
|
@Nullable private BufferedReader stdout;
|
||||||
|
|
||||||
|
public RootShell(final Context context) {
|
||||||
|
localBinaryDir = new File(context.getCodeCacheDir(), "bin");
|
||||||
|
localTemporaryDir = new File(context.getCacheDir(), "tmp");
|
||||||
|
final String packageName = context.getPackageName();
|
||||||
|
if (packageName.contains("'"))
|
||||||
|
throw new RuntimeException("Impossibly invalid package name contains a single quote");
|
||||||
|
preamble = String.format("export CALLING_PACKAGE='%s' PATH=\"%s:$PATH\" TMPDIR='%s'; magisk --sqlite \"UPDATE policies SET notification=0, logging=0 WHERE uid=%d\" >/dev/null 2>&1; id -u\n",
|
||||||
|
packageName, localBinaryDir, localTemporaryDir, android.os.Process.myUid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isExecutableInPath(final String name) {
|
||||||
|
final String path = System.getenv("PATH");
|
||||||
|
if (path == null)
|
||||||
|
return false;
|
||||||
|
for (final String dir : path.split(":"))
|
||||||
|
if (new File(dir, name).canExecute())
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunning() {
|
||||||
|
synchronized (lock) {
|
||||||
|
try {
|
||||||
|
// Throws an exception if the process hasn't finished yet.
|
||||||
|
if (process != null)
|
||||||
|
process.exitValue();
|
||||||
|
return false;
|
||||||
|
} catch (final IllegalThreadStateException ignored) {
|
||||||
|
// The existing process is still running.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a command in a root shell.
|
||||||
|
*
|
||||||
|
* @param output Lines read from stdout are appended to this list. Pass null if the
|
||||||
|
* output from the shell is not important.
|
||||||
|
* @param command Command to run as root.
|
||||||
|
* @return The exit value of the command.
|
||||||
|
*/
|
||||||
|
public int run(@Nullable final Collection<String> output, final String command)
|
||||||
|
throws IOException, RootShellException {
|
||||||
|
synchronized (lock) {
|
||||||
|
/* Start inside synchronized block to prevent a concurrent call to stop(). */
|
||||||
|
start();
|
||||||
|
final String marker = UUID.randomUUID().toString();
|
||||||
|
final String script = "echo " + marker + "; echo " + marker + " >&2; (" + command +
|
||||||
|
"); ret=$?; echo " + marker + " $ret; echo " + marker + " $ret >&2\n";
|
||||||
|
Log.v(TAG, "executing: " + command);
|
||||||
|
stdin.write(script);
|
||||||
|
stdin.flush();
|
||||||
|
String line;
|
||||||
|
int errnoStdout = Integer.MIN_VALUE;
|
||||||
|
int errnoStderr = Integer.MAX_VALUE;
|
||||||
|
int markersSeen = 0;
|
||||||
|
while ((line = stdout.readLine()) != null) {
|
||||||
|
if (line.startsWith(marker)) {
|
||||||
|
++markersSeen;
|
||||||
|
if (line.length() > marker.length() + 1) {
|
||||||
|
errnoStdout = Integer.valueOf(line.substring(marker.length() + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (markersSeen > 0) {
|
||||||
|
if (output != null)
|
||||||
|
output.add(line);
|
||||||
|
Log.v(TAG, "stdout: " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while ((line = stderr.readLine()) != null) {
|
||||||
|
if (line.startsWith(marker)) {
|
||||||
|
++markersSeen;
|
||||||
|
if (line.length() > marker.length() + 1) {
|
||||||
|
errnoStderr = Integer.valueOf(line.substring(marker.length() + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (markersSeen > 2) {
|
||||||
|
Log.v(TAG, "stderr: " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (markersSeen != 4)
|
||||||
|
throw new RootShellException(Reason.SHELL_MARKER_COUNT_ERROR, markersSeen);
|
||||||
|
if (errnoStdout != errnoStderr)
|
||||||
|
throw new RootShellException(Reason.SHELL_EXIT_STATUS_READ_ERROR);
|
||||||
|
Log.v(TAG, "exit: " + errnoStdout);
|
||||||
|
return errnoStdout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() throws IOException, RootShellException {
|
||||||
|
if (!isExecutableInPath(SU))
|
||||||
|
throw new RootShellException(Reason.NO_ROOT_ACCESS);
|
||||||
|
synchronized (lock) {
|
||||||
|
if (isRunning())
|
||||||
|
return;
|
||||||
|
if (!localBinaryDir.isDirectory() && !localBinaryDir.mkdirs())
|
||||||
|
throw new RootShellException(Reason.CREATE_BIN_DIR_ERROR);
|
||||||
|
if (!localTemporaryDir.isDirectory() && !localTemporaryDir.mkdirs())
|
||||||
|
throw new RootShellException(Reason.CREATE_TEMP_DIR_ERROR);
|
||||||
|
try {
|
||||||
|
final ProcessBuilder builder = new ProcessBuilder().command(SU);
|
||||||
|
builder.environment().put("LC_ALL", "C");
|
||||||
|
try {
|
||||||
|
process = builder.start();
|
||||||
|
} catch (final IOException e) {
|
||||||
|
// A failure at this stage means the device isn't rooted.
|
||||||
|
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(),
|
||||||
|
StandardCharsets.UTF_8));
|
||||||
|
stderr = new BufferedReader(new InputStreamReader(process.getErrorStream(),
|
||||||
|
StandardCharsets.UTF_8));
|
||||||
|
stdin.write(preamble);
|
||||||
|
stdin.flush();
|
||||||
|
// Check that the shell started successfully.
|
||||||
|
final String uid = stdout.readLine();
|
||||||
|
if (!"0".equals(uid)) {
|
||||||
|
Log.w(TAG, "Root check did not return correct UID: " + uid);
|
||||||
|
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 RootShellException(Reason.NO_ROOT_ACCESS);
|
||||||
|
}
|
||||||
|
throw new RootShellException(Reason.SHELL_START_ERROR, process.exitValue());
|
||||||
|
}
|
||||||
|
} catch (final IOException | RootShellException e) {
|
||||||
|
stop();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (process != null) {
|
||||||
|
process.destroy();
|
||||||
|
process = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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;
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.annotation.RestrictTo.Scope;
|
||||||
|
|
||||||
|
@NonNullForAll
|
||||||
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
|
public final class SharedLibraryLoader {
|
||||||
|
private static final String TAG = "WireGuard/SharedLibraryLoader";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
try (final ZipFile zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ)) {
|
||||||
|
final String mappedLibName = System.mapLibraryName(libName);
|
||||||
|
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;
|
||||||
|
final byte[] buffer = new byte[1024 * 32];
|
||||||
|
while ((len = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
out.getFD().sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,202 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.annotation.RestrictTo.Scope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
|
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 /data/adb/modules/wireguard' INT TERM EXIT; ");
|
||||||
|
script.append(String.format("rm -rf /data/adb/modules/wireguard/; mkdir -p /data/adb/modules/wireguard%s; ", INSTALL_DIR));
|
||||||
|
script.append("printf 'id=wireguard\nname=WireGuard Command Line Tools\nversion=1.0\nversionCode=1\nauthor=zx2c4\ndescription=Command line tools for WireGuard\nminMagisk=1500\n' > /data/adb/modules/wireguard/module.prop; ");
|
||||||
|
script.append("touch /data/adb/modules/wireguard/auto_mount; ");
|
||||||
|
for (final String name : EXECUTABLES) {
|
||||||
|
final File destination = new File("/data/adb/modules/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 /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
|
||||||
|
} catch (final Exception ignored) {
|
||||||
|
installAsMagiskModule = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return installAsMagiskModule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
tunnel/src/main/java/com/wireguard/config/Attribute.java
Normal file
60
tunnel/src/main/java/com/wireguard/config/Attribute.java
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@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 © 2017-2025 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"),
|
||||||
|
INCLUDED_APPLICATIONS("IncludedApplications"),
|
||||||
|
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,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
223
tunnel/src/main/java/com/wireguard/config/Config.java
Normal file
223
tunnel/src/main/java/com/wireguard/config/Config.java
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
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;
|
||||||
|
boolean seenInterfaceSection = 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;
|
||||||
|
seenInterfaceSection = true;
|
||||||
|
} 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);
|
||||||
|
if (!seenInterfaceSection)
|
||||||
|
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 ArrayList<Peer> peers = new ArrayList<>();
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
tunnel/src/main/java/com/wireguard/config/InetAddresses.java
Normal file
86
tunnel/src/main/java/com/wireguard/config/InetAddresses.java
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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]?))$");
|
||||||
|
private static final Pattern VALID_HOSTNAME = Pattern.compile("^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\\.?$");
|
||||||
|
|
||||||
|
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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether input is a valid DNS hostname.
|
||||||
|
*
|
||||||
|
* @param maybeHostname a string that is possibly a DNS hostname
|
||||||
|
* @return whether or not maybeHostname is a valid DNS hostname
|
||||||
|
*/
|
||||||
|
public static boolean isHostname(final CharSequence maybeHostname) {
|
||||||
|
return VALID_HOSTNAME.matcher(maybeHostname).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
Normal file
126
tunnel/src/main/java/com/wireguard/config/InetEndpoint.java
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ParseException(InetEndpoint.class, endpoint, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
tunnel/src/main/java/com/wireguard/config/InetNetwork.java
Normal file
79
tunnel/src/main/java/com/wireguard/config/InetNetwork.java
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
423
tunnel/src/main/java/com/wireguard/config/Interface.java
Normal file
423
tunnel/src/main/java/com/wireguard/config/Interface.java
Normal file
|
|
@ -0,0 +1,423 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> dnsSearchDomains;
|
||||||
|
private final Set<String> excludedApplications;
|
||||||
|
private final Set<String> includedApplications;
|
||||||
|
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));
|
||||||
|
dnsSearchDomains = Collections.unmodifiableSet(new LinkedHashSet<>(builder.dnsSearchDomains));
|
||||||
|
excludedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.excludedApplications));
|
||||||
|
includedApplications = Collections.unmodifiableSet(new LinkedHashSet<>(builder.includedApplications));
|
||||||
|
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 "includedapplications":
|
||||||
|
builder.parseIncludedApplications(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)
|
||||||
|
&& dnsSearchDomains.equals(other.dnsSearchDomains)
|
||||||
|
&& excludedApplications.equals(other.excludedApplications)
|
||||||
|
&& includedApplications.equals(other.includedApplications)
|
||||||
|
&& 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 DNS search domains associated with the interface.
|
||||||
|
*
|
||||||
|
* @return a set of strings
|
||||||
|
*/
|
||||||
|
public Set<String> getDnsSearchDomains() {
|
||||||
|
// The collection is already immutable.
|
||||||
|
return dnsSearchDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 set of applications included exclusively for using the interface.
|
||||||
|
*
|
||||||
|
* @return a set of package names
|
||||||
|
*/
|
||||||
|
public Set<String> getIncludedApplications() {
|
||||||
|
// The collection is already immutable.
|
||||||
|
return includedApplications;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 + includedApplications.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 = dnsServers.stream().map(InetAddress::getHostAddress).collect(Collectors.toList());
|
||||||
|
dnsServerStrings.addAll(dnsSearchDomains);
|
||||||
|
sb.append("DNS = ").append(Attribute.join(dnsServerStrings)).append('\n');
|
||||||
|
}
|
||||||
|
if (!excludedApplications.isEmpty())
|
||||||
|
sb.append("ExcludedApplications = ").append(Attribute.join(excludedApplications)).append('\n');
|
||||||
|
if (!includedApplications.isEmpty())
|
||||||
|
sb.append("IncludedApplications = ").append(Attribute.join(includedApplications)).append('\n');
|
||||||
|
listenPort.ifPresent(lp -> sb.append("ListenPort = ").append(lp).append('\n'));
|
||||||
|
mtu.ifPresent(m -> sb.append("MTU = ").append(m).append('\n'));
|
||||||
|
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> dnsSearchDomains = new LinkedHashSet<>();
|
||||||
|
// Defaults to an empty set.
|
||||||
|
private final Set<String> excludedApplications = new LinkedHashSet<>();
|
||||||
|
// Defaults to an empty set.
|
||||||
|
private final Set<String> includedApplications = 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 Builder addDnsSearchDomain(final String dnsSearchDomain) {
|
||||||
|
dnsSearchDomains.add(dnsSearchDomain);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addDnsSearchDomains(final Collection<String> dnsSearchDomains) {
|
||||||
|
this.dnsSearchDomains.addAll(dnsSearchDomains);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Interface build() throws BadConfigException {
|
||||||
|
if (keyPair == null)
|
||||||
|
throw new BadConfigException(Section.INTERFACE, Location.PRIVATE_KEY,
|
||||||
|
Reason.MISSING_ATTRIBUTE, null);
|
||||||
|
if (!includedApplications.isEmpty() && !excludedApplications.isEmpty())
|
||||||
|
throw new BadConfigException(Section.INTERFACE, Location.INCLUDED_APPLICATIONS,
|
||||||
|
Reason.INVALID_KEY, 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 includeApplication(final String application) {
|
||||||
|
includedApplications.add(application);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder includeApplications(final Collection<String> applications) {
|
||||||
|
includedApplications.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)) {
|
||||||
|
try {
|
||||||
|
addDnsServer(InetAddresses.parse(dnsServer));
|
||||||
|
} catch (final ParseException e) {
|
||||||
|
if (e.getParsingClass() != InetAddress.class || !InetAddresses.isHostname(dnsServer))
|
||||||
|
throw e;
|
||||||
|
addDnsSearchDomain(dnsServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
} catch (final ParseException e) {
|
||||||
|
throw new BadConfigException(Section.INTERFACE, Location.DNS, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder parseExcludedApplications(final CharSequence apps) {
|
||||||
|
return excludeApplications(List.of(Attribute.split(apps)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder parseIncludedApplications(final CharSequence apps) {
|
||||||
|
return includeApplications(List.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 © 2017-2025 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
307
tunnel/src/main/java/com/wireguard/config/Peer.java
Normal file
307
tunnel/src/main/java/com/wireguard/config/Peer.java
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
500
tunnel/src/main/java/com/wireguard/crypto/Curve25519.java
Normal file
500
tunnel/src/main/java/com/wireguard/crypto/Curve25519.java
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2016 Southern Storm Software, Pty Ltd.
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.crypto;
|
||||||
|
|
||||||
|
import com.wireguard.util.NonNullForAll;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of Curve25519 ECDH.
|
||||||
|
* <p>
|
||||||
|
* This implementation was imported to WireGuard from noise-java:
|
||||||
|
* https://github.com/rweather/noise-java
|
||||||
|
* <p>
|
||||||
|
* This implementation is based on that from arduinolibs:
|
||||||
|
* https://github.com/rweather/arduinolibs
|
||||||
|
* <p>
|
||||||
|
* Differences in this version are due to using 26-bit limbs for the
|
||||||
|
* representation instead of the 8/16/32-bit limbs in the original.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
private static final int NUM_LIMBS_510BIT = 20;
|
||||||
|
|
||||||
|
private final int[] A;
|
||||||
|
private final int[] AA;
|
||||||
|
private final int[] B;
|
||||||
|
private final int[] BB;
|
||||||
|
private final int[] C;
|
||||||
|
private final int[] CB;
|
||||||
|
private final int[] D;
|
||||||
|
private final int[] DA;
|
||||||
|
private final int[] E;
|
||||||
|
private final long[] t1;
|
||||||
|
private final int[] t2;
|
||||||
|
private final int[] x_1;
|
||||||
|
private final int[] x_2;
|
||||||
|
private final int[] x_3;
|
||||||
|
private final int[] z_2;
|
||||||
|
private final int[] z_3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the temporary state holder for Curve25519 evaluation.
|
||||||
|
*/
|
||||||
|
private Curve25519() {
|
||||||
|
// Allocate memory for all of the temporary variables we will need.
|
||||||
|
x_1 = new int[NUM_LIMBS_255BIT];
|
||||||
|
x_2 = new int[NUM_LIMBS_255BIT];
|
||||||
|
x_3 = new int[NUM_LIMBS_255BIT];
|
||||||
|
z_2 = new int[NUM_LIMBS_255BIT];
|
||||||
|
z_3 = new int[NUM_LIMBS_255BIT];
|
||||||
|
A = new int[NUM_LIMBS_255BIT];
|
||||||
|
B = new int[NUM_LIMBS_255BIT];
|
||||||
|
C = new int[NUM_LIMBS_255BIT];
|
||||||
|
D = new int[NUM_LIMBS_255BIT];
|
||||||
|
E = new int[NUM_LIMBS_255BIT];
|
||||||
|
AA = new int[NUM_LIMBS_255BIT];
|
||||||
|
BB = new int[NUM_LIMBS_255BIT];
|
||||||
|
DA = new int[NUM_LIMBS_255BIT];
|
||||||
|
CB = new int[NUM_LIMBS_255BIT];
|
||||||
|
t1 = new long[NUM_LIMBS_510BIT];
|
||||||
|
t2 = new int[NUM_LIMBS_510BIT];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditional swap of two values.
|
||||||
|
*
|
||||||
|
* @param select Set to 1 to swap, 0 to leave as-is.
|
||||||
|
* @param x The first value.
|
||||||
|
* @param y The second value.
|
||||||
|
*/
|
||||||
|
private static void cswap(int select, final int[] x, final int[] y) {
|
||||||
|
select = -select;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
final int dummy = select & (x[index] ^ y[index]);
|
||||||
|
x[index] ^= dummy;
|
||||||
|
y[index] ^= dummy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the Curve25519 curve.
|
||||||
|
*
|
||||||
|
* @param result Buffer to place the result of the evaluation into.
|
||||||
|
* @param offset Offset into the result buffer.
|
||||||
|
* @param privateKey The private key to use in the evaluation.
|
||||||
|
* @param publicKey The public key to use in the evaluation, or null
|
||||||
|
* if the base point of the curve should be used.
|
||||||
|
*/
|
||||||
|
public static void eval(final byte[] result, final int offset,
|
||||||
|
final byte[] privateKey, @Nullable final byte[] publicKey) {
|
||||||
|
final Curve25519 state = new Curve25519();
|
||||||
|
try {
|
||||||
|
// Unpack the public key value. If null, use 9 as the base point.
|
||||||
|
Arrays.fill(state.x_1, 0);
|
||||||
|
if (publicKey != null) {
|
||||||
|
// Convert the input value from little-endian into 26-bit limbs.
|
||||||
|
for (int index = 0; index < 32; ++index) {
|
||||||
|
final int bit = (index * 8) % 26;
|
||||||
|
final int word = (index * 8) / 26;
|
||||||
|
final int value = publicKey[index] & 0xFF;
|
||||||
|
if (bit <= (26 - 8)) {
|
||||||
|
state.x_1[word] |= value << bit;
|
||||||
|
} else {
|
||||||
|
state.x_1[word] |= value << bit;
|
||||||
|
state.x_1[word] &= 0x03FFFFFF;
|
||||||
|
state.x_1[word + 1] |= value >> (26 - bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just in case, we reduce the number modulo 2^255 - 19 to
|
||||||
|
// make sure that it is in range of the field before we start.
|
||||||
|
// This eliminates values between 2^255 - 19 and 2^256 - 1.
|
||||||
|
state.reduceQuick(state.x_1);
|
||||||
|
state.reduceQuick(state.x_1);
|
||||||
|
} else {
|
||||||
|
state.x_1[0] = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the other temporary variables.
|
||||||
|
Arrays.fill(state.x_2, 0); // x_2 = 1
|
||||||
|
state.x_2[0] = 1;
|
||||||
|
Arrays.fill(state.z_2, 0); // z_2 = 0
|
||||||
|
System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1
|
||||||
|
Arrays.fill(state.z_3, 0); // z_3 = 1
|
||||||
|
state.z_3[0] = 1;
|
||||||
|
|
||||||
|
// Evaluate the curve for every bit of the private key.
|
||||||
|
state.evalCurve(privateKey);
|
||||||
|
|
||||||
|
// Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.
|
||||||
|
state.recip(state.z_3, state.z_2);
|
||||||
|
state.mul(state.x_2, state.x_2, state.z_3);
|
||||||
|
|
||||||
|
// Convert x_2 into little-endian in the result buffer.
|
||||||
|
for (int index = 0; index < 32; ++index) {
|
||||||
|
final int bit = (index * 8) % 26;
|
||||||
|
final int word = (index * 8) / 26;
|
||||||
|
if (bit <= (26 - 8))
|
||||||
|
result[offset + index] = (byte) (state.x_2[word] >> bit);
|
||||||
|
else
|
||||||
|
result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up all temporary state before we exit.
|
||||||
|
state.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts two numbers modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The first number to subtract.
|
||||||
|
* @param y The second number to subtract.
|
||||||
|
*/
|
||||||
|
private static void sub(final int[] result, final int[] x, final int[] y) {
|
||||||
|
int index;
|
||||||
|
int borrow;
|
||||||
|
|
||||||
|
// Subtract y from x to generate the intermediate result.
|
||||||
|
borrow = 0;
|
||||||
|
for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);
|
||||||
|
result[index] = borrow & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we had a borrow, then the result has gone negative and we
|
||||||
|
// have to add 2^255 - 19 to the result to make it positive again.
|
||||||
|
// The top bits of "borrow" will be all 1's if there is a borrow
|
||||||
|
// or it will be all 0's if there was no borrow. Easiest is to
|
||||||
|
// conditionally subtract 19 and then mask off the high bits.
|
||||||
|
borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);
|
||||||
|
result[0] = borrow & 0x03FFFFFF;
|
||||||
|
for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
borrow = result[index] - ((borrow >> 26) & 0x01);
|
||||||
|
result[index] = borrow & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two numbers modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The first number to add.
|
||||||
|
* @param y The second number to add.
|
||||||
|
*/
|
||||||
|
private void add(final int[] result, final int[] x, final int[] y) {
|
||||||
|
int carry = x[0] + y[0];
|
||||||
|
result[0] = carry & 0x03FFFFFF;
|
||||||
|
for (int index = 1; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry = (carry >> 26) + x[index] + y[index];
|
||||||
|
result[index] = carry & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
reduceQuick(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all sensitive data in this object.
|
||||||
|
*/
|
||||||
|
private void destroy() {
|
||||||
|
// Destroy all temporary variables.
|
||||||
|
Arrays.fill(x_1, 0);
|
||||||
|
Arrays.fill(x_2, 0);
|
||||||
|
Arrays.fill(x_3, 0);
|
||||||
|
Arrays.fill(z_2, 0);
|
||||||
|
Arrays.fill(z_3, 0);
|
||||||
|
Arrays.fill(A, 0);
|
||||||
|
Arrays.fill(B, 0);
|
||||||
|
Arrays.fill(C, 0);
|
||||||
|
Arrays.fill(D, 0);
|
||||||
|
Arrays.fill(E, 0);
|
||||||
|
Arrays.fill(AA, 0);
|
||||||
|
Arrays.fill(BB, 0);
|
||||||
|
Arrays.fill(DA, 0);
|
||||||
|
Arrays.fill(CB, 0);
|
||||||
|
Arrays.fill(t1, 0L);
|
||||||
|
Arrays.fill(t2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the curve for every bit in a secret key.
|
||||||
|
*
|
||||||
|
* @param s The 32-byte secret key.
|
||||||
|
*/
|
||||||
|
private void evalCurve(final byte[] s) {
|
||||||
|
int sposn = 31;
|
||||||
|
int sbit = 6;
|
||||||
|
int svalue = s[sposn] | 0x40;
|
||||||
|
int swap = 0;
|
||||||
|
|
||||||
|
// Iterate over all 255 bits of "s" from the highest to the lowest.
|
||||||
|
// We ignore the high bit of the 256-bit representation of "s".
|
||||||
|
while (true) {
|
||||||
|
// Conditional swaps on entry to this bit but only if we
|
||||||
|
// didn't swap on the previous bit.
|
||||||
|
final int select = (svalue >> sbit) & 0x01;
|
||||||
|
swap ^= select;
|
||||||
|
cswap(swap, x_2, x_3);
|
||||||
|
cswap(swap, z_2, z_3);
|
||||||
|
swap = select;
|
||||||
|
|
||||||
|
// Evaluate the curve.
|
||||||
|
add(A, x_2, z_2); // A = x_2 + z_2
|
||||||
|
square(AA, A); // AA = A^2
|
||||||
|
sub(B, x_2, z_2); // B = x_2 - z_2
|
||||||
|
square(BB, B); // BB = B^2
|
||||||
|
sub(E, AA, BB); // E = AA - BB
|
||||||
|
add(C, x_3, z_3); // C = x_3 + z_3
|
||||||
|
sub(D, x_3, z_3); // D = x_3 - z_3
|
||||||
|
mul(DA, D, A); // DA = D * A
|
||||||
|
mul(CB, C, B); // CB = C * B
|
||||||
|
add(x_3, DA, CB); // x_3 = (DA + CB)^2
|
||||||
|
square(x_3, x_3);
|
||||||
|
sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2
|
||||||
|
square(z_3, z_3);
|
||||||
|
mul(z_3, z_3, x_1);
|
||||||
|
mul(x_2, AA, BB); // x_2 = AA * BB
|
||||||
|
mulA24(z_2, E); // z_2 = E * (AA + a24 * E)
|
||||||
|
add(z_2, z_2, AA);
|
||||||
|
mul(z_2, z_2, E);
|
||||||
|
|
||||||
|
// Move onto the next lower bit of "s".
|
||||||
|
if (sbit > 0) {
|
||||||
|
--sbit;
|
||||||
|
} else if (sposn == 0) {
|
||||||
|
break;
|
||||||
|
} else if (sposn == 1) {
|
||||||
|
--sposn;
|
||||||
|
svalue = s[sposn] & 0xF8;
|
||||||
|
sbit = 7;
|
||||||
|
} else {
|
||||||
|
--sposn;
|
||||||
|
svalue = s[sposn];
|
||||||
|
sbit = 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final conditional swaps.
|
||||||
|
cswap(swap, x_2, x_3);
|
||||||
|
cswap(swap, z_2, z_3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies two numbers modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The first number to multiply.
|
||||||
|
* @param y The second number to multiply.
|
||||||
|
*/
|
||||||
|
private void mul(final int[] result, final int[] x, final int[] y) {
|
||||||
|
// Multiply the two numbers to create the intermediate result.
|
||||||
|
long v = x[0];
|
||||||
|
for (int i = 0; i < NUM_LIMBS_255BIT; ++i) {
|
||||||
|
t1[i] = v * y[i];
|
||||||
|
}
|
||||||
|
for (int i = 1; i < NUM_LIMBS_255BIT; ++i) {
|
||||||
|
v = x[i];
|
||||||
|
for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {
|
||||||
|
t1[i + j] += v * y[j];
|
||||||
|
}
|
||||||
|
t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate carries and convert back into 26-bit words.
|
||||||
|
v = t1[0];
|
||||||
|
t2[0] = ((int) v) & 0x03FFFFFF;
|
||||||
|
for (int i = 1; i < NUM_LIMBS_510BIT; ++i) {
|
||||||
|
v = (v >> 26) + t1[i];
|
||||||
|
t2[i] = ((int) v) & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce the result modulo 2^255 - 19.
|
||||||
|
reduce(result, t2, NUM_LIMBS_255BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies a number by the a24 constant, modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The number to multiply by a24.
|
||||||
|
*/
|
||||||
|
private void mulA24(final int[] result, final int[] x) {
|
||||||
|
final long a24 = 121665;
|
||||||
|
long carry = 0;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += a24 * x[index];
|
||||||
|
t2[index] = ((int) carry) & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;
|
||||||
|
reduce(result, t2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raise x to the power of (2^250 - 1).
|
||||||
|
*
|
||||||
|
* @param result The result. Must not overlap with x.
|
||||||
|
* @param x The argument.
|
||||||
|
*/
|
||||||
|
private void pow250(final int[] result, final int[] x) {
|
||||||
|
// The big-endian hexadecimal expansion of (2^250 - 1) is:
|
||||||
|
// 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
|
||||||
|
//
|
||||||
|
// The naive implementation needs to do 2 multiplications per 1 bit and
|
||||||
|
// 1 multiplication per 0 bit. We can improve upon this by creating a
|
||||||
|
// pattern 0000000001 ... 0000000001. If we square and multiply the
|
||||||
|
// pattern by itself we can turn the pattern into the partial results
|
||||||
|
// 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.
|
||||||
|
// This averages out to about 1.1 multiplications per 1 bit instead of 2.
|
||||||
|
|
||||||
|
// Build a pattern of 250 bits in length of repeated copies of 0000000001.
|
||||||
|
square(A, x);
|
||||||
|
for (int j = 0; j < 9; ++j)
|
||||||
|
square(A, A);
|
||||||
|
mul(result, A, x);
|
||||||
|
for (int i = 0; i < 23; ++i) {
|
||||||
|
for (int j = 0; j < 10; ++j)
|
||||||
|
square(A, A);
|
||||||
|
mul(result, result, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply bit-shifted versions of the 0000000001 pattern into
|
||||||
|
// the result to "fill in" the gaps in the pattern.
|
||||||
|
square(A, result);
|
||||||
|
mul(result, result, A);
|
||||||
|
for (int j = 0; j < 8; ++j) {
|
||||||
|
square(A, A);
|
||||||
|
mul(result, result, A);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the reciprocal of a number modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result. Must not overlap with x.
|
||||||
|
* @param x The argument.
|
||||||
|
*/
|
||||||
|
private void recip(final int[] result, final int[] x) {
|
||||||
|
// The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.
|
||||||
|
// The big-endian hexadecimal expansion of (p - 2) is:
|
||||||
|
// 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB
|
||||||
|
// Start with the 250 upper bits of the expansion of (p - 2).
|
||||||
|
pow250(result, x);
|
||||||
|
|
||||||
|
// Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.
|
||||||
|
square(result, result);
|
||||||
|
square(result, result);
|
||||||
|
mul(result, result, x);
|
||||||
|
square(result, result);
|
||||||
|
square(result, result);
|
||||||
|
mul(result, result, x);
|
||||||
|
square(result, result);
|
||||||
|
mul(result, result, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce a number modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The value to be reduced. This array will be
|
||||||
|
* modified during the reduction.
|
||||||
|
* @param size The number of limbs in the high order half of x.
|
||||||
|
*/
|
||||||
|
private void reduce(final int[] result, final int[] x, final int size) {
|
||||||
|
// Calculate (x mod 2^255) + ((x / 2^255) * 19) which will
|
||||||
|
// either produce the answer we want or it will produce a
|
||||||
|
// value of the form "answer + j * (2^255 - 19)". There are
|
||||||
|
// 5 left-over bits in the top-most limb of the bottom half.
|
||||||
|
int carry = 0;
|
||||||
|
int limb = x[NUM_LIMBS_255BIT - 1] >> 21;
|
||||||
|
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
for (int index = 0; index < size; ++index) {
|
||||||
|
limb += x[NUM_LIMBS_255BIT + index] << 5;
|
||||||
|
carry += (limb & 0x03FFFFFF) * 19 + x[index];
|
||||||
|
x[index] = carry & 0x03FFFFFF;
|
||||||
|
limb >>= 26;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
if (size < NUM_LIMBS_255BIT) {
|
||||||
|
// The high order half of the number is short; e.g. for mulA24().
|
||||||
|
// Propagate the carry through the rest of the low order part.
|
||||||
|
for (int index = size; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += x[index];
|
||||||
|
x[index] = carry & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "j" value may still be too large due to the final carry-out.
|
||||||
|
// We must repeat the reduction. If we already have the answer,
|
||||||
|
// then this won't do any harm but we must still do the calculation
|
||||||
|
// to preserve the overall timing. The "j" value will be between
|
||||||
|
// 0 and 19, which means that the carry we care about is in the
|
||||||
|
// top 5 bits of the highest limb of the bottom half.
|
||||||
|
carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;
|
||||||
|
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += x[index];
|
||||||
|
result[index] = carry & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point "x" will either be the answer or it will be the
|
||||||
|
// answer plus (2^255 - 19). Perform a trial subtraction to
|
||||||
|
// complete the reduction process.
|
||||||
|
reduceQuick(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces a number modulo 2^255 - 19 where it is known that the
|
||||||
|
* number can be reduced with only 1 trial subtraction.
|
||||||
|
*
|
||||||
|
* @param x The number to reduce, and the result.
|
||||||
|
*/
|
||||||
|
private void reduceQuick(final int[] x) {
|
||||||
|
// Perform a trial subtraction of (2^255 - 19) from "x" which is
|
||||||
|
// equivalent to adding 19 and subtracting 2^255. We add 19 here;
|
||||||
|
// the subtraction of 2^255 occurs in the next step.
|
||||||
|
int carry = 19;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += x[index];
|
||||||
|
t2[index] = carry & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was a borrow, then the original "x" is the correct answer.
|
||||||
|
// If there was no borrow, then "t2" is the correct answer. Select the
|
||||||
|
// correct answer but do it in a way that instruction timing will not
|
||||||
|
// reveal which value was selected. Borrow will occur if bit 21 of
|
||||||
|
// "t2" is zero. Turn the bit into a selection mask.
|
||||||
|
final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);
|
||||||
|
final int nmask = ~mask;
|
||||||
|
t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index)
|
||||||
|
x[index] = (x[index] & nmask) | (t2[index] & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Squares a number modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The number to square.
|
||||||
|
*/
|
||||||
|
private void square(final int[] result, final int[] x) {
|
||||||
|
mul(result, x, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
290
tunnel/src/main/java/com/wireguard/crypto/Key.java
Normal file
290
tunnel/src/main/java/com/wireguard/crypto/Key.java
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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 © 2017-2025 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
|
||||||
|
}
|
||||||
|
}
|
||||||
54
tunnel/src/main/java/com/wireguard/crypto/KeyPair.java
Normal file
54
tunnel/src/main/java/com/wireguard/crypto/KeyPair.java
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
tunnel/src/main/java/com/wireguard/util/NonNullForAll.java
Normal file
29
tunnel/src/main/java/com/wireguard/util/NonNullForAll.java
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.util;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.meta.TypeQualifierDefault;
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.annotation.RestrictTo.Scope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This annotation can be applied to a package, class or method to indicate that all
|
||||||
|
* class fields and method parameters and return values in that element are nonnull
|
||||||
|
* by default unless overridden.
|
||||||
|
*/
|
||||||
|
@RestrictTo(Scope.LIBRARY_GROUP)
|
||||||
|
@Nonnull
|
||||||
|
@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
|
||||||
|
public @interface NonNullForAll {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 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 org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
public class BadConfigExceptionTest {
|
||||||
|
private static final Map<String, InputStream> CONFIG_MAP = new HashMap<>();
|
||||||
|
private static final String[] CONFIG_NAMES = {
|
||||||
|
"invalid-key",
|
||||||
|
"invalid-number",
|
||||||
|
"invalid-value",
|
||||||
|
"missing-attribute",
|
||||||
|
"missing-section",
|
||||||
|
"syntax-error",
|
||||||
|
"unknown-attribute",
|
||||||
|
"unknown-section"
|
||||||
|
};
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeStreams() {
|
||||||
|
for (final InputStream inputStream : CONFIG_MAP.values()) {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (final IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void readConfigs() {
|
||||||
|
for (final String config : CONFIG_NAMES) {
|
||||||
|
CONFIG_MAP.put(config, BadConfigExceptionTest.class.getClassLoader().getResourceAsStream(config + ".conf"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_INVALID_KEY_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("invalid-key"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.INVALID_KEY);
|
||||||
|
assertEquals(e.getLocation(), Location.PUBLIC_KEY);
|
||||||
|
assertEquals(e.getSection(), Section.PEER);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException thrown during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_INVALID_NUMBER_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("invalid-number"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.INVALID_NUMBER);
|
||||||
|
assertEquals(e.getLocation(), Location.PERSISTENT_KEEPALIVE);
|
||||||
|
assertEquals(e.getSection(), Section.PEER);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException thrown during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_INVALID_VALUE_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("invalid-value"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.INVALID_VALUE);
|
||||||
|
assertEquals(e.getLocation(), Location.DNS);
|
||||||
|
assertEquals(e.getSection(), Section.INTERFACE);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException throwing during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_MISSING_ATTRIBUTE_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("missing-attribute"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.MISSING_ATTRIBUTE);
|
||||||
|
assertEquals(e.getLocation(), Location.PUBLIC_KEY);
|
||||||
|
assertEquals(e.getSection(), Section.PEER);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException throwing during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_MISSING_SECTION_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("missing-section"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.MISSING_SECTION);
|
||||||
|
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||||
|
assertEquals(e.getSection(), Section.CONFIG);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException throwing during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_SYNTAX_ERROR_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("syntax-error"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.SYNTAX_ERROR);
|
||||||
|
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||||
|
assertEquals(e.getSection(), Section.PEER);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException throwing during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_UNKNOWN_ATTRIBUTE_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("unknown-attribute"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.UNKNOWN_ATTRIBUTE);
|
||||||
|
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||||
|
assertEquals(e.getSection(), Section.PEER);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException throwing during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void throws_correctly_with_UNKNOWN_SECTION_reason() {
|
||||||
|
try {
|
||||||
|
Config.parse(CONFIG_MAP.get("unknown-section"));
|
||||||
|
fail("Config parsing must fail in this test");
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
assertEquals(e.getReason(), Reason.UNKNOWN_SECTION);
|
||||||
|
assertEquals(e.getLocation(), Location.TOP_LEVEL);
|
||||||
|
assertEquals(e.getSection(), Section.CONFIG);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("IOException throwing during test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
tunnel/src/test/java/com/wireguard/config/ConfigTest.java
Normal file
49
tunnel/src/test/java/com/wireguard/config/ConfigTest.java
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.config;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
public class ConfigTest {
|
||||||
|
|
||||||
|
@Test(expected = BadConfigException.class)
|
||||||
|
public void invalid_config_throws() throws IOException, BadConfigException {
|
||||||
|
try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
|
||||||
|
Config.parse(is);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void valid_config_parses_correctly() throws IOException, ParseException {
|
||||||
|
Config config = null;
|
||||||
|
final Collection<InetNetwork> expectedAllowedIps = new HashSet<>(Arrays.asList(InetNetwork.parse("0.0.0.0/0"), InetNetwork.parse("::0/0")));
|
||||||
|
try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("working.conf")) {
|
||||||
|
config = Config.parse(is);
|
||||||
|
} catch (final BadConfigException e) {
|
||||||
|
fail("'working.conf' should never fail to parse");
|
||||||
|
}
|
||||||
|
assertNotNull("config cannot be null after parsing", config);
|
||||||
|
assertTrue(
|
||||||
|
"No applications should be excluded by default",
|
||||||
|
config.getInterface().getExcludedApplications().isEmpty()
|
||||||
|
);
|
||||||
|
assertEquals("Test config has exactly one peer", 1, config.getPeers().size());
|
||||||
|
assertEquals("Test config's allowed IPs are 0.0.0.0/0 and ::0/0", config.getPeers().get(0).getAllowedIps(), expectedAllowedIps);
|
||||||
|
assertEquals("Test config has one DNS server", 1, config.getInterface().getDnsServers().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tunnel/src/test/resources/broken.conf
Normal file
9
tunnel/src/test/resources/broken.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = l0lth1s1sd3f1n1t3lybr0k3n=
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
|
AllowedIPs = 0.0.0.0/0,::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
9
tunnel/src/test/resources/invalid-key.conf
Normal file
9
tunnel/src/test/resources/invalid-key.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
PersistentKeepalive = 0
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6Og=
|
||||||
9
tunnel/src/test/resources/invalid-number.conf
Normal file
9
tunnel/src/test/resources/invalid-number.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
PersistentKeepalive = 0L
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
9
tunnel/src/test/resources/invalid-value.conf
Normal file
9
tunnel/src/test/resources/invalid-value.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0,invalid_value
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
PersistentKeepalive = 0
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
8
tunnel/src/test/resources/missing-attribute.conf
Normal file
8
tunnel/src/test/resources/missing-attribute.conf
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
PersistentKeepalive = 0
|
||||||
5
tunnel/src/test/resources/missing-section.conf
Normal file
5
tunnel/src/test/resources/missing-section.conf
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
PersistentKeepalive = 0
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
9
tunnel/src/test/resources/syntax-error.conf
Normal file
9
tunnel/src/test/resources/syntax-error.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint =
|
||||||
|
PersistentKeepalive = 0
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
9
tunnel/src/test/resources/unknown-attribute.conf
Normal file
9
tunnel/src/test/resources/unknown-attribute.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
DontLetTheFeelingFade = 1
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
9
tunnel/src/test/resources/unknown-section.conf
Normal file
9
tunnel/src/test/resources/unknown-section.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peers]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
PersistentKeepalive = 0
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
9
tunnel/src/test/resources/working.conf
Normal file
9
tunnel/src/test/resources/working.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Interface]
|
||||||
|
Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128
|
||||||
|
DNS = 192.0.2.0
|
||||||
|
PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo=
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::0/0
|
||||||
|
Endpoint = 192.0.2.1:51820
|
||||||
|
PersistentKeepalive = 0
|
||||||
|
PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=
|
||||||
44
tunnel/tools/CMakeLists.txt
Normal file
44
tunnel/tools/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.4.1)
|
||||||
|
project("WireGuard")
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||||
|
add_link_options(LINKER:--build-id=none)
|
||||||
|
add_compile_options(-Wall -Werror)
|
||||||
|
|
||||||
|
add_executable(libwg-quick.so wireguard-tools/src/wg-quick/android.c ndk-compat/compat.c)
|
||||||
|
target_compile_options(libwg-quick.so PUBLIC -std=gnu11 -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-tools/src/*.c ndk-compat/compat.c)
|
||||||
|
add_executable(libwg.so ${WG_SOURCES})
|
||||||
|
target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
|
||||||
|
target_compile_options(libwg.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -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 "${ANDROID_HOST_PREBUILTS}/bin/make"
|
||||||
|
ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
|
||||||
|
ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
|
||||||
|
GRADLE_USER_HOME=${GRADLE_USER_HOME}
|
||||||
|
CC=${CMAKE_C_COMPILER}
|
||||||
|
CFLAGS=${CMAKE_C_FLAGS}
|
||||||
|
LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}
|
||||||
|
SYSROOT=${CMAKE_SYSROOT}
|
||||||
|
TARGET=${CMAKE_C_COMPILER_TARGET}
|
||||||
|
DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
|
||||||
|
BUILDDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../generated-src
|
||||||
|
)
|
||||||
|
|
||||||
|
# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions
|
||||||
|
file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)
|
||||||
|
add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc
|
||||||
|
-O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""
|
||||||
|
-o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}
|
||||||
|
)
|
||||||
|
add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
|
||||||
|
--api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg.so>")
|
||||||
|
add_dependencies(libwg.so elf-cleaner)
|
||||||
|
add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
|
||||||
|
--api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg-quick.so>")
|
||||||
|
add_dependencies(libwg-quick.so elf-cleaner)
|
||||||
1
tunnel/tools/libwg-go/.gitignore
vendored
Normal file
1
tunnel/tools/libwg-go/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
build/
|
||||||
52
tunnel/tools/libwg-go/Makefile
Normal file
52
tunnel/tools/libwg-go/Makefile
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Copyright © 2017-2025 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
|
||||||
|
|
||||||
|
comma := ,
|
||||||
|
CLANG_FLAGS := --target=$(TARGET) --sysroot=$(SYSROOT)
|
||||||
|
export CGO_CFLAGS := $(CLANG_FLAGS) $(subst -mthumb,-marm,$(CFLAGS))
|
||||||
|
export CGO_LDFLAGS := $(CLANG_FLAGS) $(patsubst -Wl$(comma)--build-id=%,-Wl$(comma)--build-id=none,$(LDFLAGS)) -Wl,-soname=libwg-go.so
|
||||||
|
export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
|
||||||
|
export GOOS := android
|
||||||
|
export CGO_ENABLED := 1
|
||||||
|
|
||||||
|
GO_VERSION := 1.24.3
|
||||||
|
GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
|
||||||
|
GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
|
||||||
|
GO_HASH_darwin-amd64 := 13e6fe3fcf65689d77d40e633de1e31c6febbdbcb846eb05fc2434ed2213e92b
|
||||||
|
GO_HASH_darwin-arm64 := 64a3fa22142f627e78fac3018ce3d4aeace68b743eff0afda8aae0411df5e4fb
|
||||||
|
GO_HASH_linux-amd64 := 3333f6ea53afa971e9078895eaa4ac7204a8c6b5c68c10e6bc9a33e8e391bdd8
|
||||||
|
|
||||||
|
default: $(DESTDIR)/libwg-go.so
|
||||||
|
|
||||||
|
$(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
|
||||||
|
mkdir -p "$(dir $@)"
|
||||||
|
flock "$@.lock" -c ' \
|
||||||
|
[ -f "$@" ] && exit 0; \
|
||||||
|
curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" && \
|
||||||
|
echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c && \
|
||||||
|
mv "$@.tmp" "$@"'
|
||||||
|
|
||||||
|
$(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL)
|
||||||
|
mkdir -p "$(dir $@)"
|
||||||
|
flock "$@.lock" -c ' \
|
||||||
|
[ -f "$@" ] && exit 0; \
|
||||||
|
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-$(GO_VERSION)/bin/:$(PATH)
|
||||||
|
$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
|
||||||
|
go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared
|
||||||
|
|
||||||
|
.DELETE_ON_ERROR:
|
||||||
227
tunnel/tools/libwg-go/api-android.go
Normal file
227
tunnel/tools/libwg-go/api-android.go
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
/* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Copyright © 2017-2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// #cgo LDFLAGS: -llog
|
||||||
|
// #include <android/log.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"golang.zx2c4.com/wireguard/conn"
|
||||||
|
"golang.zx2c4.com/wireguard/device"
|
||||||
|
"golang.zx2c4.com/wireguard/ipc"
|
||||||
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AndroidLogger struct {
|
||||||
|
level C.int
|
||||||
|
tag *C.char
|
||||||
|
}
|
||||||
|
|
||||||
|
func cstring(s string) *C.char {
|
||||||
|
b, err := unix.BytePtrFromString(s)
|
||||||
|
if err != nil {
|
||||||
|
b := [1]C.char{}
|
||||||
|
return &b[0]
|
||||||
|
}
|
||||||
|
return (*C.char)(unsafe.Pointer(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l AndroidLogger) Printf(format string, args ...interface{}) {
|
||||||
|
C.__android_log_write(l.level, l.tag, cstring(fmt.Sprintf(format, args...)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type TunnelHandle struct {
|
||||||
|
device *device.Device
|
||||||
|
uapi net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
var tunnelHandles map[int32]TunnelHandle
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tunnelHandles = make(map[int32]TunnelHandle)
|
||||||
|
signals := make(chan os.Signal)
|
||||||
|
signal.Notify(signals, unix.SIGUSR2)
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, os.Getpagesize())
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-signals:
|
||||||
|
n := runtime.Stack(buf, true)
|
||||||
|
if n == len(buf) {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
buf[n] = 0
|
||||||
|
C.__android_log_write(C.ANDROID_LOG_ERROR, cstring("WireGuard/GoBackend/Stacktrace"), (*C.char)(unsafe.Pointer(&buf[0])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export wgTurnOn
|
||||||
|
func wgTurnOn(interfaceName string, tunFd int32, settings string) int32 {
|
||||||
|
tag := cstring("WireGuard/GoBackend/" + interfaceName)
|
||||||
|
logger := &device.Logger{
|
||||||
|
Verbosef: AndroidLogger{level: C.ANDROID_LOG_DEBUG, tag: tag}.Printf,
|
||||||
|
Errorf: AndroidLogger{level: C.ANDROID_LOG_ERROR, tag: tag}.Printf,
|
||||||
|
}
|
||||||
|
|
||||||
|
tun, name, err := tun.CreateUnmonitoredTUNFromFD(int(tunFd))
|
||||||
|
if err != nil {
|
||||||
|
unix.Close(int(tunFd))
|
||||||
|
logger.Errorf("CreateUnmonitoredTUNFromFD: %v", err)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Verbosef("Attaching to interface %v", name)
|
||||||
|
device := device.NewDevice(tun, conn.NewStdNetBind(), logger)
|
||||||
|
|
||||||
|
err = device.IpcSet(settings)
|
||||||
|
if err != nil {
|
||||||
|
unix.Close(int(tunFd))
|
||||||
|
logger.Errorf("IpcSet: %v", err)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
device.DisableSomeRoamingForBrokenMobileSemantics()
|
||||||
|
|
||||||
|
var uapi net.Listener
|
||||||
|
|
||||||
|
uapiFile, err := ipc.UAPIOpen(name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("UAPIOpen: %v", err)
|
||||||
|
} else {
|
||||||
|
uapi, err = ipc.UAPIListen(name, uapiFile)
|
||||||
|
if err != nil {
|
||||||
|
uapiFile.Close()
|
||||||
|
logger.Errorf("UAPIListen: %v", err)
|
||||||
|
} else {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := uapi.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go device.IpcHandle(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = device.Up()
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Unable to bring up device: %v", err)
|
||||||
|
uapiFile.Close()
|
||||||
|
device.Close()
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
logger.Verbosef("Device started")
|
||||||
|
|
||||||
|
var i int32
|
||||||
|
for i = 0; i < math.MaxInt32; i++ {
|
||||||
|
if _, exists := tunnelHandles[i]; !exists {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i == math.MaxInt32 {
|
||||||
|
logger.Errorf("Unable to find empty handle")
|
||||||
|
uapiFile.Close()
|
||||||
|
device.Close()
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
tunnelHandles[i] = TunnelHandle{device: device, uapi: uapi}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
//export wgTurnOff
|
||||||
|
func wgTurnOff(tunnelHandle int32) {
|
||||||
|
handle, ok := tunnelHandles[tunnelHandle]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(tunnelHandles, tunnelHandle)
|
||||||
|
if handle.uapi != nil {
|
||||||
|
handle.uapi.Close()
|
||||||
|
}
|
||||||
|
handle.device.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export wgGetSocketV4
|
||||||
|
func wgGetSocketV4(tunnelHandle int32) int32 {
|
||||||
|
handle, ok := tunnelHandles[tunnelHandle]
|
||||||
|
if !ok {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||||
|
if bind == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
fd, err := bind.PeekLookAtSocketFd4()
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return int32(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export wgGetSocketV6
|
||||||
|
func wgGetSocketV6(tunnelHandle int32) int32 {
|
||||||
|
handle, ok := tunnelHandles[tunnelHandle]
|
||||||
|
if !ok {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||||
|
if bind == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
fd, err := bind.PeekLookAtSocketFd6()
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return int32(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export wgGetConfig
|
||||||
|
func wgGetConfig(tunnelHandle int32) *C.char {
|
||||||
|
handle, ok := tunnelHandles[tunnelHandle]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
settings, err := handle.device.IpcGet()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return C.CString(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export wgVersion
|
||||||
|
func wgVersion() *C.char {
|
||||||
|
info, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
return C.CString("unknown")
|
||||||
|
}
|
||||||
|
for _, dep := range info.Deps {
|
||||||
|
if dep.Path == "golang.zx2c4.com/wireguard" {
|
||||||
|
parts := strings.Split(dep.Version, "-")
|
||||||
|
if len(parts) == 3 && len(parts[2]) == 12 {
|
||||||
|
return C.CString(parts[2][:7])
|
||||||
|
}
|
||||||
|
return C.CString(dep.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return C.CString("unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {}
|
||||||
14
tunnel/tools/libwg-go/go.mod
Normal file
14
tunnel/tools/libwg-go/go.mod
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
module golang.zx2c4.com/wireguard/android
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/sys v0.33.0
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
)
|
||||||
16
tunnel/tools/libwg-go/go.sum
Normal file
16
tunnel/tools/libwg-go/go.sum
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
|
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||||
|
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||||
171
tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff
Normal file
171
tunnel/tools/libwg-go/goruntime-boottime-over-monotonic.diff
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
From 61f3ae8298d1c503cbc31539e0f3a73446c7db9d Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Jason A. Donenfeld" <Jason@zx2c4.com>
|
||||||
|
Date: Tue, 21 Mar 2023 15:33:56 +0100
|
||||||
|
Subject: [PATCH] [release-branch.go1.20] runtime: use CLOCK_BOOTTIME in
|
||||||
|
nanotime on Linux
|
||||||
|
|
||||||
|
This makes timers account for having expired while a computer was
|
||||||
|
asleep, which is quite common on mobile devices. Note that BOOTTIME is
|
||||||
|
identical to MONOTONIC, except that it takes into account time spent
|
||||||
|
in suspend. In Linux 4.17, the kernel will actually make MONOTONIC act
|
||||||
|
like BOOTTIME anyway, so this switch will additionally unify the
|
||||||
|
timer behavior across kernels.
|
||||||
|
|
||||||
|
BOOTTIME was introduced into Linux 2.6.39-rc1 with 70a08cca1227d in
|
||||||
|
2011.
|
||||||
|
|
||||||
|
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 | 4 ++--
|
||||||
|
src/runtime/sys_linux_mipsx.s | 2 +-
|
||||||
|
src/runtime/sys_linux_ppc64x.s | 2 +-
|
||||||
|
src/runtime/sys_linux_s390x.s | 2 +-
|
||||||
|
8 files changed, 12 insertions(+), 12 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s
|
||||||
|
index 12a294153d..17e3524b40 100644
|
||||||
|
--- a/src/runtime/sys_linux_386.s
|
||||||
|
+++ b/src/runtime/sys_linux_386.s
|
||||||
|
@@ -352,13 +352,13 @@ noswitch:
|
||||||
|
|
||||||
|
LEAL 8(SP), BX // &ts (struct timespec)
|
||||||
|
MOVL BX, 4(SP)
|
||||||
|
- MOVL $1, 0(SP) // CLOCK_MONOTONIC
|
||||||
|
+ MOVL $7, 0(SP) // CLOCK_BOOTTIME
|
||||||
|
CALL AX
|
||||||
|
JMP finish
|
||||||
|
|
||||||
|
fallback:
|
||||||
|
MOVL $SYS_clock_gettime, AX
|
||||||
|
- MOVL $1, BX // CLOCK_MONOTONIC
|
||||||
|
+ MOVL $7, BX // CLOCK_BOOTTIME
|
||||||
|
LEAL 8(SP), CX
|
||||||
|
INVOKE_SYSCALL
|
||||||
|
|
||||||
|
diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s
|
||||||
|
index c7a89ba536..01f0a6a26e 100644
|
||||||
|
--- a/src/runtime/sys_linux_amd64.s
|
||||||
|
+++ b/src/runtime/sys_linux_amd64.s
|
||||||
|
@@ -255,7 +255,7 @@ noswitch:
|
||||||
|
SUBQ $16, SP // Space for results
|
||||||
|
ANDQ $~15, SP // Align for C code
|
||||||
|
|
||||||
|
- MOVL $1, DI // CLOCK_MONOTONIC
|
||||||
|
+ MOVL $7, DI // CLOCK_BOOTTIME
|
||||||
|
LEAQ 0(SP), SI
|
||||||
|
MOVQ runtime·vdsoClockgettimeSym(SB), AX
|
||||||
|
CMPQ AX, $0
|
||||||
|
diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s
|
||||||
|
index 7b8c4f0e04..9798a1334e 100644
|
||||||
|
--- a/src/runtime/sys_linux_arm.s
|
||||||
|
+++ b/src/runtime/sys_linux_arm.s
|
||||||
|
@@ -11,7 +11,7 @@
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
#define CLOCK_REALTIME 0
|
||||||
|
-#define CLOCK_MONOTONIC 1
|
||||||
|
+#define CLOCK_BOOTTIME 7
|
||||||
|
|
||||||
|
// for EABI, as we don't support OABI
|
||||||
|
#define SYS_BASE 0x0
|
||||||
|
@@ -374,7 +374,7 @@ finish:
|
||||||
|
|
||||||
|
// func nanotime1() int64
|
||||||
|
TEXT runtime·nanotime1(SB),NOSPLIT,$12-8
|
||||||
|
- MOVW $CLOCK_MONOTONIC, R0
|
||||||
|
+ MOVW $CLOCK_BOOTTIME, R0
|
||||||
|
MOVW $spec-12(SP), R1 // timespec
|
||||||
|
|
||||||
|
MOVW runtime·vdsoClockgettimeSym(SB), R4
|
||||||
|
diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s
|
||||||
|
index 38ff6ac330..6b819c5441 100644
|
||||||
|
--- a/src/runtime/sys_linux_arm64.s
|
||||||
|
+++ b/src/runtime/sys_linux_arm64.s
|
||||||
|
@@ -14,7 +14,7 @@
|
||||||
|
#define AT_FDCWD -100
|
||||||
|
|
||||||
|
#define CLOCK_REALTIME 0
|
||||||
|
-#define CLOCK_MONOTONIC 1
|
||||||
|
+#define CLOCK_BOOTTIME 7
|
||||||
|
|
||||||
|
#define SYS_exit 93
|
||||||
|
#define SYS_read 63
|
||||||
|
@@ -338,7 +338,7 @@ noswitch:
|
||||||
|
BIC $15, R1
|
||||||
|
MOVD R1, RSP
|
||||||
|
|
||||||
|
- MOVW $CLOCK_MONOTONIC, R0
|
||||||
|
+ MOVW $CLOCK_BOOTTIME, R0
|
||||||
|
MOVD runtime·vdsoClockgettimeSym(SB), R2
|
||||||
|
CBZ R2, fallback
|
||||||
|
|
||||||
|
diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s
|
||||||
|
index 47f2da524d..a8b387f193 100644
|
||||||
|
--- a/src/runtime/sys_linux_mips64x.s
|
||||||
|
+++ b/src/runtime/sys_linux_mips64x.s
|
||||||
|
@@ -326,7 +326,7 @@ noswitch:
|
||||||
|
AND $~15, R1 // Align for C code
|
||||||
|
MOVV R1, R29
|
||||||
|
|
||||||
|
- MOVW $1, R4 // CLOCK_MONOTONIC
|
||||||
|
+ MOVW $7, R4 // CLOCK_BOOTTIME
|
||||||
|
MOVV $0(R29), R5
|
||||||
|
|
||||||
|
MOVV runtime·vdsoClockgettimeSym(SB), R25
|
||||||
|
@@ -336,7 +336,7 @@ noswitch:
|
||||||
|
// see walltime for detail
|
||||||
|
BEQ R2, R0, finish
|
||||||
|
MOVV R0, runtime·vdsoClockgettimeSym(SB)
|
||||||
|
- MOVW $1, R4 // CLOCK_MONOTONIC
|
||||||
|
+ MOVW $7, R4 // CLOCK_BOOTTIME
|
||||||
|
MOVV $0(R29), R5
|
||||||
|
JMP fallback
|
||||||
|
|
||||||
|
diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s
|
||||||
|
index 5e6b6c1504..7f5fd2a80e 100644
|
||||||
|
--- a/src/runtime/sys_linux_mipsx.s
|
||||||
|
+++ b/src/runtime/sys_linux_mipsx.s
|
||||||
|
@@ -243,7 +243,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$8-12
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT runtime·nanotime1(SB),NOSPLIT,$8-8
|
||||||
|
- MOVW $1, R4 // CLOCK_MONOTONIC
|
||||||
|
+ MOVW $7, R4 // CLOCK_BOOTTIME
|
||||||
|
MOVW $4(R29), R5
|
||||||
|
MOVW $SYS_clock_gettime, R2
|
||||||
|
SYSCALL
|
||||||
|
diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s
|
||||||
|
index d0427a4807..05ee9fede9 100644
|
||||||
|
--- a/src/runtime/sys_linux_ppc64x.s
|
||||||
|
+++ b/src/runtime/sys_linux_ppc64x.s
|
||||||
|
@@ -298,7 +298,7 @@ fallback:
|
||||||
|
JMP return
|
||||||
|
|
||||||
|
TEXT runtime·nanotime1(SB),NOSPLIT,$16-8
|
||||||
|
- 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 1448670b91..7d2ee3231c 100644
|
||||||
|
--- a/src/runtime/sys_linux_s390x.s
|
||||||
|
+++ b/src/runtime/sys_linux_s390x.s
|
||||||
|
@@ -296,7 +296,7 @@ fallback:
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT runtime·nanotime1(SB),NOSPLIT,$32-8
|
||||||
|
- MOVW $1, R2 // CLOCK_MONOTONIC
|
||||||
|
+ MOVW $7, R2 // CLOCK_BOOTTIME
|
||||||
|
|
||||||
|
MOVD R15, R7 // Backup stack pointer
|
||||||
|
|
||||||
|
--
|
||||||
|
2.17.1
|
||||||
|
|
||||||
71
tunnel/tools/libwg-go/jni.c
Normal file
71
tunnel/tools/libwg-go/jni.c
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Copyright © 2017-2021 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
struct go_string { const char *str; long n; };
|
||||||
|
extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settings);
|
||||||
|
extern void wgTurnOff(int handle);
|
||||||
|
extern int wgGetSocketV4(int handle);
|
||||||
|
extern int wgGetSocketV6(int handle);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
const char *ifname_str = (*env)->GetStringUTFChars(env, ifname, 0);
|
||||||
|
size_t ifname_len = (*env)->GetStringUTFLength(env, ifname);
|
||||||
|
const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0);
|
||||||
|
size_t settings_len = (*env)->GetStringUTFLength(env, settings);
|
||||||
|
int ret = wgTurnOn((struct go_string){
|
||||||
|
.str = ifname_str,
|
||||||
|
.n = ifname_len
|
||||||
|
}, tun_fd, (struct go_string){
|
||||||
|
.str = settings_str,
|
||||||
|
.n = settings_len
|
||||||
|
});
|
||||||
|
(*env)->ReleaseStringUTFChars(env, ifname, ifname_str);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, settings, settings_str);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOff(JNIEnv *env, jclass c, jint handle)
|
||||||
|
{
|
||||||
|
wgTurnOff(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV4(JNIEnv *env, jclass c, jint handle)
|
||||||
|
{
|
||||||
|
return wgGetSocketV4(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV6(JNIEnv *env, jclass c, jint handle)
|
||||||
|
{
|
||||||
|
return wgGetSocketV6(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
char *version = wgVersion();
|
||||||
|
if (!version)
|
||||||
|
return NULL;
|
||||||
|
ret = (*env)->NewStringUTF(env, version);
|
||||||
|
free(version);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
25
tunnel/tools/ndk-compat/compat.c
Normal file
25
tunnel/tools/ndk-compat/compat.c
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* SPDX-License-Identifier: BSD
|
||||||
|
*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define FILE_IS_EMPTY
|
||||||
|
|
||||||
|
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
|
||||||
|
#undef FILE_IS_EMPTY
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
char *strchrnul(const char *s, int c)
|
||||||
|
{
|
||||||
|
char *x = strchr(s, c);
|
||||||
|
if (!x)
|
||||||
|
return (char *)s + strlen(s);
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef FILE_IS_EMPTY
|
||||||
|
#undef FILE_IS_EMPTY
|
||||||
|
static char ____x __attribute__((unused));
|
||||||
|
#endif
|
||||||
10
tunnel/tools/ndk-compat/compat.h
Normal file
10
tunnel/tools/ndk-compat/compat.h
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/* SPDX-License-Identifier: BSD
|
||||||
|
*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24
|
||||||
|
char *strchrnul(const char *s, int c);
|
||||||
|
#endif
|
||||||
|
|
||||||
93
ui/build.gradle.kts
Normal file
93
ui/build.gradle.kts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
|
val pkg: String = providers.gradleProperty("wireguardPackageName").get()
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.kapt)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = 36
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
dataBinding = true
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
namespace = pkg
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = pkg
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
|
||||||
|
versionName = providers.gradleProperty("wireguardVersionName").get()
|
||||||
|
buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles("proguard-android-optimize.txt")
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
excludes += "DebugProbesKt.bin"
|
||||||
|
excludes += "kotlin-tooling-metadata.json"
|
||||||
|
excludes += "META-INF/*.version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
versionNameSuffix = "-debug"
|
||||||
|
}
|
||||||
|
create("googleplay") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
matchingFallbacks += "release"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
lint {
|
||||||
|
disable += "LongLogTag"
|
||||||
|
warning += "MissingTranslation"
|
||||||
|
warning += "ImpliedQuantity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":tunnel"))
|
||||||
|
implementation(libs.androidx.activity.ktx)
|
||||||
|
implementation(libs.androidx.annotation)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.coordinatorlayout)
|
||||||
|
implementation(libs.androidx.biometric)
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.fragment.ktx)
|
||||||
|
implementation(libs.androidx.preference.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.google.material)
|
||||||
|
implementation(libs.zxing.android.embedded)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
coreLibraryDesugaring(libs.desugarJdkLibs)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
|
options.compilerArgs.add("-Xlint:unchecked")
|
||||||
|
options.isDeprecation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
|
compilerOptions.jvmTarget = JvmTarget.JVM_17
|
||||||
|
}
|
||||||
35
ui/proguard-android-optimize.txt
Normal file
35
ui/proguard-android-optimize.txt
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
-allowaccessmodification
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
-dontobfuscate
|
||||||
|
-verbose
|
||||||
|
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
|
public static final ** CREATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keep class androidx.annotation.Keep
|
||||||
|
|
||||||
|
-keep @androidx.annotation.Keep class * {*;}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <init>(...);
|
||||||
|
}
|
||||||
34
ui/sampledata/interface_names.json
Normal file
34
ui/sampledata/interface_names.json
Normal file
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
ui/src/debug/res/values/strings.xml
Normal file
4
ui/src/debug/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">WireGuard β</string>
|
||||||
|
</resources>
|
||||||
11
ui/src/googleplay/AndroidManifest.xml
Normal file
11
ui/src/googleplay/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||||
|
tools:node="remove" />
|
||||||
|
</manifest>
|
||||||
169
ui/src/main/AndroidManifest.xml
Normal file
169
ui/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
~ SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.SYSTEM_ALERT_WINDOW"
|
||||||
|
android:minSdkVersion="34" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera.any"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="${applicationId}.permission.CONTROL_TUNNELS"
|
||||||
|
android:description="@string/permission_description"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/permission_label"
|
||||||
|
android:protectionLevel="dangerous" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".Application"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:banner="@mipmap/banner"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.TunnelToggleActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:theme="@style/NoBackgroundTheme" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.TvMainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/TvTheme">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.SettingsActivity"
|
||||||
|
android:label="@string/settings"
|
||||||
|
android:parentActivityName=".activity.MainActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.TunnelCreatorActivity"
|
||||||
|
android:label="@string/create_activity_title"
|
||||||
|
android:parentActivityName=".activity.MainActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||||
|
android:screenOrientation="fullSensor"
|
||||||
|
tools:replace="screenOrientation" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activity.LogViewerActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/log_viewer_title">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name=".activity.LogViewerActivity$ExportedLogContentProvider"
|
||||||
|
android:authorities="${applicationId}.exported-log"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".BootShutdownReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".updater.Updater$AppUpdatedReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".model.TunnelManager$IntentReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="${applicationId}.permission.CONTROL_TUNNELS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
|
||||||
|
<action android:name="com.wireguard.android.action.SET_TUNNEL_UP" />
|
||||||
|
<action android:name="com.wireguard.android.action.SET_TUNNEL_DOWN" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".QuickTileService"
|
||||||
|
android:exported="true"
|
||||||
|
android:icon="@drawable/ic_tile"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
|
android:value="false" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
|
android:value="true" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.APP_RESTRICTIONS"
|
||||||
|
android:resource="@xml/app_restrictions" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
157
ui/src/main/java/com/wireguard/android/Application.kt
Normal file
157
ui/src/main/java/com/wireguard/android/Application.kt
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.StrictMode
|
||||||
|
import android.os.StrictMode.ThreadPolicy
|
||||||
|
import android.os.StrictMode.VmPolicy
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import com.wireguard.android.backend.Backend
|
||||||
|
import com.wireguard.android.backend.GoBackend
|
||||||
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
|
import com.wireguard.android.configStore.FileConfigStore
|
||||||
|
import com.wireguard.android.model.TunnelManager
|
||||||
|
import com.wireguard.android.updater.Updater
|
||||||
|
import com.wireguard.android.util.RootShell
|
||||||
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
|
import com.wireguard.android.util.UserKnobs
|
||||||
|
import com.wireguard.android.util.applicationScope
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class Application : android.app.Application() {
|
||||||
|
private val futureBackend = CompletableDeferred<Backend>()
|
||||||
|
private val coroutineScope = CoroutineScope(Job() + Dispatchers.Main.immediate)
|
||||||
|
private var backend: Backend? = null
|
||||||
|
private lateinit var rootShell: RootShell
|
||||||
|
private lateinit var preferencesDataStore: DataStore<Preferences>
|
||||||
|
private lateinit var toolsInstaller: ToolsInstaller
|
||||||
|
private lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
override fun attachBaseContext(context: Context) {
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
if (BuildConfig.MIN_SDK_VERSION > Build.VERSION.SDK_INT) {
|
||||||
|
val intent = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun determineBackend(): Backend {
|
||||||
|
var backend: Backend? = null
|
||||||
|
if (UserKnobs.enableKernelModule.first() && WgQuickBackend.hasKernelSupport()) {
|
||||||
|
try {
|
||||||
|
rootShell.start()
|
||||||
|
val wgQuickBackend = WgQuickBackend(applicationContext, rootShell, toolsInstaller)
|
||||||
|
wgQuickBackend.setMultipleTunnels(UserKnobs.multipleTunnels.first())
|
||||||
|
backend = wgQuickBackend
|
||||||
|
UserKnobs.multipleTunnels.onEach {
|
||||||
|
wgQuickBackend.setMultipleTunnels(it)
|
||||||
|
}.launchIn(coroutineScope)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (backend == null) {
|
||||||
|
backend = GoBackend(applicationContext)
|
||||||
|
GoBackend.setAlwaysOnCallback { get().applicationScope.launch { get().tunnelManager.restoreState(true) } }
|
||||||
|
}
|
||||||
|
return backend
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
Log.i(TAG, USER_AGENT)
|
||||||
|
super.onCreate()
|
||||||
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
rootShell = RootShell(applicationContext)
|
||||||
|
toolsInstaller = ToolsInstaller(applicationContext, rootShell)
|
||||||
|
preferencesDataStore = PreferenceDataStoreFactory.create { applicationContext.preferencesDataStoreFile("settings") }
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
runBlocking {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(if (UserKnobs.darkTheme.first()) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO)
|
||||||
|
}
|
||||||
|
UserKnobs.darkTheme.onEach {
|
||||||
|
val newMode = if (it) {
|
||||||
|
AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
} else {
|
||||||
|
AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
}
|
||||||
|
if (AppCompatDelegate.getDefaultNightMode() != newMode) {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(newMode)
|
||||||
|
}
|
||||||
|
}.launchIn(coroutineScope)
|
||||||
|
} else {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
|
}
|
||||||
|
tunnelManager = TunnelManager(FileConfigStore(applicationContext))
|
||||||
|
tunnelManager.onCreate()
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
backend = determineBackend()
|
||||||
|
futureBackend.complete(backend!!)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Updater.monitorForUpdates()
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
|
||||||
|
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTerminate() {
|
||||||
|
coroutineScope.cancel()
|
||||||
|
super.onTerminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val USER_AGENT = String.format(Locale.ENGLISH, "WireGuard/%s (Android %d; %s; %s; %s %s; %s)", BuildConfig.VERSION_NAME, Build.VERSION.SDK_INT, if (Build.SUPPORTED_ABIS.isNotEmpty()) Build.SUPPORTED_ABIS[0] else "unknown ABI", Build.BOARD, Build.MANUFACTURER, Build.MODEL, Build.FINGERPRINT)
|
||||||
|
private const val TAG = "WireGuard/Application"
|
||||||
|
private lateinit var weakSelf: WeakReference<Application>
|
||||||
|
|
||||||
|
fun get(): Application {
|
||||||
|
return weakSelf.get()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getBackend() = get().futureBackend.await()
|
||||||
|
|
||||||
|
fun getRootShell() = get().rootShell
|
||||||
|
|
||||||
|
fun getPreferencesDataStore() = get().preferencesDataStore
|
||||||
|
|
||||||
|
fun getToolsInstaller() = get().toolsInstaller
|
||||||
|
|
||||||
|
fun getTunnelManager() = get().tunnelManager
|
||||||
|
|
||||||
|
fun getCoroutineScope() = get().coroutineScope
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
weakSelf = WeakReference(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
|
import com.wireguard.android.util.applicationScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class BootShutdownReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action ?: return
|
||||||
|
applicationScope.launch {
|
||||||
|
if (Application.getBackend() !is WgQuickBackend) return@launch
|
||||||
|
val tunnelManager = Application.getTunnelManager()
|
||||||
|
if (Intent.ACTION_BOOT_COMPLETED == action) {
|
||||||
|
Log.i(TAG, "Broadcast receiver restoring state (boot)")
|
||||||
|
tunnelManager.restoreState(false)
|
||||||
|
} else if (Intent.ACTION_SHUTDOWN == action) {
|
||||||
|
Log.i(TAG, "Broadcast receiver saving state (shutdown)")
|
||||||
|
tunnelManager.saveState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/BootShutdownReceiver"
|
||||||
|
}
|
||||||
|
}
|
||||||
203
ui/src/main/java/com/wireguard/android/QuickTileService.kt
Normal file
203
ui/src/main/java/com/wireguard/android/QuickTileService.kt
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.service.quicksettings.Tile
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.databinding.Observable
|
||||||
|
import androidx.databinding.Observable.OnPropertyChangedCallback
|
||||||
|
import com.wireguard.android.activity.MainActivity
|
||||||
|
import com.wireguard.android.activity.TunnelToggleActivity
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import com.wireguard.android.util.applicationScope
|
||||||
|
import com.wireguard.android.widget.SlashDrawable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
class QuickTileService : TileService() {
|
||||||
|
private val onStateChangedCallback = OnStateChangedCallback()
|
||||||
|
private val onTunnelChangedCallback = OnTunnelChangedCallback()
|
||||||
|
private var iconOff: Icon? = null
|
||||||
|
private var iconOn: Icon? = null
|
||||||
|
private var tunnel: ObservableTunnel? = null
|
||||||
|
|
||||||
|
/* This works around an annoying unsolved frameworks bug some people are hitting. */
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
var ret: IBinder? = null
|
||||||
|
try {
|
||||||
|
ret = super.onBind(intent)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.d(TAG, "Failed to bind to TileService", e)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
applicationScope.launch {
|
||||||
|
if (tunnel == null) {
|
||||||
|
Application.getTunnelManager().getTunnels()
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
when (val tunnel = tunnel) {
|
||||||
|
null -> {
|
||||||
|
Log.d(TAG, "No tunnel set, so launching main activity")
|
||||||
|
val intent = Intent(this@QuickTileService, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
startActivityAndCollapse(PendingIntent.getActivity(this@QuickTileService, 0, intent, PendingIntent.FLAG_IMMUTABLE))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
startActivityAndCollapse(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
unlockAndRun {
|
||||||
|
applicationScope.launch {
|
||||||
|
try {
|
||||||
|
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||||
|
updateTile()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.d(TAG, "Failed to set state, so falling back", e)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !Settings.canDrawOverlays(this@QuickTileService)) {
|
||||||
|
Log.d(TAG, "Need overlay permissions")
|
||||||
|
val permissionIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
|
||||||
|
permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
startActivityAndCollapse(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this@QuickTileService,
|
||||||
|
0,
|
||||||
|
permissionIntent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val toggleIntent = Intent(this@QuickTileService, TunnelToggleActivity::class.java)
|
||||||
|
toggleIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
startActivity(toggleIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
isAdded = true
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
iconOn = Icon.createWithResource(this, R.drawable.ic_tile)
|
||||||
|
iconOff = iconOn
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val icon = SlashDrawable(resources.getDrawable(R.drawable.ic_tile, Application.get().theme))
|
||||||
|
icon.setAnimationEnabled(false) /* Unfortunately we can't have animations, since Icons are marshaled. */
|
||||||
|
icon.setSlashed(false)
|
||||||
|
var b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
var c = Canvas(b)
|
||||||
|
icon.setBounds(0, 0, c.width, c.height)
|
||||||
|
icon.draw(c)
|
||||||
|
iconOn = Icon.createWithBitmap(b)
|
||||||
|
icon.setSlashed(true)
|
||||||
|
b = Bitmap.createBitmap(icon.intrinsicWidth, icon.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
c = Canvas(b)
|
||||||
|
icon.setBounds(0, 0, c.width, c.height)
|
||||||
|
icon.draw(c)
|
||||||
|
iconOff = Icon.createWithBitmap(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
isAdded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartListening() {
|
||||||
|
Application.getTunnelManager().addOnPropertyChangedCallback(onTunnelChangedCallback)
|
||||||
|
tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopListening() {
|
||||||
|
tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||||
|
Application.getTunnelManager().removeOnPropertyChangedCallback(onTunnelChangedCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileAdded() {
|
||||||
|
isAdded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
isAdded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTile() {
|
||||||
|
// Update the tunnel.
|
||||||
|
val newTunnel = Application.getTunnelManager().lastUsedTunnel
|
||||||
|
if (newTunnel != tunnel) {
|
||||||
|
tunnel?.removeOnPropertyChangedCallback(onStateChangedCallback)
|
||||||
|
tunnel = newTunnel
|
||||||
|
tunnel?.addOnPropertyChangedCallback(onStateChangedCallback)
|
||||||
|
}
|
||||||
|
// Update the tile contents.
|
||||||
|
val tile = qsTile ?: return
|
||||||
|
|
||||||
|
when (val tunnel = tunnel) {
|
||||||
|
null -> {
|
||||||
|
tile.label = getString(R.string.app_name)
|
||||||
|
tile.state = Tile.STATE_INACTIVE
|
||||||
|
tile.icon = iconOff
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
tile.label = tunnel.name
|
||||||
|
tile.state = if (tunnel.state == Tunnel.State.UP) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||||
|
tile.icon = if (tunnel.state == Tunnel.State.UP) iconOn else iconOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tile.updateTile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class OnStateChangedCallback : OnPropertyChangedCallback() {
|
||||||
|
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
|
||||||
|
if (sender != tunnel) {
|
||||||
|
sender.removeOnPropertyChangedCallback(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (propertyId != 0 && propertyId != BR.state)
|
||||||
|
return
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class OnTunnelChangedCallback : OnPropertyChangedCallback() {
|
||||||
|
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
|
||||||
|
if (propertyId != 0 && propertyId != BR.lastUsedTunnel)
|
||||||
|
return
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/QuickTileService"
|
||||||
|
var isAdded: Boolean = false
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.databinding.CallbackRegistry
|
||||||
|
import androidx.databinding.CallbackRegistry.NotifierCallback
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for activities that need to remember the currently-selected tunnel.
|
||||||
|
*/
|
||||||
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
private val selectionChangeRegistry = SelectionChangeRegistry()
|
||||||
|
private var created = false
|
||||||
|
var selectedTunnel: ObservableTunnel? = null
|
||||||
|
set(value) {
|
||||||
|
val oldTunnel = field
|
||||||
|
if (oldTunnel == value) return
|
||||||
|
field = value
|
||||||
|
if (created) {
|
||||||
|
if (!onSelectedTunnelChanged(oldTunnel, value)) {
|
||||||
|
field = oldTunnel
|
||||||
|
} else {
|
||||||
|
selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) {
|
||||||
|
selectionChangeRegistry.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Restore the saved tunnel if there is one; otherwise grab it from the arguments.
|
||||||
|
val savedTunnelName = when {
|
||||||
|
savedInstanceState != null -> savedInstanceState.getString(KEY_SELECTED_TUNNEL)
|
||||||
|
intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (savedTunnelName != null) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val tunnel = Application.getTunnelManager().getTunnels()[savedTunnelName]
|
||||||
|
if (tunnel == null)
|
||||||
|
created = true
|
||||||
|
selectedTunnel = tunnel
|
||||||
|
created = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
created = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
if (selectedTunnel != null) outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel!!.name)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
|
||||||
|
|
||||||
|
fun removeOnSelectedTunnelChangedListener(
|
||||||
|
listener: OnSelectedTunnelChangedListener
|
||||||
|
) {
|
||||||
|
selectionChangeRegistry.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnSelectedTunnelChangedListener {
|
||||||
|
fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SelectionChangeNotifier : NotifierCallback<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>() {
|
||||||
|
override fun onNotifyCallback(
|
||||||
|
listener: OnSelectedTunnelChangedListener,
|
||||||
|
oldTunnel: ObservableTunnel?,
|
||||||
|
ignored: Int,
|
||||||
|
newTunnel: ObservableTunnel?
|
||||||
|
) {
|
||||||
|
listener.onSelectedTunnelChanged(oldTunnel, newTunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SelectionChangeRegistry :
|
||||||
|
CallbackRegistry<OnSelectedTunnelChangedListener, ObservableTunnel, ObservableTunnel>(SelectionChangeNotifier())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_SELECTED_TUNNEL = "selected_tunnel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,382 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.content.ClipDescription.compareMimeTypes
|
||||||
|
import android.content.ContentProvider
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Intent
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.graphics.Typeface.BOLD
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.collection.CircularArray
|
||||||
|
import androidx.core.app.ShareCompat
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.textview.MaterialTextView
|
||||||
|
import com.wireguard.android.BuildConfig
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.databinding.LogViewerActivityBinding
|
||||||
|
import com.wireguard.android.util.DownloadsFileSaver
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import com.wireguard.android.util.resolveAttribute
|
||||||
|
import com.wireguard.crypto.KeyPair
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.regex.Matcher
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class LogViewerActivity : AppCompatActivity() {
|
||||||
|
private lateinit var binding: LogViewerActivityBinding
|
||||||
|
private lateinit var logAdapter: LogEntryAdapter
|
||||||
|
private var logLines = CircularArray<LogLine>()
|
||||||
|
private var rawLogLines = CircularArray<String>()
|
||||||
|
private var recyclerView: RecyclerView? = null
|
||||||
|
private var saveButton: MenuItem? = null
|
||||||
|
private val year by lazy {
|
||||||
|
val yearFormatter: DateFormat = SimpleDateFormat("yyyy", Locale.US)
|
||||||
|
yearFormatter.format(Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val defaultColor by lazy { resolveAttribute(com.google.android.material.R.attr.colorOnSurface) }
|
||||||
|
|
||||||
|
private val debugColor by lazy { ResourcesCompat.getColor(resources, R.color.debug_tag_color, theme) }
|
||||||
|
|
||||||
|
private val errorColor by lazy { ResourcesCompat.getColor(resources, R.color.error_tag_color, theme) }
|
||||||
|
|
||||||
|
private val infoColor by lazy { ResourcesCompat.getColor(resources, R.color.info_tag_color, theme) }
|
||||||
|
|
||||||
|
private val warningColor by lazy { ResourcesCompat.getColor(resources, R.color.warning_tag_color, theme) }
|
||||||
|
|
||||||
|
private var lastUri: Uri? = null
|
||||||
|
|
||||||
|
private fun revokeLastUri() {
|
||||||
|
lastUri?.let {
|
||||||
|
LOGS.remove(it.pathSegments.lastOrNull())
|
||||||
|
revokeUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
lastUri = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = LogViewerActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
logAdapter = LogEntryAdapter()
|
||||||
|
binding.recyclerView.apply {
|
||||||
|
recyclerView = this
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = logAdapter
|
||||||
|
addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) { streamingLog() }
|
||||||
|
|
||||||
|
val revokeLastActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
revokeLastUri()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.shareFab.setOnClickListener {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
revokeLastUri()
|
||||||
|
val key = KeyPair().privateKey.toHex()
|
||||||
|
LOGS[key] = rawLogBytes()
|
||||||
|
lastUri = Uri.parse("content://${BuildConfig.APPLICATION_ID}.exported-log/$key")
|
||||||
|
val shareIntent = ShareCompat.IntentBuilder(this@LogViewerActivity)
|
||||||
|
.setType("text/plain")
|
||||||
|
.setSubject(getString(R.string.log_export_subject))
|
||||||
|
.setStream(lastUri)
|
||||||
|
.setChooserTitle(R.string.log_export_title)
|
||||||
|
.createChooserIntent()
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
grantUriPermission("android", lastUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
revokeLastActivityResultLauncher.launch(shareIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.log_viewer, menu)
|
||||||
|
saveButton = menu.findItem(R.id.save_log)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.save_log -> {
|
||||||
|
saveButton?.isEnabled = false
|
||||||
|
lifecycleScope.launch { saveLog() }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val downloadsFileSaver = DownloadsFileSaver(this)
|
||||||
|
|
||||||
|
private suspend fun rawLogBytes(): ByteArray {
|
||||||
|
val builder = StringBuilder()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
for (i in 0 until rawLogLines.size()) {
|
||||||
|
builder.append(rawLogLines[i])
|
||||||
|
builder.append('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString().toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun saveLog() {
|
||||||
|
var exception: Throwable? = null
|
||||||
|
var outputFile: DownloadsFileSaver.DownloadsFile? = null
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
outputFile = downloadsFileSaver.save("wireguard-log.txt", "text/plain", true)
|
||||||
|
outputFile?.outputStream?.write(rawLogBytes())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
outputFile?.delete()
|
||||||
|
exception = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveButton?.isEnabled = true
|
||||||
|
if (outputFile == null)
|
||||||
|
return
|
||||||
|
Snackbar.make(
|
||||||
|
findViewById(android.R.id.content),
|
||||||
|
if (exception == null) getString(R.string.log_export_success, outputFile.fileName)
|
||||||
|
else getString(R.string.log_export_error, ErrorMessages[exception]),
|
||||||
|
if (exception == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.setAnchorView(binding.shareFab)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun streamingLog() = withContext(Dispatchers.IO) {
|
||||||
|
val builder = ProcessBuilder().command("logcat", "-b", "all", "-v", "threadtime", "*:V")
|
||||||
|
builder.environment()["LC_ALL"] = "C"
|
||||||
|
var process: Process? = null
|
||||||
|
try {
|
||||||
|
process = try {
|
||||||
|
builder.start()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
val stdout = BufferedReader(InputStreamReader(process!!.inputStream, StandardCharsets.UTF_8))
|
||||||
|
|
||||||
|
var posStart = 0
|
||||||
|
var timeLastNotify = System.nanoTime()
|
||||||
|
var priorModified = false
|
||||||
|
val bufferedLogLines = arrayListOf<LogLine>()
|
||||||
|
var timeout = 1000000000L / 2 // The timeout is initially small so that the view gets populated immediately.
|
||||||
|
val MAX_LINES = (1 shl 16) - 1
|
||||||
|
val MAX_BUFFERED_LINES = (1 shl 14) - 1
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val line = stdout.readLine() ?: break
|
||||||
|
if (rawLogLines.size() >= MAX_LINES)
|
||||||
|
rawLogLines.popFirst()
|
||||||
|
rawLogLines.addLast(line)
|
||||||
|
val logLine = parseLine(line)
|
||||||
|
if (logLine != null) {
|
||||||
|
bufferedLogLines.add(logLine)
|
||||||
|
} else {
|
||||||
|
if (bufferedLogLines.isNotEmpty()) {
|
||||||
|
bufferedLogLines.last().msg += "\n$line"
|
||||||
|
} else if (!logLines.isEmpty()) {
|
||||||
|
logLines[logLines.size() - 1].msg += "\n$line"
|
||||||
|
priorModified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val timeNow = System.nanoTime()
|
||||||
|
if (bufferedLogLines.size < MAX_BUFFERED_LINES && (timeNow - timeLastNotify) < timeout && stdout.ready())
|
||||||
|
continue
|
||||||
|
timeout = 1000000000L * 5 / 2 // Increase the timeout after the initial view has something in it.
|
||||||
|
timeLastNotify = timeNow
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main.immediate) {
|
||||||
|
val isScrolledToBottomAlready = recyclerView?.canScrollVertically(1) == false
|
||||||
|
if (priorModified) {
|
||||||
|
logAdapter.notifyItemChanged(posStart - 1)
|
||||||
|
priorModified = false
|
||||||
|
}
|
||||||
|
val fullLen = logLines.size() + bufferedLogLines.size
|
||||||
|
if (fullLen >= MAX_LINES) {
|
||||||
|
val numToRemove = fullLen - MAX_LINES + 1
|
||||||
|
logLines.removeFromStart(numToRemove)
|
||||||
|
logAdapter.notifyItemRangeRemoved(0, numToRemove)
|
||||||
|
posStart -= numToRemove
|
||||||
|
|
||||||
|
}
|
||||||
|
for (bufferedLine in bufferedLogLines) {
|
||||||
|
logLines.addLast(bufferedLine)
|
||||||
|
}
|
||||||
|
bufferedLogLines.clear()
|
||||||
|
logAdapter.notifyItemRangeInserted(posStart, logLines.size() - posStart)
|
||||||
|
posStart = logLines.size()
|
||||||
|
|
||||||
|
if (isScrolledToBottomAlready) {
|
||||||
|
recyclerView?.scrollToPosition(logLines.size() - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
process?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTime(timeStr: String): Date? {
|
||||||
|
val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
|
||||||
|
return try {
|
||||||
|
formatter.parse("$year-$timeStr")
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLine(line: String): LogLine? {
|
||||||
|
val m: Matcher = THREADTIME_LINE.matcher(line)
|
||||||
|
return if (m.matches()) {
|
||||||
|
LogLine(m.group(2)!!.toInt(), m.group(3)!!.toInt(), parseTime(m.group(1)!!), m.group(4)!!, m.group(5)!!, m.group(6)!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LogLine(val pid: Int, val tid: Int, val time: Date?, val level: String, val tag: String, var msg: String)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Match a single line of `logcat -v threadtime`, such as:
|
||||||
|
*
|
||||||
|
* <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre>
|
||||||
|
*/
|
||||||
|
private val THREADTIME_LINE: Pattern =
|
||||||
|
Pattern.compile("^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+(.+?)\\s*: (.*)$")
|
||||||
|
private val LOGS: MutableMap<String, ByteArray> = ConcurrentHashMap()
|
||||||
|
private const val TAG = "WireGuard/LogViewerActivity"
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class LogEntryAdapter : RecyclerView.Adapter<LogEntryAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private inner class ViewHolder(val layout: View, var isSingleLine: Boolean = true) : RecyclerView.ViewHolder(layout)
|
||||||
|
|
||||||
|
private fun levelToColor(level: String): Int {
|
||||||
|
return when (level) {
|
||||||
|
"V", "D" -> debugColor
|
||||||
|
"E" -> errorColor
|
||||||
|
"I" -> infoColor
|
||||||
|
"W" -> warningColor
|
||||||
|
else -> defaultColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = logLines.size()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.log_viewer_entry, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val line = logLines[position]
|
||||||
|
val spannable = if (position > 0 && logLines[position - 1].tag == line.tag)
|
||||||
|
SpannableString(line.msg)
|
||||||
|
else
|
||||||
|
SpannableString("${line.tag}: ${line.msg}").apply {
|
||||||
|
setSpan(StyleSpan(BOLD), 0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
setSpan(
|
||||||
|
ForegroundColorSpan(levelToColor(line.level)),
|
||||||
|
0, "${line.tag}:".length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
holder.layout.apply {
|
||||||
|
findViewById<MaterialTextView>(R.id.log_date).text = line.time.toString()
|
||||||
|
findViewById<MaterialTextView>(R.id.log_msg).apply {
|
||||||
|
setSingleLine()
|
||||||
|
text = spannable
|
||||||
|
setOnClickListener {
|
||||||
|
isSingleLine = !holder.isSingleLine
|
||||||
|
holder.isSingleLine = !holder.isSingleLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExportedLogContentProvider : ContentProvider() {
|
||||||
|
private fun logForUri(uri: Uri): ByteArray? = LOGS[uri.pathSegments.lastOrNull()]
|
||||||
|
|
||||||
|
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||||
|
|
||||||
|
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? =
|
||||||
|
logForUri(uri)?.let {
|
||||||
|
val m = MatrixCursor(arrayOf(android.provider.OpenableColumns.DISPLAY_NAME, android.provider.OpenableColumns.SIZE), 1)
|
||||||
|
m.addRow(arrayOf<Any>("wireguard-log.txt", it.size.toLong()))
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(): Boolean = true
|
||||||
|
|
||||||
|
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||||
|
|
||||||
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||||
|
|
||||||
|
override fun getType(uri: Uri): String? = logForUri(uri)?.let { "text/plain" }
|
||||||
|
|
||||||
|
override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? =
|
||||||
|
getType(uri)?.let { if (compareMimeTypes(it, mimeTypeFilter)) arrayOf(it) else null }
|
||||||
|
|
||||||
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
|
if (mode != "r") return null
|
||||||
|
val log = logForUri(uri) ?: return null
|
||||||
|
return openPipeHelper(uri, "text/plain", null, log) { output, _, _, _, l ->
|
||||||
|
try {
|
||||||
|
FileOutputStream(output.fileDescriptor).write(l!!)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
Normal file
129
ui/src/main/java/com/wireguard/android/activity/MainActivity.kt
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.addCallback
|
||||||
|
import androidx.appcompat.app.ActionBar
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.fragment.TunnelDetailFragment
|
||||||
|
import com.wireguard.android.fragment.TunnelEditorFragment
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRUD interface for WireGuard tunnels. This activity serves as the main entry point to the
|
||||||
|
* WireGuard application, and contains several fragments for listing, viewing details of, and
|
||||||
|
* editing the configuration and interface state of WireGuard tunnels.
|
||||||
|
*/
|
||||||
|
class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener {
|
||||||
|
private var actionBar: ActionBar? = null
|
||||||
|
private var isTwoPaneLayout = false
|
||||||
|
private var backPressedCallback: OnBackPressedCallback? = null
|
||||||
|
|
||||||
|
private fun handleBackPressed() {
|
||||||
|
val backStackEntries = supportFragmentManager.backStackEntryCount
|
||||||
|
// If the two-pane layout does not have an editor open, going back should exit the app.
|
||||||
|
if (isTwoPaneLayout && backStackEntries <= 1) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backStackEntries >= 1)
|
||||||
|
supportFragmentManager.popBackStack()
|
||||||
|
|
||||||
|
// Deselect the current tunnel on navigating back from the detail pane to the one-pane list.
|
||||||
|
if (backStackEntries == 1)
|
||||||
|
selectedTunnel = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackStackChanged() {
|
||||||
|
val backStackEntries = supportFragmentManager.backStackEntryCount
|
||||||
|
backPressedCallback?.isEnabled = backStackEntries >= 1
|
||||||
|
if (actionBar == null) return
|
||||||
|
// Do not show the home menu when the two-pane layout is at the detail view (see above).
|
||||||
|
val minBackStackEntries = if (isTwoPaneLayout) 2 else 1
|
||||||
|
actionBar!!.setDisplayHomeAsUpEnabled(backStackEntries >= minBackStackEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.main_activity)
|
||||||
|
actionBar = supportActionBar
|
||||||
|
isTwoPaneLayout = findViewById<View?>(R.id.master_detail_wrapper) != null
|
||||||
|
supportFragmentManager.addOnBackStackChangedListener(this)
|
||||||
|
backPressedCallback = onBackPressedDispatcher.addCallback(this) { handleBackPressed() }
|
||||||
|
onBackStackChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.main_activity, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
// The back arrow in the action bar should act the same as the back button.
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_action_edit -> {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(if (isTwoPaneLayout) R.id.detail_container else R.id.list_detail_container, TunnelEditorFragment())
|
||||||
|
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
|
addToBackStack(null)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// This menu item is handled by the editor fragment.
|
||||||
|
R.id.menu_action_save -> false
|
||||||
|
R.id.menu_settings -> {
|
||||||
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectedTunnelChanged(
|
||||||
|
oldTunnel: ObservableTunnel?,
|
||||||
|
newTunnel: ObservableTunnel?
|
||||||
|
): Boolean {
|
||||||
|
val fragmentManager = supportFragmentManager
|
||||||
|
if (fragmentManager.isStateSaved) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val backStackEntries = fragmentManager.backStackEntryCount
|
||||||
|
if (newTunnel == null) {
|
||||||
|
// Clear everything off the back stack (all editors and detail fragments).
|
||||||
|
fragmentManager.popBackStackImmediate(0, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (backStackEntries == 2) {
|
||||||
|
// Pop the editor off the back stack to reveal the detail fragment. Use the immediate
|
||||||
|
// method to avoid the editor picking up the new tunnel while it is still visible.
|
||||||
|
fragmentManager.popBackStackImmediate()
|
||||||
|
} else if (backStackEntries == 0) {
|
||||||
|
// Create and show a new detail fragment.
|
||||||
|
fragmentManager.commit {
|
||||||
|
add(if (isTwoPaneLayout) R.id.detail_container else R.id.list_detail_container, TunnelDetailFragment())
|
||||||
|
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
|
addToBackStack(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.QuickTileService
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
|
import com.wireguard.android.preference.PreferencesPreferenceDataStore
|
||||||
|
import com.wireguard.android.util.AdminKnobs
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for changing application-global persistent settings.
|
||||||
|
*/
|
||||||
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (supportFragmentManager.findFragmentById(android.R.id.content) == null) {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
add(android.R.id.content, SettingsFragment())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == android.R.id.home) {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
// Since this is pretty much abandoned by androidx, it never got updated for proper EdgeToEdge support,
|
||||||
|
// which is enabled everywhere for API 35. So handle the insets manually here.
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
view.fitsSystemWindows = true
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, key: String?) {
|
||||||
|
preferenceManager.preferenceDataStore = PreferencesPreferenceDataStore(lifecycleScope, Application.getPreferencesDataStore())
|
||||||
|
addPreferencesFromResource(R.xml.preferences)
|
||||||
|
preferenceScreen.initialExpandedChildrenCount = 5
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || QuickTileService.isAdded) {
|
||||||
|
val quickTile = preferenceManager.findPreference<Preference>("quick_tile")
|
||||||
|
quickTile?.parent?.removePreference(quickTile)
|
||||||
|
--preferenceScreen.initialExpandedChildrenCount
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val darkTheme = preferenceManager.findPreference<Preference>("dark_theme")
|
||||||
|
darkTheme?.parent?.removePreference(darkTheme)
|
||||||
|
--preferenceScreen.initialExpandedChildrenCount
|
||||||
|
}
|
||||||
|
if (AdminKnobs.disableConfigExport) {
|
||||||
|
val zipExporter = preferenceManager.findPreference<Preference>("zip_exporter")
|
||||||
|
zipExporter?.parent?.removePreference(zipExporter)
|
||||||
|
}
|
||||||
|
val wgQuickOnlyPrefs = arrayOf(
|
||||||
|
preferenceManager.findPreference("tools_installer"),
|
||||||
|
preferenceManager.findPreference("restore_on_boot"),
|
||||||
|
preferenceManager.findPreference<Preference>("multiple_tunnels")
|
||||||
|
).filterNotNull()
|
||||||
|
wgQuickOnlyPrefs.forEach { it.isVisible = false }
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (Application.getBackend() is WgQuickBackend) {
|
||||||
|
++preferenceScreen.initialExpandedChildrenCount
|
||||||
|
wgQuickOnlyPrefs.forEach { it.isVisible = true }
|
||||||
|
} else {
|
||||||
|
wgQuickOnlyPrefs.forEach { it.parent?.removePreference(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preferenceManager.findPreference<Preference>("log_viewer")?.setOnPreferenceClickListener {
|
||||||
|
startActivity(Intent(requireContext(), LogViewerActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
val kernelModuleEnabler = preferenceManager.findPreference<Preference>("kernel_module_enabler")
|
||||||
|
if (WgQuickBackend.hasKernelSupport()) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (Application.getBackend() !is WgQuickBackend) {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) { Application.getRootShell().start() }
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
kernelModuleEnabler?.parent?.removePreference(kernelModuleEnabler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kernelModuleEnabler?.parent?.removePreference(kernelModuleEnabler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone activity for creating tunnels.
|
||||||
|
*/
|
||||||
|
class TunnelCreatorActivity : BaseActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.tunnel_creator_activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.QuickTileService
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.backend.GoBackend
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class TunnelToggleActivity : AppCompatActivity() {
|
||||||
|
private val permissionActivityResultLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
|
||||||
|
|
||||||
|
private fun toggleTunnelWithPermissionsResult() {
|
||||||
|
val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val message = getString(R.string.toggle_error, error)
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
|
||||||
|
finishAffinity()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (Application.getBackend() is GoBackend) {
|
||||||
|
try {
|
||||||
|
val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
|
||||||
|
if (intent != null) {
|
||||||
|
permissionActivityResultLauncher.launch(intent)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(this@TunnelToggleActivity, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleTunnelWithPermissionsResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/TunnelToggleActivity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,431 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.activity
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.storage.StorageManager
|
||||||
|
import android.os.storage.StorageVolume
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.addCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.view.forEach
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.Observable
|
||||||
|
import androidx.databinding.ObservableBoolean
|
||||||
|
import androidx.databinding.ObservableField
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.backend.GoBackend
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.databinding.Keyed
|
||||||
|
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||||
|
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter
|
||||||
|
import com.wireguard.android.databinding.TvActivityBinding
|
||||||
|
import com.wireguard.android.databinding.TvFileListItemBinding
|
||||||
|
import com.wireguard.android.databinding.TvTunnelListItemBinding
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import com.wireguard.android.util.QuantityFormatter
|
||||||
|
import com.wireguard.android.util.TunnelImporter
|
||||||
|
import com.wireguard.android.util.UserKnobs
|
||||||
|
import com.wireguard.android.util.applicationScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class TvMainActivity : AppCompatActivity() {
|
||||||
|
private val tunnelFileImportResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||||
|
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
|
||||||
|
/* AndroidTV now comes with stubs that do nothing but display a Toast less helpful than
|
||||||
|
* what we can do, so detect this and throw an exception that we can catch later. */
|
||||||
|
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
}
|
||||||
|
if (activitiesToResolveIntent.all {
|
||||||
|
val name = it.activityInfo.packageName
|
||||||
|
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||||
|
}) {
|
||||||
|
throw ActivityNotFoundException()
|
||||||
|
}
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}) { data ->
|
||||||
|
if (data == null) return@registerForActivityResult
|
||||||
|
lifecycleScope.launch {
|
||||||
|
TunnelImporter.importTunnel(contentResolver, data) {
|
||||||
|
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var pendingTunnel: ObservableTunnel? = null
|
||||||
|
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
val tunnel = pendingTunnel
|
||||||
|
if (tunnel != null)
|
||||||
|
setTunnelStateWithPermissionsResult(tunnel)
|
||||||
|
pendingTunnel = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
tunnel.setStateAsync(Tunnel.State.TOGGLE)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val message = getString(R.string.error_up, error)
|
||||||
|
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
}
|
||||||
|
updateStats()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var binding: TvActivityBinding
|
||||||
|
private val isDeleting = ObservableBoolean()
|
||||||
|
private val files = ObservableKeyedArrayList<String, KeyedFile>()
|
||||||
|
private val filesRoot = ObservableField("")
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
if (AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_YES) {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
applicationScope.launch {
|
||||||
|
UserKnobs.setDarkTheme(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = TvActivityBinding.inflate(layoutInflater)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
binding.tunnels = Application.getTunnelManager().getTunnels()
|
||||||
|
if (binding.tunnels?.isEmpty() == true)
|
||||||
|
binding.importButton.requestFocus()
|
||||||
|
else
|
||||||
|
binding.tunnelList.requestFocus()
|
||||||
|
}
|
||||||
|
binding.isDeleting = isDeleting
|
||||||
|
binding.files = files
|
||||||
|
binding.filesRoot = filesRoot
|
||||||
|
val gridManager = binding.tunnelList.layoutManager as GridLayoutManager
|
||||||
|
gridManager.spanSizeLookup = SlatedSpanSizeLookup(gridManager)
|
||||||
|
binding.tunnelRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvTunnelListItemBinding, ObservableTunnel> {
|
||||||
|
override fun onConfigureRow(binding: TvTunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||||
|
binding.isDeleting = isDeleting
|
||||||
|
binding.isFocused = ObservableBoolean()
|
||||||
|
binding.root.setOnFocusChangeListener { _, focused ->
|
||||||
|
binding.isFocused?.set(focused)
|
||||||
|
}
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (isDeleting.get()) {
|
||||||
|
try {
|
||||||
|
item.deleteAsync()
|
||||||
|
if (this@TvMainActivity.binding.tunnels?.isEmpty() != false)
|
||||||
|
isDeleting.set(false)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val message = getString(R.string.config_delete_error, error)
|
||||||
|
Toast.makeText(this@TvMainActivity, message, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Application.getBackend() is GoBackend) {
|
||||||
|
val intent = GoBackend.VpnService.prepare(binding.root.context)
|
||||||
|
if (intent != null) {
|
||||||
|
pendingTunnel = item
|
||||||
|
permissionActivityResultLauncher.launch(intent)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTunnelStateWithPermissionsResult(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.filesRowConfigurationHandler = object : ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler<TvFileListItemBinding, KeyedFile> {
|
||||||
|
override fun onConfigureRow(binding: TvFileListItemBinding, item: KeyedFile, position: Int) {
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
if (item.file.isDirectory)
|
||||||
|
navigateTo(item.file)
|
||||||
|
else {
|
||||||
|
val uri = Uri.fromFile(item.file)
|
||||||
|
files.clear()
|
||||||
|
filesRoot.set("")
|
||||||
|
lifecycleScope.launch {
|
||||||
|
TunnelImporter.importTunnel(contentResolver, uri) {
|
||||||
|
Toast.makeText(this@TvMainActivity, it, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runOnUiThread {
|
||||||
|
this@TvMainActivity.binding.tunnelList.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.importButton.setOnClickListener {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
if (filesRoot.get()?.isEmpty() != false) {
|
||||||
|
navigateTo(File("/"))
|
||||||
|
runOnUiThread {
|
||||||
|
binding.filesList.requestFocus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
files.clear()
|
||||||
|
filesRoot.set("")
|
||||||
|
runOnUiThread {
|
||||||
|
binding.tunnelList.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
tunnelFileImportResultLauncher.launch(arrayOf("*/*"))
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
MaterialAlertDialogBuilder(binding.root.context).setMessage(R.string.tv_no_file_picker).setCancelable(false)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
data = Uri.parse("https://play.google.com/store/apps/details?id=com.cxinventor.file.explorer")
|
||||||
|
setPackage("com.android.vending")
|
||||||
|
})
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.deleteButton.setOnClickListener {
|
||||||
|
isDeleting.set(!isDeleting.get())
|
||||||
|
runOnUiThread {
|
||||||
|
binding.tunnelList.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backPressedCallback = onBackPressedDispatcher.addCallback(this) { handleBackPressed() }
|
||||||
|
val updateBackPressedCallback = object : Observable.OnPropertyChangedCallback() {
|
||||||
|
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||||
|
backPressedCallback.isEnabled = isDeleting.get() || filesRoot.get()?.isNotEmpty() == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDeleting.addOnPropertyChangedCallback(updateBackPressedCallback)
|
||||||
|
filesRoot.addOnPropertyChangedCallback(updateBackPressedCallback)
|
||||||
|
backPressedCallback.isEnabled = false
|
||||||
|
|
||||||
|
binding.executePendingBindings()
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
while (true) {
|
||||||
|
updateStats()
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pendingNavigation: File? = null
|
||||||
|
private val permissionRequestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
|
val to = pendingNavigation
|
||||||
|
if (it && to != null)
|
||||||
|
navigateTo(to)
|
||||||
|
pendingNavigation = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedRoots: Collection<KeyedFile>? = null
|
||||||
|
|
||||||
|
private suspend fun makeStorageRoots(): Collection<KeyedFile> = withContext(Dispatchers.IO) {
|
||||||
|
cachedRoots?.let { return@withContext it }
|
||||||
|
val list = HashSet<KeyedFile>()
|
||||||
|
val storageManager: StorageManager = getSystemService() ?: return@withContext list
|
||||||
|
list.addAll(storageManager.storageVolumes.mapNotNull { volume ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
volume.directory?.let { KeyedFile(it, volume.getDescription(this@TvMainActivity)) }
|
||||||
|
} else {
|
||||||
|
KeyedFile((StorageVolume::class.java.getMethod("getPathFile").invoke(volume) as File), volume.getDescription(this@TvMainActivity))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cachedRoots = list
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBelowCachedRoots(maybeChild: File): Boolean {
|
||||||
|
val cachedRoots = cachedRoots ?: return true
|
||||||
|
for (root in cachedRoots) {
|
||||||
|
if (maybeChild.canonicalPath.startsWith(root.file.canonicalPath))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateTo(directory: File) {
|
||||||
|
require(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
pendingNavigation = directory
|
||||||
|
permissionRequestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (isBelowCachedRoots(directory)) {
|
||||||
|
val roots = makeStorageRoots()
|
||||||
|
if (roots.count() == 1) {
|
||||||
|
navigateTo(roots.first().file)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
files.clear()
|
||||||
|
files.addAll(roots)
|
||||||
|
filesRoot.set(getString(R.string.tv_select_a_storage_drive))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val newFiles = withContext(Dispatchers.IO) {
|
||||||
|
val newFiles = ArrayList<KeyedFile>()
|
||||||
|
try {
|
||||||
|
directory.parentFile?.let {
|
||||||
|
newFiles.add(KeyedFile(it, "../"))
|
||||||
|
}
|
||||||
|
val listing = directory.listFiles() ?: return@withContext null
|
||||||
|
listing.forEach {
|
||||||
|
if (it.extension == "conf" || it.extension == "zip" || it.isDirectory)
|
||||||
|
newFiles.add(KeyedFile(it))
|
||||||
|
}
|
||||||
|
newFiles.sortWith { a, b ->
|
||||||
|
if (a.file.isDirectory && !b.file.isDirectory) -1
|
||||||
|
else if (!a.file.isDirectory && b.file.isDirectory) 1
|
||||||
|
else a.file.compareTo(b.file)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
newFiles
|
||||||
|
}
|
||||||
|
if (newFiles?.isEmpty() != false)
|
||||||
|
return@launch
|
||||||
|
files.clear()
|
||||||
|
files.addAll(newFiles)
|
||||||
|
filesRoot.set(directory.canonicalPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBackPressed() {
|
||||||
|
when {
|
||||||
|
isDeleting.get() -> {
|
||||||
|
isDeleting.set(false)
|
||||||
|
runOnUiThread {
|
||||||
|
binding.tunnelList.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filesRoot.get()?.isNotEmpty() == true -> {
|
||||||
|
files.clear()
|
||||||
|
filesRoot.set("")
|
||||||
|
runOnUiThread {
|
||||||
|
binding.tunnelList.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateStats() {
|
||||||
|
binding.tunnelList.forEach { viewItem ->
|
||||||
|
val listItem = DataBindingUtil.findBinding<TvTunnelListItemBinding>(viewItem)
|
||||||
|
?: return@forEach
|
||||||
|
try {
|
||||||
|
val tunnel = listItem.item!!
|
||||||
|
if (tunnel.state != Tunnel.State.UP || isDeleting.get()) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
val statistics = tunnel.getStatisticsAsync()
|
||||||
|
val rx = statistics.totalRx()
|
||||||
|
val tx = statistics.totalTx()
|
||||||
|
listItem.tunnelTransfer.text = getString(R.string.transfer_rx_tx, QuantityFormatter.formatBytes(rx), QuantityFormatter.formatBytes(tx))
|
||||||
|
listItem.tunnelTransfer.visibility = View.VISIBLE
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
listItem.tunnelTransfer.visibility = View.GONE
|
||||||
|
listItem.tunnelTransfer.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyedFile(val file: File, private val forcedKey: String? = null) : Keyed<String> {
|
||||||
|
override val key: String
|
||||||
|
get() = forcedKey ?: if (file.isDirectory) "${file.name}/" else file.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SlatedSpanSizeLookup(private val gridManager: GridLayoutManager) : SpanSizeLookup() {
|
||||||
|
private val originalHeight = gridManager.spanCount
|
||||||
|
private var newWidth = 0
|
||||||
|
private lateinit var sizeMap: Array<IntArray?>
|
||||||
|
|
||||||
|
private fun emptyUnderIndex(index: Int, size: Int): Int {
|
||||||
|
sizeMap[size - 1]?.let { return it[index] }
|
||||||
|
val sizes = IntArray(size)
|
||||||
|
val oh = originalHeight
|
||||||
|
val nw = newWidth
|
||||||
|
var empties = 0
|
||||||
|
for (i in 0 until size) {
|
||||||
|
val ox = (i + empties) / oh
|
||||||
|
val oy = (i + empties) % oh
|
||||||
|
var empty = 0
|
||||||
|
for (j in oy + 1 until oh) {
|
||||||
|
val ni = nw * j + ox
|
||||||
|
if (ni < size)
|
||||||
|
break
|
||||||
|
empty++
|
||||||
|
}
|
||||||
|
empties += empty
|
||||||
|
sizes[i] = empty
|
||||||
|
}
|
||||||
|
sizeMap[size - 1] = sizes
|
||||||
|
return sizes[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
if (newWidth == 0) {
|
||||||
|
val child = gridManager.getChildAt(0) ?: return 1
|
||||||
|
if (child.width == 0) return 1
|
||||||
|
newWidth = gridManager.width / child.width
|
||||||
|
sizeMap = Array(originalHeight * newWidth - 1) { null }
|
||||||
|
}
|
||||||
|
val total = gridManager.itemCount
|
||||||
|
if (total >= originalHeight * newWidth || total == 0)
|
||||||
|
return 1
|
||||||
|
return emptyUnderIndex(position, total) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/TvMainActivity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.configStore
|
||||||
|
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for persistent storage providers for WireGuard configurations.
|
||||||
|
*/
|
||||||
|
interface ConfigStore {
|
||||||
|
/**
|
||||||
|
* Create a persistent tunnel, which must have a unique name within the persistent storage
|
||||||
|
* medium.
|
||||||
|
*
|
||||||
|
* @param name The name of the tunnel to create.
|
||||||
|
* @param config Configuration for the new tunnel.
|
||||||
|
* @return The configuration that was actually saved to persistent storage.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun create(name: String, config: Config): Config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a persistent tunnel.
|
||||||
|
*
|
||||||
|
* @param name The name of the tunnel to delete.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun delete(name: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerate the names of tunnels present in persistent storage.
|
||||||
|
*
|
||||||
|
* @return The set of present tunnel names.
|
||||||
|
*/
|
||||||
|
fun enumerate(): Set<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the configuration for the tunnel given by `name`.
|
||||||
|
*
|
||||||
|
* @param name The identifier for the configuration in persistent storage (i.e. the name of the
|
||||||
|
* tunnel).
|
||||||
|
* @return An in-memory representation of the configuration loaded from persistent storage.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun load(name: String): Config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the configuration for the tunnel given by `name`.
|
||||||
|
*
|
||||||
|
* @param name The identifier for the existing configuration in persistent storage.
|
||||||
|
* @param replacement The new identifier for the configuration in persistent storage.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun rename(name: String, replacement: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the configuration for an existing tunnel given by `name`.
|
||||||
|
*
|
||||||
|
* @param name The identifier for the configuration in persistent storage (i.e. the name of
|
||||||
|
* the tunnel).
|
||||||
|
* @param config An updated configuration object for the tunnel.
|
||||||
|
* @return The configuration that was actually saved to persistent storage.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun save(name: String, config: Config): Config
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.configStore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.config.BadConfigException
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration store that uses a `wg-quick`-style file for each configured tunnel.
|
||||||
|
*/
|
||||||
|
class FileConfigStore(private val context: Context) : ConfigStore {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun create(name: String, config: Config): Config {
|
||||||
|
Log.d(TAG, "Creating configuration for tunnel $name")
|
||||||
|
val file = fileFor(name)
|
||||||
|
if (!file.createNewFile())
|
||||||
|
throw IOException(context.getString(R.string.config_file_exists_error, file.name))
|
||||||
|
FileOutputStream(file, false).use { it.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun delete(name: String) {
|
||||||
|
Log.d(TAG, "Deleting configuration for tunnel $name")
|
||||||
|
val file = fileFor(name)
|
||||||
|
if (!file.delete())
|
||||||
|
throw IOException(context.getString(R.string.config_delete_error, file.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enumerate(): Set<String> {
|
||||||
|
return context.fileList()
|
||||||
|
.filter { it.endsWith(".conf") }
|
||||||
|
.map { it.substring(0, it.length - ".conf".length) }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileFor(name: String): File {
|
||||||
|
return File(context.filesDir, "$name.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(BadConfigException::class, IOException::class)
|
||||||
|
override fun load(name: String): Config {
|
||||||
|
FileInputStream(fileFor(name)).use { stream -> return Config.parse(stream) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun rename(name: String, replacement: String) {
|
||||||
|
Log.d(TAG, "Renaming configuration for tunnel $name to $replacement")
|
||||||
|
val file = fileFor(name)
|
||||||
|
val replacementFile = fileFor(replacement)
|
||||||
|
if (!replacementFile.createNewFile()) throw IOException(context.getString(R.string.config_exists_error, replacement))
|
||||||
|
if (!file.renameTo(replacementFile)) {
|
||||||
|
if (!replacementFile.delete()) Log.w(TAG, "Couldn't delete marker file for new name $replacement")
|
||||||
|
throw IOException(context.getString(R.string.config_rename_error, file.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun save(name: String, config: Config): Config {
|
||||||
|
Log.d(TAG, "Saving configuration for tunnel $name")
|
||||||
|
val file = fileFor(name)
|
||||||
|
if (!file.isFile)
|
||||||
|
throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
|
||||||
|
FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/FileConfigStore"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.databinding
|
||||||
|
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.databinding.BindingAdapter
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.ObservableList
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.databinding.adapters.ListenerUtil
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
|
||||||
|
import com.wireguard.android.widget.ToggleSwitch
|
||||||
|
import com.wireguard.android.widget.ToggleSwitch.OnBeforeCheckedChangeListener
|
||||||
|
import com.wireguard.android.widget.TvCardView
|
||||||
|
import com.wireguard.config.Attribute
|
||||||
|
import com.wireguard.config.InetNetwork
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static methods for use by generated code in the Android data binding library.
|
||||||
|
*/
|
||||||
|
object BindingAdapters {
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("checked")
|
||||||
|
fun setChecked(view: ToggleSwitch, checked: Boolean) {
|
||||||
|
view.setCheckedInternal(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("filter")
|
||||||
|
fun setFilter(view: TextView, filter: InputFilter) {
|
||||||
|
view.filters = arrayOf(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("items", "layout", "fragment")
|
||||||
|
fun <E> setItems(
|
||||||
|
view: LinearLayout,
|
||||||
|
oldList: ObservableList<E>?, oldLayoutId: Int, @Suppress("UNUSED_PARAMETER") oldFragment: Fragment?,
|
||||||
|
newList: ObservableList<E>?, newLayoutId: Int, newFragment: Fragment?
|
||||||
|
) {
|
||||||
|
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||||
|
return
|
||||||
|
var listener: ItemChangeListener<E>? = ListenerUtil.getListener(view, R.id.item_change_listener)
|
||||||
|
// If the layout changes, any existing listener must be replaced.
|
||||||
|
if (listener != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||||
|
listener.setList(null)
|
||||||
|
listener = null
|
||||||
|
// Stop tracking the old listener.
|
||||||
|
ListenerUtil.trackListener<Any?>(view, null, R.id.item_change_listener)
|
||||||
|
}
|
||||||
|
// Avoid adding a listener when there is no new list or layout.
|
||||||
|
if (newList == null || newLayoutId == 0)
|
||||||
|
return
|
||||||
|
if (listener == null) {
|
||||||
|
listener = ItemChangeListener(view, newLayoutId, newFragment)
|
||||||
|
ListenerUtil.trackListener(view, listener, R.id.item_change_listener)
|
||||||
|
}
|
||||||
|
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||||
|
listener.setList(newList)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("items", "layout")
|
||||||
|
fun <E> setItems(
|
||||||
|
view: LinearLayout,
|
||||||
|
oldList: Iterable<E>?, oldLayoutId: Int,
|
||||||
|
newList: Iterable<E>?, newLayoutId: Int
|
||||||
|
) {
|
||||||
|
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||||
|
return
|
||||||
|
view.removeAllViews()
|
||||||
|
if (newList == null)
|
||||||
|
return
|
||||||
|
val layoutInflater = LayoutInflater.from(view.context)
|
||||||
|
for (item in newList) {
|
||||||
|
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, newLayoutId, view, false)
|
||||||
|
binding.setVariable(BR.collection, newList)
|
||||||
|
binding.setVariable(BR.item, item)
|
||||||
|
binding.executePendingBindings()
|
||||||
|
view.addView(binding.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter(requireAll = false, value = ["items", "layout", "configurationHandler"])
|
||||||
|
fun <K, E : Keyed<out K>> setItems(
|
||||||
|
view: RecyclerView,
|
||||||
|
oldList: ObservableKeyedArrayList<K, E>?, oldLayoutId: Int,
|
||||||
|
@Suppress("UNUSED_PARAMETER") oldRowConfigurationHandler: RowConfigurationHandler<*, *>?,
|
||||||
|
newList: ObservableKeyedArrayList<K, E>?, newLayoutId: Int,
|
||||||
|
newRowConfigurationHandler: RowConfigurationHandler<*, *>?
|
||||||
|
) {
|
||||||
|
if (view.layoutManager == null)
|
||||||
|
view.layoutManager = LinearLayoutManager(view.context, RecyclerView.VERTICAL, false)
|
||||||
|
if (oldList === newList && oldLayoutId == newLayoutId)
|
||||||
|
return
|
||||||
|
// The ListAdapter interface is not generic, so this cannot be checked.
|
||||||
|
@Suppress("UNCHECKED_CAST") var adapter = view.adapter as? ObservableKeyedRecyclerViewAdapter<K, E>?
|
||||||
|
// If the layout changes, any existing adapter must be replaced.
|
||||||
|
if (adapter != null && oldList != null && oldLayoutId != newLayoutId) {
|
||||||
|
adapter.setList(null)
|
||||||
|
adapter = null
|
||||||
|
}
|
||||||
|
// Avoid setting an adapter when there is no new list or layout.
|
||||||
|
if (newList == null || newLayoutId == 0)
|
||||||
|
return
|
||||||
|
if (adapter == null) {
|
||||||
|
adapter = ObservableKeyedRecyclerViewAdapter(view.context, newLayoutId, newList)
|
||||||
|
view.adapter = adapter
|
||||||
|
}
|
||||||
|
adapter.setRowConfigurationHandler(newRowConfigurationHandler)
|
||||||
|
// Either the list changed, or this is an entirely new listener because the layout changed.
|
||||||
|
adapter.setList(newList)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("onBeforeCheckedChanged")
|
||||||
|
fun setOnBeforeCheckedChanged(
|
||||||
|
view: ToggleSwitch,
|
||||||
|
listener: OnBeforeCheckedChangeListener?
|
||||||
|
) {
|
||||||
|
view.setOnBeforeCheckedChangeListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("onFocusChange")
|
||||||
|
fun setOnFocusChange(
|
||||||
|
view: EditText,
|
||||||
|
listener: View.OnFocusChangeListener?
|
||||||
|
) {
|
||||||
|
view.onFocusChangeListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("android:text")
|
||||||
|
fun setOptionalText(view: TextView, text: Optional<*>?) {
|
||||||
|
view.text = text?.map { it.toString() }?.orElse("") ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("android:text")
|
||||||
|
fun setInetNetworkSetText(view: TextView, networks: Iterable<InetNetwork?>?) {
|
||||||
|
view.text = if (networks != null) Attribute.join(networks) else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("android:text")
|
||||||
|
fun setInetAddressSetText(view: TextView, addresses: Iterable<InetAddress?>?) {
|
||||||
|
view.text = if (addresses != null) Attribute.join(addresses.map { it?.hostAddress }) else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("android:text")
|
||||||
|
fun setStringSetText(view: TextView, strings: Iterable<String?>?) {
|
||||||
|
view.text = if (strings != null) Attribute.join(strings) else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun tryParseInt(s: String?): Int {
|
||||||
|
if (s == null)
|
||||||
|
return 0
|
||||||
|
return try {
|
||||||
|
Integer.parseInt(s)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("isUp")
|
||||||
|
fun setIsUp(card: TvCardView, up: Boolean) {
|
||||||
|
card.isUp = up
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BindingAdapter("isDeleting")
|
||||||
|
fun setIsDeleting(card: TvCardView, deleting: Boolean) {
|
||||||
|
card.isDeleting = deleting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.databinding
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.ObservableList
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for binding an ObservableList to the children of a ViewGroup.
|
||||||
|
*/
|
||||||
|
internal class ItemChangeListener<T>(private val container: ViewGroup, private val layoutId: Int, private val fragment: Fragment?) {
|
||||||
|
private val callback = OnListChangedCallback(this)
|
||||||
|
private val layoutInflater: LayoutInflater = LayoutInflater.from(container.context)
|
||||||
|
private var list: ObservableList<T>? = null
|
||||||
|
|
||||||
|
private fun getView(position: Int, convertView: View?): View {
|
||||||
|
var binding = if (convertView != null) DataBindingUtil.getBinding<ViewDataBinding>(convertView) else null
|
||||||
|
if (binding == null) {
|
||||||
|
binding = DataBindingUtil.inflate(layoutInflater, layoutId, container, false)
|
||||||
|
}
|
||||||
|
require(list != null) { "Trying to get a view while list is still null" }
|
||||||
|
binding!!.setVariable(BR.collection, list)
|
||||||
|
binding.setVariable(BR.item, list!![position])
|
||||||
|
binding.setVariable(BR.fragment, fragment)
|
||||||
|
binding.executePendingBindings()
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setList(newList: ObservableList<T>?) {
|
||||||
|
list?.removeOnListChangedCallback(callback)
|
||||||
|
list = newList
|
||||||
|
if (list != null) {
|
||||||
|
list!!.addOnListChangedCallback(callback)
|
||||||
|
callback.onChanged(list!!)
|
||||||
|
} else {
|
||||||
|
container.removeAllViews()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnListChangedCallback<T> constructor(listener: ItemChangeListener<T>) : ObservableList.OnListChangedCallback<ObservableList<T>>() {
|
||||||
|
private val weakListener: WeakReference<ItemChangeListener<T>> = WeakReference(listener)
|
||||||
|
|
||||||
|
override fun onChanged(sender: ObservableList<T>) {
|
||||||
|
val listener = weakListener.get()
|
||||||
|
if (listener != null) {
|
||||||
|
// TODO: recycle views
|
||||||
|
listener.container.removeAllViews()
|
||||||
|
for (i in sender.indices)
|
||||||
|
listener.container.addView(listener.getView(i, null))
|
||||||
|
} else {
|
||||||
|
sender.removeOnListChangedCallback(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(
|
||||||
|
sender: ObservableList<T>, positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
val listener = weakListener.get()
|
||||||
|
if (listener != null) {
|
||||||
|
for (i in positionStart until positionStart + itemCount) {
|
||||||
|
val child = listener.container.getChildAt(i)
|
||||||
|
listener.container.removeViewAt(i)
|
||||||
|
listener.container.addView(listener.getView(i, child))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sender.removeOnListChangedCallback(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(
|
||||||
|
sender: ObservableList<T>, positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
val listener = weakListener.get()
|
||||||
|
if (listener != null) {
|
||||||
|
for (i in positionStart until positionStart + itemCount)
|
||||||
|
listener.container.addView(listener.getView(i, null))
|
||||||
|
} else {
|
||||||
|
sender.removeOnListChangedCallback(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(
|
||||||
|
sender: ObservableList<T>, fromPosition: Int,
|
||||||
|
toPosition: Int, itemCount: Int
|
||||||
|
) {
|
||||||
|
val listener = weakListener.get()
|
||||||
|
if (listener != null) {
|
||||||
|
val views = arrayOfNulls<View>(itemCount)
|
||||||
|
for (i in 0 until itemCount) views[i] = listener.container.getChildAt(fromPosition + i)
|
||||||
|
listener.container.removeViews(fromPosition, itemCount)
|
||||||
|
for (i in 0 until itemCount) listener.container.addView(views[i], toPosition + i)
|
||||||
|
} else {
|
||||||
|
sender.removeOnListChangedCallback(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(
|
||||||
|
sender: ObservableList<T>, positionStart: Int,
|
||||||
|
itemCount: Int
|
||||||
|
) {
|
||||||
|
val listener = weakListener.get()
|
||||||
|
if (listener != null) {
|
||||||
|
listener.container.removeViews(positionStart, itemCount)
|
||||||
|
} else {
|
||||||
|
sender.removeOnListChangedCallback(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
Normal file
12
ui/src/main/java/com/wireguard/android/databinding/Keyed.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.databinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for objects that have a identifying key of the given type.
|
||||||
|
*/
|
||||||
|
interface Keyed<K> {
|
||||||
|
val key: K
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.databinding
|
||||||
|
|
||||||
|
import androidx.databinding.ObservableArrayList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArrayList that allows looking up elements by some key property. As the key property must always
|
||||||
|
* be retrievable, this list cannot hold `null` elements. Because this class places no
|
||||||
|
* restrictions on the order or duplication of keys, lookup by key, as well as all list modification
|
||||||
|
* operations, require O(n) time.
|
||||||
|
*/
|
||||||
|
open class ObservableKeyedArrayList<K, E : Keyed<out K>> : ObservableArrayList<E>() {
|
||||||
|
fun containsKey(key: K) = indexOfKey(key) >= 0
|
||||||
|
|
||||||
|
operator fun get(key: K): E? {
|
||||||
|
val index = indexOfKey(key)
|
||||||
|
return if (index >= 0) get(index) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun indexOfKey(key: K): Int {
|
||||||
|
val iterator = listIterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val index = iterator.nextIndex()
|
||||||
|
if (iterator.next()!!.key == key)
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.databinding
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.ObservableList
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
|
||||||
|
*/
|
||||||
|
class ObservableKeyedRecyclerViewAdapter<K, E : Keyed<out K>> internal constructor(
|
||||||
|
context: Context, private val layoutId: Int,
|
||||||
|
list: ObservableKeyedArrayList<K, E>?
|
||||||
|
) : RecyclerView.Adapter<ObservableKeyedRecyclerViewAdapter.ViewHolder>() {
|
||||||
|
private val callback = OnListChangedCallback(this)
|
||||||
|
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
private var list: ObservableKeyedArrayList<K, E>? = null
|
||||||
|
private var rowConfigurationHandler: RowConfigurationHandler<ViewDataBinding, Any>? = null
|
||||||
|
|
||||||
|
private fun getItem(position: Int): E? = if (list == null || position < 0 || position >= list!!.size) null else list?.get(position)
|
||||||
|
|
||||||
|
override fun getItemCount() = list?.size ?: 0
|
||||||
|
|
||||||
|
override fun getItemId(position: Int) = (getKey(position)?.hashCode() ?: -1).toLong()
|
||||||
|
|
||||||
|
private fun getKey(position: Int): K? = getItem(position)?.key
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.binding.setVariable(BR.collection, list)
|
||||||
|
holder.binding.setVariable(BR.key, getKey(position))
|
||||||
|
holder.binding.setVariable(BR.item, getItem(position))
|
||||||
|
holder.binding.executePendingBindings()
|
||||||
|
if (rowConfigurationHandler != null) {
|
||||||
|
val item = getItem(position)
|
||||||
|
if (item != null) {
|
||||||
|
rowConfigurationHandler?.onConfigureRow(holder.binding, item, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false))
|
||||||
|
|
||||||
|
fun setList(newList: ObservableKeyedArrayList<K, E>?) {
|
||||||
|
list?.removeOnListChangedCallback(callback)
|
||||||
|
list = newList
|
||||||
|
list?.addOnListChangedCallback(callback)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRowConfigurationHandler(rowConfigurationHandler: RowConfigurationHandler<*, *>?) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
this.rowConfigurationHandler = rowConfigurationHandler as? RowConfigurationHandler<ViewDataBinding, Any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RowConfigurationHandler<B : ViewDataBinding, T> {
|
||||||
|
fun onConfigureRow(binding: B, item: T, position: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnListChangedCallback<E : Keyed<*>> constructor(adapter: ObservableKeyedRecyclerViewAdapter<*, E>) : ObservableList.OnListChangedCallback<ObservableList<E>>() {
|
||||||
|
private val weakAdapter: WeakReference<ObservableKeyedRecyclerViewAdapter<*, E>> = WeakReference(adapter)
|
||||||
|
|
||||||
|
override fun onChanged(sender: ObservableList<E>) {
|
||||||
|
val adapter = weakAdapter.get()
|
||||||
|
if (adapter != null)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
else
|
||||||
|
sender.removeOnListChangedCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(sender: ObservableList<E>, positionStart: Int,
|
||||||
|
itemCount: Int) {
|
||||||
|
onChanged(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(sender: ObservableList<E>, positionStart: Int,
|
||||||
|
itemCount: Int) {
|
||||||
|
onChanged(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(sender: ObservableList<E>, fromPosition: Int,
|
||||||
|
toPosition: Int, itemCount: Int) {
|
||||||
|
onChanged(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(sender: ObservableList<E>, positionStart: Int,
|
||||||
|
itemCount: Int) {
|
||||||
|
onChanged(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
init {
|
||||||
|
setList(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.databinding
|
||||||
|
|
||||||
|
import java.util.AbstractList
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Comparator
|
||||||
|
import java.util.Spliterator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
|
||||||
|
* binary search to improve lookup and replacement times to O(log(n)). However, due to the
|
||||||
|
* array-based nature of this class, insertion and removal of elements with anything but the largest
|
||||||
|
* key still require O(n) time.
|
||||||
|
*/
|
||||||
|
class ObservableSortedKeyedArrayList<K, E : Keyed<out K>>(private val comparator: Comparator<in K>) : ObservableKeyedArrayList<K, E>() {
|
||||||
|
@Transient
|
||||||
|
private val keyList = KeyList(this)
|
||||||
|
|
||||||
|
override fun add(element: E): Boolean {
|
||||||
|
val insertionPoint = getInsertionPoint(element)
|
||||||
|
if (insertionPoint < 0) {
|
||||||
|
// Skipping insertion is non-destructive if the new and existing objects are the same.
|
||||||
|
if (element === get(-insertionPoint - 1)) return false
|
||||||
|
throw IllegalArgumentException("Element with same key already exists in list")
|
||||||
|
}
|
||||||
|
super.add(insertionPoint, element)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(index: Int, element: E) {
|
||||||
|
val insertionPoint = getInsertionPoint(element)
|
||||||
|
require(insertionPoint >= 0) { "Element with same key already exists in list" }
|
||||||
|
if (insertionPoint != index) throw IndexOutOfBoundsException("Wrong index given for element")
|
||||||
|
super.add(index, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAll(elements: Collection<E>): Boolean {
|
||||||
|
var didChange = false
|
||||||
|
for (e in elements) {
|
||||||
|
if (add(e))
|
||||||
|
didChange = true
|
||||||
|
}
|
||||||
|
return didChange
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAll(index: Int, elements: Collection<E>): Boolean {
|
||||||
|
var i = index
|
||||||
|
for (e in elements)
|
||||||
|
add(i++, e)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInsertionPoint(e: E) = -Collections.binarySearch(keyList, e.key, comparator) - 1
|
||||||
|
|
||||||
|
override fun indexOfKey(key: K): Int {
|
||||||
|
val index = Collections.binarySearch(keyList, key, comparator)
|
||||||
|
return if (index >= 0) index else -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(index: Int, element: E): E {
|
||||||
|
val order = comparator.compare(element.key, get(index).key)
|
||||||
|
if (order != 0) {
|
||||||
|
// Allow replacement if the new key would be inserted adjacent to the replaced element.
|
||||||
|
val insertionPoint = getInsertionPoint(element)
|
||||||
|
if (insertionPoint < index || insertionPoint > index + 1)
|
||||||
|
throw IndexOutOfBoundsException("Wrong index given for element")
|
||||||
|
}
|
||||||
|
return super.set(index, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KeyList<K, E : Keyed<out K>>(private val list: ObservableSortedKeyedArrayList<K, E>) : AbstractList<K>(), Set<K> {
|
||||||
|
override fun get(index: Int): K = list[index].key
|
||||||
|
|
||||||
|
override val size
|
||||||
|
get() = list.size
|
||||||
|
|
||||||
|
override fun spliterator(): Spliterator<K> = super<AbstractList>.spliterator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.setFragmentResult
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.util.resolveAttribute
|
||||||
|
|
||||||
|
class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private var behavior: BottomSheetBehavior<FrameLayout>? = null
|
||||||
|
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
if (savedInstanceState != null) dismiss()
|
||||||
|
val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
|
||||||
|
if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) != true) {
|
||||||
|
val qrcode = view.findViewById<View>(R.id.create_from_qrcode)
|
||||||
|
qrcode.isEnabled = false
|
||||||
|
qrcode.visibility = View.GONE
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
override fun onGlobalLayout() {
|
||||||
|
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||||
|
val dialog = dialog as BottomSheetDialog? ?: return
|
||||||
|
behavior = dialog.behavior
|
||||||
|
behavior?.apply {
|
||||||
|
state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
peekHeight = 0
|
||||||
|
addBottomSheetCallback(bottomSheetCallback)
|
||||||
|
}
|
||||||
|
dialog.findViewById<View>(R.id.create_empty)?.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
onRequestCreateConfig()
|
||||||
|
}
|
||||||
|
dialog.findViewById<View>(R.id.create_from_file)?.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
onRequestImportConfig()
|
||||||
|
}
|
||||||
|
dialog.findViewById<View>(R.id.create_from_qrcode)?.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
onRequestScanQRCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
val gradientDrawable = GradientDrawable().apply {
|
||||||
|
setColor(requireContext().resolveAttribute(com.google.android.material.R.attr.colorSurface))
|
||||||
|
}
|
||||||
|
view.background = gradientDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
super.dismiss()
|
||||||
|
behavior?.removeBottomSheetCallback(bottomSheetCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRequestCreateConfig() {
|
||||||
|
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_CREATE))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRequestImportConfig() {
|
||||||
|
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_IMPORT))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRequestScanQRCode() {
|
||||||
|
setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_SCAN))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQUEST_KEY_NEW_TUNNEL = "request_new_tunnel"
|
||||||
|
const val REQUEST_METHOD = "request_method"
|
||||||
|
const val REQUEST_CREATE = "request_create"
|
||||||
|
const val REQUEST_IMPORT = "request_import"
|
||||||
|
const val REQUEST_SCAN = "request_scan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.PackageManager.PackageInfoFlags
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.databinding.Observable
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.setFragmentResult
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
||||||
|
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||||
|
import com.wireguard.android.model.ApplicationData
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class AppListDialogFragment : DialogFragment() {
|
||||||
|
private val appData = ObservableKeyedArrayList<String, ApplicationData>()
|
||||||
|
private var currentlySelectedApps = emptyList<String>()
|
||||||
|
private var initiallyExcluded = false
|
||||||
|
private var button: Button? = null
|
||||||
|
private var tabs: TabLayout? = null
|
||||||
|
|
||||||
|
private fun loadData() {
|
||||||
|
val activity = activity ?: return
|
||||||
|
val pm = activity.packageManager
|
||||||
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
try {
|
||||||
|
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val packageInfos = getPackagesHoldingPermissions(pm, arrayOf(Manifest.permission.INTERNET))
|
||||||
|
packageInfos.forEach {
|
||||||
|
val packageName = it.packageName
|
||||||
|
val appInfo = it.applicationInfo ?: return@forEach
|
||||||
|
val appData =
|
||||||
|
ApplicationData(appInfo.loadIcon(pm), appInfo.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
|
||||||
|
applicationData.add(appData)
|
||||||
|
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||||
|
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||||
|
if (propertyId == BR.selected)
|
||||||
|
setButtonText()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
|
withContext(Dispatchers.Main.immediate) {
|
||||||
|
appData.clear()
|
||||||
|
appData.addAll(applicationData)
|
||||||
|
setButtonText()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
withContext(Dispatchers.Main.immediate) {
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val message = activity.getString(R.string.error_fetching_apps, error)
|
||||||
|
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
|
||||||
|
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackagesHoldingPermissions(pm: PackageManager, permissions: Array<String>): List<PackageInfo> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
pm.getPackagesHoldingPermissions(permissions, PackageInfoFlags.of(0L))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
pm.getPackagesHoldingPermissions(permissions, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setButtonText() {
|
||||||
|
val numSelected = appData.count { it.isSelected }
|
||||||
|
button?.text = if (numSelected == 0)
|
||||||
|
getString(R.string.use_all_applications)
|
||||||
|
else when (tabs?.selectedTabPosition) {
|
||||||
|
0 -> resources.getQuantityString(R.plurals.exclude_n_applications, numSelected, numSelected)
|
||||||
|
1 -> resources.getQuantityString(R.plurals.include_n_applications, numSelected, numSelected)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val alertDialogBuilder = MaterialAlertDialogBuilder(requireActivity())
|
||||||
|
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
|
||||||
|
binding.executePendingBindings()
|
||||||
|
alertDialogBuilder.setView(binding.root)
|
||||||
|
tabs = binding.tabs
|
||||||
|
tabs?.apply {
|
||||||
|
selectTab(binding.tabs.getTabAt(if (initiallyExcluded) 0 else 1))
|
||||||
|
addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab?) = setButtonText()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
alertDialogBuilder.setPositiveButton(" ") { _, _ -> setSelectionAndDismiss() }
|
||||||
|
alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||||
|
alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
|
||||||
|
binding.fragment = this
|
||||||
|
binding.appData = appData
|
||||||
|
loadData()
|
||||||
|
val dialog = alertDialogBuilder.create()
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
setButtonText()
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { _ ->
|
||||||
|
val selectAll = appData.none { it.isSelected }
|
||||||
|
appData.forEach {
|
||||||
|
it.isSelected = selectAll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSelectionAndDismiss() {
|
||||||
|
val selectedApps: MutableList<String> = ArrayList()
|
||||||
|
for (data in appData) {
|
||||||
|
if (data.isSelected) {
|
||||||
|
selectedApps.add(data.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFragmentResult(
|
||||||
|
REQUEST_SELECTION, bundleOf(
|
||||||
|
KEY_SELECTED_APPS to selectedApps.toTypedArray(),
|
||||||
|
KEY_IS_EXCLUDED to (tabs?.selectedTabPosition == 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_SELECTED_APPS = "selected_apps"
|
||||||
|
const val KEY_IS_EXCLUDED = "is_excluded"
|
||||||
|
const val REQUEST_SELECTION = "request_selection"
|
||||||
|
|
||||||
|
fun newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean): AppListDialogFragment {
|
||||||
|
val extras = Bundle()
|
||||||
|
extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps)
|
||||||
|
extras.putBoolean(KEY_IS_EXCLUDED, isExcluded)
|
||||||
|
val fragment = AppListDialogFragment()
|
||||||
|
fragment.arguments = extras
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
Normal file
114
ui/src/main/java/com/wireguard/android/fragment/BaseFragment.kt
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.activity.BaseActivity
|
||||||
|
import com.wireguard.android.activity.BaseActivity.OnSelectedTunnelChangedListener
|
||||||
|
import com.wireguard.android.backend.GoBackend
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||||
|
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for fragments that need to know the currently-selected tunnel. Only does anything when
|
||||||
|
* attached to a `BaseActivity`.
|
||||||
|
*/
|
||||||
|
abstract class BaseFragment : Fragment(), OnSelectedTunnelChangedListener {
|
||||||
|
private var pendingTunnel: ObservableTunnel? = null
|
||||||
|
private var pendingTunnelUp: Boolean? = null
|
||||||
|
private val permissionActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
val tunnel = pendingTunnel
|
||||||
|
val checked = pendingTunnelUp
|
||||||
|
if (tunnel != null && checked != null)
|
||||||
|
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||||
|
pendingTunnel = null
|
||||||
|
pendingTunnelUp = null
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var selectedTunnel: ObservableTunnel?
|
||||||
|
get() = (activity as? BaseActivity)?.selectedTunnel
|
||||||
|
protected set(tunnel) {
|
||||||
|
(activity as? BaseActivity)?.selectedTunnel = tunnel
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
(activity as? BaseActivity)?.addOnSelectedTunnelChangedListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
(activity as? BaseActivity)?.removeOnSelectedTunnelChangedListener(this)
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTunnelState(view: View, checked: Boolean) {
|
||||||
|
val tunnel = when (val binding = DataBindingUtil.findBinding<ViewDataBinding>(view)) {
|
||||||
|
is TunnelDetailFragmentBinding -> binding.tunnel
|
||||||
|
is TunnelListItemBinding -> binding.item
|
||||||
|
else -> return
|
||||||
|
} ?: return
|
||||||
|
val activity = activity ?: return
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
if (Application.getBackend() is GoBackend) {
|
||||||
|
try {
|
||||||
|
val intent = GoBackend.VpnService.prepare(activity)
|
||||||
|
if (intent != null) {
|
||||||
|
pendingTunnel = tunnel
|
||||||
|
pendingTunnelUp = checked
|
||||||
|
permissionActivityResultLauncher.launch(intent)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val message = activity.getString(R.string.error_prepare, ErrorMessages[e])
|
||||||
|
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||||
|
.setAnchorView(view.findViewById(R.id.create_fab))
|
||||||
|
.show()
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTunnelStateWithPermissionsResult(tunnel, checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTunnelStateWithPermissionsResult(tunnel: ObservableTunnel, checked: Boolean) {
|
||||||
|
val activity = activity ?: return
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
tunnel.setStateAsync(Tunnel.State.of(checked))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val messageResId = if (checked) R.string.error_up else R.string.error_down
|
||||||
|
val message = activity.getString(messageResId, error)
|
||||||
|
val view = view
|
||||||
|
if (view != null)
|
||||||
|
Snackbar.make(view, message, Snackbar.LENGTH_LONG)
|
||||||
|
.setAnchorView(view.findViewById(R.id.create_fab))
|
||||||
|
.show()
|
||||||
|
else
|
||||||
|
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/BaseFragment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
|
||||||
|
import com.wireguard.config.BadConfigException
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
class ConfigNamingDialogFragment : DialogFragment() {
|
||||||
|
private var binding: ConfigNamingDialogFragmentBinding? = null
|
||||||
|
private var config: Config? = null
|
||||||
|
|
||||||
|
private fun createTunnelAndDismiss() {
|
||||||
|
val binding = binding ?: return
|
||||||
|
val activity = activity ?: return
|
||||||
|
val name = binding.tunnelNameText.text.toString()
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
Application.getTunnelManager().create(name, config)
|
||||||
|
dismiss()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
binding.tunnelNameTextLayout.error = e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val configText = requireArguments().getString(KEY_CONFIG_TEXT)
|
||||||
|
val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
config = try {
|
||||||
|
Config.parse(ByteArrayInputStream(configBytes))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
when (e) {
|
||||||
|
is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
|
||||||
|
else -> throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val activity = requireActivity()
|
||||||
|
val alertDialogBuilder = MaterialAlertDialogBuilder(activity)
|
||||||
|
alertDialogBuilder.setTitle(R.string.import_from_qr_code)
|
||||||
|
binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
|
||||||
|
binding?.apply {
|
||||||
|
executePendingBindings()
|
||||||
|
alertDialogBuilder.setView(root)
|
||||||
|
}
|
||||||
|
alertDialogBuilder.setPositiveButton(R.string.create_tunnel) { _, _ -> createTunnelAndDismiss() }
|
||||||
|
alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
|
||||||
|
val dialog = alertDialogBuilder.create()
|
||||||
|
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_CONFIG_TEXT = "config_text"
|
||||||
|
|
||||||
|
fun newInstance(configText: String?): ConfigNamingDialogFragment {
|
||||||
|
val extras = Bundle()
|
||||||
|
extras.putString(KEY_CONFIG_TEXT, configText)
|
||||||
|
val fragment = ConfigNamingDialogFragment()
|
||||||
|
fragment.arguments = extras
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.databinding.TunnelDetailFragmentBinding
|
||||||
|
import com.wireguard.android.databinding.TunnelDetailPeerBinding
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import com.wireguard.android.util.QuantityFormatter
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment that shows details about a specific tunnel.
|
||||||
|
*/
|
||||||
|
class TunnelDetailFragment : BaseFragment(), MenuProvider {
|
||||||
|
private var binding: TunnelDetailFragmentBinding? = null
|
||||||
|
private var lastState = Tunnel.State.TOGGLE
|
||||||
|
private var timerActive = true
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.tunnel_detail, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
binding = TunnelDetailFragmentBinding.inflate(inflater, container, false)
|
||||||
|
binding?.executePendingBindings()
|
||||||
|
return binding?.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
timerActive = true
|
||||||
|
lifecycleScope.launch {
|
||||||
|
while (timerActive) {
|
||||||
|
updateStats()
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||||
|
val binding = binding ?: return
|
||||||
|
binding.tunnel = newTunnel
|
||||||
|
if (newTunnel == null) {
|
||||||
|
binding.config = null
|
||||||
|
} else {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
binding.config = newTunnel.getConfigAsync()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
binding.config = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastState = Tunnel.State.TOGGLE
|
||||||
|
lifecycleScope.launch { updateStats() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
timerActive = false
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||||
|
binding ?: return
|
||||||
|
binding!!.fragment = this
|
||||||
|
onSelectedTunnelChanged(null, selectedTunnel)
|
||||||
|
super.onViewStateRestored(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateStats() {
|
||||||
|
val binding = binding ?: return
|
||||||
|
val tunnel = binding.tunnel ?: return
|
||||||
|
if (!isResumed) return
|
||||||
|
val state = tunnel.state
|
||||||
|
if (state != Tunnel.State.UP && lastState == state) return
|
||||||
|
lastState = state
|
||||||
|
try {
|
||||||
|
val statistics = tunnel.getStatisticsAsync()
|
||||||
|
for (i in 0 until binding.peersLayout.childCount) {
|
||||||
|
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||||
|
?: continue
|
||||||
|
val publicKey = peer.item!!.publicKey
|
||||||
|
val peerStats = statistics.peer(publicKey)
|
||||||
|
if (peerStats == null || (peerStats.rxBytes == 0L && peerStats.txBytes == 0L)) {
|
||||||
|
peer.transferLabel.visibility = View.GONE
|
||||||
|
peer.transferText.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
peer.transferText.text = getString(
|
||||||
|
R.string.transfer_rx_tx,
|
||||||
|
QuantityFormatter.formatBytes(peerStats.rxBytes),
|
||||||
|
QuantityFormatter.formatBytes(peerStats.txBytes)
|
||||||
|
)
|
||||||
|
peer.transferLabel.visibility = View.VISIBLE
|
||||||
|
peer.transferText.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
if (peerStats == null || peerStats.latestHandshakeEpochMillis == 0L) {
|
||||||
|
peer.latestHandshakeLabel.visibility = View.GONE
|
||||||
|
peer.latestHandshakeText.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
peer.latestHandshakeText.text = QuantityFormatter.formatEpochAgo(peerStats.latestHandshakeEpochMillis)
|
||||||
|
peer.latestHandshakeLabel.visibility = View.VISIBLE
|
||||||
|
peer.latestHandshakeText.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
for (i in 0 until binding.peersLayout.childCount) {
|
||||||
|
val peer: TunnelDetailPeerBinding = DataBindingUtil.getBinding(binding.peersLayout.getChildAt(i))
|
||||||
|
?: continue
|
||||||
|
peer.transferLabel.visibility = View.GONE
|
||||||
|
peer.transferText.visibility = View.GONE
|
||||||
|
peer.latestHandshakeLabel.visibility = View.GONE
|
||||||
|
peer.latestHandshakeText.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,333 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import com.wireguard.android.util.AdminKnobs
|
||||||
|
import com.wireguard.android.util.BiometricAuthenticator
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import com.wireguard.android.viewmodel.ConfigProxy
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment for editing a WireGuard configuration.
|
||||||
|
*/
|
||||||
|
class TunnelEditorFragment : BaseFragment(), MenuProvider {
|
||||||
|
private var haveShownKeys = false
|
||||||
|
private var binding: TunnelEditorFragmentBinding? = null
|
||||||
|
private var tunnel: ObservableTunnel? = null
|
||||||
|
|
||||||
|
private fun onConfigLoaded(config: Config) {
|
||||||
|
binding?.config = ConfigProxy(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
|
||||||
|
val ctx = activity ?: Application.get()
|
||||||
|
if (throwable == null) {
|
||||||
|
val message = ctx.getString(R.string.config_save_success, savedTunnel.name)
|
||||||
|
Log.d(TAG, message)
|
||||||
|
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||||
|
onFinished()
|
||||||
|
} else {
|
||||||
|
val error = ErrorMessages[throwable]
|
||||||
|
val message = ctx.getString(R.string.config_save_error, savedTunnel.name, error)
|
||||||
|
Log.e(TAG, message, throwable)
|
||||||
|
val binding = binding
|
||||||
|
if (binding != null)
|
||||||
|
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||||
|
else
|
||||||
|
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.config_editor, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
|
||||||
|
binding?.apply {
|
||||||
|
executePendingBindings()
|
||||||
|
privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
|
||||||
|
}
|
||||||
|
return binding?.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFinished() {
|
||||||
|
// Hide the keyboard; it rarely goes away on its own.
|
||||||
|
val activity = activity ?: return
|
||||||
|
val focusedView = activity.currentFocus
|
||||||
|
if (focusedView != null) {
|
||||||
|
val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
inputManager?.hideSoftInputFromWindow(
|
||||||
|
focusedView.windowToken,
|
||||||
|
InputMethodManager.HIDE_NOT_ALWAYS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
parentFragmentManager.popBackStackImmediate()
|
||||||
|
|
||||||
|
// If we just made a new one, save it to select the details page.
|
||||||
|
if (selectedTunnel != tunnel)
|
||||||
|
selectedTunnel = tunnel
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
if (menuItem.itemId == R.id.menu_action_save) {
|
||||||
|
binding ?: return false
|
||||||
|
val newConfig = try {
|
||||||
|
binding!!.config!!.resolve()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
|
||||||
|
val message = getString(R.string.config_save_error, tunnelName, error)
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val activity = requireActivity()
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
when {
|
||||||
|
tunnel == null -> {
|
||||||
|
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
|
||||||
|
val manager = Application.getTunnelManager()
|
||||||
|
try {
|
||||||
|
onTunnelCreated(manager.create(binding!!.name!!, newConfig), null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onTunnelCreated(null, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel!!.name != binding!!.name -> {
|
||||||
|
Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
|
||||||
|
try {
|
||||||
|
tunnel!!.setNameAsync(binding!!.name!!)
|
||||||
|
onTunnelRenamed(tunnel!!, newConfig, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onTunnelRenamed(tunnel!!, newConfig, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
|
||||||
|
try {
|
||||||
|
tunnel!!.setConfigAsync(newConfig)
|
||||||
|
onConfigSaved(tunnel!!, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onConfigSaved(tunnel!!, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun onRequestSetExcludedIncludedApplications(view: View?) {
|
||||||
|
if (binding != null) {
|
||||||
|
var isExcluded = true
|
||||||
|
var selectedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
|
||||||
|
if (selectedApps.isEmpty()) {
|
||||||
|
selectedApps = ArrayList(binding!!.config!!.`interface`.includedApplications)
|
||||||
|
if (selectedApps.isNotEmpty())
|
||||||
|
isExcluded = false
|
||||||
|
}
|
||||||
|
val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded)
|
||||||
|
childFragmentManager.setFragmentResultListener(AppListDialogFragment.REQUEST_SELECTION, viewLifecycleOwner) { _, bundle ->
|
||||||
|
requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
|
||||||
|
val newSelections = requireNotNull(bundle.getStringArray(AppListDialogFragment.KEY_SELECTED_APPS))
|
||||||
|
val excluded = requireNotNull(bundle.getBoolean(AppListDialogFragment.KEY_IS_EXCLUDED))
|
||||||
|
if (excluded) {
|
||||||
|
binding!!.config!!.`interface`.includedApplications.clear()
|
||||||
|
binding!!.config!!.`interface`.excludedApplications.apply {
|
||||||
|
clear()
|
||||||
|
addAll(newSelections)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding!!.config!!.`interface`.excludedApplications.clear()
|
||||||
|
binding!!.config!!.`interface`.includedApplications.apply {
|
||||||
|
clear()
|
||||||
|
addAll(newSelections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment.show(childFragmentManager, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
if (binding != null) outState.putParcelable(KEY_LOCAL_CONFIG, binding!!.config)
|
||||||
|
outState.putString(KEY_ORIGINAL_NAME, if (tunnel == null) null else tunnel!!.name)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectedTunnelChanged(
|
||||||
|
oldTunnel: ObservableTunnel?,
|
||||||
|
newTunnel: ObservableTunnel?
|
||||||
|
) {
|
||||||
|
tunnel = newTunnel
|
||||||
|
if (binding == null) return
|
||||||
|
binding!!.config = ConfigProxy()
|
||||||
|
if (tunnel != null) {
|
||||||
|
binding!!.name = tunnel!!.name
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
onConfigLoaded(tunnel!!.getConfigAsync())
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding!!.name = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTunnelCreated(newTunnel: ObservableTunnel?, throwable: Throwable?) {
|
||||||
|
val ctx = activity ?: Application.get()
|
||||||
|
if (throwable == null) {
|
||||||
|
tunnel = newTunnel
|
||||||
|
val message = ctx.getString(R.string.tunnel_create_success, tunnel!!.name)
|
||||||
|
Log.d(TAG, message)
|
||||||
|
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||||
|
onFinished()
|
||||||
|
} else {
|
||||||
|
val error = ErrorMessages[throwable]
|
||||||
|
val message = ctx.getString(R.string.tunnel_create_error, error)
|
||||||
|
Log.e(TAG, message, throwable)
|
||||||
|
val binding = binding
|
||||||
|
if (binding != null)
|
||||||
|
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||||
|
else
|
||||||
|
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onTunnelRenamed(
|
||||||
|
renamedTunnel: ObservableTunnel, newConfig: Config,
|
||||||
|
throwable: Throwable?
|
||||||
|
) {
|
||||||
|
val ctx = activity ?: Application.get()
|
||||||
|
if (throwable == null) {
|
||||||
|
val message = ctx.getString(R.string.tunnel_rename_success, renamedTunnel.name)
|
||||||
|
Log.d(TAG, message)
|
||||||
|
// Now save the rest of configuration changes.
|
||||||
|
Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
|
||||||
|
try {
|
||||||
|
renamedTunnel.setConfigAsync(newConfig)
|
||||||
|
onConfigSaved(renamedTunnel, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onConfigSaved(renamedTunnel, e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val error = ErrorMessages[throwable]
|
||||||
|
val message = ctx.getString(R.string.tunnel_rename_error, error)
|
||||||
|
Log.e(TAG, message, throwable)
|
||||||
|
val binding = binding
|
||||||
|
if (binding != null)
|
||||||
|
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show()
|
||||||
|
else
|
||||||
|
Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||||
|
binding ?: return
|
||||||
|
binding!!.fragment = this
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
onSelectedTunnelChanged(null, selectedTunnel)
|
||||||
|
} else {
|
||||||
|
tunnel = selectedTunnel
|
||||||
|
val config = BundleCompat.getParcelable(savedInstanceState, KEY_LOCAL_CONFIG, ConfigProxy::class.java)!!
|
||||||
|
val originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME)
|
||||||
|
if (tunnel != null && tunnel!!.name != originalName) onSelectedTunnelChanged(null, tunnel) else binding!!.config = config
|
||||||
|
}
|
||||||
|
super.onViewStateRestored(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var showingAuthenticator = false
|
||||||
|
|
||||||
|
fun onKeyClick(view: View) = onKeyFocusChange(view, true)
|
||||||
|
|
||||||
|
fun onKeyFocusChange(view: View, isFocused: Boolean) {
|
||||||
|
if (!isFocused || showingAuthenticator) return
|
||||||
|
val edit = view as? EditText ?: return
|
||||||
|
if (edit.inputType == InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) return
|
||||||
|
if (!haveShownKeys && edit.text.isNotEmpty()) {
|
||||||
|
if (AdminKnobs.disableConfigExport) return
|
||||||
|
showingAuthenticator = true
|
||||||
|
BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, this) {
|
||||||
|
showingAuthenticator = false
|
||||||
|
when (it) {
|
||||||
|
is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
|
||||||
|
haveShownKeys = true
|
||||||
|
showPrivateKey(edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
is BiometricAuthenticator.Result.Failure -> {
|
||||||
|
Snackbar.make(
|
||||||
|
binding!!.mainContainer,
|
||||||
|
it.message,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
is BiometricAuthenticator.Result.Cancelled -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showPrivateKey(edit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPrivateKey(edit: EditText) {
|
||||||
|
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
edit.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_LOCAL_CONFIG = "local_config"
|
||||||
|
private const val KEY_ORIGINAL_NAME = "original_name"
|
||||||
|
private const val TAG = "WireGuard/TunnelEditorFragment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,342 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.addCallback
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.zxing.qrcode.QRCodeReader
|
||||||
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.activity.TunnelCreatorActivity
|
||||||
|
import com.wireguard.android.databinding.ObservableKeyedRecyclerViewAdapter.RowConfigurationHandler
|
||||||
|
import com.wireguard.android.databinding.TunnelListFragmentBinding
|
||||||
|
import com.wireguard.android.databinding.TunnelListItemBinding
|
||||||
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
|
import com.wireguard.android.updater.SnackbarUpdateShower
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import com.wireguard.android.util.QrCodeFromFileScanner
|
||||||
|
import com.wireguard.android.util.TunnelImporter
|
||||||
|
import com.wireguard.android.widget.MultiselectableRelativeLayout
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment containing a list of known WireGuard tunnels. It allows creating and deleting tunnels.
|
||||||
|
*/
|
||||||
|
class TunnelListFragment : BaseFragment() {
|
||||||
|
private val actionModeListener = ActionModeListener()
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
private var backPressedCallback: OnBackPressedCallback? = null
|
||||||
|
private var binding: TunnelListFragmentBinding? = null
|
||||||
|
private val tunnelFileImportResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { data ->
|
||||||
|
if (data == null) return@registerForActivityResult
|
||||||
|
val activity = activity ?: return@registerForActivityResult
|
||||||
|
val contentResolver = activity.contentResolver ?: return@registerForActivityResult
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
if (QrCodeFromFileScanner.validContentType(contentResolver, data)) {
|
||||||
|
try {
|
||||||
|
val qrCodeFromFileScanner = QrCodeFromFileScanner(contentResolver, QRCodeReader())
|
||||||
|
val result = qrCodeFromFileScanner.scan(data)
|
||||||
|
TunnelImporter.importTunnel(parentFragmentManager, result.text) { showSnackbar(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val error = ErrorMessages[e]
|
||||||
|
val message = Application.get().resources.getString(R.string.import_error, error)
|
||||||
|
Log.e(TAG, message, e)
|
||||||
|
showSnackbar(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TunnelImporter.importTunnel(contentResolver, data) { showSnackbar(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val qrImportResultLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||||
|
val qrCode = result.contents
|
||||||
|
val activity = activity
|
||||||
|
if (qrCode != null && activity != null) {
|
||||||
|
activity.lifecycleScope.launch { TunnelImporter.importTunnel(parentFragmentManager, qrCode) { showSnackbar(it) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val snackbarUpdateShower = SnackbarUpdateShower(this)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
val checkedItems = savedInstanceState.getIntegerArrayList(CHECKED_ITEMS)
|
||||||
|
if (checkedItems != null) {
|
||||||
|
for (i in checkedItems) actionModeListener.setItemChecked(i, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
binding = TunnelListFragmentBinding.inflate(inflater, container, false)
|
||||||
|
val bottomSheet = AddTunnelsSheet()
|
||||||
|
binding?.apply {
|
||||||
|
createFab.setOnClickListener {
|
||||||
|
if (childFragmentManager.findFragmentByTag("BOTTOM_SHEET") != null)
|
||||||
|
return@setOnClickListener
|
||||||
|
childFragmentManager.setFragmentResultListener(AddTunnelsSheet.REQUEST_KEY_NEW_TUNNEL, viewLifecycleOwner) { _, bundle ->
|
||||||
|
when (bundle.getString(AddTunnelsSheet.REQUEST_METHOD)) {
|
||||||
|
AddTunnelsSheet.REQUEST_CREATE -> {
|
||||||
|
startActivity(Intent(requireActivity(), TunnelCreatorActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTunnelsSheet.REQUEST_IMPORT -> {
|
||||||
|
tunnelFileImportResultLauncher.launch("*/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
AddTunnelsSheet.REQUEST_SCAN -> {
|
||||||
|
qrImportResultLauncher.launch(
|
||||||
|
ScanOptions()
|
||||||
|
.setOrientationLocked(false)
|
||||||
|
.setBeepEnabled(false)
|
||||||
|
.setPrompt(getString(R.string.qr_code_hint))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bottomSheet.showNow(childFragmentManager, "BOTTOM_SHEET")
|
||||||
|
}
|
||||||
|
executePendingBindings()
|
||||||
|
snackbarUpdateShower.attach(mainContainer, createFab)
|
||||||
|
}
|
||||||
|
backPressedCallback = requireActivity().onBackPressedDispatcher.addCallback(this) { actionMode?.finish() }
|
||||||
|
backPressedCallback?.isEnabled = false
|
||||||
|
|
||||||
|
return binding?.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putIntegerArrayList(CHECKED_ITEMS, actionModeListener.getCheckedItems())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?) {
|
||||||
|
binding ?: return
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val tunnels = Application.getTunnelManager().getTunnels()
|
||||||
|
if (newTunnel != null) viewForTunnel(newTunnel, tunnels)?.setSingleSelected(true)
|
||||||
|
if (oldTunnel != null) viewForTunnel(oldTunnel, tunnels)?.setSingleSelected(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTunnelDeletionFinished(count: Int, throwable: Throwable?) {
|
||||||
|
val message: String
|
||||||
|
val ctx = activity ?: Application.get()
|
||||||
|
if (throwable == null) {
|
||||||
|
message = ctx.resources.getQuantityString(R.plurals.delete_success, count, count)
|
||||||
|
} else {
|
||||||
|
val error = ErrorMessages[throwable]
|
||||||
|
message = ctx.resources.getQuantityString(R.plurals.delete_error, count, count, error)
|
||||||
|
Log.e(TAG, message, throwable)
|
||||||
|
}
|
||||||
|
showSnackbar(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||||
|
super.onViewStateRestored(savedInstanceState)
|
||||||
|
binding ?: return
|
||||||
|
binding!!.fragment = this
|
||||||
|
lifecycleScope.launch { binding!!.tunnels = Application.getTunnelManager().getTunnels() }
|
||||||
|
binding!!.rowConfigurationHandler = object : RowConfigurationHandler<TunnelListItemBinding, ObservableTunnel> {
|
||||||
|
override fun onConfigureRow(binding: TunnelListItemBinding, item: ObservableTunnel, position: Int) {
|
||||||
|
binding.fragment = this@TunnelListFragment
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
if (actionMode == null) {
|
||||||
|
selectedTunnel = item
|
||||||
|
} else {
|
||||||
|
actionModeListener.toggleItemChecked(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.root.setOnLongClickListener {
|
||||||
|
actionModeListener.toggleItemChecked(position)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
if (actionMode != null)
|
||||||
|
(binding.root as MultiselectableRelativeLayout).setMultiSelected(actionModeListener.checkedItems.contains(position))
|
||||||
|
else
|
||||||
|
(binding.root as MultiselectableRelativeLayout).setSingleSelected(selectedTunnel == item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnackbar(message: CharSequence) {
|
||||||
|
val binding = binding
|
||||||
|
if (binding != null)
|
||||||
|
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG)
|
||||||
|
.setAnchorView(binding.createFab)
|
||||||
|
.show()
|
||||||
|
else
|
||||||
|
Toast.makeText(activity ?: Application.get(), message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun viewForTunnel(tunnel: ObservableTunnel, tunnels: List<*>): MultiselectableRelativeLayout? {
|
||||||
|
return binding?.tunnelList?.findViewHolderForAdapterPosition(tunnels.indexOf(tunnel))?.itemView as? MultiselectableRelativeLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ActionModeListener : ActionMode.Callback {
|
||||||
|
val checkedItems: MutableCollection<Int> = HashSet()
|
||||||
|
private var resources: Resources? = null
|
||||||
|
|
||||||
|
fun getCheckedItems(): ArrayList<Int> {
|
||||||
|
return ArrayList(checkedItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
R.id.menu_action_delete -> {
|
||||||
|
val activity = activity ?: return true
|
||||||
|
val copyCheckedItems = HashSet(checkedItems)
|
||||||
|
binding?.createFab?.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
scaleX = 1f
|
||||||
|
scaleY = 1f
|
||||||
|
}
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val tunnels = Application.getTunnelManager().getTunnels()
|
||||||
|
val tunnelsToDelete = ArrayList<ObservableTunnel>()
|
||||||
|
for (position in copyCheckedItems) tunnelsToDelete.add(tunnels[position])
|
||||||
|
val futures = tunnelsToDelete.map { async(SupervisorJob()) { it.deleteAsync() } }
|
||||||
|
onTunnelDeletionFinished(futures.awaitAll().size, null)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
onTunnelDeletionFinished(0, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkedItems.clear()
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_action_select_all -> {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val tunnels = Application.getTunnelManager().getTunnels()
|
||||||
|
for (i in 0 until tunnels.size) {
|
||||||
|
setItemChecked(i, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
actionMode = mode
|
||||||
|
backPressedCallback?.isEnabled = true
|
||||||
|
if (activity != null) {
|
||||||
|
resources = activity!!.resources
|
||||||
|
}
|
||||||
|
animateFab(binding?.createFab, false)
|
||||||
|
mode.menuInflater.inflate(R.menu.tunnel_list_action_mode, menu)
|
||||||
|
binding?.tunnelList?.adapter?.notifyDataSetChanged()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
actionMode = null
|
||||||
|
backPressedCallback?.isEnabled = false
|
||||||
|
resources = null
|
||||||
|
animateFab(binding?.createFab, true)
|
||||||
|
checkedItems.clear()
|
||||||
|
binding?.tunnelList?.adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
updateTitle(mode)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItemChecked(position: Int, checked: Boolean) {
|
||||||
|
if (checked) {
|
||||||
|
checkedItems.add(position)
|
||||||
|
} else {
|
||||||
|
checkedItems.remove(position)
|
||||||
|
}
|
||||||
|
val adapter = if (binding == null) null else binding!!.tunnelList.adapter
|
||||||
|
if (actionMode == null && !checkedItems.isEmpty() && activity != null) {
|
||||||
|
(activity as AppCompatActivity).startSupportActionMode(this)
|
||||||
|
} else if (actionMode != null && checkedItems.isEmpty()) {
|
||||||
|
actionMode!!.finish()
|
||||||
|
}
|
||||||
|
adapter?.notifyItemChanged(position)
|
||||||
|
updateTitle(actionMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleItemChecked(position: Int) {
|
||||||
|
setItemChecked(position, !checkedItems.contains(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTitle(mode: ActionMode?) {
|
||||||
|
if (mode == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val count = checkedItems.size
|
||||||
|
if (count == 0) {
|
||||||
|
mode.title = ""
|
||||||
|
} else {
|
||||||
|
mode.title = resources!!.getQuantityString(R.plurals.delete_title, count, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateFab(view: View?, show: Boolean) {
|
||||||
|
view ?: return
|
||||||
|
val animation = AnimationUtils.loadAnimation(
|
||||||
|
context, if (show) R.anim.scale_up else R.anim.scale_down
|
||||||
|
)
|
||||||
|
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||||
|
override fun onAnimationRepeat(animation: Animation?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animation?) {
|
||||||
|
if (!show) view.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animation?) {
|
||||||
|
if (show) view.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
view.startAnimation(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHECKED_ITEMS = "CHECKED_ITEMS"
|
||||||
|
private const val TAG = "WireGuard/TunnelListFragment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.model
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.databinding.BaseObservable
|
||||||
|
import androidx.databinding.Bindable
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import com.wireguard.android.databinding.Keyed
|
||||||
|
|
||||||
|
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed<String> {
|
||||||
|
override val key = name
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
var isSelected = isSelected
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyPropertyChanged(BR.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
146
ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
Normal file
146
ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.databinding.BaseObservable
|
||||||
|
import androidx.databinding.Bindable
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.databinding.Keyed
|
||||||
|
import com.wireguard.android.util.applicationScope
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates the volatile and nonvolatile state of a WireGuard tunnel.
|
||||||
|
*/
|
||||||
|
class ObservableTunnel internal constructor(
|
||||||
|
private val manager: TunnelManager,
|
||||||
|
private var name: String,
|
||||||
|
config: Config?,
|
||||||
|
state: Tunnel.State
|
||||||
|
) : BaseObservable(), Keyed<String>, Tunnel {
|
||||||
|
override val key
|
||||||
|
get() = name
|
||||||
|
|
||||||
|
@Bindable
|
||||||
|
override fun getName() = name
|
||||||
|
|
||||||
|
suspend fun setNameAsync(name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||||
|
if (name != this@ObservableTunnel.name)
|
||||||
|
manager.setTunnelName(this@ObservableTunnel, name)
|
||||||
|
else
|
||||||
|
this@ObservableTunnel.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNameChanged(name: String): String {
|
||||||
|
this.name = name
|
||||||
|
notifyPropertyChanged(BR.name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
var state = state
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onStateChange(newState: Tunnel.State) {
|
||||||
|
onStateChanged(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStateChanged(state: Tunnel.State): Tunnel.State {
|
||||||
|
if (state != Tunnel.State.UP) onStatisticsChanged(null)
|
||||||
|
this.state = state
|
||||||
|
notifyPropertyChanged(BR.state)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||||
|
if (state != this@ObservableTunnel.state)
|
||||||
|
manager.setTunnelState(this@ObservableTunnel, state)
|
||||||
|
else
|
||||||
|
this@ObservableTunnel.state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
var config = config
|
||||||
|
get() {
|
||||||
|
if (field == null)
|
||||||
|
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||||
|
applicationScope.launch {
|
||||||
|
try {
|
||||||
|
manager.getTunnelConfig(this@ObservableTunnel)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun getConfigAsync(): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
|
config ?: manager.getTunnelConfig(this@ObservableTunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setConfigAsync(config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
|
this@ObservableTunnel.config.let {
|
||||||
|
if (config != it)
|
||||||
|
manager.setTunnelConfig(this@ObservableTunnel, config)
|
||||||
|
else
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onConfigChanged(config: Config?): Config? {
|
||||||
|
this.config = config
|
||||||
|
notifyPropertyChanged(BR.config)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
var statistics: Statistics? = null
|
||||||
|
get() {
|
||||||
|
if (field == null || field?.isStale != false)
|
||||||
|
// Opportunistically fetch this if we don't have a cached one, and rely on data bindings to update it eventually
|
||||||
|
applicationScope.launch {
|
||||||
|
try {
|
||||||
|
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun getStatisticsAsync(): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||||
|
statistics.let {
|
||||||
|
if (it == null || it.isStale)
|
||||||
|
manager.getTunnelStatistics(this@ObservableTunnel)
|
||||||
|
else
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStatisticsChanged(statistics: Statistics?): Statistics? {
|
||||||
|
this.statistics = statistics
|
||||||
|
notifyPropertyChanged(BR.statistics)
|
||||||
|
return statistics
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun deleteAsync() = manager.delete(this)
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/ObservableTunnel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.android.model
|
||||||
|
|
||||||
|
object TunnelComparator : Comparator<String> {
|
||||||
|
private class NaturalSortString(originalString: String) {
|
||||||
|
class NaturalSortToken(val maybeString: String?, val maybeNumber: Int?) : Comparable<NaturalSortToken> {
|
||||||
|
override fun compareTo(other: NaturalSortToken): Int {
|
||||||
|
if (maybeString == null) {
|
||||||
|
if (other.maybeString != null || maybeNumber!! < other.maybeNumber!!) {
|
||||||
|
return -1
|
||||||
|
} else if (maybeNumber > other.maybeNumber) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
} else if (other.maybeString == null || maybeString > other.maybeString) {
|
||||||
|
return 1
|
||||||
|
} else if (maybeString < other.maybeString) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val tokens: MutableList<NaturalSortToken> = ArrayList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
for (s in NATURAL_SORT_DIGIT_FINDER.findAll(originalString.split(WHITESPACE_FINDER).joinToString(" ").lowercase())) {
|
||||||
|
try {
|
||||||
|
val n = s.value.toInt()
|
||||||
|
tokens.add(NaturalSortToken(null, n))
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
tokens.add(NaturalSortToken(s.value, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val NATURAL_SORT_DIGIT_FINDER = Regex("""\d+|\D+""")
|
||||||
|
private val WHITESPACE_FINDER = Regex("""\s""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compare(a: String, b: String): Int {
|
||||||
|
if (a == b)
|
||||||
|
return 0
|
||||||
|
val na = NaturalSortString(a)
|
||||||
|
val nb = NaturalSortString(b)
|
||||||
|
for (i in 0 until nb.tokens.size) {
|
||||||
|
if (i == na.tokens.size) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
val c = na.tokens[i].compareTo(nb.tokens[i])
|
||||||
|
if (c != 0)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
254
ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
Normal file
254
ui/src/main/java/com/wireguard/android/model/TunnelManager.kt
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package com.wireguard.android.model
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.databinding.BaseObservable
|
||||||
|
import androidx.databinding.Bindable
|
||||||
|
import com.wireguard.android.Application.Companion.get
|
||||||
|
import com.wireguard.android.Application.Companion.getBackend
|
||||||
|
import com.wireguard.android.Application.Companion.getTunnelManager
|
||||||
|
import com.wireguard.android.BR
|
||||||
|
import com.wireguard.android.R
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.android.configStore.ConfigStore
|
||||||
|
import com.wireguard.android.databinding.ObservableSortedKeyedArrayList
|
||||||
|
import com.wireguard.android.util.ErrorMessages
|
||||||
|
import com.wireguard.android.util.UserKnobs
|
||||||
|
import com.wireguard.android.util.applicationScope
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains and mediates changes to the set of available WireGuard tunnels,
|
||||||
|
*/
|
||||||
|
class TunnelManager(private val configStore: ConfigStore) : BaseObservable() {
|
||||||
|
private val tunnels = CompletableDeferred<ObservableSortedKeyedArrayList<String, ObservableTunnel>>()
|
||||||
|
private val context: Context = get()
|
||||||
|
private val tunnelMap: ObservableSortedKeyedArrayList<String, ObservableTunnel> = ObservableSortedKeyedArrayList(TunnelComparator)
|
||||||
|
private var haveLoaded = false
|
||||||
|
|
||||||
|
private fun addToList(name: String, config: Config?, state: Tunnel.State): ObservableTunnel {
|
||||||
|
val tunnel = ObservableTunnel(this, name, config, state)
|
||||||
|
tunnelMap.add(tunnel)
|
||||||
|
return tunnel
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTunnels(): ObservableSortedKeyedArrayList<String, ObservableTunnel> = tunnels.await()
|
||||||
|
|
||||||
|
suspend fun create(name: String, config: Config?): ObservableTunnel = withContext(Dispatchers.Main.immediate) {
|
||||||
|
if (Tunnel.isNameInvalid(name))
|
||||||
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||||
|
if (tunnelMap.containsKey(name))
|
||||||
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||||
|
addToList(name, withContext(Dispatchers.IO) { configStore.create(name, config!!) }, Tunnel.State.DOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(tunnel: ObservableTunnel) = withContext(Dispatchers.Main.immediate) {
|
||||||
|
val originalState = tunnel.state
|
||||||
|
val wasLastUsed = tunnel == lastUsedTunnel
|
||||||
|
// Make sure nothing touches the tunnel.
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = null
|
||||||
|
tunnelMap.remove(tunnel)
|
||||||
|
try {
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) { configStore.delete(tunnel.name) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Failure, put the tunnel back.
|
||||||
|
tunnelMap.add(tunnel)
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = tunnel
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
var lastUsedTunnel: ObservableTunnel? = null
|
||||||
|
private set(value) {
|
||||||
|
if (value == field) return
|
||||||
|
field = value
|
||||||
|
notifyPropertyChanged(BR.lastUsedTunnel)
|
||||||
|
applicationScope.launch { UserKnobs.setLastUsedTunnel(value?.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTunnelConfig(tunnel: ObservableTunnel): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
|
tunnel.onConfigChanged(withContext(Dispatchers.IO) { configStore.load(tunnel.name) })!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCreate() {
|
||||||
|
applicationScope.launch {
|
||||||
|
try {
|
||||||
|
onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames })
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTunnelsLoaded(present: Iterable<String>, running: Collection<String>) {
|
||||||
|
for (name in present)
|
||||||
|
addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||||
|
applicationScope.launch {
|
||||||
|
val lastUsedName = UserKnobs.lastUsedTunnel.first()
|
||||||
|
if (lastUsedName != null)
|
||||||
|
lastUsedTunnel = tunnelMap[lastUsedName]
|
||||||
|
haveLoaded = true
|
||||||
|
restoreState(true)
|
||||||
|
tunnels.complete(tunnelMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshTunnelStates() {
|
||||||
|
applicationScope.launch {
|
||||||
|
try {
|
||||||
|
val running = withContext(Dispatchers.IO) { getBackend().runningTunnelNames }
|
||||||
|
for (tunnel in tunnelMap)
|
||||||
|
tunnel.onStateChanged(if (running.contains(tunnel.name)) Tunnel.State.UP else Tunnel.State.DOWN)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun restoreState(force: Boolean) {
|
||||||
|
if (!haveLoaded || (!force && !UserKnobs.restoreOnBoot.first()))
|
||||||
|
return
|
||||||
|
val previouslyRunning = UserKnobs.runningTunnels.first()
|
||||||
|
if (previouslyRunning.isEmpty()) return
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } }
|
||||||
|
.awaitAll()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveState() {
|
||||||
|
UserKnobs.setRunningTunnels(tunnelMap.filter { it.state == Tunnel.State.UP }.map { it.name }.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setTunnelConfig(tunnel: ObservableTunnel, config: Config): Config = withContext(Dispatchers.Main.immediate) {
|
||||||
|
tunnel.onConfigChanged(withContext(Dispatchers.IO) {
|
||||||
|
getBackend().setState(tunnel, tunnel.state, config)
|
||||||
|
configStore.save(tunnel.name, config)
|
||||||
|
})!!
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setTunnelName(tunnel: ObservableTunnel, name: String): String = withContext(Dispatchers.Main.immediate) {
|
||||||
|
if (Tunnel.isNameInvalid(name))
|
||||||
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_invalid_name))
|
||||||
|
if (tunnelMap.containsKey(name)) {
|
||||||
|
throw IllegalArgumentException(context.getString(R.string.tunnel_error_already_exists, name))
|
||||||
|
}
|
||||||
|
val originalState = tunnel.state
|
||||||
|
val wasLastUsed = tunnel == lastUsedTunnel
|
||||||
|
// Make sure nothing touches the tunnel.
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = null
|
||||||
|
tunnelMap.remove(tunnel)
|
||||||
|
var throwable: Throwable? = null
|
||||||
|
var newName: String? = null
|
||||||
|
try {
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.DOWN, null) }
|
||||||
|
withContext(Dispatchers.IO) { configStore.rename(tunnel.name, name) }
|
||||||
|
newName = tunnel.onNameChanged(name)
|
||||||
|
if (originalState == Tunnel.State.UP)
|
||||||
|
withContext(Dispatchers.IO) { getBackend().setState(tunnel, Tunnel.State.UP, tunnel.config) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
throwable = e
|
||||||
|
// On failure, we don't know what state the tunnel might be in. Fix that.
|
||||||
|
getTunnelState(tunnel)
|
||||||
|
}
|
||||||
|
// Add the tunnel back to the manager, under whatever name it thinks it has.
|
||||||
|
tunnelMap.add(tunnel)
|
||||||
|
if (wasLastUsed)
|
||||||
|
lastUsedTunnel = tunnel
|
||||||
|
if (throwable != null)
|
||||||
|
throw throwable
|
||||||
|
newName!!
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||||
|
var newState = tunnel.state
|
||||||
|
var throwable: Throwable? = null
|
||||||
|
try {
|
||||||
|
newState = withContext(Dispatchers.IO) { getBackend().setState(tunnel, state, tunnel.getConfigAsync()) }
|
||||||
|
if (newState == Tunnel.State.UP)
|
||||||
|
lastUsedTunnel = tunnel
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
throwable = e
|
||||||
|
}
|
||||||
|
tunnel.onStateChanged(newState)
|
||||||
|
saveState()
|
||||||
|
if (throwable != null)
|
||||||
|
throw throwable
|
||||||
|
newState
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntentReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
applicationScope.launch {
|
||||||
|
val manager = getTunnelManager()
|
||||||
|
if (intent == null) return@launch
|
||||||
|
val action = intent.action ?: return@launch
|
||||||
|
if ("com.wireguard.android.action.REFRESH_TUNNEL_STATES" == action) {
|
||||||
|
manager.refreshTunnelStates()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
if (!UserKnobs.allowRemoteControlIntents.first())
|
||||||
|
return@launch
|
||||||
|
val state = when (action) {
|
||||||
|
"com.wireguard.android.action.SET_TUNNEL_UP" -> Tunnel.State.UP
|
||||||
|
"com.wireguard.android.action.SET_TUNNEL_DOWN" -> Tunnel.State.DOWN
|
||||||
|
else -> return@launch
|
||||||
|
}
|
||||||
|
val tunnelName = intent.getStringExtra("tunnel") ?: return@launch
|
||||||
|
val tunnels = manager.getTunnels()
|
||||||
|
val tunnel = tunnels[tunnelName] ?: return@launch
|
||||||
|
try {
|
||||||
|
manager.setTunnelState(tunnel, state)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) {
|
||||||
|
tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) })
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTunnelStatistics(tunnel: ObservableTunnel): Statistics = withContext(Dispatchers.Main.immediate) {
|
||||||
|
tunnel.onStatisticsChanged(withContext(Dispatchers.IO) { getBackend().getStatistics(tunnel) })!!
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WireGuard/TunnelManager"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue