Skip to content

Commit

Permalink
feat: use code exactly like go-libtor
Browse files Browse the repository at this point in the history
I want to understand why we did not see this error previously. My
initial hypothesis is that the code at ooni/go-libtor behaved behaved
differently than #1052.

To understand whether this is the case, in this patch I have copied
the code at libtor/libtor.go and adapted it such that we can use
it inside the current tree.

Part of ooni/probe#2405.
  • Loading branch information
bassosimone committed Feb 3, 2023
1 parent a904ba5 commit 49e22c8
Showing 1 changed file with 74 additions and 222 deletions.
296 changes: 74 additions & 222 deletions internal/libtor/enabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,17 @@ package libtor
//
// /* Note: we need to define inline helpers because we cannot index C arrays in Go. */
//
// static char **cstringArrayNew(size_t size) {
// char **argv = calloc(size, sizeof(char *));
// if (argv == NULL) {
// abort();
// }
// return argv;
// static char** makeCharArray(int size) {
// return calloc(sizeof(char*), size);
// }
//
// static void cstringArraySet(char **argv, size_t index, char *entry) {
// argv[index] = entry;
// }
//
// static void cstringArrayFree(char **argv, size_t size) {
// for (size_t idx = 0; idx < size; idx++) {
// free(argv[idx]);
// }
// free(argv);
// static void setArrayString(char **a, char *s, int n) {
// a[n] = s;
// }
//
// static bool filedescIsGood(tor_control_socket_t fd) {
// return fd != INVALID_TOR_CONTROL_SOCKET;
// static void freeCharArray(char **a, int size) {
// int i;
// for (i = 0; i < size; i++)
// free(a[i]);
// free(a);
// }
//
import "C"
Expand All @@ -57,234 +47,96 @@ import (
"fmt"
"net"
"os"
"sync"

"github.com/cretz/bine/process"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)

// MaybeCreator returns a valid [process.Creator], if possible, otherwise false.
func MaybeCreator() (process.Creator, bool) {
return &torCreator{}, true
return Creator, true
}

// torCreator implements [process.Creator].
type torCreator struct{}

var _ process.Creator = &torCreator{}
// Creator implements the bine.process.Creator, permitting libtor to act as an API
// backend for the bine/tor Go interface.
var Creator process.Creator = new(embeddedCreator)

