Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 90 additions & 80 deletions android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.ArrayDeque

/**
* Singleton bridge between Kotlin UI and Go core.
Expand Down Expand Up @@ -83,6 +84,68 @@ object VpnManager {
private val logBuffer = ArrayDeque<LogEntry>(MAX_LOG_LINES)
private var logBufferVersion = 0L

private val INDEXED_PROGRESS_REGEX = Regex(
"(?:scan|scanning|resolver|resolvers|mtu|accepted|rejected).{0,40}?(\\d+)\\s*/\\s*(\\d+)",
setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
)
private val TOTAL_CANDIDATES_REGEX = Regex(
"(?:valid\\s+resolvers|resolvers\\s+for\\s+scan|scan\\s+pool|resolver\\s+pool|total\\s+resolvers).{0,20}?(\\d+)",
RegexOption.IGNORE_CASE
)
private val SCAN_TOTALS_REGEX = Regex(
"via\\s+([^\\s|]+)\\s*\\|.*totals:\\s*valid=(\\d+),\\s*rejected=(\\d+)",
RegexOption.IGNORE_CASE
)
private val ACTIVE_RESOLVERS_REGEX = Regex(
"Active Resolvers\\s*[:=]\\s*[^\\d-]*(\\d+)",
RegexOption.IGNORE_CASE
)
private val TOTAL_ACTIVE_REGEX = Regex(
"total\\s+active\\s*[:=]\\s*[^\\d-]*(\\d+)",
RegexOption.IGNORE_CASE
)
private val REMAINING_REGEX = Regex(
"remaining\\s*[:=]\\s*[^\\d-]*(\\d+)",
RegexOption.IGNORE_CASE
)
private val SYNCED_MTU_REGEX = Regex(
"Selected Synced Upload MTU:\\s*(\\d+)\\s*\\|\\s*Selected Synced Download MTU:\\s*(\\d+)",
RegexOption.IGNORE_CASE
)
private val TIMESTAMP_CANDIDATES = listOf(
TimestampCandidate(
Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)(.*)$"),
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd HH:mm:ss.SSS"
),
TimestampCandidate(
Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)(.*)$"),
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd HH:mm:ss"
),
TimestampCandidate(
Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+UTC(.*)$"),
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm:ss"
),
TimestampCandidate(
Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s(.*)$"),
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm:ss"
),
TimestampCandidate(
Regex("^(\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2})(.*)$"),
"yyyy/MM/dd HH:mm:ss",
"yyyy/MM/dd HH:mm:ss"
)
)

private data class TimestampCandidate(
val regex: Regex,
val inputPattern: String,
val outputPattern: String
)

