diff --git a/config.go b/config.go index cd01c70ea..d08e75244 100644 --- a/config.go +++ b/config.go @@ -904,9 +904,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, + SkipProxyForClearNetTargets: cfg.Tor.SkipProxyForClearNetTargets, } } @@ -1320,7 +1321,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.SkipProxyForClearNetTargets { addr = fmt.Sprintf("localhost:%d", defaultPeerPort) } cfg.RawListeners = append(cfg.RawListeners, addr) diff --git a/lncfg/tor.go b/lncfg/tor.go index e7070c38c..be58e7ea5 100644 --- a/lncfg/tor.go +++ b/lncfg/tor.go @@ -2,15 +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."` - 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 f8cc9fa66..de30f5477 100644 --- a/lnd.go +++ b/lnd.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "crypto/tls" + "errors" "fmt" "io/ioutil" "net" @@ -187,6 +188,10 @@ type ListenerCfg struct { ExternalRestRegistrar RestRegistrar } +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 // validated main configuration struct and an optional listener config struct. // This function starts all main system components then blocks until a signal @@ -748,10 +753,19 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error return err } + if cfg.Tor.StreamIsolation && cfg.Tor.SkipProxyForClearNetTargets { + return errStreamIsolationWithProxySkip + } + 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.SkipProxyForClearNetTargets { + 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 tor is active and either v2 or v3 onion services have been specified, diff --git a/sample-lnd.conf b/sample-lnd.conf index 56fba9d67..61d285e4c 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.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 ; 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..e96c79acd 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 + + // 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 @@ -100,7 +105,10 @@ 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.SkipProxyForClearNetTargets, timeout, + ) } // LookupHost uses the Tor LookupHost function in order to resolve hosts over @@ -115,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, timeout, + service, proto, name, p.SOCKS, p.DNS, p.StreamIsolation, + p.SkipProxyForClearNetTargets, timeout, ) } diff --git a/tor/tor.go b/tor/tor.go index 26e36a7e2..c373a250e 100644 --- a/tor/tor.go +++ b/tor/tor.go @@ -66,9 +66,12 @@ 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) { + skipProxyForClearNetTargets bool, timeout time.Duration) (net.Conn, error) { - conn, err := dial(address, socksAddr, streamIsolation, timeout) + conn, err := dial( + address, socksAddr, streamIsolation, + skipProxyForClearNetTargets, timeout, + ) if err != nil { return nil, err } @@ -87,13 +90,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. +// +// 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, - 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 @@ -111,9 +119,22 @@ func dial(address, socksAddr string, streamIsolation bool, } } + clearDialer := &net.Dialer{Timeout: timeout} + if skipProxyForClearNetTargets { + 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 } @@ -138,11 +159,14 @@ 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, + 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, timeout) + conn, err := dial( + dnsServer, socksAddr, streamIsolation, + skipProxyForClearNetTargets, timeout, + ) if err != nil { return "", nil, err }