Skip to content

Commit

Permalink
Add support for registering hidden services
Browse files Browse the repository at this point in the history
Usage:

```
add
  Adds a service to register on the Tor network

Usage:
  tor [options] add <name> <service>

Arguments:
  <name>     Name of the service to register
  <service>  Address and port of the local service being registered, such as 127.0.0.1:8080

Options:
  -p, --port <port>  Optional port on the Tor network to listen on, if different than the service port
  -?, -h, --help     Show help and usage information
```

Service directory is set to `~/tor/[name]` since the service keys and address should be reused across tool reinstalls/updates (especially updates of the tor binaries themselves).

The configuration is saved to `~/tor/.netconfig` so it's also preserved across reinstalls.

Example configuration:

```
[tor]
 proxy = 1337
 socks = 1338
 control = 1339

[tor "echo"]
 port = 8080
 service = 127.0.0.1:8080
```

The `torrc` file is updated accordingly after configuration and before starting, so that it picks up our changes.

> NOTE: this is possible because we depend on our fork of TorSharp which implements the fix proposed in joelverhagen/TorSharp#70.

Fixes #1
  • Loading branch information
kzu committed May 19, 2021
1 parent cb98dd9 commit d4f2fd1
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 50 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ artifacts
pack
.vs
.vscode
/tor

*.suo
*.sdf
Expand Down
2 changes: 2 additions & 0 deletions dotnet-tor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-tor", "src\dotnet-to
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{95AB91E3-8D17-486B-A99D-1E074A054655}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
readme.md = readme.md
tor.cmd = tor.cmd
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TorSharp", "external\TorSharp\src\TorSharp\TorSharp.csproj", "{350FC483-DA36-4273-90C6-9808D87F5115}"
Expand Down
26 changes: 26 additions & 0 deletions src/dotnet-tor/AddCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading;

class AddCommand : Command
{
public AddCommand() : base("add", "Adds a service to register on the Tor network")
{
Add(new Argument<string>("name", "Name of the service to register"));
Add(new Argument<string>("service", "Address and port of the local service being registered, such as 127.0.0.1:8080"));
Add(new Option<int?>(new[] { "--port", "-p" }, "Optional port on the Tor network to listen on, if different than the service port"));

Handler = CommandHandler.Create<string, int?, string, CancellationToken>(Run);
}

static void Run(string name, int? port, string service, CancellationToken cancellation)
{
if (!Uri.TryCreate("http://" + service, UriKind.Absolute, out var uri))
throw new ArgumentException("Service specified is not valid. It should be an IP:PORT combination: " + service);

Tor.Config.GetSection("tor", name)
.SetNumber("port", port ?? uri.Port)
.SetString("service", service);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,19 @@
using Knapcode.TorSharp;
using Spectre.Console;

class ConfigureCommand : Command
class ConfigCommand : Command
{
readonly string torPath;

public ConfigureCommand(string torPath) : base("config", "Edits the full torrc configuration file.")
public ConfigCommand() : base("config", "Edits the full torrc configuration file.")
{
this.torPath = torPath;
Handler = CommandHandler.Create(RunAsync);
}

async Task RunAsync()
{
var settings = new TorSharpSettings
{

ZippedToolsDirectory = Path.Combine(torPath, "zip"),
ExtractedToolsDirectory = Path.Combine(torPath, "bin"),
ZippedToolsDirectory = Path.Combine(Tor.AppPath, "zip"),
ExtractedToolsDirectory = Path.Combine(Tor.AppPath, "bin"),
};

var zipPath = await AnsiConsole.Status().StartAsync("Fetching Tor tools", async _ =>
Expand All @@ -49,7 +45,6 @@ async Task RunAsync()
var torProxy = new TorSharpProxy(settings);
await torProxy.ConfigureAsync();

// TODO: won't exist yet because the tools aren't unzipped yet.
if (File.Exists(configPath))
Process.Start(editor, configPath);
}
Expand Down
28 changes: 2 additions & 26 deletions src/dotnet-tor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,11 @@
AnsiConsole.MarkupLine($"[yellow]New version v{update.Identity.Version} from {(DateTimeOffset.Now - (update.Published ?? DateTimeOffset.Now)).Humanize()} ago is available.[/] Update with: [lime]dotnet tool update -g dotnet-echo[/]");
}

var appPath = GetApplicationPath();
#if !CI
AnsiConsole.MarkupLine($"[yellow]AppPath: {appPath}[/]");
AnsiConsole.MarkupLine($"[yellow]AppPath: {Tor.AppPath}[/]");
#endif

await new TorCommand(appPath).WithConfigurableDefaults("tor").InvokeAsync(args);

// See GCM's Program.cs
static string GetApplicationPath()
{
// Assembly::Location always returns an empty string if the application was published as a single file
#pragma warning disable IL3000
bool isSingleFile = string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location);
#pragma warning restore IL3000

// Use "argv[0]" to get the full path to the entry executable - this is consistent across
// .NET Framework and .NET >= 5 when published as a single file.
string[] args = Environment.GetCommandLineArgs();
string candidatePath = args[0];

// If we have not been published as a single file on .NET 5 then we must strip the ".dll" file extension
// to get the default AppHost/SuperHost name.
if (!isSingleFile && Path.HasExtension(candidatePath))
{
return Path.ChangeExtension(candidatePath, null);
}

return candidatePath;
}
await new TorCommand().InvokeAsync(args);

static Task<IPackageSearchMetadata> GetUpdateAsync() => AnsiConsole.Status().StartAsync("Checking for updates", async context =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/dotnet-tor/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"dotnet-tor": {
"commandName": "Project",
"commandLineArgs": "-?"
"commandLineArgs": "add -?"
}
}
}
42 changes: 42 additions & 0 deletions src/dotnet-tor/Tor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.IO;
using System.Reflection;
using DotNetConfig;

static class Tor
{
public static string AppPath { get; } = GetApplicationPath();

public static string DataDir { get; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"tor");

// We rebuild on every request since other code can mutate the
// immutable structure and cause this property to become out of
// sync with the persisted state.
public static Config Config => Config.Build(Path.Combine(DataDir, Config.FileName));

// See GCM's Program.cs
static string GetApplicationPath()
{
// Assembly::Location always returns an empty string if the application was published as a single file
#pragma warning disable IL3000
bool isSingleFile = string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location);
#pragma warning restore IL3000

// Use "argv[0]" to get the full path to the entry executable - this is consistent across
// .NET Framework and .NET >= 5 when published as a single file.
string[] args = Environment.GetCommandLineArgs();
string candidatePath = args[0];

// If we have not been published as a single file on .NET 5 then we must strip the ".dll" file extension
// to get the default AppHost/SuperHost name.
if (!isSingleFile && Path.HasExtension(candidatePath))
{
return Path.ChangeExtension(candidatePath, null);
}

return candidatePath;
}
}

90 changes: 79 additions & 11 deletions src/dotnet-tor/TorCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using DotNetConfig;
Expand All @@ -13,19 +13,19 @@
class TorCommand : RootCommand
{
const string ControlPassword = "7B002936-978D-46D3-9C96-7579F97F333E";
static readonly ConfigSection config = Config.Build().GetSection("tor");
const string ConfigDelimiter = "#! dotnet-tor";

string torPath;

public TorCommand(string torPath) : base("Tor proxy service")
public TorCommand() : base("Tor proxy service")
{
this.torPath = torPath;
var config = Tor.Config.GetSection("tor");

Add(new Option<int>(new[] { "--proxy", "-p" }, () => (int?)config.GetNumber("proxy") ?? 1337, "Proxy port"));
Add(new Option<int>(new[] { "--socks", "-s" }, () => (int?)config.GetNumber("socks") ?? 1338, "Socks port"));
Add(new Option<int>(new[] { "--control", "-c" }, () => (int?)config.GetNumber("control") ?? 1339, "Control port"));

Add(new ConfigureCommand(torPath));
Add(new AddCommand());
Add(new ConfigCommand());

Handler = CommandHandler.Create<int, int, int, CancellationToken>(RunAsync);
}
Expand All @@ -34,8 +34,8 @@ async Task RunAsync(int proxy, int socks, int control, CancellationToken cancell
{
var settings = new TorSharpSettings
{
ZippedToolsDirectory = Path.Combine(torPath, "zip"),
ExtractedToolsDirectory = Path.Combine(torPath, "bin"),
ZippedToolsDirectory = Path.Combine(Tor.AppPath, "zip"),
ExtractedToolsDirectory = Path.Combine(Tor.AppPath, "bin"),
PrivoxySettings =
{
Port = proxy,
Expand All @@ -45,18 +45,86 @@ async Task RunAsync(int proxy, int socks, int control, CancellationToken cancell
ControlPassword = ControlPassword,
SocksPort = socks,
ControlPort = control,
//DataDirectory = Tor.DataDir,
},
};

await AnsiConsole.Status().StartAsync("Fetching Tor tools",
async _ => await new TorSharpToolFetcher(settings, new HttpClient()).FetchAsync());
var zipPath = await AnsiConsole.Status().StartAsync("Fetching Tor tools", async _ =>
{
var fetcher = new TorSharpToolFetcher(settings, new HttpClient());
var updates = await fetcher.CheckForUpdatesAsync();
await fetcher.FetchAsync(updates);
return updates.Tor.DestinationPath;
});

cancellation.ThrowIfCancellationRequested();
var tor = new TorSharpProxy(settings);

await tor.ConfigureAndStartAsync();
await tor.ConfigureAsync();
cancellation.ThrowIfCancellationRequested();

var configPath = Path.Combine(
settings.ExtractedToolsDirectory,
Path.GetFileNameWithoutExtension(zipPath),
"Data", "Tor", "torrc");

if (!File.Exists(configPath))
throw new ArgumentException($"Tor configuration file not found at expected location {configPath}");

var torConfig = Tor.Config;

// Clear dotnet-tor configuration from previous runs.
var allLines = (await File.ReadAllLinesAsync(configPath, cancellation)).ToList();
var begin = allLines.IndexOf(ConfigDelimiter);

if (begin != -1)
{
var end = allLines.LastIndexOf(ConfigDelimiter);
allLines.RemoveRange(begin, end != -1 && end != begin ? end - begin + 1 : allLines.Count - begin);
await File.WriteAllLinesAsync(configPath, allLines.Where(line => !string.IsNullOrEmpty(line)), cancellation);
}

var services = torConfig
.Where(x => x.Section == "tor" && x.Subsection != null)
.Select(x => x.Subsection!)
.Distinct().ToList();

if (services.Count > 0)
{
await File.AppendAllLinesAsync(configPath, new[] { ConfigDelimiter }, cancellation);
foreach (var service in services)
{
var config = torConfig.GetSection("tor", service);
var serviceDir = Path.Combine(Tor.DataDir, service);
await File.AppendAllLinesAsync(configPath, new[]
{
"HiddenServiceDir " + serviceDir,
$"HiddenServicePort {config.GetNumber("port")} {config.GetString("service")}"
}, cancellation);
}
await File.AppendAllLinesAsync(configPath, new[] { ConfigDelimiter }, cancellation);
}

await tor.StartAsync();
cancellation.ThrowIfCancellationRequested();

// Save successfully run args as settings
Tor.Config.GetSection("tor")
.SetNumber("proxy", proxy)
.SetNumber("socks", socks)
.SetNumber("control", control);

foreach (var service in services)
{
var hostName = Path.Combine(Tor.DataDir, service, "hostname");
if (File.Exists(hostName))
{
var config = torConfig.GetSection("tor", service);
var onion = await File.ReadAllTextAsync(hostName, cancellation);
AnsiConsole.MarkupLine($"[yellow]Service {service} ({config.GetString("service")}):[/] [lime]{onion.Trim()}[/]");
}
}

while (!cancellation.IsCancellationRequested)
Thread.Sleep(100);
}
Expand Down
6 changes: 3 additions & 3 deletions src/dotnet-tor/dotnet-tor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

Usage:

> dotnet tor -?
&gt; dotnet tor -?
tor
Tor proxy service

Expand All @@ -36,9 +36,9 @@ Commands:
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DotNetConfig.CommandLine" Version="1.0.3" />
<!--<PackageReference Include="Knapcode.TorSharp" Version="2.6.0" />-->
<PackageReference Include="DotNetConfig" Version="1.0.3" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
<PackageReference Include="ThisAssembly" Version="1.0.8" />
<PackageReference Include="NuGet.Protocol" Version="5.9.1" />
<PackageReference Include="Spectre.Console" Version="0.39.0" />
Expand Down
Binary file removed src/kzu.snk
Binary file not shown.
4 changes: 4 additions & 0 deletions tor.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@echo off
pushd src\dotnet-tor\bin\Debug
tor %*
popd

0 comments on commit d4f2fd1

Please sign in to comment.