Merge pull request from yyforyongyu/5771-fix-tor

tor: enable lnd surviving Tor daemon restart
This commit is contained in:
Olaoluwa Osuntokun 2021-10-11 02:35:14 -07:00 committed by GitHub
commit d7d9eb958b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1231 additions and 124 deletions

@ -139,6 +139,15 @@ const (
defaultTLSBackoff = time.Minute
defaultTLSAttempts = 0
// Set defaults for a health check which ensures that the tor
// connection is alive. Although this check is off by default (not all
// setups require it), we still set the other default values so that
// the health check can be easily enabled with sane defaults.
defaultTCInterval = time.Minute
defaultTCTimeout = time.Second * 5
defaultTCBackoff = time.Minute
defaultTCAttempts = 0
// defaultRemoteMaxHtlcs specifies the default limit for maximum
// concurrent HTLCs the remote party may add to commitment transactions.
// This value can be overridden with --default-remote-max-htlcs.
@ -541,6 +550,12 @@ func DefaultConfig() Config {
Attempts: defaultTLSAttempts,
Backoff: defaultTLSBackoff,
},
TorConnection: &lncfg.CheckConfig{
Interval: defaultTCInterval,
Timeout: defaultTCTimeout,
Attempts: defaultTCAttempts,
Backoff: defaultTCBackoff,
},
},
Gossip: &lncfg.Gossip{
MaxChannelUpdateBurst: discovery.DefaultMaxChannelUpdateBurst,

@ -2,11 +2,32 @@
## Networking & Tor
### Connectivity mode
A new flag has been added to enable a hybrid tor connectivity mode, where tor
is only used for onion address connections, and clearnet for everything else.
This new behavior can be added using the `tor.skip-proxy-for-clearnet-targets`
flag.
### Onion service
The Onion service created upon lnd startup is [now deleted during lnd shutdown
using `DEL_ONION`](https://github.com/lightningnetwork/lnd/pull/5794).
### Tor connection
A new health check, tor connection, [is added to lnd's liveness monitor upon
startup](https://github.com/lightningnetwork/lnd/pull/5794). This check will
ensure the liveness of the connection between the Tor daemon and lnd's tor
controller. To enable it, please use the following flags,
```
healthcheck.torconnection.attempts=xxx
healthcheck.torconnection.timeout=xxx
healthcheck.torconnection.backoff=xxx
healthcheck.torconnection.internal=xxx
```
Read more about the usage of the flags in the `sample-lnd.conf`.
## LN Peer-to-Peer Netowrk
### Bitcoin Blockheaders in Ping Messages

@ -0,0 +1,86 @@
package healthcheck
import (
"errors"
"fmt"
"io"
"syscall"
"github.com/lightningnetwork/lnd/tor"
)
// CheckTorServiceStatus checks whether the onion service is reachable by
// sending a GETINFO command to the Tor daemon using our tor controller.
// We will get an EOF or a broken pipe error if the Tor daemon is
// stopped/restarted as the previously created socket connection is no longer
// open. In this case, we will attempt a restart on our tor controller. If the
// tor daemon comes back, a new socket connection will then be created.
func CheckTorServiceStatus(tc *tor.Controller,
createService func() error) error {
// Send a cmd using GETINFO onions/current and checks that our known
// onion serviceID can be found.
err := tc.CheckOnionService()
if err == nil {
return nil
}
log.Debugf("Checking tor service got: %v", err)
switch {
// We will get an EOF if the connection is lost. In this case, we will
// return an error and wait for the Tor daemon to come back. We won't
// attempt to make a new connection since we know Tor daemon is down.
case errors.Is(err, io.EOF), errors.Is(err, syscall.ECONNREFUSED):
return fmt.Errorf("Tor daemon connection lost, " +
"check if Tor is up and running")
// Once Tor daemon is down, we will get a broken pipe error when we use
// the existing connection to make a GETINFO request since that socket
// has now been closed. As Tor daemon might not be running yet, we will
// attempt to make a new connection till Tor daemon is back.
case errors.Is(err, syscall.EPIPE):
log.Warnf("Tor connection lost, attempting a tor controller " +
"re-connection...")
// If the restart fails, we will attempt again during our next
// healthcheck cycle.
return restartTorController(tc, createService)
// If this is not a connection layer error, such as
// ErrServiceNotCreated or ErrServiceIDUnmatch, there's little we can
// do but to report the error to the user.
default:
return err
}
}
// restartTorController attempts to make a new connection to the Tor daemon and
// re-create the Hidden Service.
func restartTorController(tc *tor.Controller,
createService func() error) error {
err := tc.Reconnect()
// If we get a connection refused error, it means Tor daemon might not
// be started.
if errors.Is(err, syscall.ECONNREFUSED) {
return fmt.Errorf("check if Tor daemon is running")
}
// Otherwise, we get an unexpected and return it.
if err != nil {
log.Errorf("Re-connectting tor got err: %v", err)
return err
}
// Recreate the Hidden Service.
if err := createService(); err != nil {
log.Errorf("Re-create service tor got err: %v", err)
return err
}
log.Info("Successfully restarted tor connection!")
return nil
}

@ -28,6 +28,8 @@ type HealthCheckConfig struct {
DiskCheck *DiskCheckConfig `group:"diskspace" namespace:"diskspace"`
TLSCheck *CheckConfig `group:"tls" namespace:"tls"`
TorConnection *CheckConfig `group:"torconnection" namespace:"torconnection"`
}
// Validate checks the values configured for our health checks.
@ -50,6 +52,10 @@ func (h *HealthCheckConfig) Validate() error {
return errors.New("disk required ratio must be in [0:1)")
}
if err := h.TorConnection.validate("tor connection"); err != nil {
return err
}
return nil
}

2
log.go

@ -40,6 +40,7 @@ import (
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/sweep"
"github.com/lightningnetwork/lnd/tor"
"github.com/lightningnetwork/lnd/watchtower"
"github.com/lightningnetwork/lnd/watchtower/wtclient"
)
@ -161,6 +162,7 @@ func SetupLoggers(root *build.RotatingLogWriter, interceptor signal.Interceptor)
AddSubLogger(root, funding.Subsystem, interceptor, funding.UseLogger)
AddSubLogger(root, cluster.Subsystem, interceptor, cluster.UseLogger)
AddSubLogger(root, rpcperms.Subsystem, interceptor, rpcperms.UseLogger)
AddSubLogger(root, tor.Subsystem, interceptor, tor.UseLogger)
}
// AddSubLogger is a helper method to conveniently create and register the

@ -990,6 +990,22 @@ litecoin.node=ltcd
; This value must be >= 1m.
; healthcheck.tls.interval=1m
; The number of times we should attempt to check our tor connection before
; gracefully shutting down. Set this value to 0 to disable this health check.
; healthcheck.torconnection.attempts=3
; The amount of time we allow a call to our tor connection to take before we
; fail the attempt. This value must be >= 1s.
; healthcheck.torconnection.timeout=10s
; The amount of time we should backoff between failed attempts to check tor
; connection. This value must be >= 1s.
; healthcheck.torconnection.backoff=30s
; The amount of time we should wait between tor connection health checks. This
; value must be >= 1m.
; healthcheck.torconnection.interval=1m
[signrpc]

@ -1470,9 +1470,40 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
})
}
// Create a set of health checks using our configured values. If a
// health check has been disabled by setting attempts to 0, our monitor
// will not run it.
// Create liveliness monitor.
s.createLivenessMonitor(cfg, cc)
// Create the connection manager which will be responsible for
// maintaining persistent outbound connections and also accepting new
// incoming connections
cmgr, err := connmgr.New(&connmgr.Config{
Listeners: listeners,
OnAccept: s.InboundPeerConnected,
RetryDuration: time.Second * 5,
TargetOutbound: 100,
Dial: noiseDial(
nodeKeyECDH, s.cfg.net, s.cfg.ConnectionTimeout,
),
OnConnection: s.OutboundPeerConnected,
})
if err != nil {
return nil, err
}
s.connMgr = cmgr
return s, nil
}
// createLivenessMonitor creates a set of health checks using our configured
// values and uses these checks to create a liveliness monitor. Available
// health checks,
// - chainHealthCheck
// - diskCheck
// - tlsHealthCheck
// - torController, only created when tor is enabled.
// If a health check has been disabled by setting attempts to 0, our monitor
// will not run it.
func (s *server) createLivenessMonitor(cfg *Config, cc *chainreg.ChainControl) {
chainHealthCheck := healthcheck.NewObservation(
"chain backend",
cc.HealthCheck,
@ -1521,11 +1552,12 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
// If the current time is passed the certificate's
// expiry time, then it is considered expired
if time.Now().After(parsedCert.NotAfter) {
return fmt.Errorf("TLS certificate is expired as of %v", parsedCert.NotAfter)
return fmt.Errorf("TLS certificate is "+
"expired as of %v", parsedCert.NotAfter)
}
// If the certificate is not outdated, no error needs to
// be returned
// If the certificate is not outdated, no error needs
// to be returned
return nil
},
cfg.HealthChecks.TLSCheck.Interval,
@ -1534,36 +1566,36 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
cfg.HealthChecks.TLSCheck.Attempts,
)
checks := []*healthcheck.Observation{
chainHealthCheck, diskCheck, tlsHealthCheck,
}
// If Tor is enabled, add the healthcheck for tor connection.
if s.torController != nil {
torConnectionCheck := healthcheck.NewObservation(
"tor connection",
func() error {
return healthcheck.CheckTorServiceStatus(
s.torController,
s.createNewHiddenService,
)
},
cfg.HealthChecks.TorConnection.Interval,
cfg.HealthChecks.TorConnection.Timeout,
cfg.HealthChecks.TorConnection.Backoff,
cfg.HealthChecks.TorConnection.Attempts,
)
checks = append(checks, torConnectionCheck)
}
// If we have not disabled all of our health checks, we create a
// liveliness monitor with our configured checks.
s.livelinessMonitor = healthcheck.NewMonitor(
&healthcheck.Config{
Checks: []*healthcheck.Observation{
chainHealthCheck, diskCheck, tlsHealthCheck,
},
Checks: checks,
Shutdown: srvrLog.Criticalf,
},
)
// Create the connection manager which will be responsible for
// maintaining persistent outbound connections and also accepting new
// incoming connections
cmgr, err := connmgr.New(&connmgr.Config{
Listeners: listeners,
OnAccept: s.InboundPeerConnected,
RetryDuration: time.Second * 5,
TargetOutbound: 100,
Dial: noiseDial(
nodeKeyECDH, s.cfg.net, s.cfg.ConnectionTimeout,
),
OnConnection: s.OutboundPeerConnected,
})
if err != nil {
return nil, err
}
s.connMgr = cmgr
return s, nil
}
// Started returns true if the server has been started, and false otherwise.

