mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
fix(svc-net-tor): run the node onion natively in swarm
Pin tor to the manager (host-published SOCKS/DNS live on the stack host), forward HiddenServicePorts and the dnsmasq onion fast path to the node address instead of loopback (dead-ends in task netns), tolerate an empty clearnet-upstream capture, detect foreign dnsmasq drop-ins before claiming the listener, and transfer onion_self_reach to remote targets via script. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a850735b78
commit
9ae0c7eff6
7 files changed
+39
-57
No files matched your search
@@ -16,38 +16,38 @@ The diagram places Tor Onion Service in the Infinito.Nexus cosmos: the component
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph role [svc-net-tor 🐳🐝]
|
||||
subgraph role [svc-net-tor]
|
||||
svc_mirror["mirror"]
|
||||
svc_tor["tor"]
|
||||
end
|
||||
subgraph dependents [Dependents]
|
||||
dpt_svc_db_elasticsearch["svc-db-elasticsearch 🐳🐝"]
|
||||
dpt_svc_db_mariadb["svc-db-mariadb 🐳🐝"]
|
||||
dpt_svc_db_memcached["svc-db-memcached 🐳🐝"]
|
||||
dpt_svc_db_openldap["svc-db-openldap 🐳🐝"]
|
||||
dpt_svc_db_postgres["svc-db-postgres 🐳🐝"]
|
||||
dpt_svc_db_qdrant["svc-db-qdrant 🐳🐝"]
|
||||
dpt_svc_db_rabbitmq["svc-db-rabbitmq 🐳🐝"]
|
||||
dpt_svc_db_redis["svc-db-redis 🐳🐝"]
|
||||
dpt_svc_db_typesense["svc-db-typesense 🐳🐝"]
|
||||
dpt_svc_prx_openresty["svc-prx-openresty 🐳🐝"]
|
||||
dpt_web_app_akaunting["web-app-akaunting 🐳🐝"]
|
||||
dpt_web_app_baserow["web-app-baserow 🐳🐝"]
|
||||
dpt_web_app_bluesky["web-app-bluesky 🐳🐝"]
|
||||
dpt_web_app_bookwyrm["web-app-bookwyrm 🐳🐝"]
|
||||
dpt_web_app_bridgy_fed["web-app-bridgy-fed 🐳🐝"]
|
||||
dpt_web_app_checkmk["web-app-checkmk 🐳🐝"]
|
||||
dpt_more["..."]
|
||||
end
|
||||
svc_mirror -- "1:1" --> dpt_more
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_elasticsearch
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_mariadb
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_memcached
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_openldap
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_postgres
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_qdrant
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_rabbitmq
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_redis
|
||||
svc_mirror -. "0..1" .-> dpt_svc_db_typesense
|
||||
svc_mirror -. "0..1" .-> dpt_svc_prx_openresty
|
||||
svc_mirror -. "0..1" .-> dpt_web_app_akaunting
|
||||
svc_mirror -. "0..1" .-> dpt_web_app_baserow
|
||||
svc_mirror -. "0..1" .-> dpt_web_app_bluesky
|
||||
svc_mirror -. "0..1" .-> dpt_web_app_bookwyrm
|
||||
svc_mirror -. "0..1" .-> dpt_web_app_bridgy_fed
|
||||
svc_mirror -. "0..1" .-> dpt_web_app_checkmk
|
||||
```
|
||||
|
||||
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments). Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
|
||||
|
||||
@@ -12,19 +12,18 @@ tor:
|
||||
enabled: false
|
||||
swarm:
|
||||
enabled: false
|
||||
# nocheck: default-placement-manager node onion ingress: host-published SOCKS/DNS/Trans ports and the HiddenService loopback forward must live on the stack host
|
||||
placement: manager
|
||||
bond: 1
|
||||
enabled: true
|
||||
shared: true
|
||||
exclusive: true
|
||||
primary: true
|
||||
# Base image for the local Debian+tor build (files/Dockerfile).
|
||||
image: debian
|
||||
version: trixie-slim
|
||||
custom: true
|
||||
name: tor
|
||||
node: ""
|
||||
# Tor default (guard pinning). Test inventories disable it so one degraded
|
||||
# entry guard cannot stall the whole node; anonymity is not under test there.
|
||||
use_entry_guards: true
|
||||
ports:
|
||||
local:
|
||||
|
||||
@@ -85,12 +85,8 @@
|
||||
when: TOR_EGRESS_ROUTER_ENABLED | bool
|
||||
|
||||
- name: "⏳ Wait until the node onion answers internally (loopback fast path)"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- python3
|
||||
- "{{ role_path }}/files/onion_self_reach.py"
|
||||
- internal
|
||||
- "{{ TOR_NODE_ONION }}"
|
||||
ansible.builtin.script:
|
||||
cmd: "{{ role_path }}/files/onion_self_reach.py internal {{ TOR_NODE_ONION }}"
|
||||
register: _tor_self_reach
|
||||
retries: 90
|
||||
delay: 5
|
||||
@@ -101,13 +97,10 @@
|
||||
- TOR_NODE_ONION | length > 0
|
||||
|
||||
- name: "🔎 Probe the node onion over the real Tor network (non-fatal)"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- python3
|
||||
- "{{ role_path }}/files/onion_self_reach.py"
|
||||
- external
|
||||
- "{{ TOR_NODE_ONION }}"
|
||||
- "{{ lookup('tor_socks', '127.0.0.1') }}"
|
||||
ansible.builtin.script:
|
||||
cmd: >-
|
||||
{{ role_path }}/files/onion_self_reach.py external
|
||||
{{ TOR_NODE_ONION }} {{ lookup('tor_socks', '127.0.0.1') }}
|
||||
register: _tor_self_reach_ext
|
||||
retries: 90
|
||||
delay: 5
|
||||
|
||||
@@ -39,18 +39,27 @@
|
||||
- name: "💾 Egress router | Persist clearnet upstreams (first setup only)"
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ TOR_DNS_UPSTREAMS_FILE }}"
|
||||
content: |
|
||||
{% for _l in (_tor_resolv_now.content | b64decode).split('\n') if _l.startswith('nameserver ') and _l.split()[1] != '127.0.0.1' %}
|
||||
{{ _l.split()[1] }}
|
||||
{% endfor %}
|
||||
content: "{{ (_tor_upstream_list | join('\n')) ~ '\n' }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
vars:
|
||||
_tor_upstream_list: >-
|
||||
{{ (_tor_resolv_now.content | b64decode).split('\n')
|
||||
| select('match', '^nameserver ')
|
||||
| map('split') | map(attribute=1)
|
||||
| reject('equalto', '127.0.0.1') | list }}
|
||||
when: not _tor_upstreams_stat.stat.exists
|
||||
- name: "📥 Egress router | Load captured upstreams"
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ TOR_DNS_UPSTREAMS_FILE }}"
|
||||
register: _tor_dns_upstreams_raw
|
||||
- name: "🔎 Egress router | Detect foreign dnsmasq drop-ins (shared instance)"
|
||||
ansible.builtin.find:
|
||||
paths: "{{ TOR_DNSMASQ_CONF | dirname }}"
|
||||
patterns: "*.conf"
|
||||
excludes: "{{ TOR_DNSMASQ_CONF | basename }}"
|
||||
register: _tor_dnsmasq_foreign
|
||||
- name: "📝 Egress router | Render dnsmasq .onion split-DNS drop-in"
|
||||
ansible.builtin.template: # nocheck: stack-host-template (per-host dnsmasq system config in /etc/dnsmasq.d)
|
||||
src: dnsmasq-tor-onion.conf.j2
|
||||
@@ -60,6 +69,7 @@
|
||||
mode: "0644"
|
||||
vars:
|
||||
TOR_DNS_UPSTREAMS: "{{ (_tor_dns_upstreams_raw.content | b64decode).split('\n') | map('trim') | reject('equalto', '') | list }}"
|
||||
TOR_DNSMASQ_OWNS_LISTENER: "{{ _tor_dnsmasq_foreign.matched == 0 }}"
|
||||
register: _tor_dnsmasq_conf
|
||||
- name: "🚀 Egress router | Enable + (re)start dnsmasq"
|
||||
ansible.builtin.systemd:
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
{% if TOR_DNSMASQ_OWNS_LISTENER | default(true) %}
|
||||
listen-address=127.0.0.1
|
||||
bind-interfaces
|
||||
{% endif %}
|
||||
bind-dynamic
|
||||
no-hosts
|
||||
no-resolv
|
||||
filter-AAAA
|
||||
{% if TOR_NODE_ONION | length > 0 %}
|
||||
# Fast path for this node's own onion: every app subdomain of it terminates on
|
||||
# this node's OpenResty, so resolve the whole suffix to loopback and skip the
|
||||
# Tor circuit for host-side access (health checks, injections, front-proxy
|
||||
# probes). More specific than the generic `server=/onion/...` below, so only
|
||||
# the node's own onion short-circuits — all other .onion still egress via Tor.
|
||||
# Container/Playwright traffic is unaffected: it routes through the Tor SOCKS,
|
||||
# not this resolver, so it still exercises the real onion service.
|
||||
address=/{{ TOR_NODE_ONION }}/127.0.0.1
|
||||
address=/{{ TOR_NODE_ONION }}/{{ TOR_HS_BACKEND_HOST }}
|
||||
local=/{{ TOR_NODE_ONION }}/
|
||||
{% endif %}
|
||||
server=/onion/127.0.0.1#{{ TOR_DNS_PORT }}
|
||||
{% for _ns in TOR_DNS_UPSTREAMS %}
|
||||
|
||||
@@ -3,11 +3,6 @@ RunAsDaemon 0
|
||||
Log notice stdout
|
||||
DataDirectory /var/lib/tor
|
||||
|
||||
# SOCKS proxy — required to reach .onion services from the node (health checks,
|
||||
# inter-onion federation, and the Playwright runner testing over Tor). Bound on
|
||||
# all node interfaces so DiD sibling containers can reach it via
|
||||
# host.docker.internal, but restricted by SocksPolicy to loopback + private/DiD
|
||||
# ranges so it is never an open proxy.
|
||||
SocksPort {{ lookup('tor_socks', '0.0.0.0') }}
|
||||
SocksPolicy accept 127.0.0.0/8
|
||||
SocksPolicy accept 10.0.0.0/8
|
||||
@@ -28,11 +23,9 @@ VirtualAddrNetworkIPv4 {{ TOR_EGRESS_VIRTUAL_NET_IPV4 }}
|
||||
TransPort 127.0.0.1:{{ TOR_TRANS_PORT }}
|
||||
DNSPort 127.0.0.1:{{ TOR_DNS_PORT }}
|
||||
{% endif %}
|
||||
# Node onion service: one hidden service, per-app subdomains routed by Host in
|
||||
# OpenResty. All onion traffic terminates on the host-network OpenResty (:80).
|
||||
HiddenServiceDir /var/lib/tor/infinito/node/
|
||||
HiddenServiceVersion 3
|
||||
HiddenServicePort 80 127.0.0.1:80
|
||||
HiddenServicePort 80 {{ TOR_HS_BACKEND_HOST }}:80
|
||||
{% for _p in TOR_ONION_EXTRA_PORTS %}
|
||||
HiddenServicePort {{ _p.onion_port }} {{ _p.target }}
|
||||
HiddenServicePort {{ _p.onion_port }} {{ TOR_HS_BACKEND_HOST }}:{{ _p.target.rsplit(':', 1) | last }}
|
||||
{% endfor %}
|
||||
@@ -2,8 +2,6 @@
|
||||
# General
|
||||
application_id: "svc-net-tor"
|
||||
|
||||
# Tor upstream base image (a Debian release; the tor package is installed from it
|
||||
# in files/Dockerfile for provenance + reproducible pinning).
|
||||
TOR_IMAGE: "{{ lookup('config', application_id, 'services.tor.image') }}"
|
||||
TOR_VERSION: "{{ lookup('config', application_id, 'services.tor.version') }}"
|
||||
TOR_CONTAINER: "{{ lookup('config', application_id, 'services.tor.name') }}"
|
||||
@@ -14,19 +12,12 @@ TOR_USE_ENTRY_GUARDS: "{{ lookup('config', application_id, 'services.tor.use_ent
|
||||
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"
|
||||
TOR_HS_BACKEND_HOST: "{{ ansible_facts['default_ipv4']['address'] if DEPLOYMENT_MODE == 'swarm' else '127.0.0.1' }}"
|
||||
TOR_DNS_UPSTREAMS_FILE: "{{ [ DIR_VAR_LIB, application_id, 'dns-upstreams' ] | path_join }}"
|
||||
|
||||
# Host paths (under DIR_VAR_LIB); Tor's runtime DataDirectory + HiddenServiceDir.
|
||||
TOR_DATA_DIR: "{{ [ DIR_VAR_LIB, application_id, 'data' ] | path_join }}"
|
||||
TOR_CONFIG_DIR: "{{ [ DIR_VAR_LIB, application_id, 'config' ] | path_join }}"
|
||||
TOR_TORRC_HOST: "{{ [ TOR_CONFIG_DIR, 'torrc' ] | path_join }}"
|
||||
TOR_HS_DIR: "{{ [ TOR_DATA_DIR, 'infinito', 'node' ] | path_join }}"
|
||||
|
||||
# Authoritative hidden-service key files, minted by
|
||||
# `cli.administration.inventory.onion init-env` at the repo root. Their address
|
||||
# MUST equal DOMAIN_PRIMARY (== INFINITO_DOMAIN); copied into TOR_HS_DIR
|
||||
# before Tor starts so the daemon serves exactly that onion.
|
||||
TOR_HS_SRC_DIR: "{{ lookup('path_absolute', '.onion-identity', 'hs') }}"
|
||||
# TOR_EGRESS_ENABLED and TOR_ONION_EXTRA_PORTS are defined globally in
|
||||
# group_vars/all/19_tor.yml (they must not be redefined here — a role var that
|
||||
# references itself triggers a Jinja recursion error).
|
||||
Reference in new issue
Block a user