Files
computer-playbook/roles/svc-net-tor/templates/nftables.conf.j2
kevinveenbirkenbachandClaude Opus 5 b5807e66ce fix(svc-net-tor): guard the node's resolver against everything it routes
dnsmasq binds the docker bridge address with the clearnet upstreams configured and the node's own .onion records loaded, and no rule covered port 53. Any source that could route a packet to that address got an open resolver plus the split-DNS records. c89ca42c46 claimed local-service closed this; abb6f37297 showed the keyword never took effect.

The guard now carries two named sets instead of one plus a special case. 'containers' is the docker pool list, for tor's own transparent and DNS ports. 'internal' is NETWORK_INTERNAL_CIDRS, for the resolver and the SOCKS port. Every guarded entry names its set, which folds the hand-written SOCKS rule and its manual append into the same list the reverse-path sets are built from.

The resolver takes the wider set deliberately: 'internal' is the list torrc.j2 already spends on SocksPolicy accept. The narrow set carries only 10.208.0.0/12 and 10.7.7.0/24 out of 10.0.0.0/8, which leaves out swarm's ingress network, every overlay docker allocates without an ipam block, and a node whose own address sits elsewhere in 10/8. A query bound to such an address was measured timing out against the narrow set. Guarding the resolver more strictly than the proxy beside it would refuse a name lookup to a source that is handed a full SOCKS proxy six lines earlier.

NETWORK_DNS_PORT lives next to the other resolver constants rather than in meta/services.yml: ports.local describes host-bound container ports, where 53 collides with svc-dns-unbound and falls outside every PORT_BANDS band.

Measured on the rendered artifact: the table loads and reloads with a byte-identical listing, every compose source of port 53 resolves inside 'internal' and every source of 9040/9053 inside 'containers', and a mistyped set name, an empty CIDR list or a missing admitted key each fail loudly at validate time. Port 53 newly passes a strict reverse-path check, so an asymmetrically routed client loses name resolution.

test_fragments built its guard list from a fixture of three entries while the role shipped six, so it validated the template against a port list nobody deploys. It now takes the shape from the declaration and fills only the numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 01:26:03 +02:00

53 lines
1.7 KiB
Django/Jinja

{# Loaded by roles/svc-net-firewall into {{ FIREWALL_TABLE }}. #}
table inet {{ FIREWALL_TABLE }}
delete table inet {{ FIREWALL_TABLE }}
table inet {{ FIREWALL_TABLE }} {
set containers {
type ipv4_addr
flags interval
elements = { {{ TOR_EGRESS_CLIENT_CIDRS | join(', ') }} }
}
set internal {
type ipv4_addr
flags interval
elements = { {{ NETWORK_INTERNAL_CIDRS | join(', ') }} }
}
{% set _tcp_guarded = TOR_EGRESS_GUARDED_PORTS | selectattr('protocol', 'equalto', 'tcp') | map(attribute='port') | unique | list %}
{% set _udp_guarded = TOR_EGRESS_GUARDED_PORTS | selectattr('protocol', 'equalto', 'udp') | map(attribute='port') | unique | list %}
chain guard {
{% for entry in TOR_EGRESS_GUARDED_PORTS %}
ip saddr != @{{ entry.admitted }} {{ entry.protocol }} dport {{ entry.port }} drop
{% endfor %}
{% if _tcp_guarded | length > 0 %}
fib saddr . iif oif missing tcp dport { {{ _tcp_guarded | join(', ') }} } drop
{% endif %}
{% if _udp_guarded | length > 0 %}
fib saddr . iif oif missing udp dport { {{ _udp_guarded | join(', ') }} } drop
{% endif %}
}
chain input {
type filter hook input priority -10; policy accept;
jump guard
}
chain forward {
type filter hook forward priority -10; policy accept;
jump guard
}
chain output {
type nat hook output priority -150; policy accept;
ip daddr {{ TOR_EGRESS_VIRTUAL_NET_IPV4 }} meta l4proto tcp redirect to :{{ TOR_TRANS_PORT }}
}
{% if TOR_CONTAINER_DNS_HOST | length > 0 %}
chain prerouting {
type nat hook prerouting priority -150; policy accept;
ip daddr {{ TOR_EGRESS_VIRTUAL_NET_IPV4 }} meta l4proto tcp dnat ip to {{ TOR_CONTAINER_DNS_HOST }}:{{ TOR_TRANS_PORT }}
}
{% endif %}
}