GitLab is used only for code review, issue tracking and project management. Canonical locations for source code are still https://gitweb.torproject.org/ https://git.torproject.org/ and git-rw.torproject.org.

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.
parent f0818dd7
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)))
if result == "" {
return result, fmt.Errorf("could not extract addr:port from bridge line")
} else {
return result, nil
}
}
// 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)
}
}
addrPort, err := BridgeLineToAddrPort(bridgeLine)
if err != nil {
return nil
}
re := regexp.MustCompile(`Bootstrapped 100%`) return (*tc)[addrPort]
return re.Match([]byte(line))
} }
// torEncounteredError returns true if the given log line indicates that tor // AddEntry adds an entry for the given bridge and test result to our cache.
// encountered an error while bootstrapping and false otherwise. func (tc *TestCache) AddEntry(bridgeLine string, result error) {
func torEncounteredError(line string) bool {
// 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
}
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.
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 { if err != nil {
log.Printf("Failed to read line from tor's stdout: %s", err) return err
c <- err }
close(c) defer torCtrl.Close()
return
// 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)
} }
log.Printf("tor says: %q", line)
if torEncounteredError(string(line)) { // Keep reading events until one of the following happens:
if err := cmd.Process.Kill(); err != nil { // 1) tor bootstrapped to 100%
log.Printf("Failed to kill process: %s", err) // 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
} }
// FIXME: Is %v correct here? log.Printf("Controller: %s", ev.RawLines)
c <- fmt.Errorf("%v", string(line)) for _, line := range ev.RawLines {
close(c) if success.MatchString(line) {
return 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")
} }
return fmt.Errorf(matches[1])
if torHasBootstrapped(string(line)) {
log.Printf("Bootstrapping worked!")
if err := cmd.Process.Kill(); err != nil {
log.Printf("Failed to kill process: %s", err)
} }
c <- nil
close(c)
return
} }
} }
}()
// FIXME: Use context to figure out if tor died on us.
return <-c return fmt.Errorf("could not bootstrap")
} }
...@@ -2,46 +2,18 @@ package main ...@@ -2,46 +2,18 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"testing" "testing"
"time"
) )
func TestTorHasBootstrapped(t *testing.T) {
r := torHasBootstrapped(`Oct 29 10:08:31.000 [notice] Bootstrapped 100%: Done`)
if !r {
t.Errorf("torHasBootstrapped failed to realise that Tor has bootstrapped.")
}
r = torHasBootstrapped(`Oct 29 10:08:30.000 [notice] Bootstrapped 90%: Establishing a Tor circuit`)
if r {
t.Errorf("torHasBootstrapped failed to realise that Tor has not bootstrapped.")
}
}
func TestTorEncounteredError(t *testing.T) {
r := torEncounteredError(`Oct 29 10:15:33.000 [warn] Proxy Client: unable to connect to 3.135.154.16:41609 ("general SOCKS server failure")`)
if !r {
t.Errorf("torEncounteredError failed to recognise bootstrapping error.")
}
r = torEncounteredError(`Oct 29 10:08:31.000 [notice] Bootstrapped 100%: Done`)
if r {
t.Errorf("torEncounteredError incorrectly detected a bootstrapping error.")
}
r = torEncounteredError(`Oct 29 10:17:49.000 [warn] Problem bootstrapping. Stuck at 5%: Connecting to directory server. (Can't connect to bridge; PT_MISSING; count 4; recommendation warn; host 0000000000000000000000000000000000000000 at 1.2.3.4:1234)`)
if !r {
t.Errorf("torEncounteredError failed to recognise bootstrapping error.")
}
}
func TestWriteConfigToTorrc(t *testing.T) { func TestWriteConfigToTorrc(t *testing.T) {
bridgeLine := "1.2.3.4:1234" bridgeLine := "1.2.3.4:1234"
dataDir := "/foo" dataDir := "/foo"
fileBuf := new(bytes.Buffer) fileBuf := new(bytes.Buffer)
torrc := `UseBridges 1 torrc := `UseBridges 1
ControlPort unix:/foo/control-socket
SocksPort auto SocksPort auto
SafeLogging 0 SafeLogging 0
__DisablePredictedCircuits __DisablePredictedCircuits
...@@ -82,3 +54,88 @@ func TestBootstrapTorOverBridge(t *testing.T) { ...@@ -82,3 +54,88 @@ func TestBootstrapTorOverBridge(t *testing.T) {
t.Errorf("Failed to label default bridge as broken.") t.Errorf("Failed to label default bridge as broken.")
} }
} }
func TestCacheFunctions(t *testing.T) {
cache := make(TestCache)
bridgeLine := "obfs4 127.0.0.1:1 cert=foo iat-mode=0"
e := cache.IsCached(bridgeLine)
if e != nil {
t.Errorf("Cache is empty but marks bridge line as existing.")
}
cache.AddEntry(bridgeLine, nil)
e = cache.IsCached(bridgeLine)
if e == nil {
t.Errorf("Could not retrieve existing element from cache.")
}
testError := fmt.Errorf("bridge is on fire")
cache.AddEntry(bridgeLine, testError)
e = cache.IsCached(bridgeLine)
if e.Error != testError {
t.Errorf("Got test result %q but expected %q.", e.Error, testError)
}
}
func TestCacheExpiration(t *testing.T) {
cache := make(TestCache)
const shortForm = "2006-Jan-02"
expiry, _ := time.Parse(shortForm, "2000-Jan-01")
bridgeLine1 := "1.1.1.1:1111"
addrPort, _ := BridgeLineToAddrPort(bridgeLine1)
cache[addrPort] = &CacheEntry{nil, expiry}
bridgeLine2 := "2.2.2.2:2222"
addrPort, _ = BridgeLineToAddrPort(bridgeLine2)
cache[addrPort] = &CacheEntry{nil, time.Now()}
e := cache.IsCached(bridgeLine1)
if e != nil {
t.Errorf("Expired cache entry was not successfully pruned.")
}
e = cache.IsCached(bridgeLine2)
if e == nil {
t.Errorf("Valid cache entry was incorrectly pruned.")
}
}
func TestBridgeLineToAddrPort(t *testing.T) {
_, err := BridgeLineToAddrPort("foo")
if err == nil {
t.Errorf("Failed to return error for invalid bridge line.")
}
_, err = BridgeLineToAddrPort("obfs4 1.1.1.1 FINGERPRINT")
if err == nil {
t.Errorf("Failed to return error for invalid bridge line.")
}
addrPort, err := BridgeLineToAddrPort("1.1.1.1:1")
if err != nil {
t.Errorf("Failed to accept valid bridge line.")
}
if addrPort != "1.1.1.1:1" {
t.Errorf("Returned invalid addr:port tuple.")
}
_, err = BridgeLineToAddrPort("255.255.255.255:12345")
if err != nil {
t.Errorf("Failed to accept valid bridge line.")
}
_, err = BridgeLineToAddrPort("255.255.255.255:12345 FINGERPRINT")
if err != nil {
t.Errorf("Failed to accept valid bridge line.")
}
_, err = BridgeLineToAddrPort("obfs4 255.255.255.255:12345 FINGERPRINT")
if err != nil {
t.Errorf("Failed to accept valid bridge line.")
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment