2020// data\tun-state.txt data\tun-stop.txt data\tun-result.txt
2121//
2222// Routes are added with the modern NetIO API (SetIpForwardEntry2, netioapi.dll)
23- // when it is available; otherwise we fall back to route.exe. netioapi is present
24- // on normal Windows 11 and makes the ~10k relay routes fast to add.
23+ // when it is available; otherwise we fall back to the legacy iphlpapi API
24+ // (CreateIpForwardEntry), which exists on every Windows. Both are fast enough
25+ // for the several-thousand relay routes.
2526//
2627// Ports default to the launcher's 9050/9051 but can be overridden with the
2728// TUN_SOCKS_PORT / TUN_CTRL_PORT environment variables (used for testing).
@@ -63,6 +64,51 @@ internal static class Program
6364 private static readonly int SocksPort = GetEnvPort ( "TUN_SOCKS_PORT" , 9050 ) ;
6465 private static readonly int CtrlPort = GetEnvPort ( "TUN_CTRL_PORT" , 9051 ) ;
6566
67+ // Only one keeper may run at a time: a second "on" (e.g. the user pressing
68+ // T twice in a row) used to race the first one, creating a second adapter
69+ // named "TorBoostTun 1" that the fixed-name netsh calls then mis-targeted,
70+ // which made the default-route step fail and left the state file empty.
71+ private static readonly Mutex TunMutex = new Mutex ( true , @"Local\TorBoostTunHelper" ) ;
72+
73+ [ DllImport ( "kernel32.dll" , CharSet = CharSet . Unicode ) ]
74+ private static extern IntPtr LoadLibrary ( string path ) ;
75+ [ DllImport ( "kernel32.dll" , SetLastError = true ) ]
76+ private static extern IntPtr GetProcAddress ( IntPtr module , string name ) ;
77+ [ UnmanagedFunctionPointer ( CallingConvention . StdCall ) ]
78+ private delegate uint WintunDeleteAdapterFn ( [ MarshalAs ( UnmanagedType . LPWStr ) ] string name ) ;
79+
80+ private static bool WintunDeleteAdapterByName ( string name )
81+ {
82+ try
83+ {
84+ IntPtr h = LoadLibrary ( Path . Combine ( DataDir , "wintun.dll" ) ) ;
85+ if ( h == IntPtr . Zero ) return false ;
86+ IntPtr fn = GetProcAddress ( h , "WintunDeleteAdapter" ) ;
87+ if ( fn == IntPtr . Zero ) return false ;
88+ var del = ( WintunDeleteAdapterFn ) Marshal . GetDelegateForFunctionPointer ( fn , typeof ( WintunDeleteAdapterFn ) ) ;
89+ return del ( name ) == 0 ;
90+ }
91+ catch { return false ; }
92+ }
93+
94+ private static void KillStaleTun2Socks ( )
95+ {
96+ try
97+ {
98+ string dir = DataDir . TrimEnd ( '\\ ' ) ;
99+ foreach ( Process p in Process . GetProcessesByName ( "tun2socks" ) )
100+ {
101+ try
102+ {
103+ if ( p . MainModule . FileName . TrimEnd ( '\\ ' ) . StartsWith ( dir , StringComparison . OrdinalIgnoreCase ) )
104+ p . Kill ( ) ;
105+ }
106+ catch { }
107+ }
108+ }
109+ catch { }
110+ }
111+
66112 private const uint NO_ERROR = 0 ;
67113 private const uint MIB_IPPROTO_NETMGMT = 3 ;
68114 private const uint INFINITE_LIFE = 0xFFFFFFFF ;
@@ -135,6 +181,10 @@ private struct MibIpForwardRow
135181
136182 [ DllImport ( "iphlpapi.dll" ) ]
137183 private static extern uint GetBestRoute ( uint dest , uint source , out MibIpForwardRow row ) ;
184+ [ DllImport ( "iphlpapi.dll" ) ]
185+ private static extern uint CreateIpForwardEntry ( ref MibIpForwardRow row ) ;
186+ [ DllImport ( "iphlpapi.dll" ) ]
187+ private static extern uint DeleteIpForwardEntry ( ref MibIpForwardRow row ) ;
138188
139189 private static int GetEnvPort ( string name , int def )
140190 {
@@ -284,16 +334,25 @@ private static void AddTrailingPid(string line, HashSet<int> pids)
284334 catch { }
285335 }
286336
287- private static int GetTunIfIndex ( )
337+ // Finds the tun2socks adapter and its REAL name. Windows may display it
338+ // as "TorBoostTun 1" (or worse) when a stale adapter already exists, so
339+ // every netsh call must use the discovered name, never the fixed one.
340+ private static bool GetTunAdapter ( out int ifIndex , out string name )
288341 {
342+ ifIndex = - 1 ;
343+ name = "" ;
289344 string o = Run ( "netsh.exe" , "interface ipv4 show interfaces" ) ;
345+ Regex re = new Regex ( @"^\s*(\d+)\s+\d+\s+\d+\s+(\S+(?:\s+\S+)?)\s+(.+)$" ) ;
290346 foreach ( string line in o . Split ( '\n ' ) )
291347 {
292348 if ( line . IndexOf ( TunName , StringComparison . OrdinalIgnoreCase ) < 0 ) continue ;
293- string [ ] p = line . Split ( new [ ] { ' ' , '\t ' } , StringSplitOptions . RemoveEmptyEntries ) ;
294- if ( p . Length >= 2 && p [ 0 ] . All ( c => char . IsDigit ( c ) ) ) return int . Parse ( p [ 0 ] ) ;
349+ Match m = re . Match ( line ) ;
350+ if ( ! m . Success ) continue ;
351+ ifIndex = int . Parse ( m . Groups [ 1 ] . Value ) ;
352+ name = m . Groups [ 3 ] . Value . Trim ( ) ;
353+ return true ;
295354 }
296- return - 1 ;
355+ return false ;
297356 }
298357
299358 private static string IfIpToString ( uint v )
@@ -307,12 +366,6 @@ private static uint IpToUInt(string ip)
307366 return BitConverter . ToUInt32 ( b , 0 ) ;
308367 }
309368
310- private static string MaskFromPlen ( int plen )
311- {
312- uint m = plen == 0 ? 0 : ( uint ) ( 0xFFFFFFFF << ( 32 - plen ) ) ;
313- return IfIpToString ( m ) ;
314- }
315-
316369 private static MibIpForwardRow2 MakeRow2 ( uint dest , byte plen , uint nextHop , int ifIndex , uint metric )
317370 {
318371 return new MibIpForwardRow2
@@ -336,15 +389,59 @@ private static MibIpForwardRow2 MakeRow2(uint dest, byte plen, uint nextHop, int
336389 } ;
337390 }
338391
339- private static bool RouteEx ( string op , uint dest , int plen , uint nextHop , int ifIndex , uint metric )
392+ private static uint MaskUint ( int plen )
340393 {
341- string mask = MaskFromPlen ( plen ) ;
342- string gw = nextHop == 0 ? "0.0.0.0" : IfIpToString ( nextHop ) ;
343- string cmd = op + " " + IfIpToString ( dest ) + " mask " + mask + " " + gw + " IF " + ifIndex ;
344- if ( op == "add" ) cmd += " metric " + metric ;
345- string output = Run ( "route.exe" , cmd ) ;
346- if ( op == "add" ) return output . IndexOf ( "OK!" , StringComparison . OrdinalIgnoreCase ) >= 0 ;
347- return output . IndexOf ( "not found" , StringComparison . OrdinalIgnoreCase ) < 0 ;
394+ return plen == 0 ? 0 : ( uint ) ( 0xFFFFFFFF << ( 32 - plen ) ) ;
395+ }
396+
397+ // Legacy iphlpapi route API (MIB_IPFORWARDROW). Some trimmed/older
398+ // Windows builds lack netioapi.dll, and the old route.exe fallback was
399+ // far too slow for the several-thousand relay /32 routes (one spawned
400+ // process per route). CreateIpForwardEntry is present on every Windows
401+ // but is picky: the route must be MIB_IPROUTE_TYPE_INDIRECT (4) for a
402+ // gateway route and dwForwardMetric1 must hold the EFFECTIVE metric
403+ // (interface metric + route metric), exactly like route.exe computes it.
404+ private static bool LegacyRoute ( bool add , uint dest , int plen , uint nextHop , int ifIndex , uint metric )
405+ {
406+ var row = new MibIpForwardRow
407+ {
408+ dest = dest ,
409+ mask = MaskUint ( plen ) ,
410+ nextHop = nextHop ,
411+ ifIndex = ifIndex ,
412+ metric1 = ( uint ) ( InterfaceMetric ( ifIndex ) + metric ) ,
413+ type = 4 , // MIB_IPROUTE_TYPE_INDIRECT (via gateway)
414+ proto = 3 // MIB_IPROTO_NETMGMT
415+ } ;
416+ uint rc = add ? CreateIpForwardEntry ( ref row ) : DeleteIpForwardEntry ( ref row ) ;
417+ return rc == NO_ERROR ;
418+ }
419+
420+ private static readonly Dictionary < int , int > IfMetricCache = new Dictionary < int , int > ( ) ;
421+
422+ private static int InterfaceMetric ( int ifIndex )
423+ {
424+ int m ;
425+ if ( IfMetricCache . TryGetValue ( ifIndex , out m ) ) return m ;
426+ m = GetInterfaceMetric ( ifIndex ) ;
427+ IfMetricCache [ ifIndex ] = m ;
428+ return m ;
429+ }
430+
431+ private static int GetInterfaceMetric ( int ifIndex )
432+ {
433+ string o = Run ( "netsh.exe" , "interface ipv4 show interfaces" ) ;
434+ Regex re = new Regex ( @"^\s*(\d+)\s+(\d+)\s+\d+\s+\S+\s+(.+)$" ) ;
435+ foreach ( string line in o . Split ( '\n ' ) )
436+ {
437+ Match m = re . Match ( line ) ;
438+ if ( m . Success && int . Parse ( m . Groups [ 1 ] . Value ) == ifIndex )
439+ {
440+ int met ;
441+ if ( int . TryParse ( m . Groups [ 2 ] . Value , out met ) ) return met ;
442+ }
443+ }
444+ return 0 ;
348445 }
349446
350447 private static bool AddRoute ( uint dest , int plen , uint nextHop , int ifIndex , uint metric )
@@ -354,7 +451,7 @@ private static bool AddRoute(uint dest, int plen, uint nextHop, int ifIndex, uin
354451 MibIpForwardRow2 r = MakeRow2 ( dest , ( byte ) plen , nextHop , ifIndex , metric ) ;
355452 return SetIpForwardEntry2 ( ref r ) == NO_ERROR ;
356453 }
357- return RouteEx ( "add" , dest , plen , nextHop , ifIndex , metric ) ;
454+ return LegacyRoute ( true , dest , plen , nextHop , ifIndex , metric ) ;
358455 }
359456
360457 private static bool DeleteRoute ( uint dest , int plen , uint nextHop , int ifIndex )
@@ -364,7 +461,7 @@ private static bool DeleteRoute(uint dest, int plen, uint nextHop, int ifIndex)
364461 MibIpForwardRow2 r = MakeRow2 ( dest , ( byte ) plen , nextHop , ifIndex , 0 ) ;
365462 return DeleteIpForwardEntry2 ( ref r ) == NO_ERROR ;
366463 }
367- return RouteEx ( "delete" , dest , plen , nextHop , ifIndex , 0 ) ;
464+ return LegacyRoute ( false , dest , plen , nextHop , ifIndex , 0 ) ;
368465 }
369466
370467 private static bool IsPublicIpv4 ( string s )
@@ -446,6 +543,9 @@ private static bool Enable(out Process tun, out int tunIf, out int physIf, out s
446543 if ( GetStateValue ( ReadState ( ) , "status" ) == "on" ) { WriteResult ( "error: TUN is already on" ) ; return false ; }
447544 if ( ! Port53Free ( ) ) { WriteResult ( "error: port 53 is already in use on this machine" ) ; return false ; }
448545
546+ WintunDeleteAdapterByName ( TunName ) ;
547+ KillStaleTun2Socks ( ) ;
548+
449549 MibIpForwardRow physRow ;
450550 uint rc = GetBestRoute ( 0 , 0 , out physRow ) ;
451551 if ( rc != NO_ERROR ) { WriteResult ( "error: no IPv4 default route (getbestroute " + rc + ")" ) ; return false ; }
@@ -481,31 +581,26 @@ private static bool Enable(out Process tun, out int tunIf, out int physIf, out s
481581 return false ;
482582 }
483583
584+ string tunName = "" ;
484585 for ( int i = 0 ; i < 20 ; i ++ )
485586 {
486587 Thread . Sleep ( 500 ) ;
487588 if ( tun . HasExited ) break ;
488- tunIf = GetTunIfIndex ( ) ;
489- if ( tunIf > 0 ) break ;
589+ if ( GetTunAdapter ( out tunIf , out tunName ) ) break ;
490590 }
491- if ( tunIf <= 0 )
591+ if ( tunIf <= 0 || tunName . Length == 0 )
492592 {
493593 try { tun . Kill ( ) ; } catch { }
594+ WintunDeleteAdapterByName ( TunName ) ;
494595 WriteResult ( "error: wintun adapter was not created (is tun2socks running?)" ) ;
495596 return false ;
496597 }
497598
498- Run ( "netsh.exe" , "interface ipv4 set address name=" + TunName + " source=static address=" + TunAddr + " mask=" + TunMask ) ;
499- Run ( "netsh.exe" , "interface ipv4 set dnsservers name=" + TunName + " source=static address=127.0.0.1 register=none validate=no" ) ;
500- Run ( "netsh.exe" , "interface ipv4 set interface " + TunName + " metric=1" ) ;
501-
502- if ( ! AddRoute ( 0 , 0 , IpToUInt ( TunAddr ) , tunIf , 1 ) )
503- {
504- try { tun . Kill ( ) ; } catch { }
505- WriteResult ( "error: could not add default route via " + TunName ) ;
506- return false ;
507- }
599+ Run ( "netsh.exe" , "interface ipv4 set address name=" + tunName + " source=static address=" + TunAddr + " mask=" + TunMask ) ;
600+ Run ( "netsh.exe" , "interface ipv4 set dnsservers name=" + tunName + " source=static address=127.0.0.1 register=none validate=no" ) ;
601+ Run ( "netsh.exe" , "interface ipv4 set interface " + tunName + " metric=1" ) ;
508602
603+ AddRoute ( 0 , 0 , IpToUInt ( TunAddr ) , tunIf , 1 ) ;
509604 MibIpForwardRow check = new MibIpForwardRow ( ) ;
510605 bool switched = false ;
511606 for ( int i = 0 ; i < 10 ; i ++ )
@@ -517,7 +612,8 @@ private static bool Enable(out Process tun, out int tunIf, out int physIf, out s
517612 if ( ! switched )
518613 {
519614 try { tun . Kill ( ) ; } catch { }
520- WriteResult ( "error: default route did not switch to " + TunName +
615+ WintunDeleteAdapterByName ( tunName ) ;
616+ WriteResult ( "error: default route did not switch to " + tunName +
521617 " (still ifIndex " + check . ifIndex + ", gw " + IfIpToString ( check . nextHop ) + ")" ) ;
522618 return false ;
523619 }
@@ -568,6 +664,8 @@ private static void TeardownFromState(string state)
568664 }
569665 }
570666
667+ KillStaleTun2Socks ( ) ;
668+ WintunDeleteAdapterByName ( TunName ) ;
571669 WriteState ( "status=off" ) ;
572670 try { if ( File . Exists ( StopFile ) ) File . Delete ( StopFile ) ; } catch { }
573671 }
@@ -624,7 +722,19 @@ private static int RunKeeper()
624722 private static int Main ( string [ ] args )
625723 {
626724 string arg = args . Length > 0 ? args [ 0 ] . ToLowerInvariant ( ) : "status" ;
627- if ( arg == "on" ) return RunKeeper ( ) ;
725+ if ( arg == "on" )
726+ {
727+ bool acquired ;
728+ try { acquired = TunMutex . WaitOne ( 0 ) ; }
729+ catch ( AbandonedMutexException ) { acquired = true ; }
730+ if ( ! acquired )
731+ {
732+ WriteResult ( "error: TUN is already on (another instance is running)" ) ;
733+ return 1 ;
734+ }
735+ try { return RunKeeper ( ) ; }
736+ finally { try { TunMutex . ReleaseMutex ( ) ; } catch { } }
737+ }
628738 if ( arg == "off" )
629739 {
630740 TeardownFromState ( ReadState ( ) ) ;
0 commit comments