feat(svc-net-tor): add a chutney flavor for private-net test deployments

The test variant (meta/variants.yml, variant 0) switches the tor
service to the chutney flavor: the image ships chutney and the
entrypoint boots a private Tor network inside the container (all nodes
on 127.0.0.1, so the design is identical under compose host-networking
and swarm). The entrypoint bootstraps from /opt/chutney (chutney
creates its net/ state CWD-relative), extracts the DirAuthority/
TestingTorNetwork lines into /run/tor/chutney-net.conf and fails fast
when that file ends up empty, since the node would otherwise silently
join the public network. torrc %includes the conf only for the chutney
flavor.

Descriptor publish and rendezvous now run at localhost speed: per-app
onion e2e suites dropped from minutes to seconds in the compose
validation. The test variant also raises the footprint to
2g/1.0cpu/512pids (measured: at 1g/0.25 the ~9 extra tor nodes pin the
caps and desc publishing stalls); meta/services.yml stays static on
the public flavor and the old production footprint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Fable 5 committed 2026-07-24 19:31:16 +02:00
1 parent 4afc9b7702
commit 5399b48482
10 files changed
+117 -4

No files matched your search

+14 -1
View File
@@ -2,10 +2,23 @@ ARG TOR_BASE_IMAGE
ARG TOR_BASE_VERSION
FROM ${TOR_BASE_IMAGE}:${TOR_BASE_VERSION}
ARG TOR_FLAVOR
ENV TOR_FLAVOR=${TOR_FLAVOR}
RUN rm -rf /var/lib/apt/lists/* \
&& apt-get -o Acquire::Retries=3 update \
&& apt-get -o Acquire::Retries=3 install -y --no-install-recommends tor \
&& if [ "${TOR_FLAVOR}" = "chutney" ]; then \
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
git ca-certificates python3 python3-cryptography python3-networkx \
python3-paramiko python3-rpyc python3-tomli-w python3-typeguard \
python3-typing-extensions \
&& git clone --depth=1 https://gitlab.torproject.org/tpo/core/chutney /opt/chutney ; \
fi \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["tor"]
COPY entrypoint.sh /usr/local/bin/tor-entrypoint.sh
RUN chmod 0755 /usr/local/bin/tor-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/tor-entrypoint.sh"]
CMD ["-f", "/etc/tor/torrc"]
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# Tor container entrypoint.
#
# public flavor: exec tor directly against the bind-mounted torrc.
# chutney flavor: first bring up a private, offline Tor test network with
# chutney inside this container (all nodes on 127.0.0.1), extract its
# authority/testing lines into the file the rendered torrc %includes, then
# exec the node tor joined to that private net. Descriptor publication and
# rendezvous then run at localhost speed instead of over the public network.
set -eu
if [ "${TOR_FLAVOR}" = "chutney" ]; then
net="hs-v3-min"
conf=/run/tor/chutney-net.conf
mkdir -p /run/tor
echo "[tor-entrypoint] bringing up chutney private network '${net}'"
# Exception: chutney creates its net/ state dir relative to the CWD, so
# every call must run from /opt/chutney or the torrc grep below misses.
cd /opt/chutney
CHUTNEY_LISTEN_ADDRESS=127.0.0.1 \
./chutney init --net "${net}"
./chutney configure
./chutney start
./chutney wait_for_bootstrap
grep -hE '^(TestingTorNetwork|DirAuthority|DirServer|AlternateDirAuthority) ' \
net/nodes/*/torrc | sort -u >"${conf}"
if [ ! -s "${conf}" ]; then
echo "[tor-entrypoint] ERROR: ${conf} is empty — the node would silently join the public Tor network" >&2
exit 1
fi
echo "[tor-entrypoint] wrote ${conf} ($(wc -l <"${conf}") line(s))"
cd /
fi
exec tor "$@"
+1
View File
@@ -25,6 +25,7 @@ tor:
name: tor
node: ""
use_entry_guards: true
flavor: public
ports:
local:
socks: 9050
+8 -1
View File
@@ -3,7 +3,14 @@
# See docs/contributing/artefact/files/role/variants.md for the file format.
# Test deployments run without guard pinning so a single degraded entry guard
# cannot stall the whole node; production keeps the Tor default (true).
# cannot stall the whole node; production keeps the Tor default (true). They
# also join a private chutney Tor net (offline, localhost-speed onion publish
# and rendezvous) whose ~9 extra in-container tor nodes need a larger resource
# footprint than the public flavor.
- services:
tor:
use_entry_guards: false # nocheck: variants-static-override
flavor: chutney
cpus: "1.0"
mem_limit: 2g
pids_limit: 512
+29 -2
View File
@@ -1,6 +1,16 @@
---
- include_tasks: utils/once/flag.yml
- name: "✅ Assert the Tor network flavor is public or chutney"
ansible.builtin.assert:
that:
- TOR_FLAVOR in ['public', 'chutney']
fail_msg: "services.tor.flavor must be 'public' or 'chutney' (got '{{ TOR_FLAVOR }}')"
quiet: true
- name: "📤 Dispatch Tor network flavor '{{ TOR_FLAVOR }}'"
ansible.builtin.include_tasks: "flavor/{{ TOR_FLAVOR }}/main.yml"
- name: "🧅 Ensure Tor config directory exists"
ansible.builtin.file:
path: "{{ TOR_CONFIG_DIR }}"
@@ -47,6 +57,23 @@
become: true
register: _tor_torrc
- name: "📁 Ensure the Tor compose instance dir exists (build context)"
ansible.builtin.file:
path: "{{ TOR_INSTANCE_DIR }}"
state: directory
owner: root
group: root
mode: "0755"
become: true
- name: "🚀 Stage the Tor container entrypoint into the build context"
stack_host_copy:
src: "{{ role_path }}/files/entrypoint.sh"
dest: "{{ TOR_ENTRYPOINT_HOST }}"
mode: "0755"
become: true
register: _tor_entrypoint
- name: "🧩 For '{{ application_id }}': Load sys-svc-compose"
include_role:
name: sys-svc-compose
@@ -63,7 +90,7 @@
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
executable: /bin/bash
become: true
when: DEPLOYMENT_MODE == 'compose' and ((_tor_hs_keys is changed) or (_tor_torrc is changed))
when: DEPLOYMENT_MODE == 'compose' and ((_tor_hs_keys is changed) or (_tor_torrc is changed) or (_tor_entrypoint is changed))
- name: "🔄 Force Tor service redeploy to apply hidden-service key / torrc changes"
ansible.builtin.command: >-
@@ -75,7 +102,7 @@
when:
- DEPLOYMENT_MODE == 'swarm'
- IS_STACK_HOST | bool
- (_tor_hs_keys is changed) or (_tor_torrc is changed)
- (_tor_hs_keys is changed) or (_tor_torrc is changed) or (_tor_entrypoint is changed)
- name: "🧅 Wire the host-level transparent .onion egress router"
include_tasks:
@@ -0,0 +1,11 @@
---
- name: "🧪 Tor flavor 'chutney' — join a private in-container Tor test net"
ansible.builtin.debug:
msg: >-
svc-net-tor joins a private chutney Tor network bootstrapped inside the
tor container (all nodes on 127.0.0.1). Descriptor publication and
rendezvous run at localhost speed; the onion is reachable only from
hosts sharing this private net (the node's own SOCKS), which is exactly
the test surface. The heavy lifting is the container entrypoint and the
torrc %include of /run/tor/chutney-net.conf.
changed_when: false
@@ -0,0 +1,7 @@
---
- name: "🌍 Tor flavor 'public' — join the public Tor network"
ansible.builtin.debug:
msg: >-
svc-net-tor runs against the public Tor network; onion descriptor
publication and rendezvous take real-network latency.
changed_when: false
@@ -6,6 +6,7 @@
args:
TOR_BASE_IMAGE: "{{ TOR_IMAGE }}"
TOR_BASE_VERSION: "{{ TOR_VERSION }}"
TOR_FLAVOR: "{{ TOR_FLAVOR }}"
{{ lookup('compose_only', 'container_name', TOR_CONTAINER) }}
{{ lookup('container_image', application_id, 'tor') }}
{% if DEPLOYMENT_MODE == 'swarm' %}
@@ -24,6 +25,7 @@
interval: 60s
timeout: 10s
retries: 3
start_period: {{ '150s' if TOR_FLAVOR == 'chutney' else '10s' }}
{{ lookup('compose_volumes', application_id) }}
{{ lookup('compose_networks') }}
+4
View File
@@ -16,6 +16,10 @@ CookieAuthentication 1
UseEntryGuards 0
{% endif %}
{% if TOR_FLAVOR == 'chutney' %}
%include /run/tor/chutney-net.conf
{% endif %}
{% if TOR_EGRESS_ROUTER_ENABLED | bool %}
AutomapHostsOnResolve 1
AutomapHostsSuffixes .onion
+4
View File
@@ -9,6 +9,7 @@ TOR_CONTAINER: "{{ lookup('config', application_id, 'services.tor.name') }}"
TOR_SOCKS_PORT: "{{ lookup('config', application_id, 'services.tor.ports.local.socks') }}"
TOR_NODE_ONION: "{{ lookup('config', application_id, 'services.tor.node', '') }}"
TOR_USE_ENTRY_GUARDS: "{{ lookup('config', application_id, 'services.tor.use_entry_guards') | bool }}"
TOR_FLAVOR: "{{ lookup('config', application_id, 'services.tor.flavor', 'public') }}"
TOR_TRANS_PORT: "{{ lookup('config', application_id, 'services.tor.ports.local.trans') }}"
TOR_DNS_PORT: "{{ lookup('config', application_id, 'services.tor.ports.local.dns') }}"
TOR_DNSMASQ_CONF: "/etc/dnsmasq.d/tor-onion.conf"
@@ -21,3 +22,6 @@ TOR_TORRC_HOST: "{{ [ TOR_CONFIG_DIR, 'torrc' ] | path_join }}"
TOR_HS_DIR: "{{ [ TOR_DATA_DIR, 'infinito', 'node' ] | path_join }}"
TOR_HS_SRC_DIR: "{{ lookup('path_absolute', '.onion-identity', 'hs') }}"
TOR_INSTANCE_DIR: "{{ lookup('container', application_id, 'directories.instance') }}"
TOR_ENTRYPOINT_HOST: "{{ [ TOR_INSTANCE_DIR, 'entrypoint.sh' ] | path_join }}"