@ -1,51 +0,0 @@
package tor
import (
"bytes"
"io/ioutil"
"path/filepath"
"testing"
)
// TestOnionFile tests that the OnionFile implementation of the OnionStore
// interface behaves as expected.
func TestOnionFile(t *testing.T) {
t.Parallel()
tempDir, err := ioutil.TempDir("", "onion_store")
if err != nil {
t.Fatalf("unable to create temp dir: %v", err)
}
privateKey := []byte("hide_me_plz")
privateKeyPath := filepath.Join(tempDir, "secret")
// Create a new file-based onion store. A private key should not exist
// yet.
onionFile := NewOnionFile(privateKeyPath, 0600)
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey {
t.Fatalf("expected ErrNoPrivateKey, got \"%v\"", err)
}
// Store the private key and ensure what's stored matches.
if err := onionFile.StorePrivateKey(V2, privateKey); err != nil {
t.Fatalf("unable to store private key: %v", err)
}
storePrivateKey, err := onionFile.PrivateKey(V2)
if err != nil {
t.Fatalf("unable to retrieve private key: %v", err)
}
if !bytes.Equal(storePrivateKey, privateKey) {
t.Fatalf("expected private key \"%v\", got \"%v\"",
string(privateKey), string(storePrivateKey))
}
// Finally, delete the private key. We should no longer be able to
// retrieve it.
if err := onionFile.DeletePrivateKey(V2); err != nil {
t.Fatalf("unable to delete private key: %v", err)
}
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey {
t.Fatal("found deleted private key")
}
}

