chore: merge probe-engine into probe-cli (#201)
This is how I did it: 1. `git clone https://github.com/ooni/probe-engine internal/engine` 2. ``` (cd internal/engine && git describe --tags) v0.23.0 ``` 3. `nvim go.mod` (merging `go.mod` with `internal/engine/go.mod` 4. `rm -rf internal/.git internal/engine/go.{mod,sum}` 5. `git add internal/engine` 6. `find . -type f -name \*.go -exec sed -i 's@/ooni/probe-engine@/ooni/probe-cli/v3/internal/engine@g' {} \;` 7. `go build ./...` (passes) 8. `go test -race ./...` (temporary failure on RiseupVPN) 9. `go mod tidy` 10. this commit message Once this piece of work is done, we can build a new version of `ooniprobe` that is using `internal/engine` directly. We need to do more work to ensure all the other functionality in `probe-engine` (e.g. making mobile packages) are still WAI. Part of https://github.com/ooni/probe/issues/1335
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
// Dialer creates net.Conn instances where (1) we delay writes if
|
||||
// a delay is configured and (2) we split outgoing buffers if there
|
||||
// is a configured splitter function.
|
||||
type Dialer struct {
|
||||
netx.Dialer
|
||||
Delay time.Duration
|
||||
Splitter func([]byte) [][]byte
|
||||
}
|
||||
|
||||
// DialContext implements netx.Dialer.DialContext.
|
||||
func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
conn, err := d.Dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn = SleeperWriter{Conn: conn, Delay: d.Delay}
|
||||
conn = SplitterWriter{Conn: conn, Splitter: d.Splitter}
|
||||
return conn, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool/internal"
|
||||
)
|
||||
|
||||
func TestDialerFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
dialer := internal.Dialer{Dialer: internal.FakeDialer{
|
||||
Err: expected,
|
||||
}}
|
||||
conn, err := dialer.DialContext(context.Background(), "tcp", "8.8.8.8:853")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if conn != nil {
|
||||
t.Fatal("expected nil conn here")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialerSuccess(t *testing.T) {
|
||||
splitter := func([]byte) [][]byte {
|
||||
return nil // any value is fine we just a need a splitter != nil here
|
||||
}
|
||||
innerconn := &internal.FakeConn{}
|
||||
dialer := internal.Dialer{
|
||||
Delay: 12345,
|
||||
Dialer: internal.FakeDialer{Conn: innerconn},
|
||||
Splitter: splitter,
|
||||
}
|
||||
conn, err := dialer.DialContext(context.Background(), "tcp", "8.8.8.8:853")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sconn, ok := conn.(internal.SplitterWriter)
|
||||
if !ok {
|
||||
t.Fatal("the outer connection is not a splitter")
|
||||
}
|
||||
if sconn.Splitter == nil {
|
||||
t.Fatal("not the splitter we expected")
|
||||
}
|
||||
dconn, ok := sconn.Conn.(internal.SleeperWriter)
|
||||
if !ok {
|
||||
t.Fatal("the inner connection is not a sleeper")
|
||||
}
|
||||
if dconn.Delay != 12345 {
|
||||
t.Fatal("invalid delay")
|
||||
}
|
||||
if dconn.Conn != innerconn {
|
||||
t.Fatal("invalid inner connection")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FakeDialer struct {
|
||||
Conn net.Conn
|
||||
Err error
|
||||
}
|
||||
|
||||
func (d FakeDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
return d.Conn, d.Err
|
||||
}
|
||||
|
||||
type FakeConn struct {
|
||||
ReadError error
|
||||
ReadData []byte
|
||||
SetDeadlineError error
|
||||
SetReadDeadlineError error
|
||||
SetWriteDeadlineError error
|
||||
WriteData [][]byte
|
||||
WriteError error
|
||||
}
|
||||
|
||||
func (c *FakeConn) Read(b []byte) (int, error) {
|
||||
if len(c.ReadData) > 0 {
|
||||
n := copy(b, c.ReadData)
|
||||
c.ReadData = c.ReadData[n:]
|
||||
return n, nil
|
||||
}
|
||||
if c.ReadError != nil {
|
||||
return 0, c.ReadError
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (c *FakeConn) Write(b []byte) (n int, err error) {
|
||||
if c.WriteError != nil {
|
||||
return 0, c.WriteError
|
||||
}
|
||||
c.WriteData = append(c.WriteData, b)
|
||||
n = len(b)
|
||||
return
|
||||
}
|
||||
|
||||
func (*FakeConn) Close() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (*FakeConn) LocalAddr() net.Addr {
|
||||
return &net.TCPAddr{}
|
||||
}
|
||||
|
||||
func (*FakeConn) RemoteAddr() net.Addr {
|
||||
return &net.TCPAddr{}
|
||||
}
|
||||
|
||||
func (c *FakeConn) SetDeadline(t time.Time) (err error) {
|
||||
return c.SetDeadlineError
|
||||
}
|
||||
|
||||
func (c *FakeConn) SetReadDeadline(t time.Time) (err error) {
|
||||
return c.SetReadDeadlineError
|
||||
}
|
||||
|
||||
func (c *FakeConn) SetWriteDeadline(t time.Time) (err error) {
|
||||
return c.SetWriteDeadlineError
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Package internal contains the implementation of tlstool.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
// DialerConfig contains the config for creating a dialer
|
||||
type DialerConfig struct {
|
||||
Dialer netx.Dialer
|
||||
Delay time.Duration
|
||||
SNI string
|
||||
}
|
||||
|
||||
// NewSNISplitterDialer creates a new dialer that splits
|
||||
// outgoing messages such that the SNI should end up being
|
||||
// splitted into different TCP segments.
|
||||
func NewSNISplitterDialer(config DialerConfig) Dialer {
|
||||
return Dialer{
|
||||
Dialer: config.Dialer,
|
||||
Delay: config.Delay,
|
||||
Splitter: func(b []byte) [][]byte {
|
||||
return SNISplitter(b, []byte(config.SNI))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewThriceSplitterDialer creates a new dialer that splits
|
||||
// outgoing messages in three parts according to the circumvention
|
||||
// technique described by Kevin Boch in the Internet Measurement
|
||||
// Village 2020 <https://youtu.be/ksojSRFLbBM?t=1140>.
|
||||
func NewThriceSplitterDialer(config DialerConfig) Dialer {
|
||||
return Dialer{
|
||||
Dialer: config.Dialer,
|
||||
Delay: config.Delay,
|
||||
Splitter: Splitter84rest,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRandomSplitterDialer creates a new dialer that splits
|
||||
// the SNI like the fixed splitting schema used by outline. See
|
||||
// github.com/Jigsaw-Code/outline-go-tun2socks.
|
||||
func NewRandomSplitterDialer(config DialerConfig) Dialer {
|
||||
return Dialer{
|
||||
Dialer: config.Dialer,
|
||||
Delay: config.Delay,
|
||||
Splitter: Splitter3264rand,
|
||||
}
|
||||
}
|
||||
|
||||
// NewVanillaDialer creates a new vanilla dialer that does
|
||||
// nothing and is used to establish a baseline.
|
||||
func NewVanillaDialer(config DialerConfig) Dialer {
|
||||
return Dialer{Dialer: config.Dialer}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool/internal"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
)
|
||||
|
||||
var config = internal.DialerConfig{
|
||||
Dialer: netx.NewDialer(netx.Config{}),
|
||||
Delay: 10,
|
||||
SNI: "dns.google",
|
||||
}
|
||||
|
||||
func dial(t *testing.T, d netx.Dialer) {
|
||||
td := netx.NewTLSDialer(netx.Config{Dialer: d})
|
||||
conn, err := td.DialTLSContext(context.Background(), "tcp", "dns.google:853")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestNewSNISplitterDialer(t *testing.T) {
|
||||
dial(t, internal.NewSNISplitterDialer(config))
|
||||
}
|
||||
|
||||
func TestNewThriceSplitterDialer(t *testing.T) {
|
||||
dial(t, internal.NewThriceSplitterDialer(config))
|
||||
}
|
||||
|
||||
func TestNewRandomSplitterDialer(t *testing.T) {
|
||||
dial(t, internal.NewRandomSplitterDialer(config))
|
||||
}
|
||||
|
||||
func TestNewVanillaDialer(t *testing.T) {
|
||||
dial(t, internal.NewVanillaDialer(config))
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SNISplitter splits input such that SNI is splitted across
|
||||
// a bunch of different output buffers.
|
||||
func SNISplitter(input []byte, sni []byte) (output [][]byte) {
|
||||
idx := bytes.Index(input, sni)
|
||||
if idx < 0 {
|
||||
output = append(output, input)
|
||||
return
|
||||
}
|
||||
output = append(output, input[:idx])
|
||||
// TODO(bassosimone): splitting every three bytes causes
|
||||
// a bunch of Unicode chatacters (e.g., in Chinese) to be
|
||||
// sent as part of the same segment. Is that OK?
|
||||
const segmentsize = 3
|
||||
var buf []byte
|
||||
for _, chr := range input[idx : idx+len(sni)] {
|
||||
buf = append(buf, chr)
|
||||
if len(buf) == segmentsize {
|
||||
output = append(output, buf)
|
||||
buf = nil
|
||||
}
|
||||
}
|
||||
if len(buf) > 0 {
|
||||
output = append(output, buf)
|
||||
buf = nil
|
||||
}
|
||||
output = append(output, input[idx+len(sni):])
|
||||
return
|
||||
}
|
||||
|
||||
// Splitter84rest segments the specified buffer into three
|
||||
// sub-buffers containing respectively 8 bytes, 4 bytes, and
|
||||
// the rest of the buffer. This segment technique has been
|
||||
// described by Kevin Bock during the Internet Measurements
|
||||
// Village 2020: https://youtu.be/ksojSRFLbBM?t=1140.
|
||||
func Splitter84rest(input []byte) (output [][]byte) {
|
||||
if len(input) <= 12 {
|
||||
output = append(output, input)
|
||||
return
|
||||
}
|
||||
output = append(output, input[:8])
|
||||
output = append(output, input[8:12])
|
||||
output = append(output, input[12:])
|
||||
return
|
||||
}
|
||||
|
||||
// Splitter3264rand splits the specified buffer at a random
|
||||
// offset between 32 and 64 bytes. This is the methodology used
|
||||
// by github.com/Jigsaw-Code/outline-go-tun2socks.
|
||||
func Splitter3264rand(input []byte) (output [][]byte) {
|
||||
if len(input) <= 64 {
|
||||
output = append(output, input)
|
||||
return
|
||||
}
|
||||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
offset := rnd.Intn(32) + 32
|
||||
output = append(output, input[:offset])
|
||||
output = append(output, input[offset:])
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool/internal"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/randx"
|
||||
)
|
||||
|
||||
func TestSplitter84restSmall(t *testing.T) {
|
||||
input := []byte("1111222")
|
||||
output := internal.Splitter84rest(input)
|
||||
if len(output) != 1 {
|
||||
t.Fatal("invalid output length")
|
||||
}
|
||||
if string(output[0]) != "1111222" {
|
||||
t.Fatal("invalid output[0]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitter84restGood(t *testing.T) {
|
||||
input := []byte("1111222233334")
|
||||
output := internal.Splitter84rest(input)
|
||||
if len(output) != 3 {
|
||||
t.Fatal("invalid output length")
|
||||
}
|
||||
if string(output[0]) != "11112222" {
|
||||
t.Fatal("invalid output[0]")
|
||||
}
|
||||
if string(output[1]) != "3333" {
|
||||
t.Fatal("invalid output[1]")
|
||||
}
|
||||
if string(output[2]) != "4" {
|
||||
t.Fatal("invalid output[2]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitter3264randSmall(t *testing.T) {
|
||||
input := randx.Letters(64)
|
||||
output := internal.Splitter3264rand([]byte(input))
|
||||
if len(output) != 1 {
|
||||
t.Fatal("invalid output length")
|
||||
}
|
||||
if string(output[0]) != input {
|
||||
t.Fatal("invalid output[0]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitter3264Works(t *testing.T) {
|
||||
input := randx.Letters(65)
|
||||
output := internal.Splitter3264rand([]byte(input))
|
||||
for i := 0; i < 32; i++ {
|
||||
if len(output) != 2 {
|
||||
t.Fatal("invalid output length")
|
||||
}
|
||||
if len(output[0]) < 32 || len(output[0]) > 64 {
|
||||
t.Fatal("invalid output[0] length")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSNISplitterEasyCase(t *testing.T) {
|
||||
input := []byte("11112222334555foo.barbar.deadbeef.com6777778888")
|
||||
sni := []byte("barbar.deadbeef.com")
|
||||
output := internal.SNISplitter(input, sni)
|
||||
if len(output) != 9 {
|
||||
t.Fatal("invalid output length")
|
||||
}
|
||||
if string(output[0]) != "11112222334555foo." {
|
||||
t.Fatal("invalid output[0]")
|
||||
}
|
||||
if string(output[1]) != "bar" {
|
||||
t.Fatal("invalid output[1]")
|
||||
}
|
||||
if string(output[2]) != "bar" {
|
||||
t.Fatal("invalid output[2]")
|
||||
}
|
||||
if string(output[3]) != ".de" {
|
||||
t.Fatal("invalid output[3]")
|
||||
}
|
||||
if string(output[4]) != "adb" {
|
||||
t.Fatal("invalid output[4]")
|
||||
}
|
||||
if string(output[5]) != "eef" {
|
||||
t.Fatal("invalid output[5]")
|
||||
}
|
||||
if string(output[6]) != ".co" {
|
||||
t.Fatal("invalid output[6]")
|
||||
}
|
||||
if string(output[7]) != "m" {
|
||||
t.Fatal("invalid output[7]")
|
||||
}
|
||||
if string(output[8]) != "6777778888" {
|
||||
t.Fatal("invalid output[8]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSNISplitterNoMatch(t *testing.T) {
|
||||
input := []byte("11112222334555foo.barbar.deadbeef.com6777778888")
|
||||
sni := []byte("www.google.com")
|
||||
output := internal.SNISplitter(input, sni)
|
||||
if len(output) != 1 {
|
||||
t.Fatal("invalid output length")
|
||||
}
|
||||
if string(output[0]) != string(input) {
|
||||
t.Fatal("invalid output[0]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSNISplitterWithUnicode(t *testing.T) {
|
||||
input := []byte("11112222334555你好世界.com6777778888")
|
||||
sni := []byte("你好世界.com")
|
||||
output := internal.SNISplitter(input, sni)
|
||||
t.Log(string(output[2]))
|
||||
t.Log(output)
|
||||
if len(output) != 8 {
|
||||
t.Fatal("invalid output length")
|
||||
}
|
||||
if string(output[0]) != "11112222334555" {
|
||||
t.Fatal("invalid output[0]")
|
||||
}
|
||||
if string(output[1]) != "你" {
|
||||
t.Fatal("invalid output[1]")
|
||||
}
|
||||
if string(output[2]) != "好" {
|
||||
t.Fatal("invalid output[2]")
|
||||
}
|
||||
if string(output[3]) != "世" {
|
||||
t.Fatal("invalid output[3]")
|
||||
}
|
||||
if string(output[4]) != "界" {
|
||||
t.Fatal("invalid output[4]")
|
||||
}
|
||||
if string(output[5]) != ".co" {
|
||||
t.Fatal("invalid output[5]")
|
||||
}
|
||||
if string(output[6]) != "m" {
|
||||
t.Fatal("invalid output[6]")
|
||||
}
|
||||
if string(output[7]) != "6777778888" {
|
||||
t.Fatal("invalid output[7]")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SleeperWriter is a net.Conn that optionally sleeps for the
|
||||
// specified delay before posting each write.
|
||||
type SleeperWriter struct {
|
||||
net.Conn
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
func (c SleeperWriter) Write(b []byte) (int, error) {
|
||||
<-time.After(c.Delay)
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
// SplitterWriter is a writer that splits every outgoing buffer
|
||||
// according to the rules specified by the Splitter.
|
||||
//
|
||||
// Caveat
|
||||
//
|
||||
// The TLS ClientHello may be retransmitted if the server is
|
||||
// requesting us to restart the negotiation. Therefore, it is
|
||||
// not safe to just run the splitting once. Since this code
|
||||
// is meant to investigate TLS blocking, that's fine.
|
||||
type SplitterWriter struct {
|
||||
net.Conn
|
||||
Splitter func([]byte) [][]byte
|
||||
}
|
||||
|
||||
// Write implements net.Conn.Write
|
||||
func (c SplitterWriter) Write(b []byte) (int, error) {
|
||||
if c.Splitter != nil {
|
||||
return Writev(c.Conn, c.Splitter(b))
|
||||
}
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
// Writev writes all the vectors inside datalist using the specified
|
||||
// conn. Returns either an error or the number of bytes sent. Note
|
||||
// that this function skips any empty entry in datalist.
|
||||
func Writev(conn net.Conn, datalist [][]byte) (int, error) {
|
||||
var total int
|
||||
for _, data := range datalist {
|
||||
if len(data) > 0 {
|
||||
count, err := conn.Write(data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
total += count
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool/internal"
|
||||
)
|
||||
|
||||
func TestSleeperWriterWorksAsIntended(t *testing.T) {
|
||||
origconn := &internal.FakeConn{}
|
||||
const outdata = "deadbeefbadidea"
|
||||
conn := internal.SleeperWriter{
|
||||
Conn: origconn,
|
||||
Delay: 1 * time.Second,
|
||||
}
|
||||
before := time.Now()
|
||||
count, err := conn.Write([]byte(outdata))
|
||||
elapsed := time.Since(before)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != len(outdata) {
|
||||
t.Fatal("unexpected count")
|
||||
}
|
||||
if len(origconn.WriteData) != 1 {
|
||||
t.Fatal("wrong length of written data queue")
|
||||
}
|
||||
if string(origconn.WriteData[0]) != outdata {
|
||||
t.Fatal("we did not write the right data")
|
||||
}
|
||||
if elapsed < 750*time.Millisecond {
|
||||
t.Fatalf("unexpected elapsed time: %+v", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitterWriterNoSplitSuccess(t *testing.T) {
|
||||
innerconn := &internal.FakeConn{}
|
||||
conn := internal.SplitterWriter{Conn: innerconn}
|
||||
const data = "deadbeef"
|
||||
count, err := conn.Write([]byte(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != len(data) {
|
||||
t.Fatal("invalid count")
|
||||
}
|
||||
if len(innerconn.WriteData) != 1 {
|
||||
t.Fatal("invalid data queue")
|
||||
}
|
||||
if string(innerconn.WriteData[0]) != data {
|
||||
t.Fatal("invalid written data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitterWriterNoSplitFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
innerconn := &internal.FakeConn{WriteError: expected}
|
||||
conn := internal.SplitterWriter{Conn: innerconn}
|
||||
const data = "deadbeef"
|
||||
count, err := conn.Write([]byte(data))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("invalid count")
|
||||
}
|
||||
if len(innerconn.WriteData) != 0 {
|
||||
t.Fatal("invalid data queue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitterWriterSplitSuccess(t *testing.T) {
|
||||
innerconn := &internal.FakeConn{}
|
||||
conn := internal.SplitterWriter{
|
||||
Conn: innerconn,
|
||||
Splitter: func(b []byte) [][]byte {
|
||||
return [][]byte{
|
||||
b[:2], b[2:],
|
||||
}
|
||||
},
|
||||
}
|
||||
const data = "deadbeef"
|
||||
count, err := conn.Write([]byte(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != len(data) {
|
||||
t.Fatal("invalid count")
|
||||
}
|
||||
if len(innerconn.WriteData) != 2 {
|
||||
t.Fatal("invalid data queue")
|
||||
}
|
||||
if string(innerconn.WriteData[0]) != "de" {
|
||||
t.Fatal("invalid written data[0]")
|
||||
}
|
||||
if string(innerconn.WriteData[1]) != "adbeef" {
|
||||
t.Fatal("invalid written data[1]")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitterWriterSplitFailure(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
innerconn := &internal.FakeConn{WriteError: expected}
|
||||
conn := internal.SplitterWriter{
|
||||
Conn: innerconn,
|
||||
Splitter: func(b []byte) [][]byte {
|
||||
return [][]byte{
|
||||
b[:2], b[2:],
|
||||
}
|
||||
},
|
||||
}
|
||||
const data = "deadbeef"
|
||||
count, err := conn.Write([]byte(data))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("invalid count")
|
||||
}
|
||||
if len(innerconn.WriteData) != 0 {
|
||||
t.Fatal("invalid data queue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritevWorksWithAlsoEmptyData(t *testing.T) {
|
||||
conn := &internal.FakeConn{}
|
||||
datalist := [][]byte{
|
||||
[]byte("deadbeef"),
|
||||
[]byte(""),
|
||||
[]byte("dead"),
|
||||
nil,
|
||||
[]byte("badidea"),
|
||||
nil,
|
||||
}
|
||||
count, err := internal.Writev(conn, datalist)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 19 {
|
||||
t.Fatal("invalid number of bytes written")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritevFailsAsIntended(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
conn := &internal.FakeConn{WriteError: expected}
|
||||
datalist := [][]byte{
|
||||
[]byte("deadbeef"),
|
||||
[]byte(""),
|
||||
[]byte("dead"),
|
||||
nil,
|
||||
[]byte("badidea"),
|
||||
nil,
|
||||
}
|
||||
count, err := internal.Writev(conn, datalist)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("not the error we expected: %+v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("invalid number of bytes written")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Package tlstool contains a TLS tool that we are currently using
|
||||
// for running quick and dirty experiments. This tool will change
|
||||
// without notice and may be removed without notice.
|
||||
//
|
||||
// Caveats
|
||||
//
|
||||
// In particular, this experiment MAY panic when passed incorrect
|
||||
// input. This is acceptable because this is not production ready code.
|
||||
package tlstool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool/internal"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/runtimex"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||
)
|
||||
|
||||
const (
|
||||
testName = "tlstool"
|
||||
testVersion = "0.1.0"
|
||||
)
|
||||
|
||||
// Config contains the experiment configuration.
|
||||
type Config struct {
|
||||
Delay int64 `ooni:"Milliseconds to wait between writes"`
|
||||
SNI string `ooni:"Force using the specified SNI"`
|
||||
}
|
||||
|
||||
// TestKeys contains the experiment results.
|
||||
type TestKeys struct {
|
||||
Experiment map[string]*ExperimentKeys `json:"experiment"`
|
||||
}
|
||||
|
||||
// ExperimentKeys contains the specific experiment results.
|
||||
type ExperimentKeys struct {
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
// Measurer performs the measurement.
|
||||
type Measurer struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperiExperimentName.
|
||||
func (m Measurer) ExperimentName() string {
|
||||
return testName
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||
func (m Measurer) ExperimentVersion() string {
|
||||
return testVersion
|
||||
}
|
||||
|
||||
type method struct {
|
||||
name string
|
||||
newDialer func(internal.DialerConfig) internal.Dialer
|
||||
}
|
||||
|
||||
var allMethods = []method{{
|
||||
name: "vanilla",
|
||||
newDialer: internal.NewVanillaDialer,
|
||||
}, {
|
||||
name: "snisplit",
|
||||
newDialer: internal.NewSNISplitterDialer,
|
||||
}, {
|
||||
name: "random",
|
||||
newDialer: internal.NewRandomSplitterDialer,
|
||||
}, {
|
||||
name: "thrice",
|
||||
newDialer: internal.NewThriceSplitterDialer,
|
||||
}}
|
||||
|
||||
// Run implements ExperimentMeasurer.Run.
|
||||
func (m Measurer) Run(
|
||||
ctx context.Context,
|
||||
sess model.ExperimentSession,
|
||||
measurement *model.Measurement,
|
||||
callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
// TODO(bassosimone): wondering whether this experiment should
|
||||
// actually be merged with sniblocking instead?
|
||||
tk := new(TestKeys)
|
||||
tk.Experiment = make(map[string]*ExperimentKeys)
|
||||
measurement.TestKeys = tk
|
||||
address := string(measurement.Input)
|
||||
for idx, meth := range allMethods {
|
||||
// TODO(bassosimone): here we actually want to use urlgetter
|
||||
// if possible and collect standard test keys.
|
||||
err := m.run(ctx, runConfig{
|
||||
address: address,
|
||||
logger: sess.Logger(),
|
||||
newDialer: meth.newDialer,
|
||||
})
|
||||
percent := float64(idx) / float64(len(allMethods))
|
||||
callbacks.OnProgress(percent, fmt.Sprintf("%s: %+v", meth.name, err))
|
||||
tk.Experiment[meth.name] = &ExperimentKeys{
|
||||
Failure: archival.NewFailure(err),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Measurer) newDialer(logger model.Logger) netx.Dialer {
|
||||
// TODO(bassosimone): this is a resolver that should hopefully work
|
||||
// in many places. Maybe allow to configure it?
|
||||
resolver, err := netx.NewDNSClientWithOverrides(netx.Config{Logger: logger},
|
||||
"https://cloudflare.com/dns-query", "dns.cloudflare.com", "", "")
|
||||
runtimex.PanicOnError(err, "cannot initialize resolver")
|
||||
return netx.NewDialer(netx.Config{FullResolver: resolver, Logger: logger})
|
||||
}
|
||||
|
||||
type runConfig struct {
|
||||
address string
|
||||
logger model.Logger
|
||||
newDialer func(internal.DialerConfig) internal.Dialer
|
||||
}
|
||||
|
||||
func (m Measurer) run(ctx context.Context, config runConfig) error {
|
||||
dialer := config.newDialer(internal.DialerConfig{
|
||||
Dialer: m.newDialer(config.logger),
|
||||
Delay: time.Duration(m.config.Delay) * time.Millisecond,
|
||||
SNI: m.pattern(config.address),
|
||||
})
|
||||
tdialer := netx.NewTLSDialer(netx.Config{
|
||||
Dialer: dialer,
|
||||
Logger: config.logger,
|
||||
TLSConfig: m.tlsConfig(),
|
||||
})
|
||||
conn, err := tdialer.DialTLSContext(ctx, "tcp", config.address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Measurer) tlsConfig() *tls.Config {
|
||||
if m.config.SNI != "" {
|
||||
return &tls.Config{ServerName: m.config.SNI}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Measurer) pattern(address string) string {
|
||||
if m.config.SNI != "" {
|
||||
return m.config.SNI
|
||||
}
|
||||
addr, _, err := net.SplitHostPort(address)
|
||||
// TODO(bassosimone): replace this panic with proper error checking.
|
||||
runtimex.PanicOnError(err, "cannot split address")
|
||||
return addr
|
||||
}
|
||||
|
||||
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return Measurer{config: config}
|
||||
}
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment.
|
||||
//
|
||||
// Note that this structure is part of the ABI contract with probe-cli
|
||||
// therefore we should be careful when changing it.
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
return SummaryKeys{IsAnomaly: false}, nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package tlstool_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/internal/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
func TestMeasurerExperimentNameVersion(t *testing.T) {
|
||||
measurer := tlstool.NewExperimentMeasurer(tlstool.Config{})
|
||||
if measurer.ExperimentName() != "tlstool" {
|
||||
t.Fatal("unexpected ExperimentName")
|
||||
}
|
||||
if measurer.ExperimentVersion() != "0.1.0" {
|
||||
t.Fatal("unexpected ExperimentVersion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithExplicitSNI(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
measurer := tlstool.NewExperimentMeasurer(tlstool.Config{
|
||||
SNI: "dns.google",
|
||||
})
|
||||
measurement := new(model.Measurement)
|
||||
measurement.Input = "8.8.8.8:853"
|
||||
err := measurer.Run(
|
||||
ctx,
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithImplicitSNI(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
measurer := tlstool.NewExperimentMeasurer(tlstool.Config{})
|
||||
measurement := new(model.Measurement)
|
||||
measurement.Input = "dns.google:853"
|
||||
err := measurer.Run(
|
||||
ctx,
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithCancelledContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cause failure
|
||||
measurer := tlstool.NewExperimentMeasurer(tlstool.Config{})
|
||||
measurement := new(model.Measurement)
|
||||
measurement.Input = "dns.google:853"
|
||||
err := measurer.Run(
|
||||
ctx,
|
||||
&mockable.Session{},
|
||||
measurement,
|
||||
model.NewPrinterCallbacks(log.Log),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sk, err := measurer.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := sk.(tlstool.SummaryKeys); !ok {
|
||||
t.Fatal("invalid type for summary keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryKeysGeneric(t *testing.T) {
|
||||
measurement := &model.Measurement{TestKeys: &tlstool.TestKeys{}}
|
||||
m := &tlstool.Measurer{}
|
||||
osk, err := m.GetSummaryKeys(measurement)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sk := osk.(tlstool.SummaryKeys)
|
||||
if sk.IsAnomaly {
|
||||
t.Fatal("invalid isAnomaly")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user