-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Client,Network,Services: add TorClient
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
Showing
7 changed files
with
1,391 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.