Radish alpha
r
rad:z371PVmDHdjJucejRoRYJcDEvD5pp
Radicle website including documentation and guides
Radicle
Git
guides: Finalize user guide chapters on privacy
Alexis Sellier committed 1 year ago
commit 09e2434de16b037f28ccdd122e593dae94d4bf0e
parent f8523ad
5 files changed +485 -359
modified _guides/user.md
@@ -1115,12 +1115,13 @@ patch title, while the subsequent lines make up the description.
Once we've entered the details describing the patch, we save and exit our
editor. This pushes our commit to the `rad` remote, opening the patch.

-
<pre class="wide"><code>✓ Patch e5f0a5a5adaa33c3b931235967e4930ece9bb617 opened
+
<pre class="wide">
+
✓ Patch e5f0a5a5adaa33c3b931235967e4930ece9bb617 opened
✓ Synced with 8 node(s)

To rad://z3cyotNHuasWowQ2h4yF9c3tFFdvc/z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C
* [new reference]   HEAD -> refs/patches
-
</code></pre>
+
</pre>

In the above Git output, we see our patch's identifier displayed:

@@ -1413,8 +1414,6 @@ We can verify that it is merged via `rad patch show e5f0a5a`, or `rad patch
> For more details on how to work with patches, read the manual with `man
> rad-patch`.

-
<!-- Reviewed until here -->
-

### From remote viewing to adding remotes

Whenever you `init` or `clone` a repository, your node will follow all peers
@@ -1508,98 +1507,87 @@ After the `sync` command succeeds, try to `git pull` again.
And, that's a wrap! You now know the most relevant nitty gritty details of how
to leverage Radicle for code collaboration.

-
<!-- TODO
-
Consider continuing on to [Chapter 3][ch3] to further expand your Radicle
-
consciousness and learn about setting policies and configurations for how your
-
node seeds repositories.
-
-->
-

*One thing to keep in mind is that Radicle doesn't have advanced code review
features yet for patches, but that is coming in a near-term release.*

-

-

## 3. Selectively Revealing Repositories

-
In his influential work, *[A Cypherpunk's Manifesto][cyph-man]* (1993), Eric
+
In his influential work, [A Cypherpunk's Manifesto][cyph-man] (1993), Eric
Hughes eloquently encapsulated the essence of privacy, stating: ***Privacy is
the power to selectively reveal oneself to the world.*** This notion of
self-determination and control over one's personal information stands in stark
contrast to the reality of using "private repository" features on traditional
-
centralized code forges. In these systems, privacy is merely an illusion
+
centralized code forges. In those systems, privacy is merely an illusion
maintained by terms of service agreements, as data is stored in cleartext, fully
-
accessible to the platforms employees, vulnerable to potential misuse, and
-
subject to external pressures such as government requests. Users are left with
-
no real control, forced to blindly trust that their code and data won't be
-
viewed, exploited, or even used without consent for purposes like training large
-
language models.
+
accessible to the platform's employees, vulnerable to potential misuse or
+
leaks, and subject to external pressures such as government requests. Users are
+
left with no real control, forced to blindly trust that their code and data
+
won't be viewed, exploited, or even used without consent for purposes like
+
training large language models.

In contrast, Radicle's decentralized approach puts the power of privacy directly
in the hands of users by leveraging public key cryptography and granular access
control mechanisms. The *power to selectively reveal* is a foundational
-
principle in Radicle, fostering a more resilient and autonomous environment for
+
principle in Radicle, fostering a more resilient and safer environment for
code collaboration. Through selective replication, private repositories are only
-
accessible to and replicated by repository delegates and collaborators, or nodes
-
specified by the delegates, while fully encrypted connections ensure
-
confidential data transfer between these authorized nodes. Repositories are not
-
encrypted at rest. However, the combination of selective replication and
-
encrypted connections renders them invisible to the rest of the network. This
-
approach effectively creates isolated spaces for private collaboration,
-
embodying the true essence of user-controlled privacy.
+
accessible to and replicated by repository collaborators, while fully encrypted
+
connections ensure confidential data transfer between peers.
+

+
By combining selective replication and encrypted and authenticated connections,
+
repositories are invisible to the rest of the network. This approach
+
effectively creates isolated spaces for private collaboration, forgoing the
+
need for encryption at rest.

In this chapter, we'll explore how to maintain sovereignty and control over a
-
private repository while collaborating with a trusted circle. We'll demonstrate
-
how this works by including a seed node in the repository's allow list.
+
private repository while collaborating with a set of peers and a trusted
+
seed node.

### Initializing a private repository

-
When a private repository is created in Radicle, it's only visible and
-
accessible to explicitly authorized nodes. Privacy is extended to all aspects of
-
the repository, including its data, creation time, Repository ID (RID), and the
-
set of collaborators.
+
When a private repository is created in Radicle, it is only visible and
+
accessible to explicitly authorized peers. Privacy is extended to all aspects
+
of the repository, including its data, creation time, Repository ID (RID), COBs,
+
and its set of collaborators.

To maintain this privacy during data transfer, all connections between nodes,
-
including those to seed nodes, are fully encrypted using the Noise Protocol
-
Framework, which employs ChaCha20-Poly1305 for its cryptographic operations.
+
including those to seed nodes, are fully encrypted using an AEAD algorithm,
+
`ChaCha20-Poly1305`.

<aside class="span-2">
-
<strong>ChaCha20-Poly1305</strong> is a modern encryption system designed for speed and security. It combines two parts: ChaCha20, which scrambles the data to keep it secret, and Poly1305, which creates a unique "fingerprint" for the encrypted data to ensure it hasn't been tampered with. This system works efficiently on all types of devices, from smartphones to computers, without needing special hardware.
+
  <strong>ChaCha20-Poly1305</strong> is a modern AEAD encryption algorithm
+
  designed for speed and security. It combines two parts: ChaCha20, which
+
  scrambles the data to keep it secret, and Poly1305, which creates a unique
+
  "fingerprint" for the encrypted data to ensure it hasn't been tampered with.
+
  This system works efficiently on all types of devices, from smartphones to
+
  computers, without needing special hardware.
</aside>

To demonstrate how this works in practice, let's create a private repository for
a sensitive research project we're working on with our peer, Calyx. We'll start
-
by initializing Git within a new directory named `schrödingers-paradox` and
-
committing our initial research findings:
+
by initializing a repository within a new directory named
+
`schrödingers-paradox` and committing our initial research findings:

    $ cd schrödingers-paradox
    $ git init
-
    $ cp /var/run/897af.log data/897af.log
-
    $ git add data/
-
    $ git add analysis/paxels-report.md
-
    $ git commit -m "Add data and report from ship 897AF"
+
    $ cp ~/secrets/897af-captain.log data/897af-captain.log
+
    $ git commit -a -m "Add captain's log from ship 897AF"

Next, to securely share this with Calyx, we initialize it as a private
-
repository in Radicle by passing the `--private` flag with `rad init`:
+
repository in Radicle using the `--private` flag:

-
    $ rad init --private
+
<aside class="kicker"><code>Paxel</code></aside>

-
This command functions similarly to the public repository initialization
-
process. It guides us through creating a new private repository, generates a
-
Repository ID (RID), and creates a special Git remote named `rad` in our working
-
copy.
+
    $ rad init --private

-
The key difference is that setting the `--private` flag automatically configures
-
the `visibility` option to private. As a result, the repository remains
-
unannounced and unpublished on the network, reflecting its nature 🥷.
+
This invocation functions similarly to the public repository initialization
+
process; the key difference is that it automatically configures the
+
`visibility` option of the repository to private. As a result, the repository
+
remains unannounced 🥷 and unpublished on the network, reflecting its nature.

    $ rad init --private

    Initializing private radicle 👾 repository in /home/paxel/src/schrödingers-paradox..
-

-
    ✓ Name schrödingers-paradox
-
    ✓ Description Recent data from research vessel 897AF has revealed peculiar patterns that, if verified, could have significant implications for our understanding of the universe.
-
    ✓ Default branch main
-
    ✓ Repository schrödingers-paradox created.
+
    ...

    Your Repository ID (RID) is rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a.
    You can show it any time by running `rad .` from this directory.
@@ -1610,138 +1598,101 @@ unannounced and unpublished on the network, reflecting its nature 🥷.
    To make it public, run `rad publish`.
    To push changes, run `git push`.

+
If we run `rad ls --private`, we'll find the repository with its `private`
+
visibility.
+

+
<pre class="wide">
+
╭────────────────────────────────────────────────────────────────────────╮
+
│ Name                   RID                                 Visibility  │
+
├────────────────────────────────────────────────────────────────────────┤
+
│ schrödingers-paradox   rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a   private     │
+
╰────────────────────────────────────────────────────────────────────────╯
+
</pre>
+

### Curating the *allow* list

Now that we have our private repository, the next step is to grant access to our
collaborators.

The newly created private repository, `schrödingers-paradox`, will initially be
-
accessible solely to us. However, to collaborate on this repository with others,
-
we need to curate its allow list to include the Decentralized Identifiers (DIDs)
-
of Calyx, and a trusted [seed node][seeder] that we manage that has a public DNS
-
address (e.g `example.darkstar.org`). This seed node is necessary to facilitate
-
the synchronization of repository updates between Calyx and us.
+
accessible only by us, the lone delegate. This is due to the fact that
+
repository delegates always have access to their private repositories. However,
+
to collaborate on this repository with others, we need to curate its allow list
+
to include the DIDs of Calyx, and at least one [seed node][seeder] *we trust*.
+
This seed node will act as a trusted relay, necessary for the synchronization
+
of repository state between Calyx and us.

+
Though this trusted seed node needs a public DNS address, it doesn't have to
+
be advertized to the network. One can simply omit adding the address to
+
the `externalAddresses` key in the Radicle node configuration.

> 👻
>
> Private repositories are not encrypted at rest, so any node that you add to
-
> the allow list will have visibility to the contents of your private
-
> repositories, but they are completely invisible to the rest of the network.
-
>
-
> There are two possible approaches for distributing private repositories among
-
> collaborative peers:
-
>
-
> 1. Adding an intermediary seed node with a public DNS address to the allow
-
>    list (this is the option discussed in this current chapter).
-
> 2. Adding a user node or seed node configured with a Tor .onion address (this
-
>    option is discussed in [Chapter 4][ch4]).
-

+
> the allow list will have visibility over the contents of your private
+
> repository. In the [Chapter 4][ch4] we explore a different setup that works
+
> over Tor and doesn't require a trusted seed node.

-
We can add both Calyx and our managed seed node to the allow list using a single
+
We can add both Calyx and our trusted seed node to the allow list using a single
`rad id update` command. This command requires a `--title` for the update, along
-
with each authorized DID specified following an `--allow` flag:
+
with each authorized DID specified with the `--allow` flag:

-
    $ rad id update --title "Add Calyx and example.darkstar.org to allow list" \
+
    $ rad id update --title "Allow Calyx and seed.darkstar.example" \
        --allow did:key:z6Mkgom1bTxdh9fMFxFNXFMw3SbXnma6NARdsfcFuunurCap \
        --allow did:key:z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt

Once we submit the update above, it is automatically approved, as we are the
-
repository's sole delegate:
-

+
repository's sole delegate. If you look at the resulting diff, you see that
+
both Calyx and our trusted node are being added to the `allow` list under the
+
`visibility` key:
+

+
```json
+
"allow": [
+
  "did:key:z6Mkgom1bTxdh9fMFxFNXFMw3SbXnma6NARdsfcFuunurCap",
+
  "did:key:z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt"
+
]
```
-
$ rad id update --title "Add Calyx and example.darkstar.org to allow list" \
-
    --allow did:key:z6Mkgom1bTxdh9fMFxFNXFMw3SbXnma6NARdsfcFuunurCap \
-
    --allow did:key:z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt
-
✓ Identity revision 8d3b8d3d4903026f7e0d4c969d82613025d34432 created
-
╭────────────────────────────────────────────────────────────────────────╮
-
│ Title    Add Calyx and iui.darkstar.org to allow list                  │
-
│ Revision 8d3b8d3d4903026f7e0d4c969d82613025d34432                      │
-
│ Blob     219f9cd8130dad644375fa729ba7b31997358945                      │
-
│ Author   did:key:z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C      │
-
│ State    accepted                                                      │
-
│ Quorum   yes                                                           │
-
│                                                                        │
-
│                                                                        │
-
├────────────────────────────────────────────────────────────────────────┤
-
│ ✓ did:key:z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C paxel (you) │
-
╰────────────────────────────────────────────────────────────────────────╯

-
@@ -1,17 +1,21 @@
-
{
-
"payload": {
-
    "xyz.radicle.project": {
-
    "defaultBranch": "main",
-
    "description": "Recent data from research vessel 897AF has revealed peculiar patterns that, if verified, could have significant implications for our understanding of the universe.",
-
    "name": "schrödingers-paradox"
-
    }
-
},
-
"delegates": [
-
    "did:key:z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C"
-
],
-
"threshold": 1,
-
"visibility": {
-
-    "type": "private"
-
+    "type": "private",
-
+    "allow": [
-
+      "did:key:z6Mkgom1bTxdh9fMFxFNXFMw3SbXnma6NARdsfcFuunurCap",
-
+      "did:key:z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt"
-
+    ]
-
```
-