// New implements [process.Creator].
func (c *torCreator) New(ctx context.Context, args ...string) (process.Process, error) {
left, right := net.Pipe()
proc := &torProcess{
awaitStart: make(chan any, 1), // buffer
controlConn: left,
startErr: make(chan error, 1), // buffer
startOnce: sync.Once{},
waitErr: make(chan error, 1), // buffer
waitOnce: sync.Once{},
// embeddedCreator implements process.Creator, permitting libtor to act as an API
// backend for the bine/tor Go interface.
type embeddedCreator struct{}

closedWhenNotStarted: make(chan any, 1), // buffer
simulateBadControlSocket: false,
simulateFileConnFailure: false,
simulateNonzeroExitCode: false,
// New implements process.Creator, creating a new embedded tor process.
func (embeddedCreator) New(ctx context.Context, args ...string) (process.Process, error) {
if ctx == nil {
ctx = context.Background()
}
go proc.runtor(ctx, right, args...)
return proc, nil
}

// torProcess implements [process.Process].
type torProcess struct {
// ordinary state variables
awaitStart chan any
controlConn net.Conn
startErr chan error
startOnce sync.Once
waitErr chan error
waitOnce sync.Once

// for testing
closedWhenNotStarted chan any
simulateBadControlSocket bool
simulateFileConnFailure bool
simulateNonzeroExitCode bool
}

var _ process.Process = &torProcess{}

// EmbeddedControlConn implements [process.Process].
func (p *torProcess) EmbeddedControlConn() (net.Conn, error) {
// Implementation note: this function SHOULD only be called
// once and BEFORE Start is called 😬😬😬
return p.controlConn, nil
return &embeddedProcess{
ctx: ctx,
conf: C.tor_main_configuration_new(),
args: args,
}, nil
}

// Start implements [process.Process].
func (p *torProcess) Start() (err error) {
p.startOnce.Do(func() {
p.awaitStart <- true
err = <-p.startErr
})
return err
// embeddedProcess implements process.Process, permitting libtor to act as an API
// backend for the bine/tor Go interface.
type embeddedProcess struct {
ctx context.Context
conf *C.struct_tor_main_configuration_t
args []string
done chan int
}

// Wait implements [process.Process].
func (p *torProcess) Wait() (err error) {
p.waitOnce.Do(func() {
err = <-p.waitErr
})
return
}

// ErrTooManyArguments indicates that p.args contains too many arguments
var ErrTooManyArguments = errors.New("libtor: too many arguments")

// ErrCannotCreateControlSocket indicates that we cannot create a control socket.
var ErrCannotCreateControlSocket = errors.New("libtor: cannot create a control socket")

// ErrNonzeroExitCode indicates that tor returned a nonzero exit code
var ErrNonzeroExitCode = errors.New("libtor: command completed with nonzero exit code")

// runtor runs tor until completion and ensures that tor exits when
// the given ctx is cancelled or its deadline expires.
func (p *torProcess) runtor(ctx context.Context, cc net.Conn, args ...string) {
// wait for Start or context to expire
select {
case <-p.awaitStart:
case <-ctx.Done():
p.startErr <- ctx.Err() // nonblocking chan
close(p.closedWhenNotStarted)
return
// Start implements process.Process, starting up the libtor embedded process.
func (e *embeddedProcess) Start() error {
if e.done != nil {
return errors.New("already started")
}
// Create the char array for the args
args := append([]string{"tor"}, e.args...)

// Note: when writing this code I was wondering whether I needed to
// use unsafe.Pointer to track pointers that matter to C code. Reading
// this message[1] has been useful to understand that the most likely
// answer to this question is "obviously, no".
//
// See https://groups.google.com/g/golang-nuts/c/yNis7bQG_rY/m/yaJFoSx1hgIJ

// Create argc and argv for tor
argv := append([]string{"tor"}, args...)
const toomany = 256 // arbitrary low limit to make C.int and C.size_t casts always work
if len(argv) > toomany {
p.startErr <- ErrTooManyArguments // nonblocking channel
return
}
argc := C.size_t(len(argv))
// Note: here we allocate argc + 1 because a "null pointer always follows
// the last element: argv[argc] is this null pointer."
//
// See https://www.gnu.org/software/libc/manual/html_node/Program-Arguments.html
allocSiz := argc + 1
cargv := C.cstringArrayNew(allocSiz)
defer C.cstringArrayFree(cargv, argc)
for idx, entry := range argv {
C.cstringArraySet(cargv, C.size_t(idx), C.CString(entry))
charArray := C.makeCharArray(C.int(len(args)))
for i, a := range args {
C.setArrayString(charArray, C.CString(a), C.int(i))
}

// Add to config a WEAK REFERENCE to argc and argv
config := C.tor_main_configuration_new()
runtimex.PanicIfNil(config, "C.tor_main_configuration_new failed")
defer C.tor_main_configuration_free(config)
code := C.tor_main_configuration_set_command_line(config, C.int(argc), cargv)
runtimex.Assert(code == 0, "C.tor_main_configuration_set_command_line failed")

// Create OWNING file descriptor
filedesc := C.tor_main_configuration_setup_control_socket(config)
if p.simulateBadControlSocket {
filedesc = C.INVALID_TOR_CONTROL_SOCKET
// Build the tor configuration
if code := C.tor_main_configuration_set_command_line(e.conf, C.int(len(args)), charArray); code != 0 {
C.tor_main_configuration_free(e.conf)
C.freeCharArray(charArray, C.int(len(args)))
return fmt.Errorf("failed to set arguments: %v", int(code))
}
if !C.filedescIsGood(filedesc) {
p.startErr <- ErrCannotCreateControlSocket // nonblocking channel
return
}

// Convert the OWNING file descriptor into a proper file. Because
// filedesc is good, os.NewFile shouldn't fail.
filep := os.NewFile(uintptr(filedesc), "")
runtimex.Assert(filep != nil, "os.NewFile should not fail")

// Create a new net.Conn using a copy of the underlying
// file descriptor using net.FileConn (see below).
conn, err := net.FileConn(filep)
if p.simulateFileConnFailure {
err = ErrCannotCreateControlSocket
}
if err != nil {
filep.Close()
p.startErr <- err // nonblocking channel
return
}

// From the documentation of [net.FileConn]:
//
// It is the caller's responsibility to close f when
// finished. Closing c does not affect f, and closing
// f does not affect c.
//
// So, it's safe to close the filep now.
filep.Close()

// In the following we're going to possibly call Close multiple
// times. Let's be very sure that this close is idempotent.
conn = withIdempotentClose(conn)
cc = withIdempotentClose(cc)

// Make sure we close filep when the context is done. Because the
// socket is OWNING, this will also cause tor to return.
// Start tor and return
e.done = make(chan int, 1)
go func() {
defer conn.Close()
defer cc.Close()
<-ctx.Done()
defer C.freeCharArray(charArray, C.int(len(args)))
defer C.tor_main_configuration_free(e.conf)
e.done <- int(C.tor_run_main(e.conf))
}()
return nil
}

// Route messages to and from the control connection.
go sendrecvThenClose(cc, conn)
go sendrecvThenClose(conn, cc)

// Let the user know that startup was successful.
p.startErr <- nil // nonblocking channel

// Run tor until completion.
if !p.simulateNonzeroExitCode {
code = C.tor_run_main(config)
} else {
code = 1
// Wait implements process.Process, blocking until the embedded process terminates.
func (e *embeddedProcess) Wait() error {
if e.done == nil {
return errors.New("not started")
}
if code != 0 {
p.waitErr <- fmt.Errorf("%w: %d", ErrNonzeroExitCode, code) // nonblocking channel
return
select {
case <-e.ctx.Done():
return e.ctx.Err()

case code := <-e.done:
if code == 0 {
return nil
}
return fmt.Errorf("embedded tor failed: %v", code)
}
p.waitErr <- nil // nonblocking channel
}

// sendrecvThenClose routes traffic between two connections and then
// closes both of them when done with routing traffic.
func sendrecvThenClose(left, right net.Conn) {
defer left.Close()
defer right.Close()
netxlite.CopyContext(context.Background(), left, right)
}

// withIdempotentClose ensures that a connection has idempotent close.
func withIdempotentClose(c net.Conn) net.Conn {
return &idempotentClose{
Conn: c,
once: sync.Once{},
// EmbeddedControlConn implements process.Process, connecting to the control port
// of the embedded Tor isntance.
func (e *embeddedProcess) EmbeddedControlConn() (net.Conn, error) {
file := os.NewFile(uintptr(C.tor_main_configuration_setup_control_socket(e.conf)), "")
conn, err := net.FileConn(file)
if err != nil {
return nil, fmt.Errorf("unable to create control socket: %v", err)
}
}

// idempotentClose ensures close is idempotent for a net.Conn
type idempotentClose struct {
net.Conn
once sync.Once
}

func (c *idempotentClose) Close() (err error) {
c.once.Do(func() {
err = c.Conn.Close()
})
return
return conn, nil
}

0 comments on commit 49e22c8

Please sign in to comment.