diff --git a/config.go b/config.go index 5308b58c83..99011dca23 100644 --- a/config.go +++ b/config.go @@ -221,6 +221,7 @@ type torConfig struct { 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"` diff --git a/docs/configuring_tor.md b/docs/configuring_tor.md index 02feb535b6..f6372f5947 100644 --- a/docs/configuring_tor.md +++ b/docs/configuring_tor.md @@ -2,7 +2,8 @@ 1. [Overview](#overview) 2. [Getting Started](#getting-started) 3. [Tor Stream Isolation](#tor-stream-isolation) -4. [Listening for Inbound Connections](#listening-for-inbound-connections) +4. [Authentication](#authentication) +5. [Listening for Inbound Connections](#listening-for-inbound-connections) ## Overview @@ -78,6 +79,8 @@ Tor: --tor.dns= The DNS server as host:port that Tor will use for SRV queries - NOTE must have TCP resolution enabled (default: soa.nodes.lightning.directory:53) --tor.streamisolation Enable Tor stream isolation by randomizing user credentials for each connection. --tor.control= The host:port that Tor is listening on for Tor control connections (default: localhost:9051) + --tor.targetipaddress= IP address that Tor should use as the target of the hidden service + --tor.password= 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. --tor.v2 Automatically set up a v2 onion service to listen for inbound connections --tor.v3 Automatically set up a v3 onion service to listen for inbound connections --tor.privatekeypath= The path to the private key of the onion service being created @@ -133,6 +136,26 @@ specification of an additional argument: ⛰ ./lnd --tor.active --tor.streamisolation ``` +## Authentication + +In order for `lnd` to communicate with the Tor daemon securely, it must first +establish an authenticated connection. `lnd` supports the following Tor control +authentication methods (arguably, from most to least secure): + +* `SAFECOOKIE`: This authentication method relies on a cookie created and + stored by the Tor daemon and is the default assuming the Tor daemon supports + it by specifying `CookieAuthentication 1` in its configuration file. +* `HASHEDPASSWORD`: This authentication method is stateless as it relies on a + password hash scheme and may be useful if the Tor daemon is operating under a + separate host from the `lnd` node. The password hash can be obtained through + the Tor daemon with `tor --hash-password PASSWORD`, which should then be + specified in Tor's configuration file with `HashedControlPassword + PASSWORD_HASH`. Finally, to use it within `lnd`, the `--tor.password` flag + should be provided with the corresponding password. +* `NULL`: To bypass any authentication at all, this scheme can be used instead. + It doesn't require any additional flags to `lnd` or configuration options to + the Tor daemon. + ## Listening for Inbound Connections In order to listen for inbound connections through Tor, an onion service must be diff --git a/server.go b/server.go index 46ec12d4cd..68ebfc93a1 100644 --- a/server.go +++ b/server.go @@ -594,7 +594,10 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, // automatically create an onion service, we'll initiate our Tor // controller and establish a connection to the Tor server. if cfg.Tor.Active && (cfg.Tor.V2 || cfg.Tor.V3) { - s.torController = tor.NewController(cfg.Tor.Control, cfg.Tor.TargetIPAddress) + s.torController = tor.NewController( + cfg.Tor.Control, cfg.Tor.TargetIPAddress, + cfg.Tor.Password, + ) } chanGraph := chanDB.ChannelGraph() diff --git a/tor/README.md b/tor/README.md index 337f0159a4..49590c9a7d 100644 --- a/tor/README.md +++ b/tor/README.md @@ -8,8 +8,8 @@ Tor daemon. So far, supported functions include: * Routing DNS queries over Tor (A, AAAA, SRV). * Limited Tor Control functionality (synchronous messages only). So far, this includes: - * Support for SAFECOOKIE authentication only as a sane default. - * Creating v2 onion services. + * Support for SAFECOOKIE, HASHEDPASSWORD, and NULL authentication methods. + * Creating v2 and v3 onion services. In the future, the Tor Control functionality will be extended to support v3 onion services, asynchronous messages, etc. diff --git a/tor/controller.go b/tor/controller.go index 149e766b41..22a02fa9f2 100644 --- a/tor/controller.go +++ b/tor/controller.go @@ -36,6 +36,16 @@ const ( // must be running on. This is needed in order to create v3 onion // services through Tor's control port. MinTorVersion = "0.3.3.6" + + // authSafeCookie is the name of the SAFECOOKIE authentication method. + authSafeCookie = "SAFECOOKIE" + + // authHashedPassword is the name of the HASHEDPASSWORD authentication + // method. + authHashedPassword = "HASHEDPASSWORD" + + // authNull is the name of the NULL authentication method. + authNull = "NULL" ) var ( @@ -79,19 +89,30 @@ type Controller struct { // controller connections on. controlAddr string + // password, if non-empty, signals that the controller should attempt to + // authenticate itself with the backing Tor daemon through the + // HASHEDPASSWORD authentication method with this value. + password string + // version is the current version of the Tor server. version string - // The IP address which we tell the Tor server to use to connect to the LND node. - // This is required when the Tor server runs on another host, otherwise the service - // will not be reachable. + // targetIPAddress is the IP address which we tell the Tor server to use + // to connect to the LND node. This is required when the Tor server + // runs on another host, otherwise the service will not be reachable. targetIPAddress string } // NewController returns a new Tor controller that will be able to interact with // a Tor server. -func NewController(controlAddr string, targetIPAddress string) *Controller { - return &Controller{controlAddr: controlAddr, targetIPAddress: targetIPAddress} +func NewController(controlAddr string, targetIPAddress string, + password string) *Controller { + + return &Controller{ + controlAddr: controlAddr, + targetIPAddress: targetIPAddress, + password: password, + } } // Start establishes and authenticates the connection between the controller and @@ -168,26 +189,74 @@ func parseTorReply(reply string) map[string]string { } // authenticate authenticates the connection between the controller and the -// Tor server using the SAFECOOKIE or NULL authentication method. +// Tor server using either of the following supported authentication methods +// depending on its configuration: SAFECOOKIE, HASHEDPASSWORD, and NULL. func (c *Controller) authenticate() error { + protocolInfo, err := c.protocolInfo() + if err != nil { + return err + } + + // With the version retrieved, we'll cache it now in case it needs to be + // used later on. + c.version = protocolInfo.version() + + switch { + // If a password was provided, then we should attempt to use the + // HASHEDPASSWORD authentication method. + case c.password != "": + if !protocolInfo.supportsAuthMethod(authHashedPassword) { + return fmt.Errorf("%v authentication method not "+ + "supported", authHashedPassword) + } + + return c.authenticateViaHashedPassword() + + // Otherwise, attempt to authentication via the SAFECOOKIE method as it + // provides the most security. + case protocolInfo.supportsAuthMethod(authSafeCookie): + return c.authenticateViaSafeCookie(protocolInfo) + + // Fallback to the NULL method if any others aren't supported. + case protocolInfo.supportsAuthMethod(authNull): + return c.authenticateViaNull() + + // No supported authentication methods, fail. + default: + return errors.New("the Tor server must be configured with " + + "NULL, SAFECOOKIE, or HASHEDPASSWORD authentication") + } +} + +// authenticateViaNull authenticates the controller with the Tor server using +// the NULL authentication method. +func (c *Controller) authenticateViaNull() error { + _, _, err := c.sendCommand("AUTHENTICATE") + return err +} + +// authenticateViaHashedPassword authenticates the controller with the Tor +// server using the HASHEDPASSWORD authentication method. +func (c *Controller) authenticateViaHashedPassword() error { + cmd := fmt.Sprintf("AUTHENTICATE \"%s\"", c.password) + _, _, err := c.sendCommand(cmd) + return err +} + +// authenticateViaSafeCookie authenticates the controller with the Tor server +// using the SAFECOOKIE authentication method. +func (c *Controller) authenticateViaSafeCookie(info protocolInfo) error { // Before proceeding to authenticate the connection, we'll retrieve // the authentication cookie of the Tor server. This will be used // throughout the authentication routine. We do this before as once the // authentication routine has begun, it is not possible to retrieve it // mid-way. - cookie, err := c.getAuthCookie() + cookie, err := c.getAuthCookie(info) if err != nil { return fmt.Errorf("unable to retrieve authentication cookie: "+ "%v", err) } - // If cookie is empty and there's no error, we have a NULL - // authentication method that we should use instead. - if len(cookie) == 0 { - _, _, err := c.sendCommand("AUTHENTICATE") - return err - } - // Authenticating using the SAFECOOKIE authentication method is a two // step process. We'll kick off the authentication routine by sending // the AUTHCHALLENGE command followed by a hex-encoded 32-byte nonce. @@ -272,36 +341,15 @@ func (c *Controller) authenticate() error { } // getAuthCookie retrieves the authentication cookie in bytes from the Tor -// server. Cookie authentication must be enabled for this to work. The boolean -func (c *Controller) getAuthCookie() ([]byte, error) { - // Retrieve the authentication methods currently supported by the Tor - // server. - authMethods, cookieFilePath, version, err := c.ProtocolInfo() - if err != nil { - return nil, err - } - - // With the version retrieved, we'll cache it now in case it needs to be - // used later on. - c.version = version - - // Ensure that the Tor server supports the SAFECOOKIE authentication - // method or the NULL method. If NULL, we don't need the cookie info - // below this loop, so we just return. - safeCookieSupport := false - for _, authMethod := range authMethods { - if authMethod == "SAFECOOKIE" { - safeCookieSupport = true - } - if authMethod == "NULL" { - return nil, nil - } - } - - if !safeCookieSupport { - return nil, errors.New("the Tor server is currently not " + - "configured for cookie or null authentication") +// server. Cookie authentication must be enabled for this to work. +func (c *Controller) getAuthCookie(info protocolInfo) ([]byte, error) { + // Retrieve the cookie file path from the PROTOCOLINFO reply. + cookieFilePath, ok := info["COOKIEFILE"] + if !ok { + return nil, errors.New("COOKIEFILE not found in PROTOCOLINFO " + + "reply") } + cookieFilePath = strings.Trim(cookieFilePath, "\"") // Read the cookie from the file and ensure it has the correct length. cookie, err := ioutil.ReadFile(cookieFilePath) @@ -360,48 +408,36 @@ func supportsV3(version string) error { return nil } -// ProtocolInfo returns the different authentication methods supported by the -// Tor server and the version of the Tor server. -func (c *Controller) ProtocolInfo() ([]string, string, string, error) { - // We'll start off by sending the "PROTOCOLINFO" command to the Tor - // server. We should receive a reply of the following format: - // - // METHODS=COOKIE,SAFECOOKIE - // COOKIEFILE="/home/user/.tor/control_auth_cookie" - // VERSION Tor="0.3.2.10" - // - // We're interested in retrieving all of these fields, so we'll parse - // our reply to do so. - cmd := fmt.Sprintf("PROTOCOLINFO %d", ProtocolInfoVersion) - _, reply, err := c.sendCommand(cmd) - if err != nil { - return nil, "", "", err - } - - info := parseTorReply(reply) - methods, ok := info["METHODS"] - if !ok { - return nil, "", "", errors.New("auth methods not found in " + - "reply") - } +// protocolInfo is encompasses the details of a response to a PROTOCOLINFO +// command. +type protocolInfo map[string]string - cookieFile, ok := info["COOKIEFILE"] - if !ok && !strings.Contains(methods, "NULL") { - return nil, "", "", errors.New("cookie file path not found " + - "in reply") - } +// version returns the Tor version as reported by the server. +func (i protocolInfo) version() string { + version := i["Tor"] + return strings.Trim(version, "\"") +} - version, ok := info["Tor"] +// supportsAuthMethod determines whether the Tor server supports the given +// authentication method. +func (i protocolInfo) supportsAuthMethod(method string) bool { + methods, ok := i["METHODS"] if !ok { - return nil, "", "", errors.New("Tor version not found in reply") + return false } + return strings.Contains(methods, method) +} - // Finally, we'll clean up the results before returning them. - authMethods := strings.Split(methods, ",") - cookieFilePath := strings.Trim(cookieFile, "\"") - torVersion := strings.Trim(version, "\"") +// protocolInfo sends a "PROTOCOLINFO" command to the Tor server and returns its +// response. +func (c *Controller) protocolInfo() (protocolInfo, error) { + cmd := fmt.Sprintf("PROTOCOLINFO %d", ProtocolInfoVersion) + _, reply, err := c.sendCommand(cmd) + if err != nil { + return nil, err + } - return authMethods, cookieFilePath, torVersion, nil + return protocolInfo(parseTorReply(reply)), nil } // OnionType denotes the type of the onion service.