Skip to content

Commit 46fcae2

Browse files
Ginder-Singhclaude
andcommitted
Fix WireGuard custom config LAN bypass and IPv6 traffic leak
- Added LAN bypass functionality for WireGuard custom configs by implementing createModifiedPeersForLanBypass() - Fixed WireGuard ParseException by properly extracting endpoint values from Optional objects - Created separate IPv4-only LAN bypass function (modifyAllowedIpsIPv4Only) for custom configs to prevent IPv6 parsing errors - Preserved IPv6 handling in regular server configs to prevent IPv6 traffic leaks - Added comprehensive debug logging for troubleshooting peer building process - Custom configs now properly exclude private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x) from routing - Regular server configs maintain full IPv4+IPv6 LAN bypass functionality Technical details: - Custom configs use IPv4-only public IP array to avoid "::0/0" ParseException - Regular server configs retain "::0/0" in publicIpV4Array for IPv6 traffic capture - Fixed API 21 compatibility by removing Optional.get() calls - Enhanced error handling for endpoint parsing and AllowedIPs modification 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 16ec4e7 commit 46fcae2

File tree

2 files changed

+185
-5
lines changed

2 files changed

+185
-5
lines changed

CLAUDE.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,55 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4242
./gradlew bundleFdroidRelease # Build F-Droid AAB
4343
```
4444

45+
### Building Specific Modules
46+
```bash
47+
# Mobile App
48+
./gradlew :mobile:assembleGoogleDebug # Build mobile debug APK
49+
./gradlew :mobile:assembleGoogleRelease # Build mobile release APK
50+
51+
# TV App
52+
./gradlew :tv:assembleGoogleDebug # Build TV debug APK
53+
./gradlew :tv:assembleGoogleRelease # Build TV release APK
54+
```
55+
56+
### Install and Launch Apps
57+
58+
#### Mobile App
59+
```bash
60+
# Build and install mobile app
61+
./gradlew :mobile:assembleGoogleDebug
62+
"$ANDROID_HOME/platform-tools/adb" install -r mobile/build/outputs/apk/google/debug/mobile-google-debug.apk
63+
64+
# Launch mobile app
65+
"$ANDROID_HOME/platform-tools/adb" shell am start -n com.windscribe.vpn/com.windscribe.mobile.ui.AppStartActivity
66+
```
67+
68+
#### TV App
69+
```bash
70+
# Build and install TV app
71+
./gradlew :tv:assembleGoogleDebug
72+
"$ANDROID_HOME/platform-tools/adb" install -r tv/build/outputs/apk/google/debug/tv-google-debug.apk
73+
74+
# Launch TV app
75+
"$ANDROID_HOME/platform-tools/adb" shell am start -n com.windscribe.vpn/com.windscribe.tv.splash.SplashActivity
76+
```
77+
78+
#### ADB Commands
79+
```bash
80+
# Check connected devices
81+
"$ANDROID_HOME/platform-tools/adb" devices
82+
83+
# Target specific device (if multiple connected)
84+
"$ANDROID_HOME/platform-tools/adb" -s emulator-5554 install -r app.apk
85+
"$ANDROID_HOME/platform-tools/adb" -s emulator-5554 shell am start -n package/activity
86+
87+
# Check if app is running
88+
"$ANDROID_HOME/platform-tools/adb" shell ps | grep windscribe
89+
90+
# View logs
91+
"$ANDROID_HOME/platform-tools/adb" logcat -s "vpn" -v time
92+
```
93+
4594
### Testing
4695
```bash
4796
./gradlew test # Run unit tests

base/src/main/java/com/windscribe/vpn/backend/utils/VPNProfileCreator.kt

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class VPNProfileCreator @Inject constructor(
8383
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
8484
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
8585
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
86-
"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", "10.255.255.0/24","::0/0"
86+
"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", "::0/0", "10.255.255.0/24"
8787
)
8888

