/ tor / controller_test.go
controller_test.go
  1  package tor
  2  
  3  import (
  4  	"fmt"
  5  	"net"
  6  	"net/textproto"
  7  	"os"
  8  	"path/filepath"
  9  	"strconv"
 10  	"sync"
 11  	"testing"
 12  	"time"
 13  
 14  	"github.com/stretchr/testify/require"
 15  )
 16  
 17  // TestParseTorVersion is a series of tests for different version strings that
 18  // check the correctness of determining whether they support creating v3 onion
 19  // services through Tor control's port.
 20  func TestParseTorVersion(t *testing.T) {
 21  	t.Parallel()
 22  
 23  	tests := []struct {
 24  		version string
 25  		valid   bool
 26  	}{
 27  		{
 28  			version: "0.3.3.6",
 29  			valid:   true,
 30  		},
 31  		{
 32  			version: "0.3.3.7",
 33  			valid:   true,
 34  		},
 35  		{
 36  			version: "0.3.4.6",
 37  			valid:   true,
 38  		},
 39  		{
 40  			version: "0.4.3.6",
 41  			valid:   true,
 42  		},
 43  		{
 44  			version: "0.4.0.5",
 45  			valid:   true,
 46  		},
 47  		{
 48  			version: "1.3.3.6",
 49  			valid:   true,
 50  		},
 51  		{
 52  			version: "0.3.3.6-rc",
 53  			valid:   true,
 54  		},
 55  		{
 56  			version: "0.3.3.7-rc",
 57  			valid:   true,
 58  		},
 59  		{
 60  			version: "0.3.3.5-rc",
 61  			valid:   false,
 62  		},
 63  		{
 64  			version: "0.3.3.5",
 65  			valid:   false,
 66  		},
 67  		{
 68  			version: "0.3.2.6",
 69  			valid:   false,
 70  		},
 71  		{
 72  			version: "0.1.3.6",
 73  			valid:   false,
 74  		},
 75  		{
 76  			version: "0.0.6.3",
 77  			valid:   false,
 78  		},
 79  	}
 80  
 81  	for i, test := range tests {
 82  		err := supportsV3(test.version)
 83  		if test.valid != (err == nil) {
 84  			t.Fatalf("test %d with version string %v failed: %v", i,
 85  				test.version, err)
 86  		}
 87  	}
 88  }
 89  
 90  // testProxy emulates a Tor daemon and contains the info used for the tor
 91  // controller to make connections.
 92  type testProxy struct {
 93  	// server is the proxy listener.
 94  	server net.Listener
 95  
 96  	// serverConn is the established connection from the server side.
 97  	serverConn net.Conn
 98  
 99  	// serverAddr is the tcp address the proxy is listening on.
100  	serverAddr string
101  
102  	// clientConn is the established connection from the client side.
103  	clientConn *textproto.Conn
104  }
105  
106  // cleanUp is used after each test to properly close the ports/connections.
107  func (tp *testProxy) cleanUp() {
108  	// Don't bother cleaning if there's no a server created.
109  	if tp.server == nil {
110  		return
111  	}
112  
113  	if err := tp.clientConn.Close(); err != nil {
114  		log.Errorf("closing client conn got err: %v", err)
115  	}
116  	if err := tp.server.Close(); err != nil {
117  		log.Errorf("closing proxy server got err: %v", err)
118  	}
119  }
120  
121  // createTestProxy creates a proxy server to listen on a random address,
122  // creates a server and a client connection, and initializes a testProxy using
123  // these params.
124  func createTestProxy(t *testing.T) *testProxy {
125  	// Set up the proxy to listen on a unique port.
126  	addr := fmt.Sprintf(":%d", nextAvailablePort())
127  	proxy, err := net.Listen("tcp", addr)
128  	require.NoError(t, err, "failed to create proxy")
129  
130  	t.Logf("created proxy server to listen on address: %v", proxy.Addr())
131  
132  	// Accept the connection inside a goroutine.
133  	serverChan := make(chan net.Conn, 1)
134  	go func(result chan net.Conn) {
135  		conn, err := proxy.Accept()
136  		require.NoError(t, err, "failed to accept")
137  
138  		result <- conn
139  	}(serverChan)
140  
141  	// Create the connection using tor controller.
142  	client, err := textproto.Dial("tcp", proxy.Addr().String())
143  	require.NoError(t, err, "failed to create connection")
144  
145  	tc := &testProxy{
146  		server:     proxy,
147  		serverConn: <-serverChan,
148  		serverAddr: proxy.Addr().String(),
149  		clientConn: client,
150  	}
151  
152  	t.Logf("server listening on %v, client listening on %v",
153  		tc.serverConn.LocalAddr(), tc.serverConn.RemoteAddr())
154  
155  	return tc
156  }
157  
158  // TestReadResponse constructs a series of possible responses returned by Tor
159  // and asserts the readResponse can handle them correctly.
160  func TestReadResponse(t *testing.T) {
161  	// Create mock server and client connection.
162  	proxy := createTestProxy(t)
163  	t.Cleanup(proxy.cleanUp)
164  	server := proxy.serverConn
165  
166  	// Create a dummy tor controller.
167  	c := &Controller{conn: proxy.clientConn}
168  
169  	testCase := []struct {
170  		name       string
171  		serverResp string
172  
173  		// expectedReply is the reply we expect the readResponse to
174  		// return.
175  		expectedReply string
176  
177  		// expectedCode is the code we expect the server to return.
178  		expectedCode int
179  
180  		// returnedCode is the code we expect the readResponse to
181  		// return.
182  		returnedCode int
183  
184  		// expectedErr is the error we expect the readResponse to
185  		// return.
186  		expectedErr error
187  	}{
188  		{
189  			// Test a simple response.
190  			name:          "succeed on 250",
191  			serverResp:    "250 OK\n",
192  			expectedReply: "OK",
193  			expectedCode:  250,
194  			returnedCode:  250,
195  			expectedErr:   nil,
196  		},
197  		{
198  			// Test a mid reply(-) response.
199  			name: "succeed on mid reply line",
200  			serverResp: "250-field=value\n" +
201  				"250 OK\n",
202  			expectedReply: "field=value\nOK",
203  			expectedCode:  250,
204  			returnedCode:  250,
205  			expectedErr:   nil,
206  		},
207  		{
208  			// Test a data reply(+) response.
209  			name: "succeed on data reply line",
210  			serverResp: "250+field=\n" +
211  				"line1\n" +
212  				"line2\n" +
213  				".\n" +
214  				"250 OK\n",
215  			expectedReply: "field=line1,line2\nOK",
216  			expectedCode:  250,
217  			returnedCode:  250,
218  			expectedErr:   nil,
219  		},
220  		{
221  			// Test a mixed reply response.
222  			name: "succeed on mixed reply line",
223  			serverResp: "250-field=value\n" +
224  				"250+field=\n" +
225  				"line1\n" +
226  				"line2\n" +
227  				".\n" +
228  				"250 OK\n",
229  			expectedReply: "field=value\nfield=line1,line2\nOK",
230  			expectedCode:  250,
231  			returnedCode:  250,
232  			expectedErr:   nil,
233  		},
234  		{
235  			// Test unexpected code.
236  			name:          "fail on codes not matched",
237  			serverResp:    "250 ERR\n",
238  			expectedReply: "ERR",
239  			expectedCode:  500,
240  			returnedCode:  250,
241  			expectedErr:   errCodeNotMatch,
242  		},
243  		{
244  			// Test short response error.
245  			name:          "fail on short response",
246  			serverResp:    "123\n250 OK\n",
247  			expectedReply: "",
248  			expectedCode:  250,
249  			returnedCode:  0,
250  			expectedErr: textproto.ProtocolError(
251  				"short line: 123"),
252  		},
253  		{
254  			// Test short response error.
255  			name:          "fail on invalid response",
256  			serverResp:    "250?OK\n",
257  			expectedReply: "",
258  			expectedCode:  250,
259  			returnedCode:  250,
260  			expectedErr: textproto.ProtocolError(
261  				"invalid line: 250?OK"),
262  		},
263  	}
264  
265  	for _, tc := range testCase {
266  		tc := tc
267  		t.Run(tc.name, func(t *testing.T) {
268  			// Let the server mocks a given response.
269  			_, err := server.Write([]byte(tc.serverResp))
270  			require.NoError(t, err, "server failed to write")
271  
272  			// Read the response and checks all expectations
273  			// satisfied.
274  			code, reply, err := c.readResponse(tc.expectedCode)
275  			require.Equal(t, tc.expectedErr, err)
276  			require.Equal(t, tc.returnedCode, code)
277  			require.Equal(t, tc.expectedReply, reply)
278  
279  			// Check that the read buffer is cleaned.
280  			require.Zero(t, c.conn.R.Buffered(),
281  				"read buffer not empty")
282  		})
283  	}
284  }
285  
286  // TestReconnectTCMustBeRunning checks that the tor controller must be running
287  // while calling Reconnect.
288  func TestReconnectTCMustBeRunning(t *testing.T) {
289  	// Create a dummy controller.
290  	c := &Controller{}
291  
292  	// Reconnect should fail because the TC is not started.
293  	require.Equal(t, errTCNotStarted, c.Reconnect())
294  
295  	// Set the started flag.
296  	c.started = 1
297  
298  	// Set the stopped flag so the TC is stopped.
299  	c.stopped = 1
300  
301  	// Reconnect should fail because the TC is stopped.
302  	require.Equal(t, errTCStopped, c.Reconnect())
303  }
304  
305  // TestReconnectSucceed tests a reconnection will succeed when the tor
306  // controller is up and running.
307  func TestReconnectSucceed(t *testing.T) {
308  	// Create mock server and client connection.
309  	proxy := createTestProxy(t)
310  	t.Cleanup(proxy.cleanUp)
311  
312  	// Create a tor controller and mark the controller as started.
313  	c := &Controller{
314  		conn:        proxy.clientConn,
315  		started:     1,
316  		controlAddr: proxy.serverAddr,
317  	}
318  
319  	// Close the old conn before reconnection.
320  	require.NoError(t, proxy.serverConn.Close())
321  
322  	// Accept the connection inside a goroutine. When the client makes a
323  	// reconnection, the messages flow is,
324  	// 1. the client sends the command PROTOCOLINFO to the server.
325  	// 2. the server responds with its protocol version.
326  	// 3. the client reads the response and sends the command AUTHENTICATE
327  	//    to the server
328  	// 4. the server responds with the authentication info.
329  	//
330  	// From the server's PoV, We need to mock two reads and two writes
331  	// inside the connection,
332  	// 1. read the command PROTOCOLINFO sent from the client.
333  	// 2. write protocol info so the client can read it.
334  	// 3. read the command AUTHENTICATE sent from the client.
335  	// 4. write auth challenge so the client can read it.
336  	go func() {
337  		// Accept the new connection.
338  		server, err := proxy.server.Accept()
339  		require.NoError(t, err, "failed to accept")
340  
341  		t.Logf("server listening on %v, client listening on %v",
342  			server.LocalAddr(), server.RemoteAddr())
343  
344  		// Read the protocol command from the client.
345  		buf := make([]byte, 65535)
346  		_, err = server.Read(buf)
347  		require.NoError(t, err)
348  
349  		// Write the protocol info.
350  		resp := "250-PROTOCOLINFO 1\n" +
351  			"250-AUTH METHODS=NULL\n" +
352  			"250 OK\n"
353  		_, err = server.Write([]byte(resp))
354  		require.NoErrorf(t, err, "failed to write protocol info")
355  
356  		// Read the auth info from the client.
357  		_, err = server.Read(buf)
358  		require.NoError(t, err)
359  
360  		// Write the auth challenge.
361  		resp = "250 AUTHCHALLENGE SERVERHASH=fake\n"
362  		_, err = server.Write([]byte(resp))
363  		require.NoErrorf(t, err, "failed to write auth challenge")
364  	}()
365  
366  	// Reconnect should succeed.
367  	require.NoError(t, c.Reconnect())
368  
369  	// Check that the old connection is closed.
370  	_, err := proxy.clientConn.ReadLine()
371  	require.Contains(t, err.Error(), "use of closed network connection")
372  
373  	// Check that the connection has been updated.
374  	require.NotEqual(t, proxy.clientConn, c.conn)
375  }
376  
377  // TestParseTorReply tests that Tor replies are parsed correctly.
378  func TestParseTorReply(t *testing.T) {
379  	testCase := []struct {
380  		reply          string
381  		expectedParams map[string]string
382  	}{
383  		{
384  			// Test a regular reply.
385  			reply: `VERSION Tor="0.4.7.8"`,
386  			expectedParams: map[string]string{
387  				"Tor": "0.4.7.8",
388  			},
389  		},
390  		{
391  			// Test a reply with multiple values, one of them
392  			// containing spaces.
393  			reply: `AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD` +
394  				` COOKIEFILE="/path/with/spaces/Tor Browser/c` +
395  				`ontrol_auth_cookie"`,
396  			expectedParams: map[string]string{
397  				"METHODS": "COOKIE,SAFECOOKIE,HASHEDPASSWORD",
398  				"COOKIEFILE": "/path/with/spaces/Tor Browser/" +
399  					"control_auth_cookie",
400  			},
401  		},
402  		{
403  			// Test a multiline reply.
404  			reply:          "ServiceID=id\r\nOK",
405  			expectedParams: map[string]string{"ServiceID": "id"},
406  		},
407  		{
408  			// Test a reply with invalid parameters.
409  			reply:          "AUTH =invalid",
410  			expectedParams: map[string]string{},
411  		},
412  		{
413  			// Test escaping arbitrary characters.
414  			reply: `PARAM="esca\ped \"doub\lequotes\""`,
415  			expectedParams: map[string]string{
416  				`PARAM`: `escaped "doublequotes"`,
417  			},
418  		},
419  		{
420  			// Test escaping backslashes. Each single backslash
421  			// should be removed, each double backslash replaced
422  			// with a single one. Note that the single backslash
423  			// before the space escapes the space character, so
424  			// there's two spaces in a row.
425  			reply: `PARAM="escaped \\ \ \\\\"`,
426  			expectedParams: map[string]string{
427  				`PARAM`: `escaped \  \\`,
428  			},
429  		},
430  	}
431  
432  	for _, tc := range testCase {
433  		params := parseTorReply(tc.reply)
434  		require.Equal(t, tc.expectedParams, params)
435  	}
436  }
437  
438  const (
439  	// listenerFormat is the format string that is used to generate local
440  	// listener addresses.
441  	listenerFormat = "127.0.0.1:%d"
442  
443  	// defaultNodePort is the start of the range for listening ports of
444  	// harness nodes. Ports are monotonically increasing starting from this
445  	// number and are determined by the results of NextAvailablePort().
446  	defaultNodePort int = 10000
447  
448  	// uniquePortFile is the name of the file that is used to store the
449  	// last port that was used by a node. This is used to make sure that
450  	// the same port is not used by multiple nodes at the same time. The
451  	// file is located in the temp directory of a system.
452  	uniquePortFile = "rpctest-port"
453  )
454  
455  var (
456  	// portFileMutex is a mutex that is used to make sure that the port file
457  	// is not accessed by multiple goroutines of the same process at the
458  	// same time. This is used in conjunction with the lock file to make
459  	// sure that the port file is not accessed by multiple processes at the
460  	// same time either. So the lock file is to guard between processes and
461  	// the mutex is to guard between goroutines of the same process.
462  	portFileMutex sync.Mutex
463  )
464  
465  // nextAvailablePort returns the first port that is available for listening by a
466  // new node, using a lock file to make sure concurrent access for parallel tasks
467  // on the same system don't re-use the same port.
468  //
469  // NOTE: This is a copy of `lntest/port`. Since `lnd/tor` is a submodule, it
470  // cannot import the port module `lntest/port` so we need to re-define it here.
471  func nextAvailablePort() int {
472  	portFileMutex.Lock()
473  	defer portFileMutex.Unlock()
474  
475  	lockFile := filepath.Join(os.TempDir(), uniquePortFile+".lock")
476  	timeout := time.After(10 * time.Second)
477  
478  	var (
479  		lockFileHandle *os.File
480  		err            error
481  	)
482  	for {
483  		// Attempt to acquire the lock file. If it already exists, wait
484  		// for a bit and retry.
485  		lockFileHandle, err = os.OpenFile(
486  			lockFile, os.O_CREATE|os.O_EXCL, 0600,
487  		)
488  		if err == nil {
489  			// Lock acquired.
490  			break
491  		}
492  
493  		// Wait for a bit and retry.
494  		select {
495  		case <-timeout:
496  			panic("timeout waiting for lock file")
497  		case <-time.After(10 * time.Millisecond):
498  		}
499  	}
500  
501  	// Release the lock file when we're done.
502  	defer func() {
503  		// Always close file first, Windows won't allow us to remove it
504  		// otherwise.
505  		_ = lockFileHandle.Close()
506  		err := os.Remove(lockFile)
507  		if err != nil {
508  			panic(fmt.Errorf("couldn't remove lock file: %w", err))
509  		}
510  	}()
511  
512  	portFile := filepath.Join(os.TempDir(), uniquePortFile)
513  	port, err := os.ReadFile(portFile)
514  	if err != nil {
515  		if !os.IsNotExist(err) {
516  			panic(fmt.Errorf("error reading port file: %w", err))
517  		}
518  		port = []byte(strconv.Itoa(defaultNodePort))
519  	}
520  
521  	lastPort, err := strconv.Atoi(string(port))
522  	if err != nil {
523  		panic(fmt.Errorf("error parsing port: %w", err))
524  	}
525  
526  	// We take the next one.
527  	lastPort++
528  	for lastPort < 65535 {
529  		// If there are no errors while attempting to listen on this
530  		// port, close the socket and return it as available. While it
531  		// could be the case that some other process picks up this port
532  		// between the time the socket is closed, and it's reopened in
533  		// the harness node, in practice in CI servers this seems much
534  		// less likely than simply some other process already being
535  		// bound at the start of the tests.
536  		addr := fmt.Sprintf(listenerFormat, lastPort)
537  		l, err := net.Listen("tcp4", addr)
538  		if err == nil {
539  			err := l.Close()
540  			if err == nil {
541  				err := os.WriteFile(
542  					portFile,
543  					[]byte(strconv.Itoa(lastPort)), 0600,
544  				)
545  				if err != nil {
546  					panic(fmt.Errorf("error updating "+
547  						"port file: %w", err))
548  				}
549  
550  				return lastPort
551  			}
552  		}
553  		lastPort++
554  
555  		// Start from the beginning if we reached the end of the port
556  		// range. We need to do this because the lock file now is
557  		// persistent across runs on the same machine during the same
558  		// boot/uptime cycle. So in order to make this work on
559  		// developer's machines, we need to reset the port to the
560  		// default value when we reach the end of the range.
561  		if lastPort == 65535 {
562  			lastPort = defaultNodePort
563  		}
564  	}
565  
566  	// No ports available? Must be a mistake.
567  	panic("no ports available for listening")
568  }