@@ -4,16 +4,26 @@ import IPtProxy.SnowflakeClientConnected
44import IPtProxy.SnowflakeProxy
55import android.content.Context
66import android.os.Handler
7+ import com.netzarchitekten.upnp.UPnP
8+ import kotlinx.coroutines.CoroutineScope
9+ import kotlinx.coroutines.Dispatchers
10+ import kotlinx.coroutines.launch
11+ import kotlinx.coroutines.withContext
12+ import org.torproject.android.service.OrbotConstants
713import org.torproject.android.service.OrbotConstants.ONION_EMOJI
814import org.torproject.android.service.OrbotService
915import org.torproject.android.service.R
1016import org.torproject.android.service.util.Prefs
1117import org.torproject.android.service.util.showToast
1218import java.security.SecureRandom
19+ import kotlin.random.Random
1320
1421class SnowflakeProxyWrapper (private val context : Context ) {
22+
1523 private var proxy: SnowflakeProxy ? = null
1624
25+ private var mappedPorts = mutableListOf<Int >()
26+
1727 @Synchronized
1828 fun enableProxy (
1929 hasWifi : Boolean ,
@@ -22,37 +32,63 @@ class SnowflakeProxyWrapper(private val context: Context) {
2232 if (proxy != null ) return
2333 if (Prefs .limitSnowflakeProxyingWifi() && ! hasWifi) return
2434 if (Prefs .limitSnowflakeProxyingCharging() && ! hasPower) return
25- proxy = SnowflakeProxy ()
26- val stunServers = BuiltInBridges .getInstance(context)?.snowflake?.firstOrNull()?.ice
27- ?.split(" ," .toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() ? : emptyArray()
28- val stunUrl = stunServers[SecureRandom ().nextInt(stunServers.size)]
29-
30- proxy = SnowflakeProxy ()
31- with (proxy!! ) {
32- brokerUrl = OrbotService .getCdnFront(" snowflake-target-direct" )
33- capacity = 1L
34- pollInterval = 120L
35- stunServer = stunUrl
36- relayUrl = OrbotService .getCdnFront(" snowflake-relay-url" )
37- natProbeUrl = OrbotService .getCdnFront(" snowflake-nat-probe" )
38- clientConnected = SnowflakeClientConnected { onConnected() }
39- start()
40- }
4135
42- if (Prefs .showSnowflakeProxyMessage()) {
43- val message = context.getString(R .string.log_notice_snowflake_proxy_enabled)
44- Handler (context.mainLooper).post {
45- context.applicationContext.showToast(message)
36+ CoroutineScope (Dispatchers .IO ).launch {
37+ val start = Random .nextInt(49152 , 65536 - 2 )
38+
39+ for (port in (start.. start + 2 )) {
40+ if (UPnP .openPortUDP(port, OrbotConstants .TAG )) {
41+ mappedPorts.add(port)
42+ }
43+ }
44+
45+ // Snowflake Proxy needs Capacity * 2 + 1 = 3 consecutive ports mapped for unrestricted mode.
46+ // If we can't get all of these, remove the ones we have and
47+ // rather have Snowflake Proxy run in restricted mode.
48+ if (mappedPorts.size < 3 ) {
49+ releaseMappedPorts()
50+ }
51+
52+ val stunServers = BuiltInBridges .getInstance(context)?.snowflake?.firstOrNull()?.ice
53+ ?.split(" ," .toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() ? : emptyArray()
54+ val stunUrl = stunServers[SecureRandom ().nextInt(stunServers.size)]
55+
56+ proxy = SnowflakeProxy ()
57+ with (proxy!! ) {
58+ brokerUrl = OrbotService .getCdnFront(" snowflake-target-direct" )
59+ capacity = 1L
60+ pollInterval = 120L
61+ stunServer = stunUrl
62+ relayUrl = OrbotService .getCdnFront(" snowflake-relay-url" )
63+ natProbeUrl = OrbotService .getCdnFront(" snowflake-nat-probe" )
64+ clientConnected = SnowflakeClientConnected { onConnected() }
65+
66+ // TODO: Activate, when new IPtProxy is available.
67+ // Setting these to null or 0 is equivalent to not setting this at all.
68+ // ephemeralMinPort = mappedPorts.firstOrNull()
69+ // ephemeralMaxPort = mappedPorts.lastOrNull()
70+
71+ start()
72+ }
73+
74+ if (Prefs .showSnowflakeProxyMessage()) {
75+ val message = context.getString(R .string.log_notice_snowflake_proxy_enabled)
76+
77+ withContext(Dispatchers .Main ) {
78+ context.applicationContext.showToast(message)
79+ }
4680 }
4781 }
4882 }
4983
5084 @Synchronized
5185 fun stopProxy () {
5286 if (proxy == null ) return
53- proxy!! .stop()
87+ proxy? .stop()
5488 proxy = null
5589
90+ releaseMappedPorts()
91+
5692 if (Prefs .showSnowflakeProxyMessage()) {
5793 val message = context.getString(R .string.log_notice_snowflake_proxy_disabled)
5894 Handler (context.mainLooper).post {
@@ -72,6 +108,13 @@ class SnowflakeProxyWrapper(private val context: Context) {
72108 Handler (context.mainLooper).post {
73109 context.applicationContext.showToast(message)
74110 }
111+ }
112+
113+ private fun releaseMappedPorts () {
114+ for (port in mappedPorts) {
115+ UPnP .closePortUDP(port)
116+ }
75117
118+ mappedPorts = mutableListOf ()
76119 }
77120}
0 commit comments