72
tor/cmd_info.go Normal file

@ -0,0 +1,72 @@
package tor
import (
"errors"
"fmt"
)
var (
// ErrServiceNotCreated is used when we want to query info on an onion
// service while it's not been created yet.
ErrServiceNotCreated = errors.New("onion service hasn't been created")
// ErrServiceIDUnmatch is used when the serviceID the controller has
// doesn't match the serviceID the Tor daemon has.
ErrServiceIDUnmatch = errors.New("onion serviceIDs not match")
// ErrNoServiceFound is used when the Tor daemon replies no active
// onion services found for the current control connection while we
// expect one.
ErrNoServiceFound = errors.New("no active service found")
)
// CheckOnionService checks that the onion service created by the controller
// is active. It queries the Tor daemon using the endpoint "onions/current" to
// get the current onion service and checks that service ID matches the
// activeServiceID.
func (c *Controller) CheckOnionService() error {
// Check that we have a hidden service created.
if c.activeServiceID == "" {
return ErrServiceNotCreated
}
// Fetch the onion services that live in current control connection.
cmd := "GETINFO onions/current"
code, reply, err := c.sendCommand(cmd)
// Exit early if we got an error or Tor daemon didn't respond success.
// TODO(yy): unify the usage of err and code so we could rely on a
// single source to change our state.
if err != nil || code != success {
log.Debugf("query service:%v got err:%v, reply:%v",
c.activeServiceID, err, reply)
return fmt.Errorf("%w: %v", err, reply)
}
// Parse the reply, which should have the following format,
// onions/current=serviceID
// After parsing, we get a map as,
// [onion/current: serviceID]
//
// NOTE: our current tor controller does NOT support multiple onion
// services to be created at the same time, thus we expect the reply to
// only contain one serviceID. If multiple serviceIDs are returned, we
// would expected the reply to have the following format,
// onions/current=serviceID1, serviceID2, serviceID3,...
// Thus a new parser is need to parse that reply.
resp := parseTorReply(reply)
serviceID, ok := resp["onions/current"]
if !ok {
return ErrNoServiceFound
}
// Check that our active service is indeed the service acknowledged by
// Tor daemon.
if c.activeServiceID != serviceID {
return fmt.Errorf("%w: controller has: %v, Tor daemon has: %v",
ErrServiceIDUnmatch, c.activeServiceID, serviceID)
}
return nil
}

87
tor/cmd_info_test.go Normal file

@ -0,0 +1,87 @@
package tor
import (
"errors"
"io"
"syscall"
"testing"
"github.com/stretchr/testify/require"
)
func TestCheckOnionServiceFailOnServiceNotCreated(t *testing.T) {
t.Parallel()
// Create a dummy tor controller.
c := &Controller{}
// Check that CheckOnionService returns an error when the service
// hasn't been created.
require.Equal(t, ErrServiceNotCreated, c.CheckOnionService())
}
func TestCheckOnionServiceSucceed(t *testing.T) {
t.Parallel()
// Create mock server and client connection.
proxy := createTestProxy(t)
defer proxy.cleanUp()
server := proxy.serverConn
// Assign a fake service ID to the controller.
c := &Controller{conn: proxy.clientConn, activeServiceID: "fakeID"}
// Test a successful response.
serverResp := "250-onions/current=fakeID\n250 OK\n"
// Let the server mocks a given response.
_, err := server.Write([]byte(serverResp))
require.NoError(t, err, "server failed to write")
// For a successful response, we expect no error.
require.NoError(t, c.CheckOnionService())
}
func TestCheckOnionServiceFailOnServiceIDNotMatch(t *testing.T) {
t.Parallel()
// Create mock server and client connection.
proxy := createTestProxy(t)
defer proxy.cleanUp()
server := proxy.serverConn
// Assign a fake service ID to the controller.
c := &Controller{conn: proxy.clientConn, activeServiceID: "fakeID"}
// Mock a response with a different serviceID.
serverResp := "250-onions/current=unmatchedID\n250 OK\n"
// Let the server mocks a given response.
_, err := server.Write([]byte(serverResp))
require.NoError(t, err, "server failed to write")
// Check the error returned from GetServiceInfo is expected.
require.ErrorIs(t, c.CheckOnionService(), ErrServiceIDUnmatch)
}
func TestCheckOnionServiceFailOnClosedConnection(t *testing.T) {
t.Parallel()
// Create mock server and client connection.
proxy := createTestProxy(t)
defer proxy.cleanUp()
server := proxy.serverConn
// Assign a fake service ID to the controller.
c := &Controller{conn: proxy.clientConn, activeServiceID: "fakeID"}
// Close the connection from the server side.
require.NoError(t, server.Close(), "server failed to close conn")
// Check the error returned from GetServiceInfo is expected.
err := c.CheckOnionService()
eof := errors.Is(err, io.EOF)
reset := errors.Is(err, syscall.ECONNRESET)
require.Truef(t, eof || reset,
"must of EOF or RESET error, instead got: %v", err)
}

