fix: improve parser handling of script, amnezia sig packets, and preshared keys

This commit is contained in:
zaneschepke
2026-05-25 07:13:43 -04:00
parent 02aa4dcb09
commit fe12ce675c
11 changed files with 541 additions and 42 deletions
@@ -1078,6 +1078,344 @@
"Apache-2.0"
]
},
{
"uniqueId": "commons-beanutils:commons-beanutils",
"funding": [
],
"developers": [
{
"name": "Geir Magnusson Jr."
},
{
"name": "Gary Gregory"
},
{
"name": "Stian Soiland-Reyes"
},
{
"name": "Robert Burrell Donkin"
},
{
"name": "Niall Pemberton"
},
{
"name": "Scott Sanders"
},
{
"name": "James Strachan"
},
{
"name": "Tim O'Brien"
},
{
"name": "Morgan James Delagrange"
},
{
"name": "Yoav Shapira"
},
{
"name": "Rob Tompkins"
},
{
"name": "Stephen Colebourne"
},
{
"name": "Craig McClanahan"
},
{
"name": "James Carman"
},
{
"name": "John E. Conlon"
},
{
"name": "Martin van den Bemt"
},
{
"name": "David Eric Pugh"
},
{
"name": "Dion Gillard"
},
{
"name": "Simon Kitching"
},
{
"name": "Rodney Waldhoff"
},
{
"name": "Benedikt Ritter"
}
],
"artifactVersion": "1.9.4",
"description": "Apache Commons BeanUtils provides an easy-to-use but flexible wrapper around reflection and introspection.",
"scm": {
"connection": "scm:svn:http://svn.apache.org/repos/asf/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3",
"url": "http://svn.apache.org/viewvc/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3",
"developerConnection": "scm:svn:https://svn.apache.org/repos/asf/commons/proper/beanutils/tags/BEANUTILS_1_9_3_RC3"
},
"name": "Apache Commons BeanUtils",
"website": "https://commons.apache.org/proper/commons-beanutils/",
"licenses": [
"Apache-2.0"
],
"organization": {
"url": "https://www.apache.org/",
"name": "The Apache Software Foundation"
}
},
{
"uniqueId": "commons-collections:commons-collections",
"funding": [
],
"developers": [
{
"name": "Stephen Colebourne"
},
{
"name": "Arun M. Thomas"
},
{
"name": "Craig McClanahan"
},
{
"name": "James Carman"
},
{
"name": "Morgan Delagrange"
},
{
"name": "Henri Yandell"
},
{
"name": "Geir Magnusson"
},
{
"name": "Matthew Hawthorne"
},
{
"name": "Phil Steitz"
},
{
"name": "Robert Burrell Donkin"
},
{
"name": "Rodney Waldhoff"
}
],
"artifactVersion": "3.2.2",
"description": "Types that extend and augment the Java Collections Framework.",
"scm": {
"connection": "scm:svn:http://svn.apache.org/repos/asf/commons/proper/collections/trunk",
"url": "http://svn.apache.org/viewvc/commons/proper/collections/trunk",
"developerConnection": "scm:svn:https://svn.apache.org/repos/asf/commons/proper/collections/trunk"
},
"name": "Apache Commons Collections",
"website": "http://commons.apache.org/collections/",
"licenses": [
"Apache-2.0"
],
"organization": {
"url": "http://www.apache.org/",
"name": "The Apache Software Foundation"
}
},
{
"uniqueId": "commons-digester:commons-digester",
"funding": [
],
"developers": [
{
"name": "Craig McClanahan"
},
{
"name": "Tim OBrien"
},
{
"name": "Rahul Akolkar"
},
{
"name": "Robert Burrell Donkin"
},
{
"name": "Jean-Francois Arcand"
},
{
"name": "Scott Sanders"
},
{
"name": "James Strachan"
},
{
"name": "Jason van Zyl"
},
{
"name": "Simon Kitching"
},
{
"name": "Simone Tripodi"
}
],
"artifactVersion": "2.1",
"description": "The Digester package lets you configure an XML to Java object mapping module\n which triggers certain actions called rules whenever a particular \n pattern of nested XML elements is recognized.",
"scm": {
"connection": "scm:svn:http://svn.apache.org/repos/asf/commons/proper/digester/tags/DIGESTER_2_1_RC2",
"url": "http://svn.apache.org/viewvc/commons/proper/digester/tags/DIGESTER_2_1_RC2",
"developerConnection": "scm:svn:https://svn.apache.org/repos/asf/commons/proper/digester/tags/DIGESTER_2_1_RC2"
},
"name": "Commons Digester",
"website": "http://commons.apache.org/digester/",
"licenses": [
"Apache-2.0"
],
"organization": {
"url": "http://www.apache.org/",
"name": "The Apache Software Foundation"
}
},
{
"uniqueId": "commons-logging:commons-logging",
"funding": [
],
"developers": [
{
"name": "Juozas Baliuka"
},
{
"name": "Costin Manolache"
},
{
"name": "Dennis Lundberg"
},
{
"name": "Brian Stansberry"
},
{
"name": "Robert Burrell Donkin"
},
{
"name": "Scott Sanders"
},
{
"name": "Thomas Neidhart"
},
{
"name": "Richard Sitze"
},
{
"organisationUrl": "https://www.apache.org/",
"name": "Gary Gregory"
},
{
"name": "Craig McClanahan"
},
{
"name": "Morgan Delagrange"
},
{
"name": "Peter Donald"
},
{
"name": "Simon Kitching"
},
{
"name": "Rodney Waldhoff"
}
],
"artifactVersion": "1.3.0",
"description": "Apache Commons Logging is a thin adapter allowing configurable bridging to other,\n well known logging systems.",
"scm": {
"connection": "scm:git:https://gitbox.apache.org/repos/asf/commons-logging",
"url": "https://gitbox.apache.org/repos/asf/commons-logging",
"developerConnection": "scm:git:https://gitbox.apache.org/repos/asf/commons-logging"
},
"name": "Apache Commons Logging",
"website": "https://commons.apache.org/proper/commons-logging/",
"licenses": [
"Apache-2.0"
],
"organization": {
"url": "https://www.apache.org/",
"name": "The Apache Software Foundation"
}
},
{
"uniqueId": "commons-validator:commons-validator",
"funding": [
],
"developers": [
{
"name": "Martin Cooper"
},
{
"name": "James Mitchell"
},
{
"name": "James Turner"
},
{
"name": "David Graham"
},
{
"name": "Rob Leland"
},
{
"name": "Henri Yandell"
},
{
"name": "Ben Speakmon"
},
{
"name": "Niall Pemberton"
},
{
"organisationUrl": "https://www.apache.org/",
"name": "Gary Gregory"
},
{
"name": "Ted Husted"
},
{
"name": "Craig McClanahan"
},
{
"name": "David Winterfeldt"
},
{
"name": "SimoneTripodi"
},
{
"name": "Don Brown"
},
{
"name": "Nick Burch"
},
{
"name": "Benedikt Ritter"
}
],
"artifactVersion": "1.8.0",
"description": "Apache Commons Validator provides the building blocks for both client side validation and server side data validation.\n It may be used standalone or with a framework like Struts.",
"scm": {
"connection": "scm:git:https://gitbox.apache.org/repos/asf/commons-validator",
"url": "https://gitbox.apache.org/repos/asf/commons-validator",
"developerConnection": "scm:git:https://gitbox.apache.org/repos/asf/commons-validator"
},
"name": "Apache Commons Validator",
"website": "http://commons.apache.org/proper/commons-validator/",
"licenses": [
"Apache-2.0"
],
"organization": {
"url": "https://www.apache.org/",
"name": "The Apache Software Foundation"
}
},
{
"uniqueId": "de.jangassen:jfa",
"funding": [
+2
View File
@@ -52,6 +52,7 @@ androidx-sqlite = "2.6.2"
lang3 = "3.20.0"
humanReadable = "1.12.3"
datetime = "0.7.1"
commonsValidator = "1.10.1"
[bundles]
ktor-client-jvm = ["ktor-client-core-jvm", "ktor-client-cio-jvm", "ktor-client-content-negotiation-jvm", "ktor-serialization-json-jvm", "ktor-client-okhttp", "ktor-client-websockets-jvm"]
@@ -188,6 +189,7 @@ multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", vers
human-readable = { module = "nl.jacobras:Human-Readable", version.ref = "humanReadable" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
commons-validator = { module = "commons-validator:commons-validator", version.ref = "commonsValidator" }
[plugins]
+2 -1
View File
@@ -15,6 +15,7 @@ dependencies {
implementation(libs.human.readable)
implementation(libs.kotlinx.datetime)
implementation(libs.commons.validator)
}
tasks.test { useJUnitPlatform() }
@@ -29,7 +30,7 @@ publishing {
register<MavenPublication>("release") {
groupId = "com.zaneschepke.wireguardautotunnel"
artifactId = "amneziawg-parser"
version = "1.0.1"
version = "1.0.6"
from(components["java"])
pom {
name.set("AmneziaWG Parser")
@@ -93,7 +93,12 @@ data class ActiveConfig(val interfaceSection: InterfaceSection, val peers: List<
}
return ActiveConfig(
interfaceSection = Config.buildInterface(interfaceMap, emptyList()),
interfaceSection =
Config.buildInterface(
interfaceMap,
InterfaceScriptsBuilder.InterfaceScripts(),
emptyList(),
),
peers = peerMaps.map { Config.buildActivePeer(it) },
)
}
@@ -39,6 +39,7 @@ data class Config(
companion object {
fun parseQuickString(configString: String): Config {
val scripts = InterfaceScriptsBuilder()
val interfaceMap = mutableMapOf<String, String>()
val peerMaps = mutableListOf<Pair<MutableMap<String, String>, List<String>>>()
@@ -89,9 +90,23 @@ data class Config(
}
val parts = raw.split("=", limit = 2)
if (parts.size == 2) {
val key = parts[0].trim()
var value = parts[1].trim()
if (currentSectionMap === interfaceMap) {
when (key) {
"PreUp",
"PostUp",
"PreDown",
"PostDown" -> {
scripts.add(key, value)
return@forEach
}
}
}
// remove whitespaces
if (
key in
@@ -99,6 +114,7 @@ data class Config(
"PrivateKey",
"PublicKey",
"PresharedKey",
"PreSharedKey",
"H1",
"H2",
"H3",
@@ -113,12 +129,16 @@ data class Config(
return Config(
headerComments = headerComments,
`interface` = buildInterface(interfaceMap, interfaceComments),
`interface` = buildInterface(interfaceMap, scripts.build(), interfaceComments),
peers = peerMaps.map { (map, comments) -> buildPeer(map, comments) },
)
}
internal fun buildInterface(m: Map<String, String>, comments: List<String>) =
internal fun buildInterface(
m: Map<String, String>,
scripts: InterfaceScriptsBuilder.InterfaceScripts,
comments: List<String>,
) =
InterfaceSection(
comments = comments,
privateKey = m["PrivateKey"] ?: "",
@@ -129,6 +149,10 @@ data class Config(
fwMark = m.getInt("FwMark", "Interface"),
table = m["Table"],
saveConfig = m.getBool("SaveConfig", "Interface"),
preUp = scripts.preUp,
postUp = scripts.postUp,
preDown = scripts.preDown,
postDown = scripts.postDown,
jC = m.getInt("Jc", "Interface"),
jMin = m.getInt("Jmin", "Interface"),
jMax = m.getInt("Jmax", "Interface"),
@@ -154,7 +178,7 @@ data class Config(
publicKey = m["PublicKey"] ?: "",
allowedIPs = m["AllowedIPs"],
endpoint = m["Endpoint"],
presharedKey = m["PresharedKey"],
presharedKey = m["PresharedKey"] ?: m["PreSharedKey"],
persistentKeepalive = m.getInt("PersistentKeepalive", "Peer"),
comments = comments,
)
@@ -194,7 +218,7 @@ data class Config(
publicKey = m["PublicKey"] ?: "",
allowedIPs = m["AllowedIPs"],
endpoint = m["Endpoint"],
presharedKey = m["PresharedKey"],
presharedKey = m["PresharedKey"] ?: m["PreSharedKey"],
persistentKeepalive = m.getInt("PersistentKeepalive", "Peer"),
lastHandshakeSeconds = m.getLong("LastHandshakeSeconds", "Peer"),
lastHandshakeNanos = m.getLong("LastHandshakeNanos", "Peer"),
@@ -202,8 +226,8 @@ data class Config(
rxBytes = m.getLong("RxBytes", "Peer"),
)
internal fun generatePublicKeyFromPrivate(privateBase64: String): String {
val privateKey = Key.fromBase64(privateBase64)
fun generatePublicKeyFromPrivateKey(privateKeyBase64: String): String {
val privateKey = Key.fromBase64(privateKeyBase64)
val publicKey = Key.generatePublicKey(privateKey)
return publicKey.toBase64()
}
@@ -6,14 +6,13 @@ enum class ErrorType {
INVALID_PORT_RANGE,
INVALID_MTU_RANGE,
INVALID_FWMARK,
INVALID_JC_RANGE,
INVALID_JC_VALUE,
INVALID_JMIN_JMAX_ORDER,
INVALID_JMAX_MTU,
INVALID_PADDING_NEGATIVE,
INVALID_HEADER_FORMAT,
INVALID_SIGNATURE_FORMAT,
INVALID_ENDPOINT_FORMAT,
INVALID_KEEPALIVE_NEGATIVE,
INVALID_KEEPALIVE_VALUE,
INVALID_CIDR,
INVALID_IP,
INVALID_HOSTNAME,
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.parser
class InterfaceScriptsBuilder {
private val preUp = mutableListOf<String>()
private val postUp = mutableListOf<String>()
private val preDown = mutableListOf<String>()
private val postDown = mutableListOf<String>()
fun add(key: String, value: String) {
when (key) {
"PreUp" -> preUp += value
"PostUp" -> postUp += value
"PreDown" -> preDown += value
"PostDown" -> postDown += value
}
}
fun build(): InterfaceScripts =
InterfaceScripts(
preUp = preUp.toList(),
postUp = postUp.toList(),
preDown = preDown.toList(),
postDown = postDown.toList(),
)
data class InterfaceScripts(
val preUp: List<String> = emptyList(),
val postUp: List<String> = emptyList(),
val preDown: List<String> = emptyList(),
val postDown: List<String> = emptyList(),
)
}
@@ -92,14 +92,11 @@ data class InterfaceSection(
}
jC?.let {
if (it !in 4..12)
throw ConfigParseException(ErrorType.INVALID_JC_RANGE, "Interface.Jc", it)
if (it < 0) throw ConfigParseException(ErrorType.INVALID_JC_VALUE, "Interface.Jc", it)
}
if (jMin != null && jMax != null) {
if (jMin > jMax)
throw ConfigParseException(ErrorType.INVALID_JMIN_JMAX_ORDER, "Interface.Jmin/Jmax")
if (jMax >= (mtu ?: 1500))
throw ConfigParseException(ErrorType.INVALID_JMAX_MTU, "Interface.Jmax", jMax)
}
listOf(s1, s2, s3, s4).forEachIndexed { i, s ->
@@ -121,14 +118,9 @@ data class InterfaceSection(
}
}
listOf(i1, i2, i3, i4, i5).forEachIndexed { i, sig ->
if (sig != null && !NetworkUtils.isValidHexSignature(sig)) {
throw ConfigParseException(
ErrorType.INVALID_SIGNATURE_FORMAT,
"Interface.I${i + 1}",
sig,
)
}
listOf(i1 to "I1", i2 to "I2", i3 to "I3", i4 to "I4", i5 to "I5").forEach {
(sig, shortName) ->
sig?.let { NetworkUtils.validateAmneziaSignaturePacket(it, "Interface.$shortName") }
}
address
@@ -25,7 +25,7 @@ data class PeerSection(
persistentKeepalive?.let {
if (it !in 0..65535)
throw ConfigParseException(
ErrorType.INVALID_KEEPALIVE_NEGATIVE,
ErrorType.INVALID_KEEPALIVE_VALUE,
"$prefix.PersistentKeepalive",
it,
)
@@ -34,7 +34,7 @@ data class PeerSection(
endpoint?.let {
val (host, portStr) = Config.parseEndpoint(it)
val port = portStr?.toIntOrNull()
if (host == null || port == null || port !in 0..65535) {
if (host == null || port == null || port !in 1..65535) {
throw ConfigParseException(
ErrorType.INVALID_ENDPOINT_FORMAT,
"$prefix.Endpoint",
@@ -50,6 +50,12 @@ data class PeerSection(
}
}
presharedKey?.let {
if (!NetworkUtils.isValidBase64(it)) {
throw ConfigParseException(ErrorType.INVALID_BASE64_KEY, "$prefix.PresharedKey", it)
}
}
allowedIPs
?.split(",")
?.map { it.trim() }
@@ -20,6 +20,12 @@ object ConfigFormatter {
sb.appendLine("PrivateKey = ${if (hidePrivateKey) "(hidden)" else iface.privateKey}")
iface.address?.let { sb.appendLine("Address = $it") }
iface.dns?.let { sb.appendLine("DNS = $it") }
iface.preUp?.forEach { sb.appendLine("PreUp = $it") }
iface.postUp?.forEach { sb.appendLine("PostUp = $it") }
iface.preDown?.forEach { sb.appendLine("PreDown = $it") }
iface.postDown?.forEach { sb.appendLine("PostDown = $it") }
iface.listenPort?.let { sb.appendLine("ListenPort = $it") }
iface.mtu?.let { sb.appendLine("MTU = $it") }
iface.fwMark?.let { sb.appendLine("FwMark = $it") }
@@ -1,22 +1,21 @@
package com.zaneschepke.wireguardautotunnel.parser.util
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
import com.zaneschepke.wireguardautotunnel.parser.ErrorType
import java.net.InetAddress
import org.apache.commons.validator.routines.InetAddressValidator
object NetworkUtils {
private val validator = InetAddressValidator.getInstance()
private val hostnameRegex =
Regex(
"^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])*$"
)
fun isValidIp(ip: String): Boolean {
val sanitized = ip.removeSurrounding("[", "]")
if (sanitized.any { it.lowercaseChar() in 'g'..'z' }) return false
return try {
InetAddress.getAllByName(sanitized).isNotEmpty()
} catch (e: Exception) {
false
}
return validator.isValid(ip.removeSurrounding("[", "]"))
}
fun isValidCidr(cidr: String): Boolean {
@@ -41,7 +40,6 @@ object NetworkUtils {
fun isValidDnsEntry(entry: String): Boolean {
if (entry.isBlank()) return false
// Safe: isValidIp is offline, isValidHostname is regex.
return isValidIp(entry) || isValidHostname(entry)
}
@@ -51,9 +49,12 @@ object NetworkUtils {
}
fun isValidBase64(str: String): Boolean {
if (str.length != 44 || !str.endsWith("=")) return false
val base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
return str.all { it in base64Chars }
return try {
val decoded = kotlin.io.encoding.Base64.decode(str)
decoded.size == 32
} catch (_: Exception) {
false
}
}
fun isValidAmneziaHeader(header: String): Boolean {
@@ -73,10 +74,102 @@ object NetworkUtils {
}
}
fun isValidHexSignature(signature: String): Boolean {
val hex = signature.removePrefix("0x").trim()
if (hex.isEmpty() || hex.length % 2 != 0) return false
val hexChars = "0123456789abcdefABCDEF"
return hex.all { it in hexChars }
@Throws(ConfigParseException::class)
fun validateAmneziaSignaturePacket(value: String, fieldName: String) {
if (value.isBlank()) {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
var index = 0
// every tag mush start with <
while (index < value.length) {
if (value[index] != '<') {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
index++
val typeStart = index
while (index < value.length && value[index].isLetter()) {
index++
}
val tagType = value.substring(typeStart, index).lowercase()
if (tagType.isEmpty()) {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
// All tags except <t> require a space
if (tagType != "t") {
if (index >= value.length || value[index] != ' ') {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
index++
}
when (tagType) {
"b" -> index = parseStaticBytesTag(value, index, fieldName)
"r",
"rd",
"rc" -> index = parseRandomTag(value, index, fieldName)
"t" -> {} // timestamp has no parameter
else ->
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
// every tag must end with >
if (index >= value.length || value[index] != '>') {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
index++
}
}
private fun parseStaticBytesTag(value: String, start: Int, fieldName: String): Int {
var index = start
// must start with 0x
if (
index + 2 > value.length ||
!value.substring(index, index + 2).equals("0x", ignoreCase = true)
) {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
index += 2
val hexStart = index
while (index < value.length && value[index].isHexDigit()) {
index++
}
// must be a valid hex
val hexLength = index - hexStart
if (hexLength == 0 || hexLength % 2 != 0) {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
return index
}
private fun parseRandomTag(value: String, start: Int, fieldName: String): Int {
var index = start
val numStart = index
while (index < value.length && value[index].isDigit()) {
index++
}
// must have at least one digit
if (index == numStart) {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
// make sure it is a positive number
val size = value.substring(numStart, index).toIntOrNull()
if (size == null || size <= 0) {
throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, fieldName, value)
}
return index
}
private fun Char.isHexDigit(): Boolean =
this in '0'..'9' || this in 'a'..'f' || this in 'A'..'F'
}