fun updateState(newState: VpnState) {
_state.value = newState
if (newState == VpnState.CONNECTED) {
Expand Down Expand Up @@ -245,36 +308,24 @@ object VpnManager {
}

private fun parseScanLine(line: String) {
val indexedProgressMatch = Regex(
"(?:scan|scanning|resolver|resolvers|mtu|accepted|rejected).{0,40}?(\\d+)\\s*/\\s*(\\d+)",
setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
).find(line)
if (indexedProgressMatch != null) {
val total = indexedProgressMatch.groupValues[2].toIntOrNull()
INDEXED_PROGRESS_REGEX.find(line)?.let { match ->
val total = match.groupValues[2].toIntOrNull()
if (total != null && total > 0) {
_scanStatus.value = _scanStatus.value.copy(scanTotalFromCore = total)
}
}

val totalCandidatesMatch = Regex(
"(?:valid\\s+resolvers|resolvers\\s+for\\s+scan|scan\\s+pool|resolver\\s+pool|total\\s+resolvers).{0,20}?(\\d+)",
RegexOption.IGNORE_CASE
).find(line)
if (totalCandidatesMatch != null) {
val total = totalCandidatesMatch.groupValues[1].toIntOrNull()
TOTAL_CANDIDATES_REGEX.find(line)?.let { match ->
val total = match.groupValues[1].toIntOrNull()
if (total != null && total > 0) {
_scanStatus.value = _scanStatus.value.copy(scanTotalFromCore = total)
}
}

val scanMatch = Regex(
"via\\s+([^\\s|]+)\\s*\\|.*totals:\\s*valid=(\\d+),\\s*rejected=(\\d+)",
RegexOption.IGNORE_CASE
).find(line)
if (scanMatch != null) {
val resolver = scanMatch.groupValues[1]
val valid = scanMatch.groupValues[2].toIntOrNull() ?: _scanStatus.value.validCount
val rejected = scanMatch.groupValues[3].toIntOrNull() ?: _scanStatus.value.rejectedCount
SCAN_TOTALS_REGEX.find(line)?.let { match ->
val resolver = match.groupValues[1]
val valid = match.groupValues[2].toIntOrNull() ?: _scanStatus.value.validCount
val rejected = match.groupValues[3].toIntOrNull() ?: _scanStatus.value.rejectedCount
val decision = when {
line.contains("Accepted", ignoreCase = true) -> "Accepted"
line.contains("Rejected", ignoreCase = true) -> "Rejected"
Expand All @@ -290,47 +341,31 @@ object VpnManager {
return
}

val activeResolversMatch = Regex(
"Active Resolvers\\s*[:=]\\s*[^\\d-]*(\\d+)",
RegexOption.IGNORE_CASE
).find(line)
if (activeResolversMatch != null) {
ACTIVE_RESOLVERS_REGEX.find(line)?.let { match ->
_scanStatus.value = _scanStatus.value.copy(
activeResolvers = activeResolversMatch.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers
activeResolvers = match.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers
)
return
}

val totalActiveMatch = Regex(
"total\\s+active\\s*[:=]\\s*[^\\d-]*(\\d+)",
RegexOption.IGNORE_CASE
).find(line)
if (totalActiveMatch != null) {
TOTAL_ACTIVE_REGEX.find(line)?.let { match ->
_scanStatus.value = _scanStatus.value.copy(
activeResolvers = totalActiveMatch.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers
activeResolvers = match.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers
)
return
}

val remainingMatch = Regex(
"remaining\\s*[:=]\\s*[^\\d-]*(\\d+)",
RegexOption.IGNORE_CASE
).find(line)
if (remainingMatch != null) {
REMAINING_REGEX.find(line)?.let { match ->
_scanStatus.value = _scanStatus.value.copy(
activeResolvers = remainingMatch.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers
activeResolvers = match.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers
)
return
}

val syncedMatch = Regex(
"Selected Synced Upload MTU:\\s*(\\d+)\\s*\\|\\s*Selected Synced Download MTU:\\s*(\\d+)",
RegexOption.IGNORE_CASE
).find(line)
if (syncedMatch != null) {
SYNCED_MTU_REGEX.find(line)?.let { match ->
_scanStatus.value = _scanStatus.value.copy(
syncedUploadMtu = syncedMatch.groupValues[1].toIntOrNull() ?: 0,
syncedDownloadMtu = syncedMatch.groupValues[2].toIntOrNull() ?: 0
syncedUploadMtu = match.groupValues[1].toIntOrNull() ?: 0,
syncedDownloadMtu = match.groupValues[2].toIntOrNull() ?: 0
)
return
}
Expand All @@ -348,49 +383,24 @@ object VpnManager {
}

private fun normalizeLogTimestampToLocal(line: String): String {
val candidates = listOf(
// Example: 2026-04-05T10:20:30.123Z
Triple(
Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)(.*)$"),
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd HH:mm:ss.SSS"
),
// Example: 2026-04-05T10:20:30Z
Triple(
Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)(.*)$"),
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd HH:mm:ss"
),
// Example: 2026-04-05 10:20:30 UTC (check UTC variant first)
Triple(
Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+UTC(.*)$"),
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm:ss"
),
// Example: 2026-04-05 10:20:30 (from mtu_logging.go, no UTC)
Triple(
Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s(.*)$"),
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm:ss"
),
// Example: 2026/04/05 10:20:30 (from logger.go)
Triple(
Regex("^(\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2})(.*)$"),
"yyyy/MM/dd HH:mm:ss",
"yyyy/MM/dd HH:mm:ss"
)
)
if (!startsWithTimestamp(line)) return line

for ((regex, inputFormat, outputFormat) in candidates) {
val match = regex.find(line) ?: continue
for (candidate in TIMESTAMP_CANDIDATES) {
val match = candidate.regex.find(line) ?: continue
val utcStamp = match.groupValues[1]
val suffix = match.groupValues[2]
val localStamp = convertUtcToLocal(utcStamp, inputFormat, outputFormat) ?: continue
val localStamp = convertUtcToLocal(utcStamp, candidate.inputPattern, candidate.outputPattern) ?: continue
return "$localStamp$suffix"
}
return line
}

private fun startsWithTimestamp(line: String): Boolean {
if (line.length < 19) return false
if (!line[0].isDigit() || !line[1].isDigit() || !line[2].isDigit() || !line[3].isDigit()) return false
return (line[4] == '-' && line[7] == '-') || (line[4] == '/' && line[7] == '/')
}

private fun convertUtcToLocal(
utcValue: String,
inputPattern: String,
Expand Down
Loading