Skip to content

Commit 8c9557b

Browse files
author
Delta-Kronecker
committed
tun-helper: fix double-run race + stale-adapter leftovers; fast relay routes without netioapi.dll
- named mutex blocks a second 'on' keeper (pressing T twice raced and produced a 'TorBoostTun 1' adapter that fixed-name netsh mis-targeted, leaving a live tunnel with an empty state file that could not turn off) - resolve the REAL adapter name from ifIndex for every netsh call - delete stale TorBoostTun adapters (wintun API) and orphaned tun2socks before enabling, and on enable failure - relay routes now use the legacy iphlpapi API (CreateIpForwardEntry) when netioapi.dll is missing: this machine lacks it and the route.exe fallback took minutes per session; 7681 routes now add/remove in ~8s. The legacy API needs MIB_IPROUTE_TYPE_INDIRECT and the effective metric (ifmetric+metric), replicated from route.exe's own entries - launcher: T toggles off whenever a tun-helper keeper is running, even if the state file is stale/empty
1 parent 40801a3 commit 8c9557b

2 files changed

Lines changed: 172 additions & 46 deletions

File tree

scripts/start-tor.cs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ private static string ReadTunState()
111111
return "off";
112112
}
113113

114+
// TUN counts as ON when the state file says so OR a tun-helper keeper is
115+
// actually running. The state file can be emptied/left stale by a failed
116+
// enable; only trusting it made "T" keep re-enabling instead of turning
117+
// the tunnel off.
118+
private static bool TunActive()
119+
{
120+
if (ReadTunState() == "on") return true;
121+
try
122+
{
123+
foreach (Process p in Process.GetProcessesByName("tun-helper"))
124+
if (!p.HasExited) return true;
125+
}
126+
catch { }
127+
return false;
128+
}
129+
114130
private static string ReadTunResult()
115131
{
116132
try { if (File.Exists(TunResultFile)) return File.ReadAllText(TunResultFile).Trim(); }
@@ -141,15 +157,15 @@ private static bool SpawnElevated(string args, bool wait)
141157

142158
private static void ToggleTun()
143159
{
144-
if (ReadTunState() == "on")
160+
if (TunActive())
145161
{
146162
Console.WriteLine(" [i] Turning TUN OFF...");
147163
try { File.WriteAllText(TunStopFile, "stop", new UTF8Encoding(false)); } catch { }
148-
for (int i = 0; i < 40 && ReadTunState() == "on"; i++) Thread.Sleep(500);
149-
if (ReadTunState() != "on") { Console.WriteLine(" [i] TUN OFF"); return; }
164+
for (int i = 0; i < 40 && TunActive(); i++) Thread.Sleep(500);
165+
if (!TunActive()) { Console.WriteLine(" [i] TUN OFF"); return; }
150166
Console.WriteLine(" [i] keeper did not stop; forcing teardown (UAC)...");
151167
if (SpawnElevated("off", true))
152-
Console.WriteLine(" [i] " + (ReadTunState() == "on" ? "TUN still ON - check data\\tun-result.txt" : "TUN OFF"));
168+
Console.WriteLine(" [i] " + (TunActive() ? "TUN still ON - check data\\tun-result.txt" : "TUN OFF"));
153169
else
154170
Console.WriteLine(" [i] teardown cancelled - TUN still ON");
155171
}
@@ -177,15 +193,15 @@ private static void Cleanup()
177193
{
178194
if (cleaned) return;
179195
cleaned = true;
180-
if (ReadTunState() == "on")
196+
if (TunActive())
181197
{
182198
try { File.WriteAllText(TunStopFile, "stop", new UTF8Encoding(false)); } catch { }
183199
for (int i = 0; i < 8; i++)
184200
{
185201
Thread.Sleep(500);
186-
if (ReadTunState() != "on") break;
202+
if (!TunActive()) break;
187203
}
188-
if (ReadTunState() == "on")
204+
if (TunActive())
189205
try { SpawnElevated("off", true); } catch { }
190206
}
191207
if (torProc != null)
@@ -517,7 +533,7 @@ private static int Main(string[] args)
517533
}
518534
if (args.Length > 0 && args[0] == "--tun-status")
519535
{
520-
Console.WriteLine("TUN " + (ReadTunState() == "on" ? "ON" : "OFF"));
536+
Console.WriteLine("TUN " + (TunActive() ? "ON" : "OFF"));
521537
return 0;
522538
}
523539

@@ -651,7 +667,7 @@ private static int Main(string[] args)
651667
Console.WriteLine(" S run a speed test through the Tor proxy");
652668
Console.WriteLine(" C stop Tor and exit");
653669
Console.WriteLine(" Proxy OFF");
654-
Console.WriteLine(" TUN " + (ReadTunState() == "on" ? "ON" : "OFF"));
670+
Console.WriteLine(" TUN " + (TunActive() ? "ON" : "OFF"));
655671
Console.WriteLine();
656672
while (true)
657673
{

scripts/tun-helper.cs

Lines changed: 147 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
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

Comments
 (0)