Skip to content

Commit

Permalink
Client,Network,Services: add TorClient
Browse files Browse the repository at this point in the history
This commit aims to introduce a simple API for end users.

Working with network especially Tor routers is flaky and
delegating retry logic implementation to users causes NOnion to
be unreliable in CI and in normal use, this commit introduces
retry logic in some places where it's needed the most. This
should hopefully make NOnion more reliable.
  • Loading branch information
aarani committed Oct 28, 2023
1 parent 1da50a7 commit 394e101
Show file tree
Hide file tree
Showing 7 changed files with 1,391 additions and 38 deletions.
30 changes: 30 additions & 0 deletions NOnion.Tests/TorClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

using NUnit.Framework;

using NOnion.Directory;
using NOnion.Client;

using Microsoft.FSharp.Core;

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

[Test]
public void CanCreateCircuit()
{
Assert.DoesNotThrowAsync(CreateCircuit);
}
}
}
240 changes: 240 additions & 0 deletions NOnion/Client/TorClient.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
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 CircuitPurpose =
| Unknown
| Exit

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 AsyncBootstrapWithEmbeddedList() =
async {
let fallbackListString =
EmbeddedResourceUtility.ExtractEmbeddedResourceFileContents(
"fallback_dirs.inc"
)

return! CreateClientFromFallbackString fallbackListString
}

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

static member AsyncBootstrapWithGithub() =
async {
// Don't put this inside the fallbackListString or it gets disposed
// before the task gets executed
use httpClient = new HttpClient()

let! fallbackListString =
let urlToTorServerList =
"https://raw.githubusercontent.com/torproject/tor/main/src/app/config/fallback_dirs.inc"
httpClient.GetStringAsync urlToTorServerList |> Async.AwaitTask

return! CreateClientFromFallbackString fallbackListString
}

static member BootstrapWithGithubAsync() =
TorClient.AsyncBootstrapWithGithub() |> Async.StartAsTask

member __.Directory = directory

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

try
let! guard =
TorGuard.NewClientWithIdentity
ipEndPoint
(nodeDetail.GetIdentityKey() |> Some)

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 =
if numHopsToExtend = 1 then
match purpose with
| Unknown ->
directory.GetRouter
RouterType.Normal
| Exit ->
directory.GetRouter
RouterType.Exit
else
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,
purpose: CircuitPurpose,
extendByNode: Option<CircuitNodeDetail>
) =
self.CreateCircuit hopsCount purpose 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
2 changes: 2 additions & 0 deletions NOnion/NOnion.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<EmbeddedResource Include="auth_dirs.inc" />
<EmbeddedResource Include="fallback_dirs.inc" />
<Compile Include="TorLogger.fs" />
<Compile Include="Constants.fs" />
<Compile Include="DestroyReason.fs" />
Expand Down Expand Up @@ -85,6 +86,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
32 changes: 8 additions & 24 deletions NOnion/Services/TorServiceClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ open Org.BouncyCastle.Security

open NOnion
open NOnion.Cells.Relay
open NOnion.Client
open NOnion.Crypto
open NOnion.Utility
open NOnion.Directory
Expand All @@ -34,6 +35,7 @@ type TorServiceClient =

static member Connect (directory: TorDirectory) (url: string) =
async {

let publicKey, port = HiddenServicesUtility.DecodeOnionUrl url

let getIntroductionPointInfo() =
Expand Down Expand Up @@ -64,36 +66,18 @@ type TorServiceClient =
raise <| DescriptorDownloadFailedException()
| hsDirectory :: tail ->
try
let! guardEndPoint, randomGuardNode =
directory.GetRouter RouterType.Guard

let! _, randomMiddleNode =
directory.GetRouter RouterType.Normal
use torClient = new TorClient(directory)

let! hsDirectoryNode =
directory.GetCircuitNodeDetailByIdentity
hsDirectory

use! guardNode =
TorGuard.NewClientWithIdentity
guardEndPoint
(randomGuardNode.GetIdentityKey()
|> Some)

let circuit = TorCircuit guardNode

do!
circuit.Create randomGuardNode
|> Async.Ignore

do!
circuit.Extend randomMiddleNode
|> Async.Ignore

try
do!
circuit.Extend hsDirectoryNode
|> Async.Ignore
let! circuit =
torClient.CreateCircuit
2
CircuitPurpose.Unknown
(Some hsDirectoryNode)

use dirStream = new TorStream(circuit)

Expand Down
21 changes: 7 additions & 14 deletions NOnion/Services/TorServiceHost.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ open Org.BouncyCastle.Security
open NOnion
open NOnion.Cells.Relay
open NOnion.Crypto
open NOnion.Client
open NOnion.Directory
open NOnion.Utility
open NOnion.Network
Expand Down Expand Up @@ -379,21 +380,13 @@ type TorServiceHost
directory.GetCircuitNodeDetailByIdentity
directoryToUploadTo

let! guardEndPoint, randomGuardNode =
directory.GetRouter RouterType.Guard
use torClient = new TorClient(directory)

let! _, randomMiddleNode =
directory.GetRouter RouterType.Normal

use! guardNode =
TorGuard.NewClientWithIdentity
guardEndPoint
(randomGuardNode.GetIdentityKey() |> Some)

let circuit = TorCircuit guardNode
do! circuit.Create randomGuardNode |> Async.Ignore
do! circuit.Extend randomMiddleNode |> Async.Ignore
do! circuit.Extend hsDirectoryNode |> Async.Ignore
let! circuit =
torClient.CreateCircuit
2
CircuitPurpose.Unknown
(Some hsDirectoryNode)

use dirStream = new TorStream(circuit)
do! dirStream.ConnectToDirectory() |> Async.Ignore
Expand Down
Loading

0 comments on commit 394e101

Please sign in to comment.