-
You can see that the two DIDs were added to the `allow` list, which now grants
-
both Calyx and our seed node access to the `schrödingers-paradox` repository.
-

-

-
> 👻
+
> 👾
>
-
> It is possible to update a repository that is currently public to become
+
> It is also possible to update a repository that is currently public to become
> private, with a repository identity update:
>  ```
-
>  $ rad id update --title "Update visibility" --visibility private
+
>  $ rad id update --title "Make private" --visibility private
>  ```
> This won't delete historic repository data from public seed nodes or other
-
> peers that previously seeded or cloned the repository, meaning the public will
+
> peers that previously seeded or cloned the repository, meaning users will
> still be able to clone the old version of the repository.
>
-
> Instead, future updates to the repository, including and repository's private
-
> status will only be announced and accessible to those explicitly defined in
-
> the `allow` list.
-

+
> Instead, future updates to the repository, including the repository's private
+
> status will be hidden to unauthorized users, only to be announced and
+
> accessible to those explicitly defined in the `allow` list.

### Replicating to our seed node

-
Now that we've set up our private repository and managed access, let's replicate
-
`schrödingers-paradox` onto our seed node. Our repository has the RID
-
`rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a`, which we'll use with the `rad seed` command
-
to accomplish this. Since `schrödingers-paradox` is a private repository, we
-
also need to specify our (Paxel's) Node ID using the `--from` flag to explicitly
-
indicate the source node.
-

-
Let's run the command:
-

-
        darkstar$ rad seed rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a --from z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C
+
Now that we've set up our private repository, it's essential to configure it
+
from our trusted seed node. Since our seed node is likely configured with a
+
selective seeding policy, we'll have to tell it to seed our private repository
+
with the `rad seed` command.

-
This will both update the seeding policy to include
-
`rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a` and also fetch the repository from our node.
-
To ensure the replication was successful, we can check the seeding status:
+
Since `schrödingers-paradox` is a private repository, we also need to specify
+
our (Paxel's) Node ID using the `--from` flag to explicitly indicate a source
+
node to fetch from. We therefore run the following command from within an SSH
+
session on our seed node:

-
        darkstar$ rad seed
+
<aside class="kicker"><code>seed.darkstar.example</code></aside>

-
Now that the repository is on the seed node, it can serve as a reliable access
-
point for other authorized peers, since the seed node will always be running,
-
and our computer won't be. Remember, only peers on the repository's allow list
-
will be able to fetch from this seed node.
+
    $ rad seed rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a --from z6MkvZwzK64f3GuDcAs6dEcje89ddfHkBjS1v9Dkh7aCGq3C

+
This will update darkstar's seeding policy and attempt to fetch the repository
+
from our node. For the fetching to be successful, we'll have to be connected
+
to the seed node. Check that it is indeed the case with `rad node status`,
+
and use `rad node connect` or adjust your node's configuration if it isn't
+
the case.

> 👾
>
-
> In the instructions above, we are assuming the use case where the seed node is
-
> already specified as a `preferredSeeds` in our node configuration, which
-
> ensures an automatic connection is established when starting our node.
-
>
-
> If you're experiencing issues with repository replication, try these
-
> troubleshooting steps:
+
> If you're experiencing issues with repository replication, try these steps:
>
> 1. Check your current connections:
>    ```
@@ -1754,110 +1705,121 @@ will be able to fetch from this seed node.
>    ```
>    For example:
>    ```
-
>    $ rad node connect z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt@example.darkstar.org:8776
+
>    $ rad node connect z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt@seed.darkstar.example:8776
>    ```
>
-
> To find your seed node's address, run `rad node config --addresses` on the
-
> seed node itself.
+
> To find your seed node's address, run `rad node config --addresses` while logged in
+
> to the seed node.
>
> For comprehensive information on setting up and managing seed nodes, refer to
> our [Seeder's Guide][seeder].

+
Once the repository is on the seed, you should be able to see it by running
+
`rad ls --private` from within the SSH session. It can then serve as an always
+
online reliable access point for other authorized peers. Remember, only peers
+
on the repository's allow list will be able to fetch from this seed node.

### Cloning as a trusted peer
+

With our repository now successfully seeded, Calyx can clone our project. Since
`schrödingers-paradox` is a private repository, Calyx needs to specify the seed
-
node to fetch from, using the `--seed` flag:
+
node to fetch from, using the `--seed` flag. In this case, Calyx specifies
+
the Node ID of `seed.darkstar.example`:

-
    calyx$ rad clone rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a --seed z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt
+
<aside class="kicker"><code>Calyx</code></aside>

-
By providing the seed node's NID, Calyx can fetch the repository data directly.
-
There's no need for a `rad node connect` command because the seed node is
-
already hosting the repository and is accessible via a public address.
+
    $ rad clone rad:z3jEQE4VMzkR1UVeSLiN9E8AMtV6a --seed z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt

-
After cloning, Calyx begins working on the research project. Once he completes
-
his initial findings, he commits them to the repository and pushes a patch:
+
Once the repository is fetched, it's no longer necessary to specify the seed.
+
Commands such as `rad sync` will automatically try to sync with our trusted
+
seed.

-
    $ git add analysis/calyxs-report.md
-
    $ git commit -m "Calyx's report of vessel 897AF data"
-
    $ git push rad HEAD:refs/patches
-

-
> 👾
-
>
-
> Need a refresher on collaboration basics? Check out [Chapter 2][ch2], or
-
> review `man rad-patch` for details on working with patches.
+
*With a local copy of the repository, Calyx can now collaborate freely,
+
in full secrecy.*

### Removing nodes from the *allow* list

-
Our seed node became compromised, and now we have to revoke access privileges to
-
it. We use the `rad id update` command with the `--disallow` flag:
+
In the event that our seed node became compromised, we could revoke access
+
privileges to the repository from it. The same `rad id update` command is used,
+
but this time with the `--disallow` flag:

-
    $ rad id update --title "Revoke compromised node" \
+
    $ rad id update --title "Darkstar is compromised!" \
        --disallow did:key:z6MkqNZS9QWvC4wbZ8Vz4hQZ1FzN8q4XGj2KGmK9qNgQ8VWt

-
This removes the seed node from being able to access it. 
-

-
(Luckily, we were able to delete the data on the seed node before the attacker
-
got access to it!)
+
Our *no-longer-trusted* seed node will now be unable to fetch updates from us,
+
for `schrödingers-paradox`.

> 👾
>
-
> As projects evolve, you might decide that a repository no longer needs to be
-
> private. In such cases, you can easily change its visibility. Simply run `rad
-
> publish` within the repository to transform it from private to public.
-

-
*Radicle's codebase hasn't yet undergone formal security audits. While we're
-
confident in its security, undiscovered vulnerabilities may exist. If you
-
encounter any issues, we encourage you to report them to
-
**security@radicle.xyz***
+
> As time goes by, you might decide that a repository no longer needs to be
+
> private. In such a case, you can easily change its visibility. Simply run `rad
+
> publish` within a repository's working copy to transform it from private to
+
> public.

+
> 👻
+
>
+
> Radicle's codebase hasn't yet undergone formal security audits. While we're
+
> confident in its security, undiscovered vulnerabilities may exist. If you
+
> encounter any issues, we encourage you to report them to
+
> **security@radicle.xyz**.

## 4. Embracing the Onion

-
**[Tor][tor]**, aka The Onion Router, is a decentralized network that anonymizes
-
users' online activities by routing internet traffic through multiple encrypted
-
relays, providing stronger anonymity compared to traditional VPNs. To further
-
enhance collaboration on private repositories, Radicle nodes can be configured
-
to run on Tor, allowing connections to be represented by `.onion` addresses.  
+
**[Tor][tor]**, "The Onion Router", is a decentralized network that
+
anonymizes users' online activity by routing internet traffic through
+
multiple encrypted relays, providing stronger anonymity compared to traditional
+
VPNs.

-
<aside class="span-2">
-
Tor, originally developed by the U.S. Naval Research Laboratory in the 1990s, was designed to protect government communications. It was further developed by DARPA before becoming a public project. In 2006, the non-profit Tor Project was established to maintain and improve the network. Today, Tor is widely used by individuals, activists, journalists, and organizations seeking greater online privacy.</aside>
+
<aside class="span-3"> Tor, originally developed by the U.S. Naval Research
+
Laboratory in the 1990s, was designed to protect government communications. It
+
was further developed by DARPA before becoming a public project. In 2006, the
+
non-profit Tor Project was established to maintain and improve the network.
+
Today, Tor is widely used by individuals, activists, journalists, and
+
organizations seeking greater online privacy.</aside>
+

+
To further enhance repository and user privacy, Radicle nodes can be
+
configured to run behind Tor. This allows connections to be made to `.onion`
+
addresses as well as hiding one's IP address and exposing Radicle as a Tor
+
*onion service*.

-
Depending on how you configure Tor with Radicle, it offers several key
-
advantages:
+
Depending on how you configure Radicle, it offers several key advantages:

-
* **Enhanced Anonymity**: Your node's real IP address is hidden, making it
+
* **Enhanced anonymity**: Your node's real IP address is hidden, making it
  difficult for others to track your online activities or determine your
  location.
-
* **NAT Traversal**: Tor automatically handles Network Address Translation (NAT)
-
  traversal, eliminating the need for manual port forwarding or UPnP
-
  configuration. This means peers can directly connect with one another, without
-
  a seed node, even those behind restrictive firewalls.
-
* **Censorship Resistance**: Repositories accessible via Tor are more resilient
+
* **NAT traversal**: Tor automatically handles [NAT traversal][nat],
+
  eliminating the need for manual port forwarding or UPnP configuration. This
+
  means peers can directly connect with one another, without a seed node, even
+
  behind a restrictive firewall.
+
* **Censorship resistance**: Repositories accessible via Tor are more resilient
  against censorship attempts, as the Tor network is designed to circumvent
  blocking.

+
[nat]: https://en.wikipedia.org/wiki/NAT_traversal
+

Network Address Translation (NAT) typically creates barriers for direct
-
peer-to-peer (P2P) communication. Devices behind NAT gateways are often isolated
+
peer-to-peer communication. Devices behind NAT gateways are often isolated
with non-routable private IP addresses, making them inaccessible from the public
internet. This is common in most consumer internet setups.

Tor addresses this issue by providing static `.onion` addresses – cryptographic
identifiers representing nodes that are routable across the Tor network. This
-
allows peers behind NAT to connect directly without needing to know each other's
-
public IP addresses, overcoming limitations faced by traditional P2P networks.
+
allows peers behind NAT to connect directly without needing to know each
+
other's public IP addresses, overcoming limitations faced by traditional
+
peer-to-peer networks.

-
This chapter will explore a few distinct Tor configurations for Radicle:
+
This chapter will explore a few distinct ways to configure Radicle with Tor:

-
1. **Mixed Mode**: Only connections to other onion addresses go through Tor,
-
   while other connections work normally.
-
2. **Full Proxy Mode**: All traffic is routed through Tor for maximum anonymity.
-
3. **Transparent Proxy**: Leverages an existing fully transparent Tor proxy on
+
1. **Mixed Mode**: Only connections to `.onion` addresses go through Tor, while
+
   other connections work normally.
+
2. **Full Proxy Mode**: All traffic is routed through Tor for maximum anonymity,
+
    including traffic to nodes that are *not* behind Tor.
+
3. **Transparent Proxy Mode**: Leverages an existing fully transparent Tor proxy on
   the network.

Before proceeding, ensure you have Tor installed on your system. Use your
preferred package manager to install it (the package name is typically "tor").
-
For detailed instructions, refer to the [Tor installation guide][install-tor].
+
For detailed instructions, refer to the Tor [installation guide][install-tor].


### Setting up a Tor Onion Service for Radicle
@@ -1867,142 +1829,170 @@ repository. Recently, we removed a compromised seed node from the repository's
`allow` list, which inadvertently cut off Calyx's access to the repository.

To restore Calyx's access and enhance the security of our sensitive research,
-
we'll configure our own node as a Tor Onion Service (also known as a hidden
+
we'll configure our node as a Tor Onion Service (also known as a hidden
service). This setup will make our node accessible via a `.onion` address,
-
allowing Calyx to connect directly to us without relying on any intermediary
-
seed nodes.
+
allowing Calyx to connect directly to us without relying on any intermediaries.

-
Let's set up our Tor Onion Service (the following steps are for Linux-based
-
systems):
+
Let's set up our Tor Onion Service. The following steps are for Linux-based
+
systems, but they should be similar for other operating systems.

-
1. Create a new `radicle` directory for our Onion Service, ensuring it's
-
   readable and writable:
+
First we create a new `radicle` directory for our Onion Service, ensuring it's
+
readable and writable:

-
       $ sudo mkdir -m 700 /var/lib/tor/radicle
+
    $ sudo mkdir -m 700 /var/lib/tor/radicle

-
This directory will store information and cryptographic keys for your Onion
+
This directory will store information and cryptographic keys for our Onion
Service.

-
2. Open up your Tor configuration file (`torrc`) in an editor, such as vi:
+
Then we update our Tor configuration (`/etc/tor/torrc`) by adding the following
+
lines to it:

-
        $ sudo vi /etc/tor/torrc
+
    HiddenServiceDir /var/lib/tor/radicle
+
    HiddenServicePort 8776

-
3. Add the following lines to the configuration file:
+
These lines set the `HiddenServiceDir` to the path of the directory we just
+
created and specify the `HiddenServicePort` as `8776`, which is the default
+
Radicle node port.

-
        HiddenServiceDir /var/lib/tor/radicle
-
        HiddenServicePort 8776
+
Finally, we restart our Tor daemon to apply the configuration changes:

-
These lines set the `HiddenServiceDir` to the path of the folder we just created
-
(replace the path if yours is in a different location) and specify the
-
`HiddenServicePort` as 8776, which is the default port for Radicle.
+
    $ sudo systemctl restart tor

-
4. Restart Tor to apply the configuration changes:
+
Our `.onion` address is stored in a file under our Onion Service
+
directory. Let's take a look:

-
        $ sudo systemctl restart tor
-

-
5. Retrieve your `.onion` address:
-

-
        $ sudo cat /var/lib/tor/radicle/hostname
-

-
Copy this `.onion` address to your clipboard, as we'll need it to configure our
-
Radicle node in the next steps. (It should be something like
-
`1odmmeotgcfx65l5hn6ejkaruvai222vs7o7tmtllszqk5xbysolfdd.onion`)
+
    $ sudo cat /var/lib/tor/radicle/hostname
+
    1odmmeotgcfx65l5hn6ejkaruvai222vs7o7tmtllszqk5xbysolfdd.onion

+
We can copy this address to our clipboard, as we'll need it to configure our
+
Radicle node in the next steps.

> 🧠
>
-
> If you are a macOS user, these alternate commands may be handy:
-
>   - You can install Tor via `brew install tor` and restart it via `brew
-
>     services restart tor`
-
>   - Use the command `mkdir -m 700 ~/.tor/radicle` to create your hidden
-
>     service directory
-
>   - Edit the `torrc` configuration file via `vi /usr/local/etc/tor/torrc`
-
>     - The hidden service directory line should be: `HiddenServiceDir
-
>       /Users/your-user-name/.tor/radicle` -- yet replace `your-user-name`
-
>   - Get your `.onion` address via `cat ~/.tor/radicle/hostname`
-
>
-
> If you are a Debian user who got Tor from a Debian repo, you may need to
+
> If you are a **macOS** user, install Tor via `brew install tor` and restart
+
> it via `brew services restart tor`.
+
>
+
> Use `mkdir -m 700 ~/.tor/radicle` to create your onion service directory.
+
>
+
> The Tor configuration file is located at `/usr/local/etc/tor/torrc` and your
+
> `.onion` address can be found in `~/.tor/radicle/hostname`.
+
>
+
> If you are a **Debian** user who got Tor from a Debian repo, you may need to
> change the ownership of the `radicle` directory to the `debian-tor` user and
-
> group first:
+
> group after creating it:
>
>     $ sudo chown -R debian-tor:debian-tor /var/lib/tor/radicle


-
### Balancing privacy and performance with the *mixed mode*
+
#### Configuring Radicle in *Mixed Mode*

Now that we have Tor properly set up, it's time to configure the networking on
our Radicle node to work with it. There are several configuration options
available, but for our use case, we'll implement the *Mixed Mode* where only
-
connections to `.onion` addresses go through Tor, while other connections work
-
normally.
+
connections to `.onion` addresses go through Tor, while regular connections go
+
through the regular internet. For our particular use-case, we're trying to
+
collaborate on a private repository, not to hide our IP address.

This configuration offers a balance between privacy and performance:

-
* It maintains low latency for regular public repositories.
-
* It allows connectivity to Tor nodes for enhanced privacy when needed.
+
* It maintains low latency for regular public nodes.
+
* It allows connectivity through Tor, for nodes that are exposed as an onion
+
  service.
+

+
<figure class="diagram">
+
  <object type="image/svg+xml" data="/assets/images/tor-mixed-mode.svg"></object>
+
  <figcaption>Tor <em>Mixed Mode</em>.</figcaption>
+
</figure>

-
We're choosing this mode because we've already been collaborating on public
-
repositories with our IP address exposed.
+
To set this up, we first ensure that our Radicle node is stopped and then edit
+
our node's configuration file:

-
To set this up:
+
    $ rad node stop
+
    $ rad config edit

-
1. We stop our Radicle node and open our node configuration file:
+
Under the `node` key in our configuration, we define a new `onion` attribute
+
as follows:

-
        $ rad node stop
-
        $ rad config edit
+
```json
+
"node": {
+
    ...
+
    "onion": {
+
        "mode": "proxy",
+
        "address": "127.0.0.1:9050"
+
    }
+
}
+
```

-
2. Under the `node` key in our configuration, we define a new `onion` subkey
-
where we set its `mode` to `proxy` and `address` to the Tor SOCKS5 address
-
   `127.0.0.1:9050`:
+
This tells Radicle that for `.onion` addresses, we want to use the specified
+
Tor *SOCKS5* proxy address. This is the address on which the Tor daemon should
+
be listening on.

-
        "node": {
-
            ...
-
            "onion": {
-
                "mode": "proxy",
-
                 "address": "127.0.0.1:9050"
-
            },
-
            ...
-
        }
+
Next, we ensure our node is listening for incoming connections on port `8776`.
+
This is the port that our Tor proxy will be using to bridge connections to our
+
node, and is the same as the `HiddenServicePort` in our Tor configuration.

-
1. Still under the `node` key, we make our node publicly accessible via our
-
   `.onion` address, on port 8776:
+
```json
+
"node": {
+
    ...
+
    "listen": ["0.0.0.0:8776"]
+
}
+
```

-
        "node": {
-
            ...
-
            "externalAddresses": [
-
            "1odmmeotgcfx65l5hn6ejkaruvai222vs7o7tmtllszqk5xbysolfdd.onion:8776"
-
            ],
-
            ...
-
        }
+
Finally, if we want our `.onion` address to be publicly *discoverable* on the
+
network, we add it to our `externalAddresses`, including the port. This will
+
configure our node to advertize its `.onion` address to peers.

-
   *This is the `.onion` address we obtained earlier.*
+
```json
+
"node": {
+
    ...
+
    "externalAddresses": [
+
        "1odmmeotgcfx65l5hn6ejkaruvai222vs7o7tmtllszqk5xbysolfdd.onion:8776"
+
    ]
+
}
+
```

-
2. Lastly, under the `node` key again, we ensure our node listens for connection
-
   requests on `0.0.0.0:8776`:
+
> 👾 This is the `.onion` address we obtained earlier from the Tor `hostname`
+
> file.

-
        "node": {
-
                ...
-
                "listen": [
-
                    "0.0.0.0:8776"
-
                ],
-
                ...
-
        }
+
Note that this is **optional**: if you don't specify it here, peers will still
+
be able to connect to you if they know your `.onion` address.

This configuration allows our node to use Tor for `.onion` connections while
maintaining direct connections for regular traffic, balancing our privacy and
performance needs.

-
### Applying changes and making connections
+
#### Making connections

Now that we have updated our configuration, we can start up our node again:

-
    paxel$ rad node start
+
    $ rad node start

Since our node is online, Calyx can now continue to collaborate with us on
`schrödingers-paradox`. He uses the `rad node connect` command to establish a
direct connection with us:

-
    calyx$ rad node connect z6Mkhp7VUnuufpvuQ3PdysShAjL86VDRUpPpkesqiysDBGs9@odmmeotgcfx65l5hn6ejkaruvai222vs7o7tmtllszqk5xbysolfdd.onion:8776
+
<aside class="kicker"><code>Calyx</code></aside>
+

+
    $ rad node connect z6Mkhp7VUnuufpvuQ3PdysShAjL86VDRUpPpkesqiysDBGs9@odmmeotgcfx65l5hn6ejkaruvai222vs7o7tmtllszqk5xbysolfdd.onion:8776
+

+
Notice that the connect address is a combination of the peer's NID and its
+
`.onion` address:
+

+
    <nid> @ <onion> : <port>
+

+
Tor connections can take some time to establish, as a route through the network
+
of relays is computed. If you want your node to maintain a persistent
+
connection to a peer even after your node is restarted, simply add the above
+
address to the `connect` field in your node configuration:
+

+
```json
+
"node": {
+
    ...
+
    "connect": [
+
        "z6Mkhp7VUnuufpvuQ3PdysShAjL86VDRUpPpkesqiysDBGs9@odmmeotgcfx65l5hn6ejkaruvai222vs7o7tmtllszqk5xbysolfdd.onion:8776"
+
    ]
+
}
+
```

By leveraging Radicle's private repositories and the Tor network, we protected
our project's confidentiality and stood resilient in the face of attacks on our
@@ -2019,82 +2009,73 @@ intermediary seed node.

### Alternative Tor configurations

-
While we've focused on Mixed Mode, there are two additional configurations that
-
offer more comprehensive network privacy through Tor. These options route all
-
Radicle traffic through the Tor network, which may impact latency but provide
-
enhanced anonymity.
-

-
**Full Proxy Mode**
-

-
Full Proxy Mode routes all traffic through Tor for maximum anonymity. This mode
-
leverages the same Tor connection we set up earlier. Here's how to configure it:
-

-
1. In your Radicle configuration file, assign the global proxy to the Tor SOCKS5
-
   address `127.0.0.1:9050`:
+
While Mixed Mode allows us to connect to Tor nodes and traverse NATs, there are
+
two additional configurations that offer more comprehensive network privacy
+
through Tor, at the expense of network latency. These options route all Radicle
+
traffic through the Tor network, including to nodes that are not configured
+
with Tor.

-
        "node": {
-
            ...
-
            "proxy": "127.0.0.1:9050",
-
            ...
-
        }
+
#### Full Proxy Mode

-
2. Configure onion connections to use the global proxy by setting the mode to
-
   `forward`:
+
Full Proxy Mode routes all traffic through Tor for maximum anonymity.

-
        "node": {
-
            ...
-
            "onion": {
-
                "mode": "forward"
-
            },
-
            ...
-
        }
+
<figure class="diagram">
+
  <object type="image/svg+xml" data="/assets/images/tor-full-proxy-mode.svg"></object>
+
  <figcaption>Tor <em>Full Proxy Mode</em>.</figcaption>
+
</figure>

-
3. Make your node publicly accessible via its `.onion` address, on port 8776:
+
Instead of setting up a proxy only for `.onion` addresses, we set up a global
+
proxy for all address types:

-
        "node": {
-
            ...
-
            "externalAddresses": [
-
                "your-onion-address.onion:8776"
-
            ],
-
            ...
-
        }
+
```json
+
"node": {
+
    ...
+
    "proxy": "127.0.0.1:9050"
+
}
+
```

-
   *Be sure to replace `your-onion-address.onion` with your actual `.onion`
-
   address.*
+
Then, we change the `mode` for `.onion` addresses to `forward`, and remove
+
the `proxy` configuration from there:

-
4. Ensure your node listens for connection requests on `0.0.0.0:8776`:
+
```json
+
"node": {
+
    ...
+
    "onion": {
+
        "mode": "forward"
+
    }
+
}
+
```

-
        "node": {
-
            ...
-
            "listen": [
-
                "0.0.0.0:8776"
-
            ],
-
            ...
-
        }    
+
The `listen` and `externalAddresses` configuration doesn't change.

-
**Transparent Proxy Mode**
+
#### Transparent Proxy Mode

-
If you've already set up a fully transparent Tor proxy on your network, you can
-
use this simpler *Transparent Proxy Mode* configuration:
+
If you've already set up a fully transparent Tor proxy on your network, simply
+
omit the global proxy configuration, and set `.onion` configuration to
+
`forward`:

-
Don't set any other `proxy` settings. Instead, set the onion mode to `forward`:
+
<aside class="span-2">Configuring a transparent Tor proxy for your entire
+
network is beyond the scope of this guide. If you're interested in this setup,
+
consult the Tor Project documentation for detailed instructions.</aside>

-
        "node": {
-
            ...
-
            "onion": {
-
                "mode": "forward"
-
            },
-
            ...
-
        }
+
```json
+
"node": {
+
    ...
+
    "onion": {
+
        "mode": "forward"
+
    }
+
}
+
```

-
<aside>Configuring a transparent Tor proxy for your entire network is beyond the scope
-
of this guide. If you're interested in this setup, consult the Tor Project
-
documentation for detailed instructions.</aside>
+
This will configure your node to use your operating system's DNS resolver
+
to resolve `.onion` addresses.

-
*Radicle's codebase hasn't yet undergone formal security audits. While we're
-
confident in its security, undiscovered vulnerabilities may exist. If you
-
encounter any issues, we encourage you to report them to
-
**security@radicle.xyz***
+
> 👻
+
>
+
> Radicle's codebase hasn't yet undergone formal security audits. While we're
+
> confident in its security, undiscovered vulnerabilities may exist. If you
+
> encounter any issues, we encourage you to report them to
+
> **security@radicle.xyz**.

[proto]: /guides/protocol/
[seeder]: /guides/seeder/
@@ -2110,4 +2091,4 @@ encounter any issues, we encourage you to report them to
[ch1]: /guides/user/#1-getting-started
[ch2]: /guides/user/#2-collaborating-the-radicle-way
[ch3]: /guides/user/#3-grow-resilient-with-seeding
-
[ch4]: /guides/user/#4-embracing-the-onion

\ No newline at end of file
+
[ch4]: /guides/user/#4-embracing-the-onion
modified assets/css/guide.css
@@ -122,6 +122,7 @@ pre {
}

pre code {
+
  background: none !important;
  padding: 0;
}

@@ -274,6 +275,12 @@ figure pre {
  margin: 0;
}

+
li pre {
+
  margin-top: 1rem;
+
  margin-right: 1rem;
+
  padding: 0.75rem;
+
}
+

figure img, figure object {
  display: block;
  margin: 0 auto;
@@ -347,7 +354,7 @@ a {
  text-decoration: unset;
}

-
p a {
+
li a, p a {
  border-bottom: 1px solid var(--color-fg-dim);
}
p a:hover {
@@ -407,7 +414,7 @@ span.highlight-secondary {
  font-size: 0.75rem !important;
  margin: 2rem 0 3rem 0;
  padding: 0;
-
  max-height: 16rem;
+
  max-height: 18rem;
}

.toc a {
added assets/images/tor-full-proxy-mode.svg
@@ -0,0 +1,83 @@
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 568.6068795283663 434.95101002887304" width="568.6068795283663" height="434.95101002887304">
+
  <defs>
+
    <style>
+
      @import url("/assets/css/fonts.css");
+
      svg { font-size: 16px; font-family: "Inter", sans-serif; }
+
    </style>
+
  </defs>
+
  <g stroke-linecap="round" transform="translate(10 17.97897946733997) rotate(0 49.240810459650675 49.24081045965056)">
+
    <path d="M65.11 3.56 C72.47 5.67, 80.47 11.58, 85.81 17.58 C91.15 23.58, 95.61 31.54, 97.14 39.55 C98.66 47.55, 97.71 57.67, 94.97 65.6 C92.22 73.53, 86.82 81.82, 80.68 87.14 C74.54 92.45, 65.95 95.95, 58.13 97.49 C50.32 99.03, 41.45 99.03, 33.79 96.36 C26.12 93.68, 17.6 87.64, 12.14 81.44 C6.69 75.24, 2.52 67.32, 1.06 59.18 C-0.41 51.04, 0.68 40.37, 3.37 32.61 C6.06 24.84, 11.22 17.88, 17.21 12.57 C23.19 7.27, 30.64 2.08, 39.28 0.79 C47.92 -0.5, 63.3 3.6, 69.03 4.82 C74.77 6.04, 73.94 7.31, 73.68 8.12 M60.87 2.09 C68.72 3.67, 77.78 9.28, 83.61 15.03 C89.44 20.78, 93.63 28.88, 95.85 36.6 C98.08 44.31, 99.01 53.32, 96.98 61.31 C94.95 69.3, 89.3 78.58, 83.66 84.51 C78.02 90.45, 71.15 94.86, 63.15 96.9 C55.16 98.95, 43.68 98.99, 35.7 96.78 C27.72 94.57, 20.88 89.3, 15.25 83.66 C9.62 78.02, 4.01 70.6, 1.94 62.94 C-0.13 55.27, 0.77 45.72, 2.82 37.68 C4.88 29.64, 8.86 20.74, 14.28 14.69 C19.69 8.65, 27.56 3.37, 35.32 1.42 C43.08 -0.53, 56.36 2.87, 60.85 3.01 C65.33 3.14, 62.38 1.84, 62.23 2.24" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(41.99730023544953 57.40127893985016) rotate(0 17.424999237060547 10)">
+
    <text x="17.424999237060547" y="14.016" fill="#8888ff" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Peer</text>
+
  </g>
+
  <g stroke-linecap="round" transform="translate(243.95993195721303 326.4693891095719) rotate(0 49.24081045965045 49.24081045965056)">
+
    <path d="M67.45 3.11 C74.94 5.35, 82.49 12.52, 87.48 19.2 C92.46 25.89, 96.21 35.11, 97.39 43.21 C98.57 51.31, 97.44 60.19, 94.57 67.8 C91.71 75.41, 86.6 83.81, 80.19 88.87 C73.77 93.93, 64.31 97.03, 56.07 98.15 C47.83 99.28, 38.28 98.81, 30.75 95.62 C23.21 92.43, 15.9 85.53, 10.86 79.01 C5.82 72.49, 1.83 64.59, 0.5 56.5 C-0.84 48.42, -0.18 38.12, 2.84 30.48 C5.86 22.85, 11.85 15.55, 18.6 10.7 C25.36 5.84, 34.77 2.2, 43.38 1.35 C52 0.49, 65.4 4.8, 70.29 5.57 C75.19 6.33, 73.2 5.45, 72.76 5.92 M54.22 0.31 C62.17 0.32, 72.39 4.06, 79.06 9.06 C85.74 14.06, 90.87 22.61, 94.27 30.31 C97.67 38.01, 100.5 47.17, 99.45 55.27 C98.39 63.36, 92.95 72.37, 87.95 78.87 C82.95 85.37, 76.85 90.88, 69.46 94.27 C62.06 97.66, 51.7 99.95, 43.58 99.23 C35.46 98.51, 27.39 94.98, 20.74 89.93 C14.08 84.87, 7.28 76.71, 3.67 68.9 C0.05 61.09, -1.79 51.06, -0.97 43.06 C-0.15 35.06, 3.38 27.52, 8.6 20.92 C13.81 14.33, 22.7 7.1, 30.32 3.49 C37.93 -0.12, 50.38 -0.36, 54.29 -0.74 C58.21 -1.12, 53.64 0.03, 53.8 1.19" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(275.95723219266256 365.8916885820821) rotate(0 17.424999237060547 10)">
+
    <text x="17.424999237060547" y="14.016" fill="#8888ff" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Peer</text>
+
  </g>
+
  <g stroke-linecap="round" transform="translate(460.12525860906544 10) rotate(0 49.24081045965045 49.24081045965056)">
+
    <path d="M34.52 2.6 C41.88 -0.35, 52.2 -0.72, 60.44 1.2 C68.67 3.12, 77.88 8.44, 83.93 14.12 C89.97 19.8, 94.44 27.4, 96.68 35.29 C98.92 43.18, 99.19 53.57, 97.35 61.48 C95.5 69.39, 91.27 76.81, 85.61 82.74 C79.94 88.68, 71.46 94.72, 63.36 97.11 C55.25 99.5, 45.06 99.03, 36.98 97.08 C28.89 95.14, 20.57 90.92, 14.87 85.44 C9.17 79.96, 5.04 72.01, 2.77 64.21 C0.51 56.41, -0.59 46.92, 1.28 38.64 C3.15 30.35, 7.85 20.9, 14 14.51 C20.14 8.12, 33.37 2.53, 38.16 0.29 C42.95 -1.95, 42.39 0.26, 42.72 1.09 M48.01 0.35 C55.98 -0.36, 67.24 2.28, 74.36 6.19 C81.49 10.1, 86.59 16.86, 90.77 23.83 C94.94 30.8, 99.1 39.62, 99.42 48.01 C99.73 56.4, 96.59 66.69, 92.66 74.15 C88.72 81.6, 82.9 88.62, 75.78 92.74 C68.66 96.86, 58.54 98.96, 49.93 98.86 C41.31 98.76, 31.47 95.95, 24.11 92.14 C16.75 88.32, 9.74 83.03, 5.78 75.96 C1.82 68.9, 0.45 58.36, 0.36 49.75 C0.26 41.15, 1.52 31.48, 5.22 24.34 C8.91 17.21, 15.23 10.82, 22.51 6.93 C29.79 3.04, 44.75 1.89, 48.91 0.98 C53.07 0.07, 47.4 0.65, 47.46 1.46" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(492.12255884451497 49.42229947251019) rotate(0 17.424999237060547 10)">
+
    <text x="17.424999237060547" y="14.016" fill="#8888ff" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Peer</text>
+
  </g>
+
  <g stroke-linecap="round" transform="translate(158.85098471674928 89.02151148082146) rotate(0 47.997148734979646 45.68939014702187)">
+
    <path d="M61.74 2.33 C69.91 3.86, 78.44 9.23, 84.09 15.41 C89.73 21.59, 94.21 31.33, 95.61 39.39 C97.02 47.45, 96.03 56.25, 92.52 63.77 C89.01 71.28, 82 79.88, 74.55 84.47 C67.1 89.06, 56.55 91.25, 47.81 91.32 C39.06 91.39, 29.14 89.08, 22.07 84.89 C14.99 80.69, 8.96 73.52, 5.34 66.14 C1.71 58.77, -0.74 49.08, 0.34 40.62 C1.42 32.15, 6.34 21.92, 11.83 15.35 C17.32 8.77, 24.61 3.39, 33.28 1.18 C41.95 -1.02, 58.36 1.7, 63.85 2.11 C69.34 2.52, 66.54 2.82, 66.23 3.62 M36.14 1.82 C43.8 -1, 53 -0.35, 61.22 1.97 C69.44 4.29, 79.67 9.3, 85.45 15.74 C91.22 22.18, 94.67 32.2, 95.86 40.63 C97.05 49.06, 96.36 58.97, 92.61 66.31 C88.87 73.66, 81.1 80.55, 73.39 84.69 C65.67 88.84, 55.13 91.07, 46.31 91.16 C37.49 91.25, 27.42 89.77, 20.45 85.23 C13.49 80.69, 8.01 71.84, 4.54 63.93 C1.07 56.02, -1.39 45.85, -0.37 37.75 C0.65 29.66, 4.7 21.21, 10.66 15.37 C16.63 9.53, 31.5 4.77, 35.43 2.73 C39.36 0.68, 33.96 2.38, 34.22 3.1" stroke="#ffa083" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(185.9715241036056 114.9036240266064) rotate(0 20.9375 20)">
+
    <text x="20.9375" y="14.016" fill="#ffa083" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Tor</text>
+
    <text x="20.9375" y="34.016" fill="#ffa083" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Relay</text>
+
  </g>
+
  <g stroke-linecap="round">
+
    <g transform="translate(103.9523525193922 94.97506807501793) rotate(0 20.754503275990828 10.930892542754407)">
+
      <path d="M-0.17 1.02 C6.77 4.76, 35.33 18.65, 42.3 22.16 M-1.72 0.51 C5.08 3.89, 34.65 16.36, 41.76 20.2" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
  <g stroke-linecap="round">
+
    <g transform="translate(404.3643613584163 91.93165333194588) rotate(0 24.60424194583311 -6.782557003275258)">
+
      <path d="M0.36 0.14 C8.58 -2.18, 41.05 -11.22, 49.27 -13.45 M-0.12 -0.27 C8.05 -2.52, 40.59 -12.05, 48.87 -14.36" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
  <g stroke-linecap="round">
+
    <g transform="translate(300.8403991662567 312.49757029904686) rotate(0 1.0673190615093517 -9.743473163811188)">
+
      <path d="M0.06 0.11 C0.43 -3.1, 2.27 -15.9, 2.59 -19.16 M-0.57 -0.3 C-0.26 -3.69, 2.04 -16.87, 2.39 -19.96" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
  <g stroke-linecap="round" transform="translate(295.6899290929523 59.43611403464308) rotate(0 47.997148734979646 45.68939014702187)">
+
    <path d="M31.98 2.24 C39.61 -0.81, 51.4 -0.93, 59.8 1.05 C68.21 3.04, 76.45 8.08, 82.41 14.13 C88.37 20.19, 93.74 29.09, 95.56 37.38 C97.38 45.66, 96.56 56.21, 93.33 63.84 C90.11 71.46, 83.49 78.58, 76.22 83.14 C68.96 87.7, 58.52 90.82, 49.74 91.2 C40.96 91.57, 30.88 89.51, 23.56 85.38 C16.24 81.25, 9.86 73.82, 5.82 66.41 C1.79 58.99, -1.46 49.21, -0.65 40.89 C0.16 32.57, 4.78 23, 10.69 16.51 C16.59 10.01, 30.37 4.43, 34.79 1.93 C39.21 -0.57, 36.93 0.7, 37.2 1.5 M69.52 3.99 C76.97 6.78, 85.84 15.52, 90.07 22.7 C94.3 29.88, 95.65 38.66, 94.91 47.08 C94.16 55.5, 90.71 66.13, 85.61 73.24 C80.51 80.34, 72.33 86.87, 64.31 89.69 C56.29 92.51, 45.62 91.86, 37.48 90.15 C29.34 88.43, 21.3 84.95, 15.47 79.41 C9.63 73.87, 4.74 64.84, 2.45 56.91 C0.17 48.98, -1.03 39.62, 1.75 31.82 C4.53 24.02, 12.15 15.54, 19.13 10.11 C26.11 4.68, 35.33 0.2, 43.63 -0.74 C51.92 -1.68, 64.75 3.36, 68.89 4.49 C73.02 5.62, 68.47 5.04, 68.44 6.03" stroke="#ffa083" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(322.8104684798086 85.31822658042802) rotate(0 20.9375 20)">
+
    <text x="20.9375" y="14.016" fill="#ffa083" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Tor</text>
+
    <text x="20.9375" y="34.016" fill="#ffa083" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Relay</text>
+
  </g>
+
  <g stroke-linecap="round" transform="translate(250.64668144360576 187.3589373587863) rotate(0 47.997148734979646 45.68939014702187)">
+
    <path d="M67.65 4.55 C75.17 7.15, 83.29 13.87, 88.08 20.65 C92.87 27.43, 96.31 36.83, 96.4 45.24 C96.49 53.64, 93.37 64.17, 88.62 71.06 C83.87 77.95, 75.86 83.21, 67.9 86.58 C59.93 89.95, 49.39 92.33, 40.82 91.28 C32.26 90.23, 23.15 85.9, 16.51 80.28 C9.87 74.65, 3.42 65.64, 0.98 57.52 C-1.46 49.41, -0.64 39.45, 1.86 31.59 C4.37 23.73, 9.55 15.41, 16.02 10.35 C22.5 5.29, 31.02 1.78, 40.71 1.25 C50.4 0.72, 67.64 5.51, 74.16 7.17 C80.68 8.84, 80.33 10.6, 79.84 11.27 M41.76 -0.79 C49.66 -2.42, 60.67 -0.54, 68.63 2.97 C76.58 6.47, 84.76 13.18, 89.49 20.24 C94.23 27.31, 97.08 36.96, 97.04 45.34 C97 53.72, 94.01 63.54, 89.23 70.52 C84.45 77.51, 76.3 83.96, 68.35 87.26 C60.4 90.57, 50.1 91.35, 41.53 90.34 C32.97 89.34, 23.47 86.61, 16.96 81.25 C10.46 75.89, 5.07 66.45, 2.49 58.17 C-0.09 49.9, -0.71 39.34, 1.5 31.61 C3.71 23.89, 9.2 17.27, 15.74 11.83 C22.28 6.38, 36.37 0.86, 40.74 -1.05 C45.11 -2.96, 41.7 -0.87, 41.98 0.37" stroke="#ffa083" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(277.76722083046207 213.24104990457118) rotate(0 20.9375 20)">
+
    <text x="20.9375" y="14.016" fill="#ffa083" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Tor</text>
+
    <text x="20.9375" y="34.016" fill="#ffa083" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Relay</text>
+
  </g>
+
  <g stroke-linecap="round">
+
    <g transform="translate(264.5509668975001 117.26276698181437) rotate(0 11.617632317859488 -2.1679597137701876)">
+
      <path d="M0.31 0.38 C4.18 -0.29, 19.54 -3.39, 23.44 -4.25 M-0.2 0.1 C3.6 -0.45, 19.15 -2.75, 23.12 -3.5" stroke="#ffa083" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
  <g stroke-linecap="round">
+
    <g transform="translate(255.18190619558663 169.66050724295633) rotate(0 6.569672534478286 6.4982404660564725)">
+
      <path d="M0 -0.11 C2.11 1.89, 10.8 10.38, 12.99 12.54 M-0.66 -0.64 C1.34 1.64, 10.24 10.66, 12.48 13.01" stroke="#ffa083" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
  <g stroke-linecap="round">
+
    <g transform="translate(325.1159534091057 179.31113752337114) rotate(0 3.8664573842822847 -11.43044409157892)">
+
      <path d="M0.44 -0.53 C1.63 -4.27, 6.31 -18.91, 7.52 -22.55 M0 0.39 C1.07 -3.5, 5.76 -19.42, 6.98 -23.36" stroke="#ffa083" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
</svg>
added assets/images/tor-mixed-mode.svg
@@ -0,0 +1,50 @@
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 599.2362879299221 443.05879460575545" width="599.2362879299221" height="443.05879460575545">
+
  <defs>
+
    <style>
+
      @import url("/assets/css/fonts.css");
+
      svg { font-size: 16px; font-family: "Inter", sans-serif; }
+
    </style>
+
  </defs>
+
  <g stroke-linecap="round" transform="translate(10.000000000000227 26.086764044222264) rotate(0 49.24081045965056 49.24081045965056)">
+
    <path d="M60.59 0.71 C68.35 1.87, 77.18 7.44, 83.19 13.46 C89.19 19.47, 94.19 28.85, 96.62 36.82 C99.05 44.79, 99.75 53.47, 97.77 61.27 C95.79 69.07, 90.75 77.84, 84.72 83.61 C78.7 89.39, 69.64 93.82, 61.64 95.95 C53.65 98.07, 44.42 98.18, 36.74 96.36 C29.07 94.54, 21.36 90.53, 15.61 85.01 C9.85 79.49, 4.45 71.11, 2.22 63.25 C-0.01 55.39, 0.11 45.97, 2.23 37.86 C4.35 29.75, 9.3 20.65, 14.95 14.59 C20.6 8.52, 27.27 3.16, 36.15 1.47 C45.03 -0.23, 62.16 3.28, 68.22 4.42 C74.29 5.56, 72.8 7.65, 72.54 8.32 M58.32 2.43 C66.05 3.19, 76.61 6.69, 82.89 12.02 C89.18 17.35, 93.75 26.34, 96.05 34.41 C98.35 42.49, 98.37 52.72, 96.71 60.48 C95.04 68.24, 91.12 75.11, 86.06 80.95 C81 86.8, 74.24 92.73, 66.36 95.57 C58.49 98.42, 47.25 99.41, 38.81 98.03 C30.37 96.65, 21.79 92.63, 15.71 87.3 C9.62 81.97, 4.95 74.24, 2.31 66.06 C-0.32 57.87, -1.78 46.27, -0.09 38.19 C1.6 30.11, 6.91 23.34, 12.45 17.56 C17.99 11.79, 25.4 6.19, 33.17 3.52 C40.94 0.86, 54.75 1.95, 59.07 1.57 C63.39 1.19, 59.29 0.74, 59.1 1.24" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(41.99730023544976 65.50906351673245) rotate(0 17.424999237060547 10)">
+
    <text x="17.424999237060547" y="14.016" fill="#8888ff" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Peer</text>
+
  </g>
+
  <g stroke-linecap="round" transform="translate(243.95993195721303 334.57717368645433) rotate(0 49.24081045965056 49.24081045965056)">
+
    <path d="M43.4 1.25 C51.05 -0.48, 61.3 0.48, 69.06 3.59 C76.82 6.7, 85.17 13.14, 89.96 19.92 C94.74 26.7, 97.15 35.95, 97.77 44.28 C98.39 52.6, 96.94 62.31, 93.68 69.88 C90.41 77.46, 84.8 85.18, 78.16 89.75 C71.52 94.33, 61.96 96.49, 53.84 97.35 C45.72 98.21, 36.88 97.98, 29.44 94.91 C22 91.85, 14.08 85.63, 9.23 78.97 C4.37 72.31, 1.12 63.29, 0.31 54.95 C-0.5 46.6, 1.07 36.61, 4.36 28.9 C7.65 21.18, 11.54 13.31, 20.06 8.66 C28.58 4, 47.89 1.7, 55.49 0.98 C63.08 0.25, 65.95 3.67, 65.63 4.28 M57.98 0.15 C65.72 0.39, 75.69 4.94, 82.07 10.34 C88.45 15.74, 93.85 24.81, 96.26 32.55 C98.66 40.29, 98.05 48.78, 96.5 56.78 C94.96 64.78, 91.98 74.28, 87.01 80.54 C82.04 86.79, 74.75 91.58, 66.7 94.32 C58.66 97.05, 47.13 98.28, 38.74 96.95 C30.34 95.62, 22.08 91.79, 16.33 86.34 C10.57 80.89, 6.71 71.94, 4.21 64.27 C1.7 56.6, -0.16 48.19, 1.31 40.32 C2.78 32.45, 8.01 23.46, 13.02 17.04 C18.04 10.62, 23.86 4.5, 31.39 1.81 C38.92 -0.89, 53.83 1.01, 58.2 0.87 C62.57 0.73, 57.66 0.15, 57.61 0.98" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(275.95723219266256 373.9994731589645) rotate(0 17.424999237060547 10)">
+
    <text x="17.424999237060547" y="14.016" fill="#8888ff" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Peer</text>
+
  </g>
+
  <g stroke-linecap="round" transform="translate(490.754667010621 10) rotate(0 49.24081045965056 49.24081045965056)">
+
    <path d="M29.28 4.45 C36.15 0.79, 46.87 0.18, 55.14 1.1 C63.41 2.03, 72.51 5.12, 78.89 10 C85.28 14.87, 90.25 22.98, 93.46 30.35 C96.66 37.72, 98.9 46.14, 98.13 54.21 C97.37 62.27, 93.62 71.95, 88.87 78.74 C84.12 85.53, 77.31 91.7, 69.66 94.94 C62.01 98.19, 51.34 99.31, 42.98 98.21 C34.62 97.1, 26.09 93.25, 19.49 88.33 C12.88 83.4, 6.65 75.99, 3.34 68.64 C0.02 61.29, -1.55 52.44, -0.4 44.23 C0.75 36.02, 4.72 26.23, 10.25 19.38 C15.79 12.54, 28.19 6.07, 32.8 3.17 C37.41 0.27, 37.58 1.2, 37.91 1.98 M59.91 2.18 C67.86 2.71, 75.7 6.51, 81.47 11.76 C87.24 17.01, 91.9 26.03, 94.53 33.69 C97.16 41.36, 98.41 49.92, 97.26 57.76 C96.12 65.59, 93.04 74.54, 87.66 80.71 C82.29 86.89, 73.07 92.07, 65.03 94.8 C56.98 97.53, 47.5 98.53, 39.39 97.09 C31.28 95.65, 22.49 91.68, 16.36 86.15 C10.22 80.62, 4.9 71.69, 2.58 63.9 C0.25 56.11, 0.79 47.2, 2.4 39.41 C4 31.62, 7 23.25, 12.2 17.14 C17.39 11.03, 25.53 5.58, 33.56 2.75 C41.6 -0.08, 56.2 0.31, 60.41 0.18 C64.62 0.04, 58.99 0.99, 58.81 1.91" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(522.7519672460705 49.42229947251019) rotate(0 17.424999237060547 10)">
+
    <text x="17.424999237060547" y="14.016" fill="#8888ff" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Peer</text>
+
  </g>
+
  <g stroke-linecap="round" transform="translate(208.39855713103043 72.80594232705675) rotate(0 86.28390923692393 75.41793359559034)">
+
    <path d="M103.46 0.79 C114.45 1.5, 126.71 7.64, 136.29 13.85 C145.87 20.06, 155.14 29.19, 160.96 38.05 C166.78 46.91, 170 56.83, 171.19 67.01 C172.38 77.19, 171.4 89.37, 168.1 99.14 C164.8 108.9, 159.06 118.06, 151.41 125.62 C143.75 133.17, 132.88 140.33, 122.18 144.48 C111.49 148.64, 99.08 150.45, 87.22 150.56 C75.36 150.66, 61.95 149.34, 51.01 145.12 C40.07 140.9, 29.37 132.97, 21.59 125.23 C13.82 117.49, 7.94 108.13, 4.35 98.67 C0.77 89.22, -1.07 78.72, 0.07 68.5 C1.21 58.29, 5.43 46.51, 11.2 37.39 C16.97 28.27, 25.04 19.66, 34.7 13.79 C44.36 7.93, 56.28 3.95, 69.18 2.2 C82.08 0.44, 103.75 2.51, 112.1 3.26 C120.44 4, 119.73 5.39, 119.23 6.65 M126.44 7.47 C136.94 11.11, 146.79 20.52, 153.84 28.37 C160.88 36.22, 165.76 44.93, 168.7 54.58 C171.64 64.23, 173.26 75.7, 171.48 86.27 C169.71 96.83, 164.39 109.37, 158.05 117.99 C151.7 126.61, 142.91 132.44, 133.4 137.99 C123.9 143.55, 112.63 149.52, 101.01 151.35 C89.4 153.17, 75.48 151.98, 63.72 148.95 C51.96 145.92, 39.26 139.65, 30.43 133.17 C21.6 126.7, 15.7 119.23, 10.73 110.11 C5.76 101, 1.66 88.8, 0.62 78.48 C-0.41 68.16, 0.52 57.48, 4.53 48.18 C8.55 38.89, 16.46 29.83, 24.73 22.74 C33 15.65, 43.16 9.28, 54.18 5.64 C65.19 2, 78.98 0.54, 90.81 0.92 C102.63 1.29, 119.21 6.39, 125.12 7.91 C131.04 9.42, 126.81 8.76, 126.29 10" stroke="#ff80b3" stroke-width="2" fill="none"></path>
+
  </g>
+
  <g transform="translate(254.12052980218027 128.3953436541284) rotate(0 40.54999923706055 20)">
+
    <text x="40.54999923706055" y="14.016" fill="#ff80b3" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Radicle</text>
+
    <text x="40.54999923706055" y="34.016" fill="#ff80b3" text-anchor="middle" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">Seed Node</text>
+
  </g>
+
  <g stroke-linecap="round">
+
    <g transform="translate(106.7188020609808 98.0281862123361) rotate(0 43.591912538837846 16.94084967717174)">
+
      <path d="M-0.31 1.03 C14.36 6.77, 72.34 28.02, 87.12 33.44 M1.72 0.53 C16.31 6.52, 71.73 29.05, 86.14 34.75" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
  <g stroke-linecap="round">
+
    <g transform="translate(386.79664803349806 123.63134267198359) rotate(0 49.621984908011655 -20.2836074052625)">
+
      <path d="M-0.27 -0.1 C16.35 -6.81, 83.6 -34.66, 100.33 -41.22 M1.78 -1.2 C18.3 -7.68, 83.61 -33.35, 99.95 -40.03" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
  <mask></mask>
+
  <g stroke-linecap="round">
+
    <g transform="translate(292.2289649290151 320.3279833495559) rotate(0 0.5967840028920364 -43.26484114071866)">
+
      <path d="M1.04 -0.62 C1.37 -15.25, 1.08 -72.39, 1.17 -86.62 M0.13 1.67 C0.36 -12.26, 0.06 -70.18, 0.22 -85.13" stroke="#8888ff" stroke-width="2" fill="none"></path>
+
    </g>
+
  </g>
+
</svg>
modified scripts/cleanup-svgs.sh
@@ -6,6 +6,11 @@ if [ $# -lt 1 ]; then
  exit 1
fi

+
if ! command -v tidy 2>&1; then
+
  echo "error: the 'tidy' executable is required for formatting"
+
  exit 1
+
fi
+

# Function to process a single file
process() {
    FILE="$1"
@@ -17,9 +22,9 @@ process() {
    fi

    # Format SVG.
-
    xmllint --format "$FILE" --output "$FILE"
-
    # Remove `<?xml>` header.
-
    sed -i 1d "$FILE"
+
    tidy -xml -i -m -q -w 0 "$FILE"
+
    # Remove `<?xml>` header, if present.
+
    sed -i '1{/^<?xml/d;}' "$FILE"

    # Remove all instances of `font-family="..."` and `font-size="..."`
    sed -i -e 's/font-family="[^"]*"//g' -e 's/font-size="16px"//g' "$FILE"