Skip to content

Conversation

@elldeeone
Copy link

Tor + Kaspa

  • Tor routes traffic through layered relays so peers never learn each other’s clearnet IPs.
  • Applications integrate by sending every socket through Tor’s SOCKS proxy and, when inbound reachability is needed, publishing a hidden service via the control port.
  • This branch wires rusty-kaspa into that flow: when I point the daemon at a Tor control listener and SOCKS endpoint, it authenticates, waits for bootstrap, can publish its own .onion address, and forces outbound P2P through Tor.
  • I mirrored Bitcoin Core’s Tor methodology so operators see familiar semantics, but all of the implementation is Rust/Kaspa-specific.

Inspiration & Approach

  • I intentionally shadowed Bitcoin Core’s integration strategy: system Tor, control-port orchestration, persistent onion keys, per-connection SOCKS auth isolation (IsolateSOCKSAuth), and ADDRv2/BIP155 signaling for onion gossip.
  • Terminology like “BIP155” appears because Tor operators already know it; Kaspa previously lacked that service bit, so I added it to advertise onion capability.
  • Everything else—FlowContext plumbing, ConnectionManager hooks, gRPC transport, logging—is native to Kaspa; no Bitcoin code was imported.

Intent & Expectations

  • I’m not expecting this PR to merge as-is; my goal is to surface the work and spark architectural discussion.
  • Please treat it as exploratory: prioritize feedback on security, ergonomics, and long-term feasibility over polishing.
  • If the direction resonates, I’m happy to chase the invasive follow-ups (DNS plumbing, listener defaults, bundled Tor, automated tests) before proposing anything merge-worthy.

Proof-of-Concept Status

  • End-to-end demo works in my lab: kaspad --tor-control=… --tor-cookie=… --proxy-net=onion=127.0.0.1:9050 --listen-onion --tor-only routes outbound traffic exclusively through SOCKS and advertises a persistent hidden service.
  • It’s still “P(roof)-ish”: everything lives in the daemon, but I haven’t bundled Tor, failure recovery is minimal, and validation has been manual. Treat it like an advanced feature flag for now.
  • Tor bandwidth (roughly 2–5 Mbps) makes full sync over onions impractical—especially with pruning. I expect mixed-mode usage (clearnet IBD, Tor steady-state) until we build smarter paths.