@ -52,7 +52,9 @@ var _ OnionStore = (*OnionFile)(nil)
// NewOnionFile creates a file-based implementation of the OnionStore interface
// to store an onion service's private key.
func NewOnionFile(privateKeyPath string, privateKeyPerm os.FileMode) *OnionFile {
func NewOnionFile(privateKeyPath string,
privateKeyPerm os.FileMode) *OnionFile {
return &OnionFile{
privateKeyPath: privateKeyPath,
privateKeyPerm: privateKeyPerm,
@ -64,8 +66,8 @@ func (f *OnionFile) StorePrivateKey(_ OnionType, privateKey []byte) error {
return ioutil.WriteFile(f.privateKeyPath, privateKey, f.privateKeyPerm)
}
// PrivateKey retrieves the private key from its expected path. If the file does
// not exist, then ErrNoPrivateKey is returned.
// PrivateKey retrieves the private key from its expected path. If the file
// does not exist, then ErrNoPrivateKey is returned.
func (f *OnionFile) PrivateKey(_ OnionType) ([]byte, error) {
if _, err := os.Stat(f.privateKeyPath); os.IsNotExist(err) {
return nil, ErrNoPrivateKey
@ -78,8 +80,8 @@ func (f *OnionFile) DeletePrivateKey(_ OnionType) error {
return os.Remove(f.privateKeyPath)
}
// AddOnionConfig houses all of the required parameters in order to successfully
// create a new onion service or restore an existing one.
// AddOnionConfig houses all of the required parameters in order to
// successfully create a new onion service or restore an existing one.
type AddOnionConfig struct {
// Type denotes the type of the onion service that should be created.
Type OnionType
@ -87,9 +89,9 @@ type AddOnionConfig struct {
// VirtualPort is the externally reachable port of the onion address.
VirtualPort int
// TargetPorts is the set of ports that the service will be listening on
// locally. The Tor server will use choose a random port from this set
// to forward the traffic from the virtual port.
// TargetPorts is the set of ports that the service will be listening
// on locally. The Tor server will use choose a random port from this
// set to forward the traffic from the virtual port.
//
// NOTE: If nil/empty, the virtual port will be used as the only target
// port.
@ -103,25 +105,17 @@ type AddOnionConfig struct {
Store OnionStore
}
// AddOnion creates an onion service and returns its onion address. Once
// created, the new onion service will remain active until the connection
// between the controller and the Tor server is closed.
func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
// Before sending the request to create an onion service to the Tor
// server, we'll make sure that it supports V3 onion services if that
// was the type requested.
if cfg.Type == V3 {
if err := supportsV3(c.version); err != nil {
return nil, err
}
}
// We'll start off by checking if the store contains an existing private
// key. If it does not, then we should request the server to create a
// new onion service and return its private key. Otherwise, we'll
// request the server to recreate the onion server from our private key.
// prepareKeyparam takes a config and prepares the key param to be used inside
// ADD_ONION.
func (c *Controller) prepareKeyparam(cfg AddOnionConfig) (string, error) {
// We'll start off by checking if the store contains an existing
// private key. If it does not, then we should request the server to
// create a new onion service and return its private key. Otherwise,
// we'll request the server to recreate the onion server from our
// private key.
var keyParam string
switch cfg.Type {
// TODO(yy): drop support for v2.
case V2:
keyParam = "NEW:RSA1024"
case V3:
@ -139,10 +133,22 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
keyParam = string(privateKey)
default:
return nil, err
return "", err
}
}
return keyParam, nil
}
// prepareAddOnion constructs a cmd command string based on the specified
// config.
func (c *Controller) prepareAddOnion(cfg AddOnionConfig) (string, error) {
// Create the keyParam.
keyParam, err := c.prepareKeyparam(cfg)
if err != nil {
return "", err
}
// Now, we'll create a mapping from the virtual port to each target
// port. If no target ports were specified, we'll use the virtual port
// to provide a one-to-one mapping.
@ -155,8 +161,8 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
portParam += fmt.Sprintf("Port=%d,%d ", cfg.VirtualPort,
targetPort)
} else {
portParam += fmt.Sprintf("Port=%d,%s:%d ", cfg.VirtualPort,
c.targetIPAddress, targetPort)
portParam += fmt.Sprintf("Port=%d,%s:%d ",
cfg.VirtualPort, c.targetIPAddress, targetPort)
}
}
@ -171,6 +177,37 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
// Send the command to create the onion service to the Tor server and
// await its response.
cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam)
return cmd, nil
}
// AddOnion creates an ephemeral onion service and returns its onion address.
// Once created, the new onion service will remain active until either,
// - the onion service is removed via `DEL_ONION`.
// - the Tor daemon terminates.
// - the controller connection that originated the `ADD_ONION` is closed.
// Each connection can only see its own ephemeral services. If a service needs
// to survive beyond current controller connection, use the "Detach" flag when
// creating new service via `ADD_ONION`.
func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
// Before sending the request to create an onion service to the Tor
// server, we'll make sure that it supports V3 onion services if that
// was the type requested.
// TODO(yy): drop support for v2.
if cfg.Type == V3 {
if err := supportsV3(c.version); err != nil {
return nil, err
}
}
// Construct the cmd command.
cmd, err := c.prepareAddOnion(cfg)
if err != nil {
return nil, err
}
// Send the command to create the onion service to the Tor server and
// await its response.
_, reply, err := c.sendCommand(cmd)
if err != nil {
return nil, err
@ -207,6 +244,9 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
}
}
c.activeServiceID = serviceID
log.Debugf("serviceID:%s added to tor controller", serviceID)
// Finally, we'll return the onion address composed of the service ID,
// along with the onion suffix, and the port this onion service can be
// reached at externally.
@ -215,3 +255,47 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
Port: cfg.VirtualPort,
}, nil
}
// DelOnion tells the Tor daemon to remove an onion service, which satisfies
// either,
// - the onion service was created on the same control connection as the
// "DEL_ONION" command.
// - the onion service was created using the "Detach" flag.
func (c *Controller) DelOnion(serviceID string) error {
log.Debugf("removing serviceID:%s from tor controller", serviceID)
cmd := fmt.Sprintf("DEL_ONION %s", serviceID)
// Send the command to create the onion service to the Tor server and
// await its response.
code, _, err := c.sendCommand(cmd)
// Tor replies with "250 OK" on success, or a 512 if there are an
// invalid number of arguments, or a 552 if it doesn't recognize the
// ServiceID.
switch code {
// Replied 250 OK.
case success:
return nil
// Replied 512 for invalid arguments. This is most likely that the
// serviceID is not set(empty string).
case invalidNumOfArguments:
return fmt.Errorf("invalid arguments: %w", err)
// Replied 552, which means either,
// - the serviceID is invalid.
// - the onion service is not owned by the current control connection
// and,
// - the onion service is not a detached service.
// In either case, we will ignore the error and log a warning as there
// not much we can do from the controller side.
case serviceIDNotRecognized:
log.Warnf("removing serviceID:%v not found", serviceID)
return nil
default:
return fmt.Errorf("undefined response code: %v, err: %w",
code, err)
}
}

