mirror of
https://github.com/amnezia-vpn/amneziawg-android.git
synced 2026-07-03 14:05:13 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a84f0cf5f | |||
| efafc995fe | |||
| 0b134f27bc | |||
| 4116c83624 | |||
| 762e82c71f | |||
| 8db4f52988 | |||
| 728c92916f |
@@ -0,0 +1,141 @@
|
||||
name: Android build (signed AAB / debug APK)
|
||||
run-name: "Android • ${{ inputs.target }} • ${{ inputs.ref || github.ref_name }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: "What to build"
|
||||
type: choice
|
||||
required: true
|
||||
default: release_aab
|
||||
options:
|
||||
- release_aab
|
||||
- debug_apk
|
||||
ref:
|
||||
description: "Git ref (branch/tag/sha)"
|
||||
required: false
|
||||
default: "master"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build (${{ inputs.target }})"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Configure build
|
||||
id: cfg
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ inputs.target }}" in
|
||||
release_aab)
|
||||
echo "gradle_task=bundleRelease" >> "$GITHUB_OUTPUT"
|
||||
echo "kind=aab" >> "$GITHUB_OUTPUT"
|
||||
echo "artifact_name=release-aab" >> "$GITHUB_OUTPUT"
|
||||
echo "needs_signing=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
debug_apk)
|
||||
echo "gradle_task=assembleDebug" >> "$GITHUB_OUTPUT"
|
||||
echo "kind=apk" >> "$GITHUB_OUTPUT"
|
||||
echo "artifact_name=debug-apk" >> "$GITHUB_OUTPUT"
|
||||
echo "needs_signing=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown target: ${{ inputs.target }}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Decode keystore from secret
|
||||
if: ${{ steps.cfg.outputs.needs_signing == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${KEYSTORE_B64}" ]]; then
|
||||
echo "Missing secret ANDROID_KEYSTORE_BASE64" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${KEYSTORE_B64}" | base64 --decode > release.keystore.jks
|
||||
echo "KEYSTORE_PATH=${PWD}/release.keystore.jks" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
chmod +x ./gradlew
|
||||
./gradlew --no-daemon --stacktrace ${{ steps.cfg.outputs.gradle_task }}
|
||||
|
||||
- name: Locate built artifact
|
||||
id: locate
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${{ steps.cfg.outputs.kind }}" == "aab" ]]; then
|
||||
FILE="./ui/build/outputs/bundle/release/ui-release.aab"
|
||||
else
|
||||
FILE="./ui/build/outputs/apk/debug/ui-debug.apk"
|
||||
fi
|
||||
|
||||
if [[ -z "${FILE}" ]]; then
|
||||
echo "Built file not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "built_file=${FILE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sign AAB
|
||||
if: ${{ steps.cfg.outputs.needs_signing == 'true' }}
|
||||
id: sign
|
||||
shell: bash
|
||||
env:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
AAB="${{ steps.locate.outputs.built_file }}"
|
||||
SIGNED_AAB="${AAB%.aab}-signed.aab"
|
||||
|
||||
if [[ -z "${KEYSTORE_PASSWORD}" || -z "${KEY_ALIAS}" ]]; then
|
||||
echo "Missing one of secrets: ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jarsigner \
|
||||
-keystore "${KEYSTORE_PATH}" \
|
||||
-storepass "${KEYSTORE_PASSWORD}" \
|
||||
-signedjar "${SIGNED_AAB}" \
|
||||
"${AAB}" \
|
||||
"${KEY_ALIAS}"
|
||||
|
||||
echo "final_file=${SIGNED_AAB}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.cfg.outputs.artifact_name }}
|
||||
path: ${{ steps.sign.outputs.final_file || steps.locate.outputs.built_file }}
|
||||
if-no-files-found: error
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
amneziawgVersionCode=9
|
||||
amneziawgVersionCode=10
|
||||
amneziawgVersionName=2.0.0
|
||||
amneziawgPackageName=org.amnezia.awg
|
||||
|
||||
|
||||
@@ -54,13 +54,19 @@ public final class InetEndpoint {
|
||||
}
|
||||
if (uri.getPort() < 0 || uri.getPort() > 65535)
|
||||
throw new ParseException(InetEndpoint.class, endpoint, "Missing/invalid port number");
|
||||
String host = uri.getHost();
|
||||
if (host == null)
|
||||
throw new ParseException(InetEndpoint.class, endpoint, "Missing host");
|
||||
// URI.getHost() wraps IPv6 literals in brackets; strip them so the numeric parser accepts it.
|
||||
if (host.length() > 1 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']')
|
||||
host = host.substring(1, host.length() - 1);
|
||||
try {
|
||||
InetAddresses.parse(uri.getHost());
|
||||
InetAddresses.parse(host);
|
||||
// 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());
|
||||
return new InetEndpoint(host, 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());
|
||||
return new InetEndpoint(host, false, uri.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.amnezia.awg.config;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class InetEndpointTest {
|
||||
|
||||
@Test
|
||||
public void ipv6_literal_is_parsed_as_canonical_numeric_host() throws ParseException {
|
||||
final InetEndpoint endpoint = InetEndpoint.parse("[2001:db8::1]:51820");
|
||||
// URI.getHost() yields the bracketed form; the stored host must be the bare, canonical one.
|
||||
assertEquals("Host is stored without brackets", "2001:db8::1", endpoint.getHost());
|
||||
assertEquals("Port is parsed", 51820, endpoint.getPort());
|
||||
// toString() must re-add brackets so the userspace string is valid for amneziawg-go.
|
||||
assertEquals("toString re-brackets the literal", "[2001:db8::1]:51820", endpoint.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ipv4_endpoint_is_unchanged() throws ParseException {
|
||||
final InetEndpoint endpoint = InetEndpoint.parse("192.0.2.1:51820");
|
||||
assertEquals("192.0.2.1", endpoint.getHost());
|
||||
assertEquals(51820, endpoint.getPort());
|
||||
assertEquals("192.0.2.1:51820", endpoint.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hostname_endpoint_is_preserved() throws ParseException {
|
||||
final InetEndpoint endpoint = InetEndpoint.parse("vpn.example.com:51820");
|
||||
assertEquals("vpn.example.com", endpoint.getHost());
|
||||
assertEquals(51820, endpoint.getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ipv6_endpoint_round_trips_to_userspace_string() throws BadConfigException {
|
||||
final Peer peer = new Peer.Builder()
|
||||
.parsePublicKey("vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg=")
|
||||
.parseEndpoint("[2001:db8::1]:51820")
|
||||
.build();
|
||||
assertTrue(
|
||||
"Userspace string keeps the bracketed IPv6 endpoint",
|
||||
peer.toAwgUserspaceString().contains("endpoint=[2001:db8::1]:51820")
|
||||
);
|
||||
}
|
||||
}
|
||||
Submodule tunnel/tools/amneziawg-tools updated: 5c6ffd6168...5d6179a6d0
@@ -63,11 +63,11 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
|
||||
@get:Bindable
|
||||
val isAbleToExcludePrivateIps: Boolean
|
||||
get() = allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS || allowedIpsState == AllowedIpsState.CONTAINS_IPV4_WILDCARD
|
||||
get() = allowedIpsState == AllowedIpsState.CONTAINS_PUBLIC_NETWORKS || allowedIpsState == AllowedIpsState.CONTAINS_WILDCARD
|
||||
|
||||
@get:Bindable
|
||||
val isExcludingPrivateIps: Boolean
|
||||
get() = allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
|
||||
get() = allowedIpsState == AllowedIpsState.CONTAINS_PUBLIC_NETWORKS
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
allowedIps = parcel.readString() ?: ""
|
||||
@@ -106,12 +106,11 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
// the above sets of (valid) *networks*. We are not checking for a superset based on
|
||||
// the individual addresses in each set.
|
||||
val networkStrings: Collection<String> = getAllowedIpsSet()
|
||||
// If allowedIps contains both the wildcard and the public networks, then private
|
||||
// networks aren't excluded!
|
||||
if (networkStrings.containsAll(IPV4_WILDCARD))
|
||||
AllowedIpsState.CONTAINS_IPV4_WILDCARD
|
||||
else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS))
|
||||
AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS
|
||||
// A wildcard in either family means private IPs can still be excluded.
|
||||
if (networkStrings.containsAll(IPV4_WILDCARD) || networkStrings.containsAll(IPV6_WILDCARD))
|
||||
AllowedIpsState.CONTAINS_WILDCARD
|
||||
else if (networkStrings.containsAll(IPV4_PUBLIC_NETWORKS) || networkStrings.containsAll(IPV6_PUBLIC_NETWORKS))
|
||||
AllowedIpsState.CONTAINS_PUBLIC_NETWORKS
|
||||
else
|
||||
AllowedIpsState.OTHER
|
||||
} else {
|
||||
@@ -128,31 +127,29 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
|
||||
private fun getAllowedIpsSet() = setOf(*Attribute.split(allowedIps))
|
||||
|
||||
// Replace the first instance of the wildcard with the public network list, or vice versa.
|
||||
// DNS servers only need to handled specially when we're excluding private IPs.
|
||||
// For each family, replace the first instance of the wildcard with its public network list, or vice versa.
|
||||
fun setExcludingPrivateIps(excludingPrivateIps: Boolean) {
|
||||
if (!isAbleToExcludePrivateIps || isExcludingPrivateIps == excludingPrivateIps) return
|
||||
val oldNetworks = if (excludingPrivateIps) IPV4_WILDCARD else IPV4_PUBLIC_NETWORKS
|
||||
val newNetworks = if (excludingPrivateIps) IPV4_PUBLIC_NETWORKS else IPV4_WILDCARD
|
||||
val replacements = listOf(IPV4_WILDCARD to IPV4_PUBLIC_NETWORKS, IPV6_WILDCARD to IPV6_PUBLIC_NETWORKS)
|
||||
.map { (wildcard, public) -> if (excludingPrivateIps) wildcard to public else public to wildcard }
|
||||
val input: Collection<String> = getAllowedIpsSet()
|
||||
val outputSize = input.size - oldNetworks.size + newNetworks.size
|
||||
val output: MutableCollection<String?> = LinkedHashSet(outputSize)
|
||||
var replaced = false
|
||||
// Replace the first instance of the wildcard with the public network list, or vice versa.
|
||||
val output: MutableCollection<String?> = LinkedHashSet(input.size)
|
||||
val replaced = BooleanArray(replacements.size)
|
||||
for (network in input) {
|
||||
if (oldNetworks.contains(network)) {
|
||||
if (!replaced) {
|
||||
for (replacement in newNetworks) if (!output.contains(replacement)) output.add(replacement)
|
||||
replaced = true
|
||||
val family = replacements.indexOfFirst { it.first.contains(network) }
|
||||
if (family >= 0) {
|
||||
if (!replaced[family]) {
|
||||
for (replacement in replacements[family].second) if (!output.contains(replacement)) output.add(replacement)
|
||||
replaced[family] = true
|
||||
}
|
||||
} else if (!output.contains(network)) {
|
||||
output.add(network)
|
||||
}
|
||||
}
|
||||
// DNS servers only need to handled specially when we're excluding private IPs.
|
||||
// DNS servers only need to be handled specially when we're excluding private IPs.
|
||||
if (excludingPrivateIps) output.addAll(dnsRoutes) else output.removeAll(dnsRoutes)
|
||||
allowedIps = Attribute.join(output)
|
||||
allowedIpsState = if (excludingPrivateIps) AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS else AllowedIpsState.CONTAINS_IPV4_WILDCARD
|
||||
allowedIpsState = if (excludingPrivateIps) AllowedIpsState.CONTAINS_PUBLIC_NETWORKS else AllowedIpsState.CONTAINS_WILDCARD
|
||||
notifyPropertyChanged(BR.allowedIps)
|
||||
notifyPropertyChanged(BR.excludingPrivateIps)
|
||||
}
|
||||
@@ -169,8 +166,10 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
}
|
||||
|
||||
private fun setInterfaceDns(dnsServers: CharSequence) {
|
||||
val newDnsRoutes = Attribute.split(dnsServers).filter { !it.contains(":") }.map { "$it/32" }
|
||||
if (allowedIpsState == AllowedIpsState.CONTAINS_IPV4_PUBLIC_NETWORKS) {
|
||||
val newDnsRoutes = Attribute.split(dnsServers)
|
||||
.filter { it.contains(":") || it.matches(Regex("""^\d{1,3}(\.\d{1,3}){3}$""")) }
|
||||
.map { if (it.contains(":")) "$it/128" else "$it/32" }
|
||||
if (allowedIpsState == AllowedIpsState.CONTAINS_PUBLIC_NETWORKS) {
|
||||
val input = getAllowedIpsSet()
|
||||
// Yes, this is quadratic in the number of DNS servers, but most users have 1 or 2.
|
||||
val output = input.filter { !dnsRoutes.contains(it) || newDnsRoutes.contains(it) }.plus(newDnsRoutes).distinct()
|
||||
@@ -209,7 +208,7 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
}
|
||||
|
||||
private enum class AllowedIpsState {
|
||||
CONTAINS_IPV4_PUBLIC_NETWORKS, CONTAINS_IPV4_WILDCARD, INVALID, OTHER
|
||||
CONTAINS_PUBLIC_NETWORKS, CONTAINS_WILDCARD, INVALID, OTHER
|
||||
}
|
||||
|
||||
private class InterfaceDnsListener constructor(peerProxy: PeerProxy) : OnPropertyChangedCallback() {
|
||||
@@ -290,5 +289,11 @@ class PeerProxy : BaseObservable, Parcelable {
|
||||
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
|
||||
)
|
||||
private val IPV4_WILDCARD = setOf("0.0.0.0/0")
|
||||
// ::/0 minus the private ranges fc00::/7 (unique local) and fe80::/10 (link-local).
|
||||
private val IPV6_PUBLIC_NETWORKS = setOf(
|
||||
"::/1", "8000::/2", "c000::/3", "e000::/4", "f000::/5", "f800::/6",
|
||||
"fe00::/9", "fec0::/10", "ff00::/8"
|
||||
)
|
||||
private val IPV6_WILDCARD = setOf("::/0")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user