What Landed

  1. Tor runtime & hidden service plumbing (kaspad/src/tor_manager.rs, kaspad/src/daemon.rs)
    • I wrapped tor-interface, negotiate password/cookie auth, stream bootstrap events, publish/remove hidden services, and persist Ed25519 onion keys under ~/.rusty-kaspa/<net>/tor/p2p_onion.key.
  2. CLI & config surface (kaspad/src/args.rs, kaspad/src/daemon.rs)
    • I added flags for control/cookie paths, tor-only mode, per-network SOCKS overrides, onion listener publication, bootstrap timeouts, and key locations. ProxySettings::resolve now emits SocksProxyParams with auth metadata and auto-promotes Tor proxies to randomized credentials.
  3. P2P SOCKS integration (protocol/p2p/src/core/connection_handler.rs, protocol/flows/src/service.rs)
    • I updated outbound dials to consult SocksProxyConfig, pick default/IPv4/IPv6/onion endpoints, and generate per-stream username/password pairs whenever Tor isolation is requested.
  4. AddrV2/onion awareness (protocol/flows/src/flow_context.rs, protocol/flows/src/v5/address.rs, protocol/p2p/proto/p2p.proto, utils/src/networking.rs, components/addressmanager/*)
    • I taught version handshakes to advertise BIP155-style ADDRv2 support, ensured onion gossip only happens when both peers run Tor, updated protobuf/storage layers to encode onion hosts, and tightened the address manager’s --onlynet/--tor-only filtering.
  5. Runtime orchestration (kaspad/src/daemon.rs, protocol/flows/src/service.rs)
    • I gate P2P startup on Tor bootstrap via watch channels, handle onion publication/teardown through control-port commands, and automatically drop onion peers when Tor isn’t active.

Known Gaps / Risks

  • DNS & hostname leakage: proxy targets still resolve via the OS, and DNS seeders perform clearnet lookups (kaspad/src/args.rs, components/connectionmanager/src/lib.rs). Full parity requires host-preserving proxy targets plus a name-proxy pipeline.
  • Listener semantics: clearnet listeners stay up unless the operator opts out; Bitcoin disables them automatically when Tor is active. Matching that behavior would mean changing default --listen/--onlynet handling.
  • Tor lifecycle robustness: I currently assume an externally managed Tor daemon—there’s no bundled binary, no retry logic when the control port drops, and no automatic re-publication after Tor restarts.
  • Testing coverage: outside of some onion-filtering unit tests, validation was manual (logs, lsof, Tor control output). There’s no integration harness that spins up a Tor stub yet.
  • Performance realities: Tor’s bandwidth ceiling makes Tor-only syncing largely impractical today; expect clearnet IBD followed by Tor steady-state for the foreseeable future.

Call for Discussion

  • Is the security/maintenance cost of native Tor support acceptable for upstream Kaspa?
  • Should I finish the DNS/name-proxy refactor and align listener defaults before even asking for mergeability?
  • How should we document limitations (bandwidth, Tor restarts, lack of bundled daemon) so operators don’t misinterpret the feature?

@saefstroem
Copy link
Collaborator

saefstroem commented Dec 17, 2025

Running P2P over Tor will impact block discovery for other peers due to slow performance. This could have negative effects on the anticone count of the DAG and in general be penalizing in terms of network connectivity. I am wondering if this has been considered and if-so what are the mitigations in place?

@D-Stacks
Copy link
Collaborator

D-Stacks commented Jan 7, 2026

Running P2P over Tor will impact block discovery for other peers due to slow performance. This could have negative effects on the anticone count of the DAG and in general be penalizing in terms of network connectivity. I am wondering if this has been considered and if-so what are the mitigations in place?
Just my 2 cents, but,

  1. in general the p2p network is really only as slow as the fastest link(s), if we ensure all clear-net nodes only have partial connectivity to the TOR network (something like a set inbound / outbound limit at maybe something like 10% - 20% of total network) I think the impact will be minimal. As I have been measuring peer connectivity with my perigee implementation, I can ascertain that block delivery is really only dependent on the fastest link(s) in practice. Or to visualize it as an analogy, Imagine a highway, TOR nodes will be on the creeper lane, all other nodes will be on the fast lane.. as long as we ensure TOR nodes do not congest the whole highway, in practice their impact should be minimal.

  2. This doesn't hold true if miners actually mine blocks directly into the TOR network, thereby emitting latency to the whole network but I do think this would be considered bad practice on a wide array of metrics, including economically, for the miner, as kaspa mining is about a race to mine the tips.

In short: I think it is just about proper segregation of the TOR network from the clear-net, and limiting it's exposure over clear-net, so it cannot dominate latency in the clear-net, which will remain the part where blocks will be actually mined and communicated to other miners.

@D-Stacks
Copy link
Collaborator

D-Stacks commented Jan 7, 2026

Btw when I have time I'll try to review this, TOR has also been on my p2p bucket list. Just some overall input from my side on how I see it:

  1. I think TOR Addresses and handling IMO should be segregated on a much finer granularity imo, probably with a separate address store, hub hash-map etc..

  2. If we think about integrating TOR we should think about designing a frame-work to include different overlay-networks as a whole, and place TOR within it, although the only one besides TOR that I can really think of is I2P.. but in case we want to include more overlay networks we will have a clear and easy pathway to do this.

Is the security/maintenance cost of native Tor support acceptable for upstream Kaspa?

It depends what you mean with native, I think the maintenance to support connecting to, and proxying over TOR should be minimal, I guess it is mostly just about expanding the address handling to support TOR addresses, keeping some other types of sockets up, and to ensure segregation of TOR and clear-net addresses. I wouldn't directly support the use of in-process spawned TOR, i.e. something like arti. This puts the maintenance burden back on us, if things end up breaking with them, or there is some versioning misalignment etc.. etc.. we get the brunt of it. If it is their client we are proxying over, then they get to at least share some of it. Also I think the people who wish to use TOR are probably the same that don't mind reading up on some set-up guide, or probably don't even need to, once they know how to point kaspa into the right direction, and don't need some all-encompassing --run-over-tor flag. Even if We do wish to support it natively, I think getting out a "minimal viable product" out first is always the way to go.

Should I finish the DNS/name-proxy refactor and align listener defaults before even asking for mergeability?

I'll be honest, I didn't understand the issue with dns seeders, is the problem simply that we also need to PR to the dns seeders to make them hand out tor addresses, or do we need a .onion dns seeder?

How should we document limitations (bandwidth, Tor restarts, lack of bundled daemon) so operators don’t misinterpret the feature?

Personally I would just include a set-up guide in our README.md, probably over or under the Start a devnet node section, with instructions on how to proxy over TOR with a bunch of disclaimers.

That being said, I am not sure what the overall enthusiasm for TOR is here, I have less qualms about them dragging down the network as a whole, but more so about them being so slow, that we cannot support them into the future.. Imo it needs to be bench-marked under high load testnet conditions, It is a very real possibility that TOR might simply be too slow to tango with kaspa's throughput. Maintaining and putting effort into something that cannot catch-up anyway probably isn't worth the effort.

@coderofstuff coderofstuff requested a review from D-Stacks January 14, 2026 04:05
Copy link
Collaborator

@D-Stacks D-Stacks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think I want larger structural changes, so not really requesting individual changes per-se (even though i kind of am) , but the changes reflect more of a general architectural overhaul of how I think the addresses should be managed and integrated into kaspad. I think the finer grained changes will come as they are implemented and I probably cannot account for all changes required with a simple review. I refrained from reviewing tor set-up code, and code involved with proxying over tor, as well as tor things added to the flow context, as i do not think i am familiar enough with actual tor configurations etc... Review is more of a guidance on the route I think this pr should go related to integrating onion addresses into the kaspad, less so about the actual tor proxying and set-up.I believe this integration of onion address handling should be handled first, the actual set-up details can be done afterwards.

return Err(Error::custom("Please specify peer IP address"));
}
let ip: RpcIpAddress = argv.remove(0).parse()?;
let ip: RpcIpAddress = argv.remove(0).parse().map_err(NetAddressError::from)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can NetAddressError be emitted from the parse() call?

self.address_store.set(address, 0);
}

fn prune_onion_addresses(&mut self) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment at line 85

}
}

/// A network address, equivalent of a [SocketAddr].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment here is no longer accurate.

}

impl NetAddress {
pub fn new(ip: IpAddress, port: u16) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of new, new_onion and new_kind, I'd rather have this just accept an AddressKind explictly in new, i.e. remove new, new_onion, and rename from_kind to new. refractor accordingly.

Self { ip: kind, port }
}

pub fn is_ip(&self) -> bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we have a use-case for it somewhere in the code-base, which we probably do, we should add is_onion as well. in cases where we explicitly want to check if it is onion, we can then use that call, rather then having to infer that the inverse of is_ip is an onion.

try_from!(&protowire::BanResponseMessage, RpcResult<kaspa_rpc_core::BanResponse>);

try_from!(item: &protowire::UnbanRequestMessage, kaspa_rpc_core::UnbanRequest, { Self { ip: RpcIpAddress::from_str(&item.ip)? } });
try_from!(item: &protowire::UnbanRequestMessage, kaspa_rpc_core::UnbanRequest, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here


try_from!(item: &protowire::GetPeerAddressesKnownAddressMessage, kaspa_rpc_core::RpcPeerAddress, { Self::from_str(&item.addr)? });
try_from!(item: &protowire::GetPeerAddressesKnownAddressMessage, kaspa_rpc_core::RpcIpAddress, { Self::from_str(&item.addr)? });
try_from!(item: &protowire::GetPeerAddressesKnownAddressMessage, kaspa_rpc_core::RpcPeerAddress, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in message.rs, this shouldn't error, but be defined in rpc.proto.

try_from!(item: &protowire::GetPeerAddressesKnownAddressMessage, kaspa_rpc_core::RpcPeerAddress, {
Self::from_str(&item.addr).map_err(RpcError::from)?
});
try_from!(item: &protowire::GetPeerAddressesKnownAddressMessage, kaspa_rpc_core::RpcIpAddress, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

use std::str::FromStr;

#[derive(Clone, Debug, Deserialize)]
#[serde(rename = "lowercase")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not too familiar with Wrpc, maybe something for coderofstuff or IzioDev to go over. but that should probably be after potential changes to Netaddress in core.

Servers.toml
release
package-sizes.js
CLAUDE.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this is something to do with AI assistance, and it probably helps regarding not accidentally uploading this, consider PRing this separately, or put a big TODO: remove this, and CLAUDE.md, before merging over it. As I think it is independent from actual PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just leave one comment for this file, so I think we should start with:

#[derive(Clone)]
pub struct DbAddressesStore {
    db: Arc<DB>,
    // a cached db access for standard "clear-net" addresses
    std_access: CachedDbAccess<DbStdAddressKey, StdEntry>,
    // a cached db access for tor overlay addresses 
    onion_access: CachedDbAccess<DbOnionAddressKey, OnionEntry>
}

this implies separate serialization of the keys and entries and setting a new Database prefix for the new store etc.. etc.. for keys I would go for SocketAddr, or the current NetAddress (see comments regarding it in utils/networking), for StdEntry. For OnionEntry use the appropriate OnionAddress.

this also means renaming methods to set_std / set_onion, get_std / get_onion etc.. you get the drift.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants