lnd/tor/controller_test.go
2025-03-10 16:58:16 +08:00

568 lines
15 KiB
Go

package tor
import (
"fmt"
"net"
"net/textproto"
"os"
"path/filepath"
"strconv"
"sync"
"testing"
"time"
"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
// services through Tor control's port.
func TestParseTorVersion(t *testing.T) {
t.Parallel()
tests := []struct {
version string
valid bool
}{
{
version: "0.3.3.6",
valid: true,
},
{
version: "0.3.3.7",
valid: true,
},
{
version: "0.3.4.6",
valid: true,
},
{
version: "0.4.3.6",
valid: true,
},
{
version: "0.4.0.5",
valid: true,
},
{
version: "1.3.3.6",
valid: true,
},
{
version: "0.3.3.6-rc",
valid: true,
},
{
version: "0.3.3.7-rc",
valid: true,
},
{
version: "0.3.3.5-rc",
valid: false,
},
{
version: "0.3.3.5",
valid: false,
},
{
version: "0.3.2.6",
valid: false,
},
{
version: "0.1.3.6",
valid: false,
},
{
version: "0.0.6.3",
valid: false,
},
}
for i, test := range tests {
err := supportsV3(test.version)
if test.valid != (err == nil) {
t.Fatalf("test %d with version string %v failed: %v", i,
test.version, err)
}
}
}
// 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 cleaning 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 a unique port.
addr := fmt.Sprintf(":%d", nextAvailablePort())
proxy, err := net.Listen("tcp", addr)
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,
}
t.Logf("server listening on %v, client listening on %v",
tc.serverConn.LocalAddr(), tc.serverConn.RemoteAddr())
return tc
}
// TestReadResponse constructs 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)
t.Cleanup(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)
t.Cleanup(proxy.cleanUp)
// Create a tor controller and mark the controller as started.
c := &Controller{
conn: proxy.clientConn,
started: 1,
controlAddr: proxy.serverAddr,
}
// Close the old conn before reconnection.
require.NoError(t, proxy.serverConn.Close())
// Accept the connection inside a goroutine. When the client makes a
// reconnection, the messages flow is,
// 1. the client sends the command PROTOCOLINFO to the server.
// 2. the server responds with its protocol version.
// 3. the client reads the response and sends the command AUTHENTICATE
// to the server
// 4. the server responds with the authentication info.
//
// From the server's PoV, We need to mock two reads and two writes
// inside the connection,
// 1. read the command PROTOCOLINFO sent from the client.
// 2. write protocol info so the client can read it.
// 3. read the command AUTHENTICATE sent from the client.
// 4. write auth challenge so the client can read it.
go func() {
// Accept the new connection.
server, err := proxy.server.Accept()
require.NoError(t, err, "failed to accept")
t.Logf("server listening on %v, client listening on %v",
server.LocalAddr(), server.RemoteAddr())
// Read the protocol command from the client.
buf := make([]byte, 65535)
_, err = server.Read(buf)
require.NoError(t, err)
// 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.
_, 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")
}()
// 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)
}
// TestParseTorReply tests that Tor replies are parsed correctly.
func TestParseTorReply(t *testing.T) {
testCase := []struct {
reply string
expectedParams map[string]string
}{
{
// Test a regular reply.
reply: `VERSION Tor="0.4.7.8"`,
expectedParams: map[string]string{
"Tor": "0.4.7.8",
},
},
{
// Test a reply with multiple values, one of them
// containing spaces.
reply: `AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD` +
` COOKIEFILE="/path/with/spaces/Tor Browser/c` +
`ontrol_auth_cookie"`,
expectedParams: map[string]string{
"METHODS": "COOKIE,SAFECOOKIE,HASHEDPASSWORD",
"COOKIEFILE": "/path/with/spaces/Tor Browser/" +
"control_auth_cookie",
},
},
{
// Test a multiline reply.
reply: "ServiceID=id\r\nOK",
expectedParams: map[string]string{"ServiceID": "id"},
},
{
// Test a reply with invalid parameters.
reply: "AUTH =invalid",
expectedParams: map[string]string{},
},
{
// Test escaping arbitrary characters.
reply: `PARAM="esca\ped \"doub\lequotes\""`,
expectedParams: map[string]string{
`PARAM`: `escaped "doublequotes"`,
},
},
{
// Test escaping backslashes. Each single backslash
// should be removed, each double backslash replaced
// with a single one. Note that the single backslash
// before the space escapes the space character, so
// there's two spaces in a row.
reply: `PARAM="escaped \\ \ \\\\"`,
expectedParams: map[string]string{
`PARAM`: `escaped \ \\`,
},
},
}
for _, tc := range testCase {
params := parseTorReply(tc.reply)
require.Equal(t, tc.expectedParams, params)
}
}
const (
// listenerFormat is the format string that is used to generate local
// listener addresses.
listenerFormat = "127.0.0.1:%d"
// defaultNodePort is the start of the range for listening ports of
// harness nodes. Ports are monotonically increasing starting from this
// number and are determined by the results of NextAvailablePort().
defaultNodePort int = 10000
// uniquePortFile is the name of the file that is used to store the
// last port that was used by a node. This is used to make sure that
// the same port is not used by multiple nodes at the same time. The
// file is located in the temp directory of a system.
uniquePortFile = "rpctest-port"
)
var (
// portFileMutex is a mutex that is used to make sure that the port file
// is not accessed by multiple goroutines of the same process at the
// same time. This is used in conjunction with the lock file to make
// sure that the port file is not accessed by multiple processes at the
// same time either. So the lock file is to guard between processes and
// the mutex is to guard between goroutines of the same process.
portFileMutex sync.Mutex
)
// nextAvailablePort returns the first port that is available for listening by a
// new node, using a lock file to make sure concurrent access for parallel tasks
// on the same system don't re-use the same port.
//
// NOTE: This is a copy of `lntest/port`. Since `lnd/tor` is a submodule, it
// cannot import the port module `lntest/port` so we need to re-define it here.
func nextAvailablePort() int {
portFileMutex.Lock()
defer portFileMutex.Unlock()
lockFile := filepath.Join(os.TempDir(), uniquePortFile+".lock")
timeout := time.After(10 * time.Second)
var (
lockFileHandle *os.File
err error
)
for {
// Attempt to acquire the lock file. If it already exists, wait
// for a bit and retry.
lockFileHandle, err = os.OpenFile(
lockFile, os.O_CREATE|os.O_EXCL, 0600,
)
if err == nil {
// Lock acquired.
break
}
// Wait for a bit and retry.
select {
case <-timeout:
panic("timeout waiting for lock file")
case <-time.After(10 * time.Millisecond):
}
}
// Release the lock file when we're done.
defer func() {
// Always close file first, Windows won't allow us to remove it
// otherwise.
_ = lockFileHandle.Close()
err := os.Remove(lockFile)
if err != nil {
panic(fmt.Errorf("couldn't remove lock file: %w", err))
}
}()
portFile := filepath.Join(os.TempDir(), uniquePortFile)
port, err := os.ReadFile(portFile)
if err != nil {
if !os.IsNotExist(err) {
panic(fmt.Errorf("error reading port file: %w", err))
}
port = []byte(strconv.Itoa(defaultNodePort))
}
lastPort, err := strconv.Atoi(string(port))
if err != nil {
panic(fmt.Errorf("error parsing port: %w", err))
}
// We take the next one.
lastPort++
for lastPort < 65535 {
// If there are no errors while attempting to listen on this
// port, close the socket and return it as available. While it
// could be the case that some other process picks up this port
// between the time the socket is closed, and it's reopened in
// the harness node, in practice in CI servers this seems much
// less likely than simply some other process already being
// bound at the start of the tests.
addr := fmt.Sprintf(listenerFormat, lastPort)
l, err := net.Listen("tcp4", addr)
if err == nil {
err := l.Close()
if err == nil {
err := os.WriteFile(
portFile,
[]byte(strconv.Itoa(lastPort)), 0600,
)
if err != nil {
panic(fmt.Errorf("error updating "+
"port file: %w", err))
}
return lastPort
}
}
lastPort++
// Start from the beginning if we reached the end of the port
// range. We need to do this because the lock file now is
// persistent across runs on the same machine during the same
// boot/uptime cycle. So in order to make this work on
// developer's machines, we need to reset the port to the
// default value when we reach the end of the range.
if lastPort == 65535 {
lastPort = defaultNodePort
}
}
// No ports available? Must be a mistake.
panic("no ports available for listening")
}