From be666b55b6129edbc0cc652563e7b46eef067a88 Mon Sep 17 00:00:00 2001 From: Adrian-Stefan Mares Date: Sun, 20 Jun 2021 11:14:23 +0200 Subject: [PATCH 1/3] tor: Allow direct connections to clearnet targets --- sample-lnd.conf | 10 ++++++++++ tor/net.go | 13 ++++++++++--- tor/tor.go | 46 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/sample-lnd.conf b/sample-lnd.conf index 0e6c70a4b..235347047 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -827,6 +827,13 @@ litecoin.node=ltcd ; Allow outbound and inbound connections to be routed through Tor ; tor.active=true +; Allow the node to connect to non-onion services directly via clearnet. This +; allows the node operator to use direct connections to peers not running behind +; Tor, thus allowing lower latency and better connection stability. +; WARNING: This option will reveal the source IP address of the node, and should +; be used only if privacy is not a concern. +; tor.directconnections=true + ; The port that Tor's exposed SOCKS5 proxy is listening on. Using Tor allows ; outbound-only connections (listening will be disabled) -- NOTE port must be ; between 1024 and 65535 @@ -841,6 +848,9 @@ litecoin.node=ltcd ; connection. With this mode active, each connection will use a new circuit. ; This means that multiple applications (other than lnd) using Tor won't be mixed ; in with lnd's traffic. +; +; This option may not be used while direct connections are enabled, since direct +; connections compromise source IP privacy by default. ; tor.streamisolation=true ; The host:port that Tor is listening on for Tor control connections (default: diff --git a/tor/net.go b/tor/net.go index d389cc73a..26990edf0 100644 --- a/tor/net.go +++ b/tor/net.go @@ -72,7 +72,7 @@ func (r *ClearNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) return net.ResolveTCPAddr(network, address) } -// ProxyNet is an implementation of the Net interface that defines behaviour +// ProxyNet is an implementation of the Net interface that defines behavior // for Tor network connections. type ProxyNet struct { // SOCKS is the host:port which Tor's exposed SOCKS5 proxy is listening @@ -88,6 +88,11 @@ type ProxyNet struct { // means that our traffic may be harder to correlate as each connection // will now use a distinct circuit. StreamIsolation bool + + // DirectConnections allows the proxy network to use direct connections + // to non-onion service targets. If enabled, the node IP address will be + // revealed while communicating with such targets. + DirectConnections bool } // Dial uses the Tor Dial function in order to establish connections through @@ -100,7 +105,9 @@ func (p *ProxyNet) Dial(network, address string, default: return nil, errors.New("cannot dial non-tcp network via Tor") } - return Dial(address, p.SOCKS, p.StreamIsolation, timeout) + return Dial( + address, p.SOCKS, p.StreamIsolation, p.DirectConnections, timeout, + ) } // LookupHost uses the Tor LookupHost function in order to resolve hosts over @@ -116,7 +123,7 @@ func (p *ProxyNet) LookupSRV(service, proto, return LookupSRV( service, proto, name, p.SOCKS, p.DNS, - p.StreamIsolation, timeout, + p.StreamIsolation, p.DirectConnections, timeout, ) } diff --git a/tor/tor.go b/tor/tor.go index 26e36a7e2..9db879195 100644 --- a/tor/tor.go +++ b/tor/tor.go @@ -66,9 +66,11 @@ func (c *proxyConn) RemoteAddr() net.Addr { // around net.Conn in order to expose the actual remote address we're dialing, // rather than the proxy's address. func Dial(address, socksAddr string, streamIsolation bool, - timeout time.Duration) (net.Conn, error) { + directConnections bool, timeout time.Duration) (net.Conn, error) { - conn, err := dial(address, socksAddr, streamIsolation, timeout) + conn, err := dial( + address, socksAddr, streamIsolation, directConnections, timeout, + ) if err != nil { return nil, err } @@ -87,13 +89,18 @@ func Dial(address, socksAddr string, streamIsolation bool, }, nil } -// dial establishes a connection to the address via Tor's SOCKS proxy. Only TCP -// is supported over Tor. The argument streamIsolation determines if we should -// force stream isolation for this new connection. If we do, then this means -// this new connection will use a fresh circuit, rather than possibly re-using -// an existing circuit. +// dial establishes a connection to the address via the provided TOR SOCKS +// proxy. Only TCP traffic may be routed via Tor. +// +// streamIsolation determines if we should force stream isolation for this new +// connection. If enabled, new connections will use a fresh circuit, rather than +// possibly re-using an existing circuit. +// +// directConnections argument allows the dialer to directly connect to the +// provided address if it does not represent an union service, skipping the +// SOCKS proxy. func dial(address, socksAddr string, streamIsolation bool, - timeout time.Duration) (net.Conn, error) { + directConnections bool, timeout time.Duration) (net.Conn, error) { // If we were requested to force stream isolation for this connection, // we'll populate the authentication credentials with random data as @@ -111,9 +118,22 @@ func dial(address, socksAddr string, streamIsolation bool, } } + clearDialer := &net.Dialer{Timeout: timeout} + if directConnections { + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + // The SOCKS proxy is skipped if the target + // is not an union address. + if !IsOnionHost(host) { + return clearDialer.Dial("tcp", address) + } + } + // Establish the connection through Tor's SOCKS proxy. - proxyDialer := &net.Dialer{Timeout: timeout} - dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, proxyDialer) + dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, clearDialer) if err != nil { return nil, err } @@ -139,10 +159,12 @@ func LookupHost(host, socksAddr string) ([]string, error) { // must have TCP resolution enabled for the given port. func LookupSRV(service, proto, name, socksAddr, dnsServer string, streamIsolation bool, - timeout time.Duration) (string, []*net.SRV, error) { + directConnections bool, timeout time.Duration) (string, []*net.SRV, error) { // Connect to the DNS server we'll be using to query SRV records. - conn, err := dial(dnsServer, socksAddr, streamIsolation, timeout) + conn, err := dial( + dnsServer, socksAddr, streamIsolation, directConnections, timeout, + ) if err != nil { return "", nil, err } From c4221c3c3aa074b1a040a6418ffdbe0ebeaa0903 Mon Sep 17 00:00:00 2001 From: Adrian-Stefan Mares Date: Sun, 20 Jun 2021 11:16:03 +0200 Subject: [PATCH 2/3] config+lnd: Update Tor configuration for hybrid node mode --- config.go | 9 +++++---- lncfg/tor.go | 1 + lnd.go | 20 +++++++++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/config.go b/config.go index d795feed0..d07e6c22b 100644 --- a/config.go +++ b/config.go @@ -906,9 +906,10 @@ func ValidateConfig(cfg Config, usageMessage string, // our real information. if cfg.Tor.Active { cfg.net = &tor.ProxyNet{ - SOCKS: cfg.Tor.SOCKS, - DNS: cfg.Tor.DNS, - StreamIsolation: cfg.Tor.StreamIsolation, + SOCKS: cfg.Tor.SOCKS, + DNS: cfg.Tor.DNS, + StreamIsolation: cfg.Tor.StreamIsolation, + DirectConnections: cfg.Tor.DirectConnections, } } @@ -1316,7 +1317,7 @@ func ValidateConfig(cfg Config, usageMessage string, // connections. if len(cfg.RawListeners) == 0 { addr := fmt.Sprintf(":%d", defaultPeerPort) - if cfg.Tor.Active { + if cfg.Tor.Active && !cfg.Tor.DirectConnections { addr = fmt.Sprintf("localhost:%d", defaultPeerPort) } cfg.RawListeners = append(cfg.RawListeners, addr) diff --git a/lncfg/tor.go b/lncfg/tor.go index e7070c38c..8051e399f 100644 --- a/lncfg/tor.go +++ b/lncfg/tor.go @@ -6,6 +6,7 @@ type Tor struct { SOCKS string `long:"socks" description:"The host:port that Tor's exposed SOCKS5 proxy is listening on"` DNS string `long:"dns" description:"The DNS server as host:port that Tor will use for SRV queries - NOTE must have TCP resolution enabled"` StreamIsolation bool `long:"streamisolation" description:"Enable Tor stream isolation by randomizing user credentials for each connection."` + DirectConnections bool `long:"directconnections" description:"Allow the node to establish direct connections to services not running behind Tor."` Control string `long:"control" description:"The host:port that Tor is listening on for Tor control connections"` TargetIPAddress string `long:"targetipaddress" description:"IP address that Tor should use as the target of the hidden service"` Password string `long:"password" description:"The password used to arrive at the HashedControlPassword for the control port. If provided, the HASHEDPASSWORD authentication method will be used instead of the SAFECOOKIE one."` diff --git a/lnd.go b/lnd.go index bd0f2839d..a00dc9ca1 100644 --- a/lnd.go +++ b/lnd.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "crypto/tls" + "errors" "fmt" "io/ioutil" "net" @@ -168,6 +169,10 @@ type ListenerCfg struct { ExternalRestRegistrar RestRegistrar } +var errStreamIsolationWithDirectConnections = errors.New( + "direct connections cannot be used while stream isolation is enabled", +) + // Main is the true entry point for lnd. It accepts a fully populated and // validated main configuration struct and an optional listener config struct. // This function starts all main system components then blocks until a signal @@ -752,10 +757,19 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error return err } + if cfg.Tor.StreamIsolation && cfg.Tor.DirectConnections { + return errStreamIsolationWithDirectConnections + } + if cfg.Tor.Active { - srvrLog.Infof("Proxying all network traffic via Tor "+ - "(stream_isolation=%v)! NOTE: Ensure the backend node "+ - "is proxying over Tor as well", cfg.Tor.StreamIsolation) + if cfg.Tor.DirectConnections { + srvrLog.Info("Onion services are accessible via Tor! NOTE: " + + "Traffic to clearnet services is not routed via Tor.") + } else { + srvrLog.Infof("Proxying all network traffic via Tor "+ + "(stream_isolation=%v)! NOTE: Ensure the backend node "+ + "is proxying over Tor as well", cfg.Tor.StreamIsolation) + } } // If the watchtower client should be active, open the client database. From 3378ad01207c8d43b12be73a0293eb08d53a56dd Mon Sep 17 00:00:00 2001 From: Adrian-Stefan Mares Date: Sun, 1 Aug 2021 14:28:32 +0200 Subject: [PATCH 3/3] config+lnd+tor: Update naming --- config.go | 10 +++++----- lncfg/tor.go | 24 ++++++++++++------------ lnd.go | 10 +++++----- sample-lnd.conf | 2 +- tor/net.go | 15 ++++++++------- tor/tor.go | 22 ++++++++++++---------- 6 files changed, 43 insertions(+), 40 deletions(-) diff --git a/config.go b/config.go index d07e6c22b..6e836ea64 100644 --- a/config.go +++ b/config.go @@ -906,10 +906,10 @@ func ValidateConfig(cfg Config, usageMessage string, // our real information. if cfg.Tor.Active { cfg.net = &tor.ProxyNet{ - SOCKS: cfg.Tor.SOCKS, - DNS: cfg.Tor.DNS, - StreamIsolation: cfg.Tor.StreamIsolation, - DirectConnections: cfg.Tor.DirectConnections, + SOCKS: cfg.Tor.SOCKS, + DNS: cfg.Tor.DNS, + StreamIsolation: cfg.Tor.StreamIsolation, + SkipProxyForClearNetTargets: cfg.Tor.SkipProxyForClearNetTargets, } } @@ -1317,7 +1317,7 @@ func ValidateConfig(cfg Config, usageMessage string, // connections. if len(cfg.RawListeners) == 0 { addr := fmt.Sprintf(":%d", defaultPeerPort) - if cfg.Tor.Active && !cfg.Tor.DirectConnections { + if cfg.Tor.Active && !cfg.Tor.SkipProxyForClearNetTargets { addr = fmt.Sprintf("localhost:%d", defaultPeerPort) } cfg.RawListeners = append(cfg.RawListeners, addr) diff --git a/lncfg/tor.go b/lncfg/tor.go index 8051e399f..be58e7ea5 100644 --- a/lncfg/tor.go +++ b/lncfg/tor.go @@ -2,16 +2,16 @@ package lncfg // Tor holds the configuration options for the daemon's connection to tor. type Tor struct { - Active bool `long:"active" description:"Allow outbound and inbound connections to be routed through Tor"` - SOCKS string `long:"socks" description:"The host:port that Tor's exposed SOCKS5 proxy is listening on"` - DNS string `long:"dns" description:"The DNS server as host:port that Tor will use for SRV queries - NOTE must have TCP resolution enabled"` - StreamIsolation bool `long:"streamisolation" description:"Enable Tor stream isolation by randomizing user credentials for each connection."` - DirectConnections bool `long:"directconnections" description:"Allow the node to establish direct connections to services not running behind Tor."` - Control string `long:"control" description:"The host:port that Tor is listening on for Tor control connections"` - TargetIPAddress string `long:"targetipaddress" description:"IP address that Tor should use as the target of the hidden service"` - Password string `long:"password" description:"The password used to arrive at the HashedControlPassword for the control port. If provided, the HASHEDPASSWORD authentication method will be used instead of the SAFECOOKIE one."` - V2 bool `long:"v2" description:"Automatically set up a v2 onion service to listen for inbound connections"` - V3 bool `long:"v3" description:"Automatically set up a v3 onion service to listen for inbound connections"` - PrivateKeyPath string `long:"privatekeypath" description:"The path to the private key of the onion service being created"` - WatchtowerKeyPath string `long:"watchtowerkeypath" description:"The path to the private key of the watchtower onion service being created"` + Active bool `long:"active" description:"Allow outbound and inbound connections to be routed through Tor"` + SOCKS string `long:"socks" description:"The host:port that Tor's exposed SOCKS5 proxy is listening on"` + DNS string `long:"dns" description:"The DNS server as host:port that Tor will use for SRV queries - NOTE must have TCP resolution enabled"` + StreamIsolation bool `long:"streamisolation" description:"Enable Tor stream isolation by randomizing user credentials for each connection."` + SkipProxyForClearNetTargets bool `long:"skip-proxy-for-clearnet-targets" description:"Allow the node to establish direct connections to services not running behind Tor."` + Control string `long:"control" description:"The host:port that Tor is listening on for Tor control connections"` + TargetIPAddress string `long:"targetipaddress" description:"IP address that Tor should use as the target of the hidden service"` + Password string `long:"password" description:"The password used to arrive at the HashedControlPassword for the control port. If provided, the HASHEDPASSWORD authentication method will be used instead of the SAFECOOKIE one."` + V2 bool `long:"v2" description:"Automatically set up a v2 onion service to listen for inbound connections"` + V3 bool `long:"v3" description:"Automatically set up a v3 onion service to listen for inbound connections"` + PrivateKeyPath string `long:"privatekeypath" description:"The path to the private key of the onion service being created"` + WatchtowerKeyPath string `long:"watchtowerkeypath" description:"The path to the private key of the watchtower onion service being created"` } diff --git a/lnd.go b/lnd.go index a00dc9ca1..9a8f89d56 100644 --- a/lnd.go +++ b/lnd.go @@ -169,8 +169,8 @@ type ListenerCfg struct { ExternalRestRegistrar RestRegistrar } -var errStreamIsolationWithDirectConnections = errors.New( - "direct connections cannot be used while stream isolation is enabled", +var errStreamIsolationWithProxySkip = errors.New( + "while stream isolation is enabled, the TOR proxy may not be skipped", ) // Main is the true entry point for lnd. It accepts a fully populated and @@ -757,12 +757,12 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error return err } - if cfg.Tor.StreamIsolation && cfg.Tor.DirectConnections { - return errStreamIsolationWithDirectConnections + if cfg.Tor.StreamIsolation && cfg.Tor.SkipProxyForClearNetTargets { + return errStreamIsolationWithProxySkip } if cfg.Tor.Active { - if cfg.Tor.DirectConnections { + if cfg.Tor.SkipProxyForClearNetTargets { srvrLog.Info("Onion services are accessible via Tor! NOTE: " + "Traffic to clearnet services is not routed via Tor.") } else { diff --git a/sample-lnd.conf b/sample-lnd.conf index 235347047..030c1fc24 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -832,7 +832,7 @@ litecoin.node=ltcd ; Tor, thus allowing lower latency and better connection stability. ; WARNING: This option will reveal the source IP address of the node, and should ; be used only if privacy is not a concern. -; tor.directconnections=true +; tor.skip-proxy-for-clearnet-targets=true ; The port that Tor's exposed SOCKS5 proxy is listening on. Using Tor allows ; outbound-only connections (listening will be disabled) -- NOTE port must be diff --git a/tor/net.go b/tor/net.go index 26990edf0..e96c79acd 100644 --- a/tor/net.go +++ b/tor/net.go @@ -89,10 +89,10 @@ type ProxyNet struct { // will now use a distinct circuit. StreamIsolation bool - // DirectConnections allows the proxy network to use direct connections - // to non-onion service targets. If enabled, the node IP address will be - // revealed while communicating with such targets. - DirectConnections bool + // SkipProxyForClearNetTargets allows the proxy network to use direct + // connections to non-onion service targets. If enabled, the node IP + // address will be revealed while communicating with such targets. + SkipProxyForClearNetTargets bool } // Dial uses the Tor Dial function in order to establish connections through @@ -106,7 +106,8 @@ func (p *ProxyNet) Dial(network, address string, return nil, errors.New("cannot dial non-tcp network via Tor") } return Dial( - address, p.SOCKS, p.StreamIsolation, p.DirectConnections, timeout, + address, p.SOCKS, p.StreamIsolation, + p.SkipProxyForClearNetTargets, timeout, ) } @@ -122,8 +123,8 @@ func (p *ProxyNet) LookupSRV(service, proto, name string, timeout time.Duration) (string, []*net.SRV, error) { return LookupSRV( - service, proto, name, p.SOCKS, p.DNS, - p.StreamIsolation, p.DirectConnections, timeout, + service, proto, name, p.SOCKS, p.DNS, p.StreamIsolation, + p.SkipProxyForClearNetTargets, timeout, ) } diff --git a/tor/tor.go b/tor/tor.go index 9db879195..c373a250e 100644 --- a/tor/tor.go +++ b/tor/tor.go @@ -66,10 +66,11 @@ func (c *proxyConn) RemoteAddr() net.Addr { // around net.Conn in order to expose the actual remote address we're dialing, // rather than the proxy's address. func Dial(address, socksAddr string, streamIsolation bool, - directConnections bool, timeout time.Duration) (net.Conn, error) { + skipProxyForClearNetTargets bool, timeout time.Duration) (net.Conn, error) { conn, err := dial( - address, socksAddr, streamIsolation, directConnections, timeout, + address, socksAddr, streamIsolation, + skipProxyForClearNetTargets, timeout, ) if err != nil { return nil, err @@ -96,11 +97,11 @@ func Dial(address, socksAddr string, streamIsolation bool, // connection. If enabled, new connections will use a fresh circuit, rather than // possibly re-using an existing circuit. // -// directConnections argument allows the dialer to directly connect to the -// provided address if it does not represent an union service, skipping the -// SOCKS proxy. +// skipProxyForClearNetTargets argument allows the dialer to directly connect +// to the provided address if it does not represent an union service, skipping +// the SOCKS proxy. func dial(address, socksAddr string, streamIsolation bool, - directConnections bool, timeout time.Duration) (net.Conn, error) { + skipProxyForClearNetTargets bool, timeout time.Duration) (net.Conn, error) { // If we were requested to force stream isolation for this connection, // we'll populate the authentication credentials with random data as @@ -119,7 +120,7 @@ func dial(address, socksAddr string, streamIsolation bool, } clearDialer := &net.Dialer{Timeout: timeout} - if directConnections { + if skipProxyForClearNetTargets { host, _, err := net.SplitHostPort(address) if err != nil { return nil, err @@ -158,12 +159,13 @@ func LookupHost(host, socksAddr string) ([]string, error) { // proxy by connecting directly to a DNS server and querying it. The DNS server // must have TCP resolution enabled for the given port. func LookupSRV(service, proto, name, socksAddr, - dnsServer string, streamIsolation bool, - directConnections bool, timeout time.Duration) (string, []*net.SRV, error) { + dnsServer string, streamIsolation bool, skipProxyForClearNetTargets bool, + timeout time.Duration) (string, []*net.SRV, error) { // Connect to the DNS server we'll be using to query SRV records. conn, err := dial( - dnsServer, socksAddr, streamIsolation, directConnections, timeout, + dnsServer, socksAddr, streamIsolation, + skipProxyForClearNetTargets, timeout, ) if err != nil { return "", nil, err