Runbook
Kohl over Tor
On-chain privacy hides amounts and parties. Dandelion++ blurs which peer originated a gossip. Tor hides your IP from the peer graph. Use both — none of them alone is enough.
Why Tor still matters
| Layer | Hides | Does not hide |
|---|---|---|
| On-chain privacy | Sender set, receiver, amount on chain | Your IP when you peer or submit |
| Dandelion++ | First-hop origin on the peer graph | Clearnet listeners, long-lived peer ID |
| Tor | Node / wallet IP from peers & observers | Bad opsec, hostile remote RPC, malware |
Honest limits: timing correlation, mixing clearnet and Tor for the same
wallet, and open RPC on 0.0.0.0 all leak. This is hygiene, not a guarantee.
Pick a level
Outbound only
Dial peers via Tor. No inbound onion. Lowest effort.
Full onion node
Hidden service for P2P (and optional RPC). Peers dial you without learning your IP.
Wallet over Tor
Wallet reaches only your node via SOCKS → onion RPC (or local RPC if co-located).
L1 — Outbound only
Install Tor so SOCKS is on 127.0.0.1:9050, then:
# Confirm Tor works
curl -sS --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ip
# Node: all TCP via Tor; P2P bound to localhost (no clearnet inbound)
torsocks ./target/release/kohl \
--chain kohl-ash \
--validator \
--listen-addr /ip4/127.0.0.1/tcp/30333 \
--rpc-port 9944 \
--network-backend libp2p \
--mining-seed <64-hex>
- There is no
--proxy-serverflag on current Kohl / Substrate builds. - Prefer
--network-backend libp2pwhen experimenting with onion multiaddrs. - You still need bootnodes reachable from Tor (clearnet via exit, or onion).
L2 — Onion service for P2P
1. Tor hidden service
SocksPort 9050
## P2P onion → local Kohl
HiddenServiceDir /var/lib/tor/kohl-p2p/
HiddenServicePort 30333 127.0.0.1:30333
## Optional separate RPC onion (wallet only)
# HiddenServiceDir /var/lib/tor/kohl-rpc/
# HiddenServicePort 9944 127.0.0.1:9944
Restart Tor, then:
sudo cat /var/lib/tor/kohl-p2p/hostname
# e.g. abcdef…xyz.onion
2. Stable PeerId + node
mkdir -p ~/.kohl
./target/release/kohl key generate-node-key --file ~/.kohl/node-key
ONION=$(sudo cat /var/lib/tor/kohl-p2p/hostname | tr -d '\n' | sed 's/\.onion$//')
./target/release/kohl \
--chain kohl-ash \
--validator \
--base-path ~/.kohl/chain \
--node-key-file ~/.kohl/node-key \
--listen-addr /ip4/127.0.0.1/tcp/30333 \
--public-addr "/onion3/${ONION}:30333" \
--rpc-port 9944 \
--rpc-cors localhost \
--network-backend libp2p \
--mining-seed <64-hex>
Multiaddr uses the 56-character v3 name without the
.onion suffix. Publish to peers:
/onion3/<v3>:30333/p2p/<PeerId>
L3 — Wallet / RPC over Tor
- Same machine: keep RPC on
127.0.0.1:9944; wallet uses localhost. Node handles Tor for p2p. - Remote wallet: enable a dedicated RPC onion; client uses SOCKS:
curl --socks5-hostname 127.0.0.1:9050 \
-H 'content-type: application/json' \
-d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \
http://<rpc-onion>.onion:9944
Treat an onion RPC like a secret URL. Do not expose author_* broadly.
Prefer a full node you control over third-party public RPC.
Checklist
check.torproject.orgvia SOCKS reportsIsTor: true--listen-addris localhost when using a hidden service- Tor
HiddenServicePorttarget matches Kohl’s listen port - Onion multiaddr omits
.onion; PeerId stable via--node-key-file - RPC not on
0.0.0.0unless intentional and filtered - Logs show
Dandelion++ enabled(kohl::dandelion)
Go deeper
- docs/tor-runbook.md — full troubleshooting & profiles
- README — quick start & Dandelion++
- node/src/dandelion — stem/fluff implementation