Merge pull request #5410 from adriansmares/feature/add-hybrid-mode

Add Tor hybrid connectivity mode
This commit is contained in:
Olaoluwa Osuntokun 2021-08-22 12:05:25 -07:00 committed by GitHub
commit ec3af13081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 34 deletions

View File

@ -904,9 +904,10 @@ func ValidateConfig(cfg Config, usageMessage string,
// our real information. // our real information.
if cfg.Tor.Active { if cfg.Tor.Active {
cfg.net = &tor.ProxyNet{ cfg.net = &tor.ProxyNet{
SOCKS: cfg.Tor.SOCKS, SOCKS: cfg.Tor.SOCKS,
DNS: cfg.Tor.DNS, DNS: cfg.Tor.DNS,
StreamIsolation: cfg.Tor.StreamIsolation, StreamIsolation: cfg.Tor.StreamIsolation,
SkipProxyForClearNetTargets: cfg.Tor.SkipProxyForClearNetTargets,
} }
} }
@ -1320,7 +1321,7 @@ func ValidateConfig(cfg Config, usageMessage string,
// connections. // connections.
if len(cfg.RawListeners) == 0 { if len(cfg.RawListeners) == 0 {
addr := fmt.Sprintf(":%d", defaultPeerPort) addr := fmt.Sprintf(":%d", defaultPeerPort)
if cfg.Tor.Active { if cfg.Tor.Active && !cfg.Tor.SkipProxyForClearNetTargets {
addr = fmt.Sprintf("localhost:%d", defaultPeerPort) addr = fmt.Sprintf("localhost:%d", defaultPeerPort)
} }
cfg.RawListeners = append(cfg.RawListeners, addr) cfg.RawListeners = append(cfg.RawListeners, addr)

View File

@ -2,15 +2,16 @@ package lncfg
// Tor holds the configuration options for the daemon's connection to tor. // Tor holds the configuration options for the daemon's connection to tor.
type Tor struct { type Tor struct {
Active bool `long:"active" description:"Allow outbound and inbound connections to be routed through Tor"` 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"` 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"` 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."` 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"` SkipProxyForClearNetTargets bool `long:"skip-proxy-for-clearnet-targets" description:"Allow the node to establish direct connections to services not running behind Tor."`
TargetIPAddress string `long:"targetipaddress" description:"IP address that Tor should use as the target of the hidden service"` Control string `long:"control" description:"The host:port that Tor is listening on for Tor control connections"`
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."` TargetIPAddress string `long:"targetipaddress" description:"IP address that Tor should use as the target of the hidden service"`
V2 bool `long:"v2" description:"Automatically set up a v2 onion service to listen for inbound connections"` 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."`
V3 bool `long:"v3" description:"Automatically set up a v3 onion service to listen for inbound connections"` V2 bool `long:"v2" description:"Automatically set up a v2 onion service to listen for inbound connections"`
PrivateKeyPath string `long:"privatekeypath" description:"The path to the private key of the onion service being created"` V3 bool `long:"v3" description:"Automatically set up a v3 onion service to listen for inbound connections"`
WatchtowerKeyPath string `long:"watchtowerkeypath" description:"The path to the private key of the watchtower onion service being created"` 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"`
} }

20
lnd.go
View File

@ -8,6 +8,7 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -187,6 +188,10 @@ type ListenerCfg struct {
ExternalRestRegistrar RestRegistrar 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 // Main is the true entry point for lnd. It accepts a fully populated and
// validated main configuration struct and an optional listener config struct. // validated main configuration struct and an optional listener config struct.
// This function starts all main system components then blocks until a signal // 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 return err
} }
if cfg.Tor.StreamIsolation && cfg.Tor.SkipProxyForClearNetTargets {
return errStreamIsolationWithProxySkip
}
if cfg.Tor.Active { if cfg.Tor.Active {
srvrLog.Infof("Proxying all network traffic via Tor "+ if cfg.Tor.SkipProxyForClearNetTargets {
"(stream_isolation=%v)! NOTE: Ensure the backend node "+ srvrLog.Info("Onion services are accessible via Tor! NOTE: " +
"is proxying over Tor as well", cfg.Tor.StreamIsolation) "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, // If tor is active and either v2 or v3 onion services have been specified,

View File

@ -827,6 +827,13 @@ litecoin.node=ltcd
; Allow outbound and inbound connections to be routed through Tor ; Allow outbound and inbound connections to be routed through Tor
; tor.active=true ; 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 ; 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 ; outbound-only connections (listening will be disabled) -- NOTE port must be
; between 1024 and 65535 ; between 1024 and 65535
@ -841,6 +848,9 @@ litecoin.node=ltcd
; connection. With this mode active, each connection will use a new circuit. ; 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 ; This means that multiple applications (other than lnd) using Tor won't be mixed
; in with lnd's traffic. ; 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 ; tor.streamisolation=true
; The host:port that Tor is listening on for Tor control connections (default: ; The host:port that Tor is listening on for Tor control connections (default:

View File

@ -72,7 +72,7 @@ func (r *ClearNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error)
return net.ResolveTCPAddr(network, address) 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. // for Tor network connections.
type ProxyNet struct { type ProxyNet struct {
// SOCKS is the host:port which Tor's exposed SOCKS5 proxy is listening // 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 // means that our traffic may be harder to correlate as each connection
// will now use a distinct circuit. // will now use a distinct circuit.
StreamIsolation bool 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 // Dial uses the Tor Dial function in order to establish connections through
@ -100,7 +105,10 @@ func (p *ProxyNet) Dial(network, address string,
default: default:
return nil, errors.New("cannot dial non-tcp network via Tor") 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 // 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) { name string, timeout time.Duration) (string, []*net.SRV, error) {
return LookupSRV( return LookupSRV(
service, proto, name, p.SOCKS, p.DNS, service, proto, name, p.SOCKS, p.DNS, p.StreamIsolation,
p.StreamIsolation, timeout, p.SkipProxyForClearNetTargets, timeout,
) )
} }

View File

@ -66,9 +66,12 @@ func (c *proxyConn) RemoteAddr() net.Addr {
// around net.Conn in order to expose the actual remote address we're dialing, // around net.Conn in order to expose the actual remote address we're dialing,
// rather than the proxy's address. // rather than the proxy's address.
func Dial(address, socksAddr string, streamIsolation bool, 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 { if err != nil {
return nil, err return nil, err
} }
@ -87,13 +90,18 @@ func Dial(address, socksAddr string, streamIsolation bool,
}, nil }, nil
} }
// dial establishes a connection to the address via Tor's SOCKS proxy. Only TCP // dial establishes a connection to the address via the provided TOR SOCKS
// is supported over Tor. The argument streamIsolation determines if we should // proxy. Only TCP traffic may be routed via Tor.
// 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 // streamIsolation determines if we should force stream isolation for this new
// an existing circuit. // 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, 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, // If we were requested to force stream isolation for this connection,
// we'll populate the authentication credentials with random data as // 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. // Establish the connection through Tor's SOCKS proxy.
proxyDialer := &net.Dialer{Timeout: timeout} dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, clearDialer)
dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, proxyDialer)
if err != nil { if err != nil {
return nil, err 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 // proxy by connecting directly to a DNS server and querying it. The DNS server
// must have TCP resolution enabled for the given port. // must have TCP resolution enabled for the given port.
func LookupSRV(service, proto, name, socksAddr, func LookupSRV(service, proto, name, socksAddr,
dnsServer string, streamIsolation bool, dnsServer string, streamIsolation bool, skipProxyForClearNetTargets bool,
timeout time.Duration) (string, []*net.SRV, error) { timeout time.Duration) (string, []*net.SRV, error) {
// Connect to the DNS server we'll be using to query SRV records. // 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 { if err != nil {
return "", nil, err return "", nil, err
} }