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 | ||
import ( | ||
"bufio" | ||
"context" | ||
"fmt" | ||
"io" | ||
... | ... | @@ -11,29 +10,84 @@ import ( |
"os/exec" | ||
"regexp" | ||
"time" | ||
"github.com/yawning/bulb" | ||
) | ||
const ( | ||
// Sixty seconds is a reasonable timeout according to: | ||
// <https://bugs.torproject.org/32126#comment:1> | ||
TorBootstrapTimeout = 60 * time.Second | ||
CacheValidity = 24 * time.Hour | ||
) | ||
// torHasBootstrapped returns true if the given log line indicates that tor has | ||
// successfully bootstrapped and false otherwise. | ||
func torHasBootstrapped(line string) bool { | ||
// CacheEntry represents an entry in our cache of bridges that we recently | ||
// tested. Error is nil if a bridge works, and otherwise holds an error | ||
// 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%`) | ||
return re.Match([]byte(line)) | ||
if result == "" { | ||
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 | ||
// encountered an error while bootstrapping and false otherwise. | ||
func torEncounteredError(line string) bool { | ||
// IsCached returns a cache entry if the given bridge line has been tested | ||
// recently (as determined by CacheValidity), and nil otherwise. | ||
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. | ||
re := regexp.MustCompile(`(Problem bootstrapping|Bridge line|unable to connect)`) | ||
return re.Match([]byte(line)) | ||
addrPort, err := BridgeLineToAddrPort(bridgeLine) | ||
if err != nil { | ||
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 | ||
... | ... | @@ -41,23 +95,60 @@ func torEncounteredError(line string) bool { |
func writeConfigToTorrc(tmpFh io.Writer, dataDir, bridgeLine string) error { | ||
_, err := fmt.Fprintf(tmpFh, "UseBridges 1\n"+ | ||
"ControlPort unix:%s\n"+ | ||
"SocksPort auto\n"+ | ||
"SafeLogging 0\n"+ | ||
"__DisablePredictedCircuits\n"+ | ||
"DataDirectory %s\n"+ | ||
"ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy\n"+ | ||
"PathsNeededToBuildCircuits 0.25\n"+ | ||
"Bridge %s", dataDir, bridgeLine) | ||
"Bridge %s", getDomainSocketPath(dataDir), dataDir, bridgeLine) | ||
return err | ||
} | ||
// bootstrapTorOverBridge attempts to bootstrap a Tor connection over the given | ||
// bridge line. This function returns nil if the bootstrap succeeds and an | ||
// error otherwise. | ||
// makeControlConnection attempts to establish a control connection with Tor's | ||
// given domain socket. If successful, it returns the connection. 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 { | ||
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. | ||
tmpFh, err := ioutil.TempFile(os.TempDir(), "torrc-") | ||
... | ... | @@ -81,59 +172,52 @@ func bootstrapTorOverBridge(bridgeLine string) error { |
ctx, cancel := context.WithTimeout(context.Background(), TorBootstrapTimeout) | ||
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) | ||
// Start tor but don't wait for the process to complete, so our call | ||
// returns right away. | ||
cmd := exec.CommandContext(ctx, "tor", "-f", tmpFh.Name()) | ||
if err = cmd.Start(); err != nil { | ||
return err | ||
} | ||
// Read tor's log messages from stdout and try to figure out when/if tor | ||
// bootstrapped successfully. | ||
c := make(chan error) | ||
go func() { | ||
stdoutReader := bufio.NewReader(stdout) | ||
for { | ||
// If we hit our timeout, the tor process is terminated and we'll | ||
// end up with an error here. | ||
line, _, err := stdoutReader.ReadLine() | ||
if err != nil { | ||
log.Printf("Failed to read line from tor's stdout: %s", err) | ||
c <- 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 | ||
} | ||
torCtrl, err := makeControlConnection(getDomainSocketPath(tmpDir)) | ||
if err != nil { | ||
return err | ||
} | ||
defer torCtrl.Close() | ||
// Start our async reader and listen for STATUS_CLIENT events, which | ||
// include bootstrap messages: | ||
// <https://gitweb.torproject.org/torspec.git/tree/control-spec.txt?id=b7cfa8619947be4a377366365f5ddee8e0733330#n2499> | ||
torCtrl.StartAsyncReader() | ||
if _, err := torCtrl.Request("SETEVENTS STATUS_CLIENT"); err != nil { | ||
return fmt.Errorf("command SETEVENTS STATUS_CLIENT failed: %v", err) | ||
} | ||
if torHasBootstrapped(string(line)) { | ||
log.Printf("Bootstrapping worked!") | ||
if err := cmd.Process.Kill(); err != nil { | ||
log.Printf("Failed to kill process: %s", err) | ||
// Keep reading events until one of the following happens: | ||
// 1) tor bootstrapped to 100% | ||
// 2) tor encountered a warning while bootstrapping | ||
// 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 | ||
close(c) | ||
return | ||
return fmt.Errorf(matches[1]) | ||
} | ||
} | ||
}() | ||
// 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)