2018-04-26 08:44:12 +02:00
|
|
|
package tor
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/textproto"
|
2023-06-13 16:22:44 +02:00
|
|
|
"os"
|
2022-08-15 08:45:16 +02:00
|
|
|
"regexp"
|
2018-08-28 23:08:19 +02:00
|
|
|
"strconv"
|
2018-04-26 08:44:12 +02:00
|
|
|
"strings"
|
|
|
|
"sync/atomic"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// success is the Tor Control response code representing a successful
|
|
|
|
// request.
|
|
|
|
success = 250
|
|
|
|
|
2021-09-23 12:37:30 +02:00
|
|
|
// invalidNumOfArguments is the Tor Control response code representing
|
|
|
|
// there being an invalid number of arguments.
|
|
|
|
invalidNumOfArguments = 512
|
|
|
|
|
|
|
|
// serviceIDNotRecognized is the Tor Control response code representing
|
|
|
|
// the specified ServiceID is not recognized.
|
|
|
|
serviceIDNotRecognized = 552
|
|
|
|
|
2018-04-26 08:44:12 +02:00
|
|
|
// nonceLen is the length of a nonce generated by either the controller
|
|
|
|
// or the Tor server
|
|
|
|
nonceLen = 32
|
|
|
|
|
|
|
|
// cookieLen is the length of the authentication cookie.
|
|
|
|
cookieLen = 32
|
|
|
|
|
|
|
|
// ProtocolInfoVersion is the `protocolinfo` version currently supported
|
|
|
|
// by the Tor server.
|
|
|
|
ProtocolInfoVersion = 1
|
2018-08-28 23:08:19 +02:00
|
|
|
|
|
|
|
// MinTorVersion is the minimum supported version that the Tor server
|
|
|
|
// must be running on. This is needed in order to create v3 onion
|
|
|
|
// services through Tor's control port.
|
|
|
|
MinTorVersion = "0.3.3.6"
|
2019-10-26 11:00:57 +02:00
|
|
|
|
|
|
|
// 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"
|
2018-04-26 08:44:12 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// serverKey is the key used when computing the HMAC-SHA256 of a message
|
|
|
|
// from the server.
|
|
|
|
serverKey = []byte("Tor safe cookie authentication " +
|
|
|
|
"server-to-controller hash")
|
|
|
|
|
|
|
|
// controllerKey is the key used when computing the HMAC-SHA256 of a
|
|
|
|
// message from the controller.
|
|
|
|
controllerKey = []byte("Tor safe cookie authentication " +
|
|
|
|
"controller-to-server hash")
|
2021-09-24 12:31:39 +02:00
|
|
|
|
|
|
|
// errCodeNotMatch is used when an expected response code is not
|
|
|
|
// returned.
|
|
|
|
errCodeNotMatch = errors.New("unexpected code")
|
2021-09-27 13:24:19 +02:00
|
|
|
|
|
|
|
// errTCNotStarted is used when we require the tor controller to be
|
|
|
|
// started while it's not.
|
|
|
|
errTCNotStarted = errors.New("tor controller must be started")
|
|
|
|
|
|
|
|
// errTCNotStarted is used when we require the tor controller to be
|
|
|
|
// not stopped while it is.
|
|
|
|
errTCStopped = errors.New("tor controller must not be stopped")
|
2022-08-15 08:45:16 +02:00
|
|
|
|
|
|
|
// replyFieldRegexp is the regular expression used to find fields in a
|
|
|
|
// reply. Parameters within a reply should be of the form KEY=VALUE or
|
|
|
|
// KEY="VALUE", where quoted values might contain spaces, newlines and
|
|
|
|
// quoted pairs. If the parameter doesn't contain "=", then we can
|
|
|
|
// assume it doesn't provide any relevant information that isn't already
|
|
|
|
// known. Read more on this topic:
|
|
|
|
// https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n188
|
|
|
|
replyFieldRegexp = regexp.MustCompile(
|
|
|
|
`[^" \r\n]+=(?:"(?:[^"\\]|\\[\0-\x7F])*"|[^" \r\n]*)`,
|
|
|
|
)
|
2018-04-26 08:44:12 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Controller is an implementation of the Tor Control protocol. This is used in
|
|
|
|
// order to communicate with a Tor server. Its only supported method of
|
|
|
|
// authentication is the SAFECOOKIE method.
|
|
|
|
//
|
|
|
|
// NOTE: The connection to the Tor server must be authenticated before
|
|
|
|
// proceeding to send commands. Otherwise, the connection will be closed.
|
|
|
|
//
|
|
|
|
// TODO:
|
2022-08-22 20:58:42 +02:00
|
|
|
// - if adding support for more commands, extend this with a command queue?
|
|
|
|
// - place under sub-package?
|
|
|
|
// - support async replies from the server
|
2018-04-26 08:44:12 +02:00
|
|
|
type Controller struct {
|
|
|
|
// started is used atomically in order to prevent multiple calls to
|
|
|
|
// Start.
|
|
|
|
started int32
|
|
|
|
|
|
|
|
// stopped is used atomically in order to prevent multiple calls to
|
|
|
|
// Stop.
|
|
|
|
stopped int32
|
|
|
|
|
|
|
|
// conn is the underlying connection between the controller and the
|
|
|
|
// Tor server. It provides read and write methods to simplify the
|
|
|
|
// text-based messages within the connection.
|
|
|
|
conn *textproto.Conn
|
|
|
|
|
|
|
|
// controlAddr is the host:port the Tor server is listening locally for
|
|
|
|
// controller connections on.
|
|
|
|
controlAddr string
|
2018-08-28 23:08:19 +02:00
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
// 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
|
|
|
|
|
2018-08-28 23:08:19 +02:00
|
|
|
// version is the current version of the Tor server.
|
|
|
|
version string
|
2019-09-14 17:05:32 +02:00
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
// 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.
|
2019-09-14 17:05:32 +02:00
|
|
|
targetIPAddress string
|
2021-09-23 12:37:30 +02:00
|
|
|
|
|
|
|
// activeServiceID is the Onion ServiceID created by ADD_ONION.
|
|
|
|
activeServiceID string
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewController returns a new Tor controller that will be able to interact with
|
|
|
|
// a Tor server.
|
2019-10-26 11:00:57 +02:00
|
|
|
func NewController(controlAddr string, targetIPAddress string,
|
|
|
|
password string) *Controller {
|
|
|
|
|
|
|
|
return &Controller{
|
|
|
|
controlAddr: controlAddr,
|
|
|
|
targetIPAddress: targetIPAddress,
|
|
|
|
password: password,
|
|
|
|
}
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
|
|
|
|
2021-09-23 09:31:53 +02:00
|
|
|
// Start establishes and authenticates the connection between the controller
|
|
|
|
// and a Tor server. Once done, the controller will be able to send commands
|
|
|
|
// and expect responses.
|
2018-04-26 08:44:12 +02:00
|
|
|
func (c *Controller) Start() error {
|
|
|
|
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-23 12:34:51 +02:00
|
|
|
log.Info("Starting tor controller")
|
|
|
|
|
2018-04-26 08:44:12 +02:00
|
|
|
conn, err := textproto.Dial("tcp", c.controlAddr)
|
|
|
|
if err != nil {
|
2024-02-26 12:19:38 +01:00
|
|
|
return fmt.Errorf("unable to connect to Tor server: %w", err)
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
c.conn = conn
|
|
|
|
|
|
|
|
return c.authenticate()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop closes the connection between the controller and the Tor server.
|
|
|
|
func (c *Controller) Stop() error {
|
|
|
|
if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-23 12:34:51 +02:00
|
|
|
log.Info("Stopping tor controller")
|
|
|
|
|
2021-09-23 12:37:30 +02:00
|
|
|
// Remove the onion service.
|
|
|
|
if err := c.DelOnion(c.activeServiceID); err != nil {
|
|
|
|
log.Errorf("DEL_ONION got error: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset service ID.
|
|
|
|
c.activeServiceID = ""
|
|
|
|
|
2018-04-26 08:44:12 +02:00
|
|
|
return c.conn.Close()
|
|
|
|
}
|
|
|
|
|
2021-09-27 13:24:19 +02:00
|
|
|
// Reconnect makes a new socket connection between the tor controller and
|
|
|
|
// daemon. It will attempt to close the old connection, make a new connection
|
|
|
|
// and authenticate, and finally reset the activeServiceID that the controller
|
|
|
|
// is aware of.
|
|
|
|
//
|
|
|
|
// NOTE: Any old onion services will be removed once this function is called.
|
|
|
|
// In the case of a Tor daemon restart, previously created onion services will
|
|
|
|
// no longer be there. If the function is called without a Tor daemon restart,
|
|
|
|
// because the control connection is reset, all the onion services belonging to
|
|
|
|
// the old connection will be removed.
|
|
|
|
func (c *Controller) Reconnect() error {
|
|
|
|
// Require the tor controller to be running when we want to reconnect.
|
|
|
|
// This means the started flag must be 1 and the stopped flag must be
|
|
|
|
// 0.
|
|
|
|
if c.started != 1 {
|
|
|
|
return errTCNotStarted
|
|
|
|
}
|
|
|
|
if c.stopped != 0 {
|
|
|
|
return errTCStopped
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("Re-connectting tor controller")
|
|
|
|
|
|
|
|
// If we have an old connection, try to close it. We might receive an
|
|
|
|
// error if the connection has already been closed by Tor daemon(ie,
|
|
|
|
// daemon restarted), so we ignore the error here.
|
|
|
|
if c.conn != nil {
|
|
|
|
if err := c.conn.Close(); err != nil {
|
|
|
|
log.Debugf("closing old conn got err: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make a new connection and authenticate.
|
|
|
|
conn, err := textproto.Dial("tcp", c.controlAddr)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to connect to Tor server: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.conn = conn
|
|
|
|
|
|
|
|
// Authenticate the connection between the controller and Tor daemon.
|
|
|
|
if err := c.authenticate(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset the activeServiceID. This value would only be set if a
|
|
|
|
// previous onion service was created. Because the old connection has
|
|
|
|
// been closed at this point, the old onion service is no longer
|
|
|
|
// active.
|
|
|
|
c.activeServiceID = ""
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-04-26 08:44:12 +02:00
|
|
|
// sendCommand sends a command to the Tor server and returns its response, as a
|
|
|
|
// single space-delimited string, and code.
|
|
|
|
func (c *Controller) sendCommand(command string) (int, string, error) {
|
2021-09-24 12:31:39 +02:00
|
|
|
id, err := c.conn.Cmd(command)
|
|
|
|
if err != nil {
|
2018-04-26 08:44:12 +02:00
|
|
|
return 0, "", err
|
|
|
|
}
|
|
|
|
|
2021-09-24 12:31:39 +02:00
|
|
|
// Make sure our reader only process the response returned from the
|
|
|
|
// above command.
|
|
|
|
c.conn.StartResponse(id)
|
|
|
|
defer c.conn.EndResponse(id)
|
2021-09-23 12:34:51 +02:00
|
|
|
|
2021-09-24 12:31:39 +02:00
|
|
|
code, reply, err := c.readResponse(success)
|
2018-04-26 08:44:12 +02:00
|
|
|
if err != nil {
|
2021-09-23 12:34:51 +02:00
|
|
|
log.Debugf("sendCommand:%s got err:%v, reply:%v",
|
|
|
|
command, err, reply)
|
2018-04-26 08:44:12 +02:00
|
|
|
return code, reply, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return code, reply, nil
|
|
|
|
}
|
|
|
|
|
2021-09-24 12:31:39 +02:00
|
|
|
// readResponse reads the replies from Tor to the controller. The reply has the
|
|
|
|
// following format,
|
|
|
|
//
|
2022-08-22 20:58:42 +02:00
|
|
|
// Reply = SyncReply / AsyncReply
|
|
|
|
// SyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine
|
|
|
|
// AsyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine
|
2021-09-24 12:31:39 +02:00
|
|
|
//
|
2022-08-22 20:58:42 +02:00
|
|
|
// MidReplyLine = StatusCode "-" ReplyLine
|
|
|
|
// DataReplyLine = StatusCode "+" ReplyLine CmdData
|
|
|
|
// EndReplyLine = StatusCode SP ReplyLine
|
|
|
|
// ReplyLine = [ReplyText] CRLF
|
|
|
|
// ReplyText = XXXX
|
|
|
|
// StatusCode = 3DIGIT
|
2021-09-24 12:31:39 +02:00
|
|
|
//
|
|
|
|
// Unless specified otherwise, multiple lines in a single reply from Tor daemon
|
|
|
|
// to the controller are guaranteed to share the same status code. Read more on
|
|
|
|
// this topic:
|
2022-08-22 20:58:42 +02:00
|
|
|
//
|
|
|
|
// https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n158
|
2021-09-24 12:31:39 +02:00
|
|
|
//
|
|
|
|
// NOTE: this code is influenced by https://github.com/Yawning/bulb.
|
|
|
|
func (c *Controller) readResponse(expected int) (int, string, error) {
|
|
|
|
// Clean the buffer inside the conn. This is needed when we encountered
|
|
|
|
// an error while reading the response, the remaining lines need to be
|
|
|
|
// cleaned before next read.
|
|
|
|
defer func() {
|
|
|
|
if _, err := c.conn.R.Discard(c.conn.R.Buffered()); err != nil {
|
|
|
|
log.Errorf("clean read buffer failed: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
reply, code := "", 0
|
|
|
|
hasMoreLines := true
|
|
|
|
|
|
|
|
for hasMoreLines {
|
|
|
|
line, err := c.conn.Reader.ReadLine()
|
|
|
|
if err != nil {
|
|
|
|
return 0, reply, err
|
|
|
|
}
|
|
|
|
log.Tracef("Reading line: %v", line)
|
|
|
|
|
|
|
|
// Line being shortter than 4 is not allowed.
|
|
|
|
if len(line) < 4 {
|
|
|
|
err = textproto.ProtocolError("short line: " + line)
|
|
|
|
return 0, reply, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the status code.
|
|
|
|
code, err = strconv.Atoi(line[0:3])
|
|
|
|
if err != nil {
|
|
|
|
return code, reply, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch line[3] {
|
|
|
|
// EndReplyLine = StatusCode SP ReplyLine.
|
|
|
|
// Example: 250 OK
|
|
|
|
// This is the end of the response, so we mark hasMoreLines to
|
|
|
|
// be false to exit the loop.
|
|
|
|
case ' ':
|
|
|
|
reply += line[4:]
|
|
|
|
hasMoreLines = false
|
|
|
|
|
|
|
|
// MidReplyLine = StatusCode "-" ReplyLine.
|
|
|
|
// Example: 250-version=...
|
|
|
|
// This is a continued response, so we keep reading the next
|
|
|
|
// line.
|
|
|
|
case '-':
|
|
|
|
reply += line[4:]
|
|
|
|
|
|
|
|
// DataReplyLine = StatusCode "+" ReplyLine CmdData.
|
|
|
|
// Example: 250+config-text=
|
|
|
|
// line1
|
|
|
|
// line2
|
|
|
|
// more lines...
|
|
|
|
// .
|
|
|
|
// This is a data response, meaning the following multiple
|
|
|
|
// lines are the actual data, and a dot(.) in the end means the
|
|
|
|
// end of the data response. The response will be formatted as,
|
|
|
|
// key=line1,line2,...
|
|
|
|
// The above example will then be,
|
|
|
|
// config-text=line1,line2,...
|
|
|
|
case '+':
|
|
|
|
// Add the key(config-text=)
|
|
|
|
reply += line[4:]
|
|
|
|
|
|
|
|
// Add the values.
|
|
|
|
resp, err := c.conn.Reader.ReadDotLines()
|
|
|
|
if err != nil {
|
|
|
|
return code, reply, err
|
|
|
|
}
|
|
|
|
reply += strings.Join(resp, ",")
|
|
|
|
|
|
|
|
// Invalid line separator found.
|
|
|
|
default:
|
|
|
|
err = textproto.ProtocolError("invalid line: " + line)
|
|
|
|
return code, reply, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// We check the code here so that the error message is parsed
|
|
|
|
// from the line.
|
|
|
|
if code != expected {
|
|
|
|
return code, reply, errCodeNotMatch
|
|
|
|
}
|
|
|
|
|
|
|
|
// Separate each line using "\n".
|
|
|
|
if hasMoreLines {
|
|
|
|
reply += "\n"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Tracef("Parsed reply: %v", reply)
|
|
|
|
return code, reply, nil
|
|
|
|
}
|
|
|
|
|
2022-08-15 08:45:16 +02:00
|
|
|
// unescapeValue removes escape codes from the value in the Tor reply. A
|
|
|
|
// backslash followed by any character represents that character, so we remove
|
|
|
|
// any backslash not preceded by another backslash.
|
|
|
|
func unescapeValue(value string) string {
|
|
|
|
newString := ""
|
|
|
|
justRemovedBackslash := false
|
|
|
|
|
|
|
|
for _, char := range value {
|
|
|
|
if char == '\\' && !justRemovedBackslash {
|
|
|
|
justRemovedBackslash = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
newString += string(char)
|
|
|
|
justRemovedBackslash = false
|
|
|
|
}
|
|
|
|
|
|
|
|
return newString
|
|
|
|
}
|
|
|
|
|
2018-09-06 10:48:46 +02:00
|
|
|
// parseTorReply parses the reply from the Tor server after receiving a command
|
|
|
|
// from a controller. This will parse the relevant reply parameters into a map
|
2018-04-26 08:44:12 +02:00
|
|
|
// of keys and values.
|
|
|
|
func parseTorReply(reply string) map[string]string {
|
|
|
|
params := make(map[string]string)
|
|
|
|
|
2022-08-15 08:45:16 +02:00
|
|
|
// Find all fields of a reply. The -1 indicates that we want this to
|
|
|
|
// find all instances of the regexp.
|
|
|
|
contents := replyFieldRegexp.FindAllString(reply, -1)
|
2018-04-26 08:44:12 +02:00
|
|
|
for _, content := range contents {
|
|
|
|
// Each parameter within the reply should be of the form
|
2022-08-15 08:45:16 +02:00
|
|
|
// KEY=VALUE or KEY="VALUE".
|
2018-09-18 02:56:24 +02:00
|
|
|
keyValue := strings.SplitN(content, "=", 2)
|
2018-04-26 08:44:12 +02:00
|
|
|
key := keyValue[0]
|
|
|
|
value := keyValue[1]
|
2022-08-15 08:45:16 +02:00
|
|
|
|
|
|
|
// Quoted strings need extra processing.
|
|
|
|
if strings.HasPrefix(value, `"`) {
|
|
|
|
// Remove quotes around the value.
|
|
|
|
value = value[1 : len(value)-1]
|
|
|
|
|
|
|
|
// Unescape the value.
|
|
|
|
value = unescapeValue(value)
|
|
|
|
}
|
|
|
|
|
2018-04-26 08:44:12 +02:00
|
|
|
params[key] = value
|
|
|
|
}
|
|
|
|
|
|
|
|
return params
|
|
|
|
}
|
|
|
|
|
|
|
|
// authenticate authenticates the connection between the controller and the
|
2019-10-26 11:00:57 +02:00
|
|
|
// Tor server using either of the following supported authentication methods
|
|
|
|
// depending on its configuration: SAFECOOKIE, HASHEDPASSWORD, and NULL.
|
2018-04-26 08:44:12 +02:00
|
|
|
func (c *Controller) authenticate() error {
|
2019-10-26 11:00:57 +02:00
|
|
|
protocolInfo, err := c.protocolInfo()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-09-24 12:31:39 +02:00
|
|
|
log.Debugf("received protocol info: %v", protocolInfo)
|
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
// 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 {
|
2018-04-26 08:44:12 +02:00
|
|
|
// 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.
|
2019-10-26 11:00:57 +02:00
|
|
|
cookie, err := c.getAuthCookie(info)
|
2018-04-26 08:44:12 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to retrieve authentication cookie: "+
|
|
|
|
"%v", 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.
|
|
|
|
clientNonce := make([]byte, nonceLen)
|
|
|
|
if _, err := rand.Read(clientNonce); err != nil {
|
2024-02-26 12:19:38 +01:00
|
|
|
return fmt.Errorf("unable to generate client nonce: %w", err)
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
cmd := fmt.Sprintf("AUTHCHALLENGE SAFECOOKIE %x", clientNonce)
|
|
|
|
_, reply, err := c.sendCommand(cmd)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If successful, the reply from the server should be of the following
|
|
|
|
// format:
|
|
|
|
//
|
|
|
|
// "250 AUTHCHALLENGE"
|
|
|
|
// SP "SERVERHASH=" ServerHash
|
|
|
|
// SP "SERVERNONCE=" ServerNonce
|
|
|
|
// CRLF
|
|
|
|
//
|
|
|
|
// We're interested in retrieving the SERVERHASH and SERVERNONCE
|
|
|
|
// parameters, so we'll parse our reply to do so.
|
|
|
|
replyParams := parseTorReply(reply)
|
|
|
|
|
|
|
|
// Once retrieved, we'll ensure these values are of proper length when
|
|
|
|
// decoded.
|
|
|
|
serverHash, ok := replyParams["SERVERHASH"]
|
|
|
|
if !ok {
|
|
|
|
return errors.New("server hash not found in reply")
|
|
|
|
}
|
|
|
|
decodedServerHash, err := hex.DecodeString(serverHash)
|
|
|
|
if err != nil {
|
2024-02-26 12:19:38 +01:00
|
|
|
return fmt.Errorf("unable to decode server hash: %w", err)
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
|
|
|
if len(decodedServerHash) != sha256.Size {
|
|
|
|
return errors.New("invalid server hash length")
|
|
|
|
}
|
|
|
|
|
|
|
|
serverNonce, ok := replyParams["SERVERNONCE"]
|
|
|
|
if !ok {
|
|
|
|
return errors.New("server nonce not found in reply")
|
|
|
|
}
|
|
|
|
decodedServerNonce, err := hex.DecodeString(serverNonce)
|
|
|
|
if err != nil {
|
2024-02-26 12:19:38 +01:00
|
|
|
return fmt.Errorf("unable to decode server nonce: %w", err)
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
|
|
|
if len(decodedServerNonce) != nonceLen {
|
|
|
|
return errors.New("invalid server nonce length")
|
|
|
|
}
|
|
|
|
|
|
|
|
// The server hash above was constructed by computing the HMAC-SHA256
|
|
|
|
// of the message composed of the cookie, client nonce, and server
|
|
|
|
// nonce. We'll redo this computation ourselves to ensure the integrity
|
|
|
|
// and authentication of the message.
|
|
|
|
hmacMessage := bytes.Join(
|
|
|
|
[][]byte{cookie, clientNonce, decodedServerNonce}, []byte{},
|
|
|
|
)
|
|
|
|
computedServerHash := computeHMAC256(serverKey, hmacMessage)
|
|
|
|
if !hmac.Equal(computedServerHash, decodedServerHash) {
|
|
|
|
return fmt.Errorf("expected server hash %x, got %x",
|
|
|
|
decodedServerHash, computedServerHash)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the MAC check was successful, we'll proceed with the last step of
|
|
|
|
// the authentication routine. We'll now send the AUTHENTICATE command
|
|
|
|
// followed by a hex-encoded client hash constructed by computing the
|
|
|
|
// HMAC-SHA256 of the same message, but this time using the controller's
|
|
|
|
// key.
|
|
|
|
clientHash := computeHMAC256(controllerKey, hmacMessage)
|
|
|
|
if len(clientHash) != sha256.Size {
|
|
|
|
return errors.New("invalid client hash length")
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd = fmt.Sprintf("AUTHENTICATE %x", clientHash)
|
|
|
|
if _, _, err := c.sendCommand(cmd); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getAuthCookie retrieves the authentication cookie in bytes from the Tor
|
2019-10-26 11:00:57 +02:00
|
|
|
// 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")
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
2019-10-26 11:00:57 +02:00
|
|
|
cookieFilePath = strings.Trim(cookieFilePath, "\"")
|
2018-04-26 08:44:12 +02:00
|
|
|
|
|
|
|
// Read the cookie from the file and ensure it has the correct length.
|
2023-06-13 16:22:44 +02:00
|
|
|
cookie, err := os.ReadFile(cookieFilePath)
|
2018-04-26 08:44:12 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(cookie) != cookieLen {
|
|
|
|
return nil, errors.New("invalid authentication cookie length")
|
|
|
|
}
|
|
|
|
|
|
|
|
return cookie, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// computeHMAC256 computes the HMAC-SHA256 of a key and message.
|
|
|
|
func computeHMAC256(key, message []byte) []byte {
|
|
|
|
mac := hmac.New(sha256.New, key)
|
|
|
|
mac.Write(message)
|
|
|
|
return mac.Sum(nil)
|
|
|
|
}
|
|
|
|
|
2018-08-28 23:08:19 +02:00
|
|
|
// supportsV3 is a helper function that parses the current version of the Tor
|
2022-01-13 17:29:43 +01:00
|
|
|
// server and determines whether it supports creating v3 onion services through
|
2018-08-28 23:08:19 +02:00
|
|
|
// Tor's control port. The version string should be of the format:
|
2022-08-22 20:58:42 +02:00
|
|
|
//
|
2018-08-28 23:08:19 +02:00
|
|
|
// major.minor.revision.build
|
|
|
|
func supportsV3(version string) error {
|
|
|
|
// We'll split the minimum Tor version that's supported and the given
|
|
|
|
// version in order to individually compare each number.
|
|
|
|
parts := strings.Split(version, ".")
|
|
|
|
if len(parts) != 4 {
|
|
|
|
return errors.New("version string is not of the format " +
|
|
|
|
"major.minor.revision.build")
|
|
|
|
}
|
|
|
|
|
|
|
|
// It's possible that the build number (the last part of the version
|
|
|
|
// string) includes a pre-release string, e.g. rc, beta, etc., so we'll
|
|
|
|
// parse that as well.
|
|
|
|
build := strings.Split(parts[len(parts)-1], "-")
|
|
|
|
parts[len(parts)-1] = build[0]
|
|
|
|
|
2019-05-03 20:22:05 +02:00
|
|
|
// Ensure that each part of the version string corresponds to a number.
|
|
|
|
for _, part := range parts {
|
|
|
|
if _, err := strconv.Atoi(part); err != nil {
|
2018-08-28 23:08:19 +02:00
|
|
|
return err
|
|
|
|
}
|
2019-05-03 20:22:05 +02:00
|
|
|
}
|
2018-08-28 23:08:19 +02:00
|
|
|
|
2019-05-03 20:22:05 +02:00
|
|
|
// Once we've determined we have a proper version string of the format
|
|
|
|
// major.minor.revision.build, we can just do a string comparison to
|
|
|
|
// determine if it satisfies the minimum version supported.
|
|
|
|
if version < MinTorVersion {
|
|
|
|
return fmt.Errorf("version %v below minimum version supported "+
|
|
|
|
"%v", version, MinTorVersion)
|
2018-08-28 23:08:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
// protocolInfo is encompasses the details of a response to a PROTOCOLINFO
|
|
|
|
// command.
|
|
|
|
type protocolInfo map[string]string
|
2018-04-26 08:44:12 +02:00
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
// version returns the Tor version as reported by the server.
|
|
|
|
func (i protocolInfo) version() string {
|
|
|
|
version := i["Tor"]
|
|
|
|
return strings.Trim(version, "\"")
|
|
|
|
}
|
2018-04-26 08:44:12 +02:00
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
// supportsAuthMethod determines whether the Tor server supports the given
|
|
|
|
// authentication method.
|
|
|
|
func (i protocolInfo) supportsAuthMethod(method string) bool {
|
|
|
|
methods, ok := i["METHODS"]
|
2018-04-26 08:44:12 +02:00
|
|
|
if !ok {
|
2019-10-26 11:00:57 +02:00
|
|
|
return false
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|
2019-10-26 11:00:57 +02:00
|
|
|
return strings.Contains(methods, method)
|
|
|
|
}
|
2018-04-26 08:44:12 +02:00
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
// 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
|
|
|
|
}
|
2018-04-26 08:44:12 +02:00
|
|
|
|
2019-10-26 11:00:57 +02:00
|
|
|
return protocolInfo(parseTorReply(reply)), nil
|
2018-04-26 08:44:12 +02:00
|
|
|
}
|