Saturday, May 09, 2026 AM03:21:24 HKT

This commit is contained in:
2026-05-09 03:21:32 +08:00
commit 41f17b127c
884 changed files with 263824 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# Tor Source Code Adjustment
Due to the nature of introduction attacks, different adjustments can be made to the tor source layer which will counter onion downtime. By using these patches, tor will more effectively handle introduction traffic and introduction point rotation.
It's important to note the two issues that tor's onion service circuit protocol has that introduction cell DDOSERs exploit and how these patches counter them.
When your tor process launches an onion service, it will, by default, connect to three
'introduction' points. These points are long-lasting circuits to specific relays. The tor process then creates a descriptor of these introduction points and signs it with your onion service key, publishing it to certain HSDIRs on the tor network. When a user goes to connect to your onion service, they will first pull the descriptor of these introduction points from a HSDIR and then make an 'introduction cell' request to one of them. The introduction point will then relay this request to the onion service's tor process. Included within this introduction cell is where the user wants to meet the onion service. This meeting point is called the rendezvous point. The onion service needs to build out a circuit to this rendezvous point to start the connection with the user.
While this design has strong protection for privacy, there are a few issues. Mainly, there is no way to know if the introduction cell is from a user or an attacker. The onion service NEEDS to build the circuit to the rendezvous point to find out. And that process is both computationally expensive and hard on the tor network to do a lot. While just sending an introduction cell is cheap,.
The Tor Project fixes this with Proof of Work (POW). POW is basically a computationally expensive puzzle a user's computer would need to solve for them to send an introduction cell. This switches back the computation required by the onion service to the attacker. But there is an oversight in its design. An attacker can still send an introduction cell with no POW, and it can still be accepted. For compatibility sake, this makes sense. Only require POW when it is needed, and still accept cells from clients who don't have POW yet. But when you are attacked, you want your tor process not to accept these trash zero-work introduction cells.
Thus the minwork.patch. It creates a new option that sets a minimum required effort for an introduction cell to be accepted in the Tor process. A user can still send a zero-work introduction cell, but the tor process will not build the circuit to the rendezvous point. This immediately kills the introduction attack in its tracks.
Being that it sets a requirement for POW, this means users will need to have POW. It also means that gobalance **will not work** with it. As POW requires a specific "seed" per front for the POW puzzle, you can't combine multiple introduction points from different fronts to provide the load balancing. This is a limitation that can't be overcome without all fronts communicating a shared seed. With no way to set this seed from the tor control port, it's a standstill until that work is done or a patch is developed for it.
### Introduction points being "expired" (spent)
While the POW patch does protect against building trash circuits to the rendezvous point, it doesn't counter your introduction points being expired.
It might come as a surprise to you that introduction points have a lifetime both in time and in requests. It makes sense, as you don't really want a compromised introduction point to just send a massive amount of introduction cells to overload your front or keep a circuit alive forever.
You can find the specific limits outlined in tor's C source at /src/core/or/or.h lines starting at 959. There is a single value that you need to care about. Specifically INTRO_POINT_MIN_LIFETIME_INTRODUCTIONS.
When an introduction cell attack happens, they spam introduction points. This causes the minimal value of 16384 (maximum of 32768) to get "spent." Causing the introduction point circuit to expire and be closed. This is extra problematic for onion services, being that it publishes these introduction points to a HSDIR the user will need to request from. The tor process also caches this information, and after the whole descriptor is expired, it can take a few minutes for the tor process to grab a new one.
Keeping your introduction points alive for longer counters the 'onion site not found' error. The solution is to increase the number of circuits an introduction point can pass before becoming expired. When compiling your updated tor, take time to edit the or.h INTRO_POINT_MIN_LIFETIME_INTRODUCTIONS value to a more fitting number. Generally, add a zero or two there. It hurts the front's privacy, but not as bad as building a lot of introduction circuits does. Specifically to introduction points that it then publishes for the world to see.
### Script to make it easy.
Included is an automated script you will need to run as root on a Debian 12 system to compile an ideal tor process. You only need to do this once, and then replace the tor process on the endgame fronts with the patched tor process.
Just put the tor-patch directory into a Debian 12 system and run the tor-build.sh with sudo. It will pull the tor binary to the working directory after it's all done. Transfer that binary over to your other endgame fronts (assuming they are also Debian 12). Put it in /usr/sbin/tor like
`mv tor-patched-binary /usr/sbin/tor`
### Questions? Ask /u/paris.
+237
View File
@@ -0,0 +1,237 @@
diff --git a/doc/man/tor.1.txt b/doc/man/tor.1.txt
index 1589809b1a..71178e65c9 100644
--- a/doc/man/tor.1.txt
+++ b/doc/man/tor.1.txt
@@ -3121,6 +3121,15 @@ The following options are per onion service:
These options are applicable to both onion services and their clients:
+[[HiddenServicePoWEffort]] **HiddenServicePoWEffort** __NUM__::
+
+ The minimum required proof-of-work effort level needed to reach the given
+ Hidden Service. When this option is set to an integer larger than -1, the
+ service will no longer automatically increase its effort level, but will always
+ use the specified effort level as its effort. If this option is set to -1 (the
+ default), Tor will automatically tune the effort level to an appropriate value.
+ (Default: -1)
+
[[CompiledProofOfWorkHash]] **CompiledProofOfWorkHash** **0**|**1**|**auto**::
When proof-of-work DoS mitigation is active, both the services themselves
and the clients which connect will use a dynamically generated hash
diff --git a/src/app/config/config.c b/src/app/config/config.c
index a10329c552..1cacae4454 100644
--- a/src/app/config/config.c
+++ b/src/app/config/config.c
@@ -517,6 +517,7 @@ static const config_var_t option_vars_[] = {
VAR("HiddenServicePoWDefensesEnabled", LINELIST_S, RendConfigLines, NULL),
VAR("HiddenServicePoWQueueRate", LINELIST_S, RendConfigLines, NULL),
VAR("HiddenServicePoWQueueBurst", LINELIST_S, RendConfigLines, NULL),
+ VAR("HiddenServicePoWEffort", LINELIST_S, RendConfigLines, NULL),
VAR("HiddenServiceStatistics", BOOL, HiddenServiceStatistics_option, "1"),
V(ClientOnionAuthDir, FILENAME, NULL),
OBSOLETE("CloseHSClientCircuitsImmediatelyOnTimeout"),
diff --git a/src/core/or/connection_edge.c b/src/core/or/connection_edge.c
index f21779a80c..2230162fd3 100644
--- a/src/core/or/connection_edge.c
+++ b/src/core/or/connection_edge.c
@@ -983,8 +983,9 @@ export_hs_client_circuit_id(edge_connection_t *edge_conn,
char *buf = NULL;
const char dst_ipv6[] = "::1";
+
/* See RFC4193 regarding fc00::/7 */
- const char src_ipv6_prefix[] = "fc00:dead:beef:4dad:";
+ const char src_ipv6_prefix[] = "fc00:dead:beef:4dad";
uint16_t dst_port = 0;
uint16_t src_port = 1; /* default value */
uint32_t gid = 0; /* default value */
@@ -1000,9 +1001,24 @@ export_hs_client_circuit_id(edge_connection_t *edge_conn,
dst_port = edge_conn->hs_ident->orig_virtual_port;
}
+ /* Include PoW effort (if any?) in the proxy string. */
+ uint16_t pow_effort_low = 0;
+ uint16_t pow_effort_high = 0;
+
+ if (edge_conn->on_circuit) {
+ origin_circuit_t *circ = TO_ORIGIN_CIRCUIT(edge_conn->on_circuit);
+
+ if (circ) {
+ const uint32_t effort = circ->hs_pow_effort;
+ pow_effort_low = effort & 0x0000ffff;
+ pow_effort_high = effort >> 16;
+ }
+ }
+
/* Build the string */
- tor_asprintf(&buf, "PROXY TCP6 %s:%x:%x %s %d %d\r\n",
+ tor_asprintf(&buf, "PROXY TCP6 %s:%x:%x:%x:%x %s %d %d\r\n",
src_ipv6_prefix,
+ pow_effort_high, pow_effort_low,
gid >> 16, gid & 0x0000ffff,
dst_ipv6, src_port, dst_port);
diff --git a/src/feature/hs/hs_circuit.c b/src/feature/hs/hs_circuit.c
index 4904f3ddf9..0b0a9eeafd 100644
--- a/src/feature/hs/hs_circuit.c
+++ b/src/feature/hs/hs_circuit.c
@@ -830,6 +830,19 @@ handle_rend_pqueue_cb(mainloop_event_t *ev, void *arg)
continue; /* do not increment count, this one's free */
}
+ /* We check if the effort is above our min. manually set value, if any. */
+ const uint32_t min_effort = service->config.pow_effort;
+
+ if (! service->config.pow_effort_auto &&
+ req->rdv_data.pow_effort < min_effort) {
+ log_info(LD_REND,
+ "Top rend request didn't meet minimum required "
+ "manually specified effort level; discarding and "
+ "moving to the next one.");
+ free_pending_rend(req);
+ continue;
+ }
+
/* Launch the rendezvous circuit. */
launch_rendezvous_point_circuit(service, &req->ip_auth_pubkey,
&req->ip_enc_key_kp, &req->rdv_data, now);
diff --git a/src/feature/hs/hs_config.c b/src/feature/hs/hs_config.c
index 296941138b..08627f96c0 100644
--- a/src/feature/hs/hs_config.c
+++ b/src/feature/hs/hs_config.c
@@ -409,6 +409,8 @@ config_service_v3(const hs_opts_t *hs_opts,
config->has_pow_defenses_enabled = hs_opts->HiddenServicePoWDefensesEnabled;
config->pow_queue_rate = hs_opts->HiddenServicePoWQueueRate;
config->pow_queue_burst = hs_opts->HiddenServicePoWQueueBurst;
+ config->pow_effort_auto = hs_opts->HiddenServicePoWEffort == -1;
+ config->pow_effort = hs_opts->HiddenServicePoWEffort;
log_info(LD_REND, "Service PoW defenses are %s",
config->has_pow_defenses_enabled ? "enabled" : "disabled");
@@ -417,6 +419,11 @@ config_service_v3(const hs_opts_t *hs_opts,
config->pow_queue_rate);
log_info(LD_REND, "Service PoW queue burst set to: %" PRIu32,
config->pow_queue_burst);
+
+ if (! config->pow_effort_auto) {
+ log_info(LD_REND, "Service PoW minimum effort manually set to: %" PRIu32,
+ config->pow_effort);
+ }
}
/* We do not load the key material for the service at this stage. This is
diff --git a/src/feature/hs/hs_options.inc b/src/feature/hs/hs_options.inc
index 4ec62d592b..4de1d9ad88 100644
--- a/src/feature/hs/hs_options.inc
+++ b/src/feature/hs/hs_options.inc
@@ -35,4 +35,7 @@ CONF_VAR(HiddenServicePoWDefensesEnabled, BOOL, 0, "0")
CONF_VAR(HiddenServicePoWQueueRate, POSINT, 0, "250")
CONF_VAR(HiddenServicePoWQueueBurst, POSINT, 0, "2500")
+// "-1" implies "automatic".
+CONF_VAR(HiddenServicePoWEffort, INT, 0, "-1")
+
END_CONF_STRUCT(hs_opts_t)
diff --git a/src/feature/hs/hs_service.c b/src/feature/hs/hs_service.c
index 3cc8c23e0b..d0d34a5521 100644
--- a/src/feature/hs/hs_service.c
+++ b/src/feature/hs/hs_service.c
@@ -301,7 +301,8 @@ initialize_pow_defenses(hs_service_t *service)
/* We recalculate and update the suggested effort every HS_UPDATE_PERIOD
* seconds. */
- pow_state->suggested_effort = 0;
+ pow_state->suggested_effort = service->config.pow_effort;
+
pow_state->rend_handled = 0;
pow_state->total_effort = 0;
pow_state->next_effort_update = (time(NULL) + HS_UPDATE_PERIOD);
@@ -2704,6 +2705,13 @@ update_suggested_effort(hs_service_t *service, time_t now)
/* Make life easier */
hs_pow_service_state_t *pow_state = service->state.pow_state;
+ /* If the operator have manually specified the effort, we simply return that
+ * here. */
+ if (! service->config.pow_effort_auto) {
+ pow_state->suggested_effort = service->config.pow_effort;
+ return;
+ }
+
/* Calculate the new suggested effort, using an additive-increase
* multiplicative-decrease estimation scheme. */
enum {
diff --git a/src/feature/hs/hs_service.h b/src/feature/hs/hs_service.h
index 36d67719ca..bf993bb0bb 100644
--- a/src/feature/hs/hs_service.h
+++ b/src/feature/hs/hs_service.h
@@ -271,6 +271,12 @@ typedef struct hs_service_config_t {
uint32_t pow_queue_rate;
uint32_t pow_queue_burst;
+ /** PoW effort is set manually or automatically by the service? */
+ bool pow_effort_auto;
+
+ /** PoW min. required effort if pow_effort_auto is false. */
+ uint32_t pow_effort;
+
/** If set, contains the Onion Balance master ed25519 public key (taken from
* an .onion addresses) that this tor instance serves as backend. */
smartlist_t *ob_master_pubkeys;
diff --git a/src/test/test_hs_service.c b/src/test/test_hs_service.c
index dc60c7ca29..588e8c4228 100644
--- a/src/test/test_hs_service.c
+++ b/src/test/test_hs_service.c
@@ -2197,7 +2197,7 @@ test_export_client_circuit_id(void *arg)
/* Check contents */
cp1 = buf_get_contents(conn->outbuf, &sz);
tt_str_op(cp1, OP_EQ,
- "PROXY TCP6 fc00:dead:beef:4dad::0:29a ::1 666 42\r\n");
+ "PROXY TCP6 fc00:dead:beef:4dad:0:0:0:29a ::1 666 42\r\n");
/* Change circ GID and see that the reported circuit ID also changes */
or_circ->global_identifier = 22;
@@ -2214,7 +2214,7 @@ test_export_client_circuit_id(void *arg)
export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol);
cp1 = buf_get_contents(conn->outbuf, &sz);
tt_str_op(cp1, OP_EQ,
- "PROXY TCP6 fc00:dead:beef:4dad::ffff:ffff ::1 65535 42\r\n");
+ "PROXY TCP6 fc00:dead:beef:4dad:0:0:ffff:ffff ::1 65535 42\r\n");
tor_free(cp1);
/* Check that GID with UINT16_MAX works. */
@@ -2223,7 +2223,7 @@ test_export_client_circuit_id(void *arg)
export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol);
cp1 = buf_get_contents(conn->outbuf, &sz);
tt_str_op(cp1, OP_EQ,
- "PROXY TCP6 fc00:dead:beef:4dad::0:ffff ::1 65535 42\r\n");
+ "PROXY TCP6 fc00:dead:beef:4dad:0:0:0:ffff ::1 65535 42\r\n");
tor_free(cp1);
/* Check that GID with UINT16_MAX + 7 works. */
@@ -2231,7 +2231,25 @@ test_export_client_circuit_id(void *arg)
export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol);
cp1 = buf_get_contents(conn->outbuf, &sz);
- tt_str_op(cp1, OP_EQ, "PROXY TCP6 fc00:dead:beef:4dad::1:6 ::1 6 42\r\n");
+ tt_str_op(cp1, OP_EQ, "PROXY TCP6 fc00:dead:beef:4dad:0:0:1:6 ::1 6 42\r\n");
+
+ /* Check that GID UINT32_MAX works and set a PoW effort. */
+ or_circ->global_identifier = UINT32_MAX;
+ or_circ->hs_pow_effort = 1337;
+
+ export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol);
+ cp1 = buf_get_contents(conn->outbuf, &sz);
+ tt_str_op(cp1, OP_EQ,
+ "PROXY TCP6 fc00:dead:beef:4dad:0:539:ffff:ffff ::1 65535 42\r\n");
+
+ /* Check that GID UINT32_MAX works and set a PoW effort to UINT16_MAX + 7. */
+ or_circ->global_identifier = UINT32_MAX;
+ or_circ->hs_pow_effort = UINT16_MAX + 7;
+
+ export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol);
+ cp1 = buf_get_contents(conn->outbuf, &sz);
+ tt_str_op(cp1, OP_EQ,
+ "PROXY TCP6 fc00:dead:beef:4dad:1:6:ffff:ffff ::1 65535 42\r\n");
done:
UNMOCK(connection_write_to_buf_impl_);
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
apt-get update
apt-get upgrade
DEBIAN_FRONTEND=noninteractiv apt-get install -y -q git
#update release branch to latest stable!
git clone https://gitlab.torproject.org/tpo/core/tor.git --branch release-0.4.8
DEBIAN_FRONTEND=noninteractive apt-get install -y -q -t unstable autoconf
DEBIAN_FRONTEND=noninteractive apt-get install -y -q apt-utils autotools-dev automake build-essential ca-certificates file libevent-dev liblzma-dev libscrypt-dev libseccomp-dev libssl-dev pkg-config python3 zlib1g-dev libzstd-dev
cp minwork.patch tor
cd tor || { echo "Error: No tor git folder. Check if you have the right branch!"; exit 1; }
#patch with POW
git apply minwork.patch > /dev/null 2>&1 || { echo "Error: Failed to patch the Tor source code."; exit 1; }
#Updating the min introduction value from 16384 to 163840.
sed -i 's/16384/163840/g' src/core/or/or.h || { echo "Error: Failed to update min introduction value."; exit 1; }
./autogen.sh
./configure --enable-fatal-warnings --disable-asciidoc --enable-gpl --enable-zstd --enable-lzma --disable-module-relay --disable-module-dirauth --disable-html-manual --prefix="/usr/" --sysconfdir="/etc/"
make -j4 -k all || { echo "Error: Failed to compile Tor."; exit 1; }
cd ..
mv tor/src/app/tor tor-patched-binary
cp tor-patched-binary /usr/sbin/tor
# echo -n "Finished Binary Patch!"
# echo -n "----------"
# echo -n "Do you want to install the patched tor binary? [y/n] "
# read answer || { echo "Error: User input not provided."; exit 1; }
#
# if [[ "$answer" =~ ^[Yy]$ ]]; then
# mv tor-patched-binary /usr/sbin/tor
# else
# echo "Patched tor binary saved as tor-patched-binary."
# fi