212
tor/cmd_onion_test.go Normal file

@ -0,0 +1,212 @@
package tor
import (
"bytes"
"errors"
"io/ioutil"
"path/filepath"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// TestOnionFile tests that the OnionFile implementation of the OnionStore
// interface behaves as expected.
func TestOnionFile(t *testing.T) {
t.Parallel()
tempDir, err := ioutil.TempDir("", "onion_store")
if err != nil {
t.Fatalf("unable to create temp dir: %v", err)
}
privateKey := []byte("hide_me_plz")
privateKeyPath := filepath.Join(tempDir, "secret")
// Create a new file-based onion store. A private key should not exist
// yet.
onionFile := NewOnionFile(privateKeyPath, 0600)
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey {
t.Fatalf("expected ErrNoPrivateKey, got \"%v\"", err)
}
// Store the private key and ensure what's stored matches.
if err := onionFile.StorePrivateKey(V2, privateKey); err != nil {
t.Fatalf("unable to store private key: %v", err)
}
storePrivateKey, err := onionFile.PrivateKey(V2)
if err != nil {
t.Fatalf("unable to retrieve private key: %v", err)
}
if !bytes.Equal(storePrivateKey, privateKey) {
t.Fatalf("expected private key \"%v\", got \"%v\"",
string(privateKey), string(storePrivateKey))
}
// Finally, delete the private key. We should no longer be able to
// retrieve it.
if err := onionFile.DeletePrivateKey(V2); err != nil {
t.Fatalf("unable to delete private key: %v", err)
}
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey {
t.Fatal("found deleted private key")
}
}
// TestPrepareKeyParam checks that the key param is created as expected.
func TestPrepareKeyParam(t *testing.T) {
testKey := []byte("hide_me_plz")
dummyErr := errors.New("dummy")
// Create a dummy controller.
controller := NewController("", "", "")
// Test that a V3 keyParam is used.
cfg := AddOnionConfig{Type: V3}
keyParam, err := controller.prepareKeyparam(cfg)
require.Equal(t, "NEW:ED25519-V3", keyParam)
require.NoError(t, err)
// Create a mock store which returns the test private key.
store := &mockStore{}
store.On("PrivateKey", cfg.Type).Return(testKey, nil)
// Check that the test private is returned.
cfg = AddOnionConfig{Type: V3, Store: store}
keyParam, err = controller.prepareKeyparam(cfg)
require.Equal(t, string(testKey), keyParam)
require.NoError(t, err)
store.AssertExpectations(t)
// Create a mock store which returns ErrNoPrivateKey.
store = &mockStore{}
store.On("PrivateKey", cfg.Type).Return(nil, ErrNoPrivateKey)
// Check that the V3 keyParam is returned.
cfg = AddOnionConfig{Type: V3, Store: store}
keyParam, err = controller.prepareKeyparam(cfg)
require.Equal(t, "NEW:ED25519-V3", keyParam)
require.NoError(t, err)
store.AssertExpectations(t)
// Create a mock store which returns an dummy error.
store = &mockStore{}
store.On("PrivateKey", cfg.Type).Return(nil, dummyErr)
// Check that an error is returned.
cfg = AddOnionConfig{Type: V3, Store: store}
keyParam, err = controller.prepareKeyparam(cfg)
require.Empty(t, keyParam)
require.ErrorIs(t, dummyErr, err)
store.AssertExpectations(t)
}
// TestPrepareAddOnion checks that the cmd used to add onion service is created
// as expected.
func TestPrepareAddOnion(t *testing.T) {
t.Parallel()
// Create a mock store.
store := &mockStore{}
testKey := []byte("hide_me_plz")
testCases := []struct {
name string
targetIPAddress string
cfg AddOnionConfig
expectedCmd string
expectedErr error
}{
{
name: "empty target IP and ports",
targetIPAddress: "",
cfg: AddOnionConfig{VirtualPort: 9735},
expectedCmd: "ADD_ONION NEW:RSA1024 Port=9735,9735 ",
expectedErr: nil,
},
{
name: "specified target IP and empty ports",
targetIPAddress: "127.0.0.1",
cfg: AddOnionConfig{VirtualPort: 9735},
expectedCmd: "ADD_ONION NEW:RSA1024 " +
"Port=9735,127.0.0.1:9735 ",
expectedErr: nil,
},
{
name: "specified target IP and ports",
targetIPAddress: "127.0.0.1",
cfg: AddOnionConfig{
VirtualPort: 9735,
TargetPorts: []int{18000, 18001},
},
expectedCmd: "ADD_ONION NEW:RSA1024 " +
"Port=9735,127.0.0.1:18000 " +
"Port=9735,127.0.0.1:18001 ",
expectedErr: nil,
},
{
name: "specified private key from store",
targetIPAddress: "",
cfg: AddOnionConfig{
VirtualPort: 9735,
Store: store,
},
expectedCmd: "ADD_ONION hide_me_plz " +
"Port=9735,9735 ",
expectedErr: nil,
},
}
for _, tc := range testCases {
tc := tc
if tc.cfg.Store != nil {
store.On("PrivateKey", tc.cfg.Type).Return(
testKey, tc.expectedErr,
)
}
controller := NewController("", tc.targetIPAddress, "")
t.Run(tc.name, func(t *testing.T) {
cmd, err := controller.prepareAddOnion(tc.cfg)
require.Equal(t, tc.expectedErr, err)
require.Equal(t, tc.expectedCmd, cmd)
// Check that the mocker is satisfied.
store.AssertExpectations(t)
})
}
}
// mockStore implements a mock of the interface OnionStore.
type mockStore struct {
mock.Mock
}
// A compile-time constraint to ensure mockStore satisfies the OnionStore
// interface.
var _ OnionStore = (*mockStore)(nil)
func (m *mockStore) StorePrivateKey(ot OnionType, key []byte) error {
args := m.Called(ot, key)
return args.Error(0)
}
func (m *mockStore) PrivateKey(ot OnionType) ([]byte, error) {
args := m.Called(ot)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (m *mockStore) DeletePrivateKey(ot OnionType) error {
args := m.Called(ot)
return args.Error(0)
}

@ -20,6 +20,14 @@ const (
// request.
success = 250
// 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
// nonceLen is the length of a nonce generated by either the controller
// or the Tor server
nonceLen = 32
@ -57,6 +65,18 @@ var (
// message from the controller.
controllerKey = []byte("Tor safe cookie authentication " +
"controller-to-server hash")
// errCodeNotMatch is used when an expected response code is not
// returned.
errCodeNotMatch = errors.New("unexpected code")
// 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")
)
// Controller is an implementation of the Tor Control protocol. This is used in
@ -100,6 +120,9 @@ type Controller struct {
// 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
// activeServiceID is the Onion ServiceID created by ADD_ONION.
activeServiceID string
}
// NewController returns a new Tor controller that will be able to interact with
@ -114,14 +137,16 @@ func NewController(controlAddr string, targetIPAddress string,
}
}
// 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.
// 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.
func (c *Controller) Start() error {
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
return nil
}
log.Info("Starting tor controller")
conn, err := textproto.Dial("tcp", c.controlAddr)
if err != nil {
return fmt.Errorf("unable to connect to Tor server: %v", err)
@ -138,26 +163,210 @@ func (c *Controller) Stop() error {
return nil
}
log.Info("Stopping tor controller")
// 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 = ""
return c.conn.Close()
}
// 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
}
// 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) {
if err := c.conn.Writer.PrintfLine(command); err != nil {
id, err := c.conn.Cmd(command)
if err != nil {
return 0, "", err
}
// We'll use ReadResponse as it has built-in support for multi-line
// text protocol responses.
code, reply, err := c.conn.Reader.ReadResponse(success)
// Make sure our reader only process the response returned from the
// above command.
c.conn.StartResponse(id)
defer c.conn.EndResponse(id)
code, reply, err := c.readResponse(success)
if err != nil {
log.Debugf("sendCommand:%s got err:%v, reply:%v",
command, err, reply)
return code, reply, err
}
return code, reply, nil
}
// readResponse reads the replies from Tor to the controller. The reply has the
// following format,
//
// Reply = SyncReply / AsyncReply
// SyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine
// AsyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine
//
// MidReplyLine = StatusCode "-" ReplyLine
// DataReplyLine = StatusCode "+" ReplyLine CmdData
// EndReplyLine = StatusCode SP ReplyLine
// ReplyLine = [ReplyText] CRLF
// ReplyText = XXXX
// StatusCode = 3DIGIT
//
// 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:
// https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n158
//
// 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
}
// 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
// of keys and values.
@ -196,6 +405,8 @@ func (c *Controller) authenticate() error {
return err
}
log.Debugf("received protocol info: %v", protocolInfo)
// With the version retrieved, we'll cache it now in case it needs to be
// used later on.
c.version = protocolInfo.version()