8989
fun createIkEV2Profile(
@@ -412,8 +412,15 @@ class VPNProfileCreator @Inject constructor(
412412

413413
val reader: Reader = StringReader(configFile.content)
414414
val bufferedReader = BufferedReader(reader)
415-
val config: Config = bufferedReader.use {
416-
Config.parse(bufferedReader)
415+
val config: Config = try {
416+
bufferedReader.use {
417+
Config.parse(bufferedReader)
418+
}
419+
} catch (e: Exception) {
420+
logger.error("Full WireGuard config parse exception: ${e.javaClass.simpleName}: ${e.message}")
421+
logger.error("Stack trace: ${e.stackTrace.joinToString("\n")}")
422+
logger.error("Config content: ${configFile.content}")
423+
throw e
417424
}
418425
interFaceBuilder.parsePrivateKey(config.getInterface().keyPair.privateKey.toBase64())
419426
interFaceBuilder.addAddresses(config.getInterface().addresses)
@@ -426,10 +433,16 @@ class VPNProfileCreator @Inject constructor(
426433
} else {
427434
interFaceBuilder.addDnsServers(config.getInterface().dnsServers)
428435
}
429-
val configWithSettings = Config.Builder()
430-
.addPeers(config.peers)
436+
val configWithSettings = try {
437+
val modifiedPeers = createModifiedPeersForLanBypass(config.peers)
438+
Config.Builder()
439+
.addPeers(modifiedPeers)
431440
.setInterface(interFaceBuilder.build())
432441
.build()
442+
} catch (e: Exception) {
443+
logger.error("Exception creating config with LAN bypass: ${e.javaClass.simpleName}: ${e.message}")
444+
throw e
445+
}
433446
val lastSelectedLocation =
434447
LastSelectedLocation(configFile.getPrimaryKey(), nickName = configFile.name)
435448
saveSelectedLocation(lastSelectedLocation)
@@ -576,6 +589,96 @@ class VPNProfileCreator @Inject constructor(
576589
return builder.build()
577590
}
578591

592+
private fun createModifiedPeersForLanBypass(originalPeers: List<Peer>): List<Peer> {
593+
return if (preferencesHelper.lanByPass) {
594+
originalPeers.mapIndexed { index, peer ->
595+
try {
596+
val builder = Peer.Builder()
597+
builder.parsePublicKey(peer.publicKey.toBase64())
598+
599+
// Handle endpoint safely - extract value from Optional
600+
val endpointStr = try {
601+
if (peer.endpoint != null && peer.endpoint.isPresent) {
602+
val endpoint = peer.endpoint.orElse(null)
603+
if (endpoint != null) {
604+
"${endpoint.host}:${endpoint.port}"
605+
} else {
606+
""
607+
}
608+
} else {
609+
""
610+
}
611+
} catch (e: Exception) {
612+
""
613+
}
614+
if (endpointStr.isNotEmpty()) {
615+
builder.parseEndpoint(endpointStr)
616+
}
617+
618+
// Handle persistent keepalive - check if present first
619+
val keepalive = try {
620+
if (peer.persistentKeepalive != null && peer.persistentKeepalive.isPresent) {
621+
peer.persistentKeepalive.orElse(0)
622+
} else {
623+
0
624+
}
625+
} catch (e: Exception) {
626+
0
627+
}
628+
builder.setPersistentKeepalive(keepalive)
629+
630+
// Format allowedIPs properly - separate IPv4 and IPv6
631+
val allowedIpsString = peer.allowedIps.joinToString(", ")
632+
val dnsRoutes = ""
633+
634+
// Split IPv4 and IPv6, only modify IPv4 part
635+
val ipParts = allowedIpsString.split(",").map { it.trim() }
636+
val ipv4Parts = ipParts.filter { !it.contains(":") }
637+
val ipv6Parts = ipParts.filter { it.contains(":") }
638+
639+
// Only modify IPv4 part for LAN bypass - use IPv4-only function
640+
val modifiedIpv4 = if (ipv4Parts.isNotEmpty()) {
641+
modifyAllowedIpsIPv4Only(ipv4Parts.joinToString(", "), dnsRoutes)
642+
} else {
643+
""
644+
}
645+
646+
// Combine modified IPv4 with original IPv6 - clean up trailing commas
647+
val finalAllowedIps = if (modifiedIpv4.isNotEmpty() && ipv6Parts.isNotEmpty()) {
648+
"${modifiedIpv4.trim(' ', ',')}, ${ipv6Parts.joinToString(", ")}"
649+
} else if (modifiedIpv4.isNotEmpty()) {
650+
modifiedIpv4.trim(' ', ',')
651+
} else if (ipv6Parts.isNotEmpty()) {
652+
ipv6Parts.joinToString(", ")
653+
} else {
654+
allowedIpsString
655+
}
656+
657+
builder.parseAllowedIPs(finalAllowedIps)
658+
659+
// Handle pre-shared key if present - safely check Optional
660+
try {
661+
if (peer.preSharedKey != null && peer.preSharedKey.isPresent) {
662+
val key = peer.preSharedKey.orElse(null)
663+
if (key != null) {
664+
builder.parsePreSharedKey(key.toBase64())
665+
}
666+
}
667+
} catch (e: Exception) {
668+
// No pre-shared key present, which is valid
669+
}
670+
671+
builder.build()
672+
} catch (e: Exception) {
673+
logger.error("Exception building peer $index: ${e.javaClass.simpleName}: ${e.message}")
674+
throw e
675+
}
676+
}
677+
} else {
678+
originalPeers
679+
}
680+
}
681+
579682
private fun getIkev2Credentials(): Pair<String, String> {
580683
val serverCredentials = getServerCredentials(true)
581684
val mUsername: String
@@ -729,6 +832,34 @@ class VPNProfileCreator @Inject constructor(
729832
return Attribute.join(output)
730833
}
731834

835+
private fun modifyAllowedIpsIPv4Only(allowedIps: String, dnsRoutes: String): String {
836+
// Create IPv4-only array by filtering out IPv6 addresses
837+
val ipv4OnlyNetworks = publicIpV4Array.filter { !it.contains(":") }.toTypedArray()
838+
val ipv4PublicNetworks = HashSet(listOf(*ipv4OnlyNetworks))
839+
val ipv4Wildcard = "0.0.0.0/0"
840+
val allNetworks = HashSet(listOf(ipv4Wildcard))
841+
val input: Collection<String> = HashSet(listOf(*Attribute.split(allowedIps)))
842+
val outputSize = input.size - allNetworks.size + ipv4PublicNetworks.size
843+
val output: MutableCollection<String?> = LinkedHashSet(outputSize)
844+
var replaced = false
845+
for (network in input) {
846+
if (allNetworks.contains(network)) {
847+
if (!replaced) {
848+
for (replacement in ipv4PublicNetworks) {
849+
if (!output.contains(replacement)) {
850+
output.add(replacement)
851+
}
852+
}
853+
replaced = true
854+
}
855+
} else if (!output.contains(network)) {
856+
output.add(network)
857+
}
858+
}
859+
output.addAll(listOf(*Attribute.split(dnsRoutes)))
860+
return Attribute.join(output)
861+
}
862+
732863
private fun setSplitMode(profile: VpnProfile) {
733864
if (preferencesHelper.splitTunnelToggle && (preferencesHelper.splitRoutingMode == PreferencesKeyConstants.EXCLUSIVE_MODE)) {
734865
preferencesHelper.lastConnectedUsingSplit = true

0 commit comments

Comments
 (0)