Use control port to determine if bridge works.
Parsing Tor's log file is ugly and error-prone. It's cleaner to use Tor's control port to figure out if a bridge bootstrapped correctly or not. The library bulb helps us accomplish this.
package main | package main | ||
import ( | import ( | ||
"bufio" | |||
"context" | "context" | ||
"fmt" | "fmt" | ||
"io" | "io" | ||
... | @@ -11,29 +10,84 @@ import ( | ... | @@ -11,29 +10,84 @@ import ( |
"os/exec" | "os/exec" | ||
"regexp" | "regexp" | ||
"time" | "time" | ||
"github.com/yawning/bulb" | |||
) | ) | ||
const ( | const ( | ||
// Sixty seconds is a reasonable timeout according to: | // Sixty seconds is a reasonable timeout according to: | ||
// <https://bugs.torproject.org/32126#comment:1> | // <https://bugs.torproject.org/32126#comment:1> | ||
TorBootstrapTimeout = 60 * time.Second | TorBootstrapTimeout = 60 * time.Second | ||
CacheValidity = 24 * time.Hour | |||
) | ) | ||
// torHasBootstrapped returns true if the given log line indicates that tor has | // CacheEntry represents an entry in our cache of bridges that we recently | ||
// successfully bootstrapped and false otherwise. | // tested. Error is nil if a bridge works, and otherwise holds an error | ||
func torHasBootstrapped(line string) bool { | // string. Time determines when we tested the bridge. | ||
type CacheEntry struct { | |||
Error error | |||
Time time.Time | |||
} | |||
type TestCache map[string]*CacheEntry | |||
var cache TestCache = make(TestCache) | |||
// Regular expressions that match tor's bootstrap status events. | |||
var success = regexp.MustCompile(`PROGRESS=100`) | |||
var warning = regexp.MustCompile(`STATUS_CLIENT WARN BOOTSTRAP`) | |||
// BridgeLineToAddrPort takes a bridge line as input and returns a string | |||
// consisting of the bridge's addr:port. | |||
func BridgeLineToAddrPort(bridgeLine string) (string, error) { | |||
// Represents an addr:port tuple. | |||
re := regexp.MustCompile(`(?:[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]{1,5}`) | |||
result := string(re.Find([]byte(bridgeLine))) | |||
re := regexp.MustCompile(`Bootstrapped 100%`) | if result == "" { | ||
return re.Match([]byte(line)) | return result, fmt.Errorf("could not extract addr:port from bridge line") | ||
} else { | |||
return result, nil | |||
} | |||
} | } | ||
// torEncounteredError returns true if the given log line indicates that tor | // IsCached returns a cache entry if the given bridge line has been tested | ||
// encountered an error while bootstrapping and false otherwise. | // recently (as determined by CacheValidity), and nil otherwise. | ||
func torEncounteredError(line string) bool { | func (tc *TestCache) IsCached(bridgeLine string) *CacheEntry { | ||
// First, prune expired cache entries. | |||
now := time.Now() | |||
for index, entry := range *tc { | |||
if entry.Time.Before(now.Add(-CacheValidity)) { | |||
delete(*tc, index) | |||
} | |||
} | |||
|
|||
// FIXME: Find a better way to handle this. | addrPort, err := BridgeLineToAddrPort(bridgeLine) | ||
re := regexp.MustCompile(`(Problem bootstrapping|Bridge line|unable to connect)`) | if err != nil { | ||
return re.Match([]byte(line)) | return nil | ||
} | |||
return (*tc)[addrPort] | |||
} | |||
// AddEntry adds an entry for the given bridge and test result to our cache. | |||
func (tc *TestCache) AddEntry(bridgeLine string, result error) { | |||
addrPort, err := BridgeLineToAddrPort(bridgeLine) | |||
if err != nil { | |||
return | |||
} | |||
log.Printf("Caching %q: %q", addrPort, result) | |||
(*tc)[addrPort] = &CacheEntry{result, time.Now()} | |||
} | |||
// getDomainSocketPath takes as input the path to our data directory and | |||
// returns the path to the domain socket for tor's control port. | |||
func getDomainSocketPath(dataDir string) string { | |||
return fmt.Sprintf("%s/control-socket", dataDir) | |||
} | } | ||
// writeConfigToTorrc writes the content of a Tor config file (including the | // writeConfigToTorrc writes the content of a Tor config file (including the | ||
... | @@ -41,23 +95,60 @@ func torEncounteredError(line string) bool { | ... | @@ -41,23 +95,60 @@ func torEncounteredError(line string) bool { |
func writeConfigToTorrc(tmpFh io.Writer, dataDir, bridgeLine string) error { | func writeConfigToTorrc(tmpFh io.Writer, dataDir, bridgeLine string) error { | ||
_, err := fmt.Fprintf(tmpFh, "UseBridges 1\n"+ | _, err := fmt.Fprintf(tmpFh, "UseBridges 1\n"+ | ||
"ControlPort unix:%s\n"+ | |||
"SocksPort auto\n"+ | "SocksPort auto\n"+ | ||
"SafeLogging 0\n"+ | "SafeLogging 0\n"+ | ||
"__DisablePredictedCircuits\n"+ | "__DisablePredictedCircuits\n"+ | ||
"DataDirectory %s\n"+ | "DataDirectory %s\n"+ | ||
"ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy\n"+ | "ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy\n"+ | ||
"PathsNeededToBuildCircuits 0.25\n"+ | "PathsNeededToBuildCircuits 0.25\n"+ | ||
"Bridge %s", dataDir, bridgeLine) | "Bridge %s", getDomainSocketPath(dataDir), dataDir, bridgeLine) | ||
return err | return err | ||
} | } | ||
// bootstrapTorOverBridge attempts to bootstrap a Tor connection over the given | // makeControlConnection attempts to establish a control connection with Tor's | ||
// bridge line. This function returns nil if the bootstrap succeeds and an | // given domain socket. If successful, it returns the connection. Otherwise, | ||
// error otherwise. | // it returns an error. | ||
func makeControlConnection(domainSocket string) (*bulb.Conn, error) { | |||
var torCtrl *bulb.Conn | |||
var err error | |||
// Try connecting to tor's control socket. It may take a second or two for | |||
// it to be ready. | |||
for attempts := 0; attempts < 10; attempts++ { | |||
torCtrl, err = bulb.Dial("unix", domainSocket) | |||
if err == nil { | |||
if err := torCtrl.Authenticate(""); err != nil { | |||
return nil, fmt.Errorf("authentication with tor's control port failed: %v", err) | |||
} | |||
return torCtrl, nil | |||
} else { | |||
time.Sleep(1 * time.Second) | |||
} | |||
} | |||
return nil, fmt.Errorf("unable to connect to domain socket") | |||
} | |||
// bootstrapTorOverBridge implements a cache around | |||
// bootstrapTorOverBridgeWrapped. | |||
func bootstrapTorOverBridge(bridgeLine string) error { | func bootstrapTorOverBridge(bridgeLine string) error { | ||
if cacheEntry := cache.IsCached(bridgeLine); cacheEntry != nil { | |||
return cacheEntry.Error | |||
} | |||
log.Printf("Creating temporary torrc file.") | err := bootstrapTorOverBridgeWrapped(bridgeLine) | ||
cache.AddEntry(bridgeLine, err) | |||
return err | |||
} | |||
// bootstrapTorOverBridgeWrapped attempts to bootstrap a Tor connection over | |||
// the given bridge line. This function returns nil if the bootstrap succeeds | |||
// and an error otherwise. | |||
func bootstrapTorOverBridgeWrapped(bridgeLine string) error { | |||
// Create our torrc. | // Create our torrc. | ||
tmpFh, err := ioutil.TempFile(os.TempDir(), "torrc-") | tmpFh, err := ioutil.TempFile(os.TempDir(), "torrc-") | ||
... | @@ -81,59 +172,52 @@ func bootstrapTorOverBridge(bridgeLine string) error { | ... | @@ -81,59 +172,52 @@ func bootstrapTorOverBridge(bridgeLine string) error { |
ctx, cancel := context.WithTimeout(context.Background(), TorBootstrapTimeout) | ctx, cancel := context.WithTimeout(context.Background(), TorBootstrapTimeout) | ||
defer cancel() | defer cancel() | ||
cmd := exec.CommandContext(ctx, "tor", "-f", tmpFh.Name()) | |||
stdout, err := cmd.StdoutPipe() | |||
if err != nil { | |||
return err | |||
} | |||
log.Printf("Using bridge line %q.", bridgeLine) | log.Printf("Using bridge line %q.", bridgeLine) | ||
// Start tor but don't wait for the process to complete, so our call | // Start tor but don't wait for the process to complete, so our call | ||
// returns right away. | // returns right away. | ||
cmd := exec.CommandContext(ctx, "tor", "-f", tmpFh.Name()) | |||
if err = cmd.Start(); err != nil { | if err = cmd.Start(); err != nil { | ||
return err | return err | ||
} | } | ||
// Read tor's log messages from stdout and try to figure out when/if tor | torCtrl, err := makeControlConnection(getDomainSocketPath(tmpDir)) | ||
// bootstrapped successfully. | if err != nil { | ||
c := make(chan error) | return err | ||
go func() { | } | ||
stdoutReader := bufio.NewReader(stdout) | defer torCtrl.Close() | ||
for { | |||
// If we hit our timeout, the tor process is terminated and we'll | // Start our async reader and listen for STATUS_CLIENT events, which | ||
// end up with an error here. | // include bootstrap messages: | ||
line, _, err := stdoutReader.ReadLine() | // <https://gitweb.torproject.org/torspec.git/tree/control-spec.txt?id=b7cfa8619947be4a377366365f5ddee8e0733330#n2499> | ||
if err != nil { | torCtrl.StartAsyncReader() | ||
log.Printf("Failed to read line from tor's stdout: %s", err) | if _, err := torCtrl.Request("SETEVENTS STATUS_CLIENT"); err != nil { | ||
c <- err | return fmt.Errorf("command SETEVENTS STATUS_CLIENT failed: %v", err) | ||
close(c) | } | ||
return | |||
} | |||
log.Printf("tor says: %q", line) | |||
if torEncounteredError(string(line)) { | |||
if err := cmd.Process.Kill(); err != nil { | |||
log.Printf("Failed to kill process: %s", err) | |||
} | |||
// FIXME: Is %v correct here? | |||
c <- fmt.Errorf("%v", string(line)) | |||
close(c) | |||
return | |||
} | |||
if torHasBootstrapped(string(line)) { | // Keep reading events until one of the following happens: | ||
log.Printf("Bootstrapping worked!") | // 1) tor bootstrapped to 100% | ||
if err := cmd.Process.Kill(); err != nil { | // 2) tor encountered a warning while bootstrapping | ||
log.Printf("Failed to kill process: %s", err) | // 3) we hit our timeout, which interrupts our call to NextEvent() | ||
for { | |||
ev, err := torCtrl.NextEvent() | |||
if err != nil { | |||
return err | |||
} | |||
log.Printf("Controller: %s", ev.RawLines) | |||
for _, line := range ev.RawLines { | |||
if success.MatchString(line) { | |||
return nil | |||
} else if warning.MatchString(line) { | |||
re := regexp.MustCompile(`WARNING="([^"]*)"`) | |||
matches := re.FindStringSubmatch(line) | |||
if len(matches) != 2 { | |||
log.Printf("Unexpected number of substring matches: %q", matches) | |||
return fmt.Errorf("could not bootstrap") | |||
} | } | ||
c <- nil | return fmt.Errorf(matches[1]) | ||
close(c) | |||
return | |||
} | } | ||
} | } | ||
}() | } | ||
// FIXME: Use context to figure out if tor died on us. | |||
return <-c | return fmt.Errorf("could not bootstrap") | ||
} | } |
-
mentioned in issue trac#31874 (closed)
-
mentioned in issue #1 (closed)