@ -1,6 +1,12 @@
package tor
import "testing"
import (
"net"
"net/textproto"
"testing"
"github.com/stretchr/testify/require"
)
// TestParseTorVersion is a series of tests for different version strings that
// check the correctness of determining whether they support creating v3 onion
@ -74,3 +80,283 @@ func TestParseTorVersion(t *testing.T) {
}
}
}
// testProxy emulates a Tor daemon and contains the info used for the tor
// controller to make connections.
type testProxy struct {
// server is the proxy listener.
server net.Listener
// serverConn is the established connection from the server side.
serverConn net.Conn
// serverAddr is the tcp address the proxy is listening on.
serverAddr string
// clientConn is the established connection from the client side.
clientConn *textproto.Conn
}
// cleanUp is used after each test to properly close the ports/connections.
func (tp *testProxy) cleanUp() {
// Don't bother cleanning if there's no a server created.
if tp.server == nil {
return
}
if err := tp.clientConn.Close(); err != nil {
log.Errorf("closing client conn got err: %v", err)
}
if err := tp.server.Close(); err != nil {
log.Errorf("closing proxy server got err: %v", err)
}
}
// createTestProxy creates a proxy server to listen on a random address,
// creates a server and a client connection, and initializes a testProxy using
// these params.
func createTestProxy(t *testing.T) *testProxy {
// Set up the proxy to listen on given port.
//
// NOTE: we use a port 0 here to indicate we want a free port selected
// by the system.
proxy, err := net.Listen("tcp", ":0")
require.NoError(t, err, "failed to create proxy")
t.Logf("created proxy server to listen on address: %v", proxy.Addr())
// Accept the connection inside a goroutine.
serverChan := make(chan net.Conn, 1)
go func(result chan net.Conn) {
conn, err := proxy.Accept()
require.NoError(t, err, "failed to accept")
result <- conn
}(serverChan)
// Create the connection using tor controller.
client, err := textproto.Dial("tcp", proxy.Addr().String())
require.NoError(t, err, "failed to create connection")
tc := &testProxy{
server: proxy,
serverConn: <-serverChan,
serverAddr: proxy.Addr().String(),
clientConn: client,
}
return tc
}
// TestReadResponse contructs a series of possible responses returned by Tor
// and asserts the readResponse can handle them correctly.
func TestReadResponse(t *testing.T) {
// Create mock server and client connection.
proxy := createTestProxy(t)
defer proxy.cleanUp()
server := proxy.serverConn
// Create a dummy tor controller.
c := &Controller{conn: proxy.clientConn}
testCase := []struct {
name string
serverResp string
// expectedReply is the reply we expect the readResponse to
// return.
expectedReply string
// expectedCode is the code we expect the server to return.
expectedCode int
// returnedCode is the code we expect the readResponse to
// return.
returnedCode int
// expectedErr is the error we expect the readResponse to
// return.
expectedErr error
}{
{
// Test a simple response.
name: "succeed on 250",
serverResp: "250 OK\n",
expectedReply: "OK",
expectedCode: 250,
returnedCode: 250,
expectedErr: nil,
},
{
// Test a mid reply(-) response.
name: "succeed on mid reply line",
serverResp: "250-field=value\n" +
"250 OK\n",
expectedReply: "field=value\nOK",
expectedCode: 250,
returnedCode: 250,
expectedErr: nil,
},
{
// Test a data reply(+) response.
name: "succeed on data reply line",
serverResp: "250+field=\n" +
"line1\n" +
"line2\n" +
".\n" +
"250 OK\n",
expectedReply: "field=line1,line2\nOK",
expectedCode: 250,
returnedCode: 250,
expectedErr: nil,
},
{
// Test a mixed reply response.
name: "succeed on mixed reply line",
serverResp: "250-field=value\n" +
"250+field=\n" +
"line1\n" +
"line2\n" +
".\n" +
"250 OK\n",
expectedReply: "field=value\nfield=line1,line2\nOK",
expectedCode: 250,
returnedCode: 250,
expectedErr: nil,
},
{
// Test unexpected code.
name: "fail on codes not matched",
serverResp: "250 ERR\n",
expectedReply: "ERR",
expectedCode: 500,
returnedCode: 250,
expectedErr: errCodeNotMatch,
},
{
// Test short response error.
name: "fail on short response",
serverResp: "123\n250 OK\n",
expectedReply: "",
expectedCode: 250,
returnedCode: 0,
expectedErr: textproto.ProtocolError(
"short line: 123"),
},
{
// Test short response error.
name: "fail on invalid response",
serverResp: "250?OK\n",
expectedReply: "",
expectedCode: 250,
returnedCode: 250,
expectedErr: textproto.ProtocolError(
"invalid line: 250?OK"),
},
}
for _, tc := range testCase {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// Let the server mocks a given response.
_, err := server.Write([]byte(tc.serverResp))
require.NoError(t, err, "server failed to write")
// Read the response and checks all expectations
// satisfied.
code, reply, err := c.readResponse(tc.expectedCode)
require.Equal(t, tc.expectedErr, err)
require.Equal(t, tc.returnedCode, code)
require.Equal(t, tc.expectedReply, reply)
// Check that the read buffer is cleaned.
require.Zero(t, c.conn.R.Buffered(),
"read buffer not empty")
})
}
}
// TestReconnectTCMustBeRunning checks that the tor controller must be running
// while calling Reconnect.
func TestReconnectTCMustBeRunning(t *testing.T) {
// Create a dummy controller.
c := &Controller{}
// Reconnect should fail because the TC is not started.
require.Equal(t, errTCNotStarted, c.Reconnect())
// Set the started flag.
c.started = 1
// Set the stopped flag so the TC is stopped.
c.stopped = 1
// Reconnect should fail because the TC is stopped.
require.Equal(t, errTCStopped, c.Reconnect())
}
// TestReconnectSucceed tests a reconnection will succeed when the tor
// controller is up and running.
func TestReconnectSucceed(t *testing.T) {
// Create mock server and client connection.
proxy := createTestProxy(t)
defer proxy.cleanUp()
// Create a tor controller and mark the controller as started.
c := &Controller{
conn: proxy.clientConn,
started: 1,
controlAddr: proxy.serverAddr,
}
// Accept the connection inside a goroutine. We will also write some
// data so that the reconnection can succeed. We will mock three writes
// and two reads inside our proxy server,
// - write protocol info
// - read auth info
// - write auth challenge
// - read auth challenge
// - write OK
go func() {
// Accept the new connection.
server, err := proxy.server.Accept()
require.NoError(t, err, "failed to accept")
// Write the protocol info.
resp := "250-PROTOCOLINFO 1\n" +
"250-AUTH METHODS=NULL\n" +
"250 OK\n"
_, err = server.Write([]byte(resp))
require.NoErrorf(t, err, "failed to write protocol info")
// Read the auth info from the client.
buf := make([]byte, 65535)
_, err = server.Read(buf)
require.NoError(t, err)
// Write the auth challenge.
resp = "250 AUTHCHALLENGE SERVERHASH=fake\n"
_, err = server.Write([]byte(resp))
require.NoErrorf(t, err, "failed to write auth challenge")
// Read the auth challenge resp from the client.
_, err = server.Read(buf)
require.NoError(t, err)
// Write OK resp.
resp = "250 OK\n"
_, err = server.Write([]byte(resp))
require.NoErrorf(t, err, "failed to write response auth")
}()
// Reconnect should succeed.
require.NoError(t, c.Reconnect())
// Check that the old connection is closed.
_, err := proxy.clientConn.ReadLine()
require.Contains(t, err.Error(), "use of closed network connection")
// Check that the connection has been updated.
require.NotEqual(t, proxy.clientConn, c.conn)
}

26
tor/log.go Normal file

@ -0,0 +1,26 @@
package tor
import (
"github.com/btcsuite/btclog"
)
// Subsystem defines the logging code for this subsystem.
const Subsystem = "TORC" // TORC as in Tor Controller.
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log = btclog.Disabled
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

@ -66,14 +66,15 @@ 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,
skipProxyForClearNetTargets bool, timeout time.Duration) (net.Conn, error) {
skipProxyForClearNetTargets bool,
timeout time.Duration) (net.Conn, error) {
conn, err := dial(
conn, err := dialProxy(
address, socksAddr, streamIsolation,
skipProxyForClearNetTargets, timeout,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("dial proxy failed: %w", err)
}
// Now that the connection is established, we'll create our internal
@ -90,7 +91,7 @@ func Dial(address, socksAddr string, streamIsolation bool,
}, nil
}
// dial establishes a connection to the address via the provided TOR SOCKS
// dialProxy 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
@ -100,8 +101,9 @@ func Dial(address, socksAddr string, streamIsolation bool,
// 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,
skipProxyForClearNetTargets bool, timeout time.Duration) (net.Conn, error) {
func dialProxy(address, socksAddr string, streamIsolation bool,
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
@ -136,7 +138,7 @@ func dial(address, socksAddr string, streamIsolation bool,
// Establish the connection through Tor's SOCKS proxy.
dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, clearDialer)
if err != nil {
return nil, err
return nil, fmt.Errorf("establish sock proxy: %w", err)
}
return dialer.Dial("tcp", address)
@ -163,7 +165,7 @@ func LookupSRV(service, proto, name, socksAddr,
timeout time.Duration) (string, []*net.SRV, error) {
// Connect to the DNS server we'll be using to query SRV records.
conn, err := dial(
conn, err := dialProxy(
dnsServer, socksAddr, streamIsolation,
skipProxyForClearNetTargets, timeout,
)