Skip to content

Commit

Permalink
Client,Network,Services: add TorClient
Browse files Browse the repository at this point in the history
I also added a new method to TorGuard
to hopefully implement Guard pooling/caching.
  • Loading branch information
aarani committed Apr 14, 2023
1 parent 737ac26 commit 14cee0d
Show file tree
Hide file tree
Showing 8 changed files with 1,434 additions and 33 deletions.
27 changes: 27 additions & 0 deletions NOnion.Tests/TorClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NOnion.Directory;
using NOnion.Client;
using NUnit.Framework;
using System.Threading.Tasks;
using Microsoft.FSharp.Core;

namespace NOnion.Tests
{
public class TorClientTests
{
private async Task CreateCircuit()
{
var client = await TorClient.BootstrapWithEmbeddedListAsync();
var circuit = await client.CreateCircuitAsync(3, FSharpOption<Network.CircuitNodeDetail>.None);
}

[Test]
public void CanCreateCircuit()
{
Assert.DoesNotThrowAsync(CreateCircuit);
}
}
}
212 changes: 212 additions & 0 deletions NOnion/Client/TorClient.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
namespace NOnion.Client

open System
open System.IO
open System.Net
open System.Net.Http
open System.Text.RegularExpressions

open NOnion.Directory
open NOnion.Utility
open NOnion
open NOnion.Network

type TorClient internal (directory: TorDirectory) =
static let maximumBootstrapTries = 5

static let maximumExtendByNodeRetry = 5

static let convertFallbackIncToList(fallbackIncString: string) =
let ipv4Pattern = "\"([0-9\\.]+)\\sorport=(\\S*)\\sid=(\\S*)\""
let matches = Regex.Matches(fallbackIncString, ipv4Pattern)

matches
|> Seq.cast
|> Seq.map(fun (regMatch: Match) ->
regMatch.Groups.[1].Value, int regMatch.Groups.[2].Value
)
|> Seq.toList

static let selectRandomEndpoints(fallbackList: List<string * int>) =
fallbackList
|> SeqUtils.TakeRandom maximumBootstrapTries
|> Seq.map(fun (ipString, port) ->
let ipAddress = IPAddress.Parse ipString
IPEndPoint(ipAddress, port)
)
|> Seq.toList

static let bootstrapDirectory(ipEndPointList: List<IPEndPoint>) =
async {
let rec tryBootstrap(ipEndPointList: List<IPEndPoint>) =
async {
match ipEndPointList with
| ipEndPoint :: tail ->
try
let! directory =
TorDirectory.Bootstrap
ipEndPoint
(Path.GetTempPath() |> DirectoryInfo)

return directory
with
| :? NOnionException -> return! tryBootstrap tail
| [] -> return failwith "Maximum bootstrap tries reached!"
}

return! tryBootstrap ipEndPointList
}

static let createClientFromFallbackString(fallbackListString: string) =
async {
let! directory =
fallbackListString
|> convertFallbackIncToList
|> selectRandomEndpoints
|> bootstrapDirectory

return new TorClient(directory)
}

//FIXME: lock this
let mutable guardsToDispose: List<TorGuard> = List.empty

static member BootstrapWithEmbeddedList() =
async {
let fallbackListString = File.ReadAllText "fallback_dirs.inc"

return! createClientFromFallbackString fallbackListString
}

static member BootstrapWithEmbeddedListAsync() =
TorClient.BootstrapWithEmbeddedList() |> Async.StartAsTask

static member BootstrapWithGithub() =
async {
let! fallbackListString =
let urlToTorServerList =
"https://raw.githubusercontent.com/torproject/tor/main/src/app/config/fallback_dirs.inc"

use webClient = new HttpClient()

webClient.GetStringAsync(urlToTorServerList) |> Async.AwaitTask

return! createClientFromFallbackString fallbackListString
}

member __.CreateCircuit
(hopsCount: int)
(extendByNodeOpt: Option<CircuitNodeDetail>)
=
async {
let rec createNewGuard() =
async {
let! ipEndPoint, nodeDetail =
directory.GetRouter RouterType.Guard

try
let! guard = TorGuard.NewClient ipEndPoint
guardsToDispose <- guard :: guardsToDispose
return guard, nodeDetail
with
| :? GuardConnectionFailedException ->
return! createNewGuard()
}

let rec tryCreateCircuit(tryNumber: int) =
async {
if tryNumber > maximumExtendByNodeRetry then
return
raise
<| NOnionException
"Destination node can't be reached"
else
try
let! guard, guardDetail = createNewGuard()
let circuit = TorCircuit guard

do!
circuit.Create guardDetail
|> Async.Ignore<uint16>

let rec extend
(numHopsToExtend: int)
(nodesSoFar: List<CircuitNodeDetail>)
=
async {
if numHopsToExtend > 0 then
let rec findUnusedNode() =
async {
let! _ipEndPoint, nodeDetail =
directory.GetRouter
RouterType.Normal

if (List.contains
nodeDetail
nodesSoFar) then
return! findUnusedNode()
else
return nodeDetail
}

let! newUnusedNode = findUnusedNode()

do!
circuit.Extend newUnusedNode
|> Async.Ignore<uint16>

return!
extend
(numHopsToExtend - 1)
(newUnusedNode :: nodesSoFar)
else
()
}

do!
extend
(hopsCount - 1)
(List.singleton guardDetail)

match extendByNodeOpt with
| Some extendByNode ->
try
do!
circuit.Extend extendByNode
|> Async.Ignore<uint16>
with
| :? NOnionException ->
return
raise
DestinationNodeCantBeReachedException
| None -> ()

return circuit
with
| :? DestinationNodeCantBeReachedException ->
return! tryCreateCircuit(tryNumber + 1)
| ex ->
match FSharpUtil.FindException<NOnionException> ex
with
| Some _nonionEx ->
return! tryCreateCircuit tryNumber
| None -> return raise <| FSharpUtil.ReRaise ex
}

let startTryNumber = 1

return! tryCreateCircuit startTryNumber
}

member self.CreateCircuitAsync
(
hopsCount: int,
extendByNode: Option<CircuitNodeDetail>
) =
self.CreateCircuit hopsCount extendByNode |> Async.StartAsTask


interface IDisposable with
member __.Dispose() =
for guard in guardsToDispose do
(guard :> IDisposable).Dispose()
2 changes: 2 additions & 0 deletions NOnion/Exceptions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ type NOnionSocketException
"Got socket exception during data transfer",
innerException
)

exception internal DestinationNodeCantBeReachedException
4 changes: 4 additions & 0 deletions NOnion/NOnion.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

<ItemGroup>
<Compile Include="TorLogger.fs" />
<Content Include="fallback_dirs.inc">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Compile Include="Constants.fs" />
<Compile Include="DestroyReason.fs" />
<Compile Include="RelayIntroduceStatus.fs" />
Expand Down Expand Up @@ -81,6 +84,7 @@
<Compile Include="Directory\HiddenServiceFirstLayerDescriptorDocument.fs" />
<Compile Include="Directory\HiddenServiceSecondLayerDescriptorDocument.fs" />
<Compile Include="Directory\TorDirectory.fs" />
<Compile Include="Client\TorClient.fs" />
<Compile Include="Services\TorServiceHost.fs" />
<Compile Include="Services\TorServiceClient.fs" />
</ItemGroup>
Expand Down
78 changes: 76 additions & 2 deletions NOnion/Network/TorGuard.fs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ module ExceptionUtil =
| None -> return raise <| FSharpUtil.ReRaise exn
}

type TorGuard private (client: TcpClient, sslStream: SslStream) =
type TorGuard
private
(
client: TcpClient,
sslStream: SslStream,
_deathCallback: Option<unit -> unit>
) =
let shutdownToken = new CancellationTokenSource()

let mutable circuitsMap: Map<uint16, ITorCircuit> = Map.empty
Expand Down Expand Up @@ -157,7 +163,7 @@ type TorGuard private (client: TcpClient, sslStream: SslStream) =
|> sprintf "TorGuard: ssl connection to %s guard node authenticated"
|> TorLogger.Log

let guard = new TorGuard(tcpClient, sslStream)
let guard = new TorGuard(tcpClient, sslStream, None)
do! guard.Handshake()

ipEndpoint.ToString()
Expand All @@ -172,6 +178,74 @@ type TorGuard private (client: TcpClient, sslStream: SslStream) =
static member NewClientAsync ipEndpoint =
TorGuard.NewClient ipEndpoint |> Async.StartAsTask

static member internal NewClientWithDeathCallback
(
ipEndpoint: IPEndPoint,
deathCallback: unit -> unit
) =
async {
let tcpClient = new TcpClient()

let innerConnectAsync(client: TcpClient) =
async {
ipEndpoint.ToString()
|> sprintf "TorGuard: trying to connect to %s guard node"
|> TorLogger.Log

do!
client.ConnectAsync(ipEndpoint.Address, ipEndpoint.Port)
|> Async.AwaitTask
|> FSharpUtil.WithTimeout
Constants.GuardConnectionTimeout
}

do!
ExceptionUtil.RunGuardJobWithExceptionHandling<unit>(
innerConnectAsync tcpClient
)

let sslStream =
new SslStream(
tcpClient.GetStream(),
false,
fun _ _ _ _ -> true
)

ipEndpoint.ToString()
|> sprintf "TorGuard: creating ssl connection to %s guard node"
|> TorLogger.Log

let innerAuthenticateAsClient(stream: SslStream) =
stream.AuthenticateAsClientAsync(
String.Empty,
null,
SslProtocols.Tls12,
false
)
|> Async.AwaitTask
|> FSharpUtil.WithTimeout Constants.CircuitOperationTimeout

do!
ExceptionUtil.RunGuardJobWithExceptionHandling<unit>(
innerAuthenticateAsClient sslStream
)

ipEndpoint.ToString()
|> sprintf "TorGuard: ssl connection to %s guard node authenticated"
|> TorLogger.Log

let guard = new TorGuard(tcpClient, sslStream, Some deathCallback)
do! guard.Handshake()

ipEndpoint.ToString()
|> sprintf "TorGuard: connection with %s established"
|> TorLogger.Log

guard.StartListening()

return guard
}

member self.Send (circuitId: uint16) (cellToSend: ICell) =
async {
let! sendResult =
Expand Down
Loading

0 comments on commit 14cee0d

Please sign in to comment.