Merge pull request #4048 from wpaulino/tor-hashed-password
server+tor: add support for Tor HASHEDPASSWORD authentication method
This commit is contained in:
commit
e17ad8bc84
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
// protocolInfo is encompasses the details of a response to a PROTOCOLINFO
|
||||
// command.
|
||||
type protocolInfo map[string]string
|
||||
|
||||
// version returns the Tor version as reported by the server.
|
||||
func (i protocolInfo) version() string {
|
||||
version := i["Tor"]
|
||||
return strings.Trim(version, "\"")
|
||||
}
|
||||
|
||||
// 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 false
|
||||
}
|
||||
return strings.Contains(methods, method)
|
||||
}
|
||||
|
||||
// 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 nil, err
|
||||
}
|
||||
|
||||
info := parseTorReply(reply)
|
||||
methods, ok := info["METHODS"]
|
||||
if !ok {
|
||||
return nil, "", "", errors.New("auth methods not found in " +
|
||||
"reply")
|
||||
}
|
||||
|
||||
cookieFile, ok := info["COOKIEFILE"]
|
||||
if !ok && !strings.Contains(methods, "NULL") {
|
||||
return nil, "", "", errors.New("cookie file path not found " +
|
||||
"in reply")
|
||||
}
|
||||
|
||||
version, ok := info["Tor"]
|
||||
if !ok {
|
||||
return nil, "", "", errors.New("Tor version not found in reply")
|
||||
}
|
||||
|
||||
// Finally, we'll clean up the results before returning them.
|
||||
authMethods := strings.Split(methods, ",")
|
||||
cookieFilePath := strings.Trim(cookieFile, "\"")
|
||||
torVersion := strings.Trim(version, "\"")
|
||||
|
||||
return authMethods, cookieFilePath, torVersion, nil
|
||||
return protocolInfo(parseTorReply(reply)), nil
|
||||
}
|
||||
|
||||
// OnionType denotes the type of the onion service.
|
||||
|
Loading…
Reference in New Issue
Block a user