refactor(netx): remove the self censorship mechanism (#364)

We're currently use jafar for QA and jafar is a better mechanism,
even though it is not portable outside of Linux.

This self censorship mechanism was less cool and added a bunch
of (also cognitive) complexity to netx.

If we ever want to go down a self censorship like road, we probably
want to do as little work as possible in the problem and as much
work as possible inside a helper like jafar.

Part of https://github.com/ooni/probe/issues/1591.
This commit is contained in:
Simone Basso 2021-06-08 19:40:17 +02:00 committed by GitHub
parent c553afdbd5
commit adbde7246b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 88 additions and 538 deletions

View File

@ -19,7 +19,6 @@ import (
"github.com/ooni/probe-cli/v3/internal/engine"
"github.com/ooni/probe-cli/v3/internal/engine/legacy/assetsdir"
"github.com/ooni/probe-cli/v3/internal/engine/model"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
"github.com/ooni/probe-cli/v3/internal/humanize"
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/version"
@ -41,7 +40,6 @@ type Options struct {
Proxy string
Random bool
ReportFile string
SelfCensorSpec string
TorArgs []string
TorBinary string
Tunnel string
@ -108,10 +106,6 @@ func init() {
&globalOptions.ReportFile, "reportfile", 'o',
"Set the report file path", "PATH",
)
getopt.FlagLong(
&globalOptions.SelfCensorSpec, "self-censor-spec", 0,
"Enable and configure self censorship", "JSON",
)
getopt.FlagLong(
&globalOptions.TorArgs, "tor-args", 0,
"Extra args for tor binary (may be specified multiple times)",
@ -305,9 +299,6 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
extraOptions := mustMakeMap(currentOptions.ExtraOptions)
annotations := mustMakeMap(currentOptions.Annotations)
err := selfcensor.MaybeEnable(currentOptions.SelfCensorSpec)
fatalOnError(err, "cannot parse --self-censor-spec argument")
logger := &log.Logger{Level: log.InfoLevel, Handler: &logHandler{Writer: os.Stderr}}
if currentOptions.Verbose {
logger.Level = log.DebugLevel
@ -323,7 +314,7 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
homeDir := gethomedir(currentOptions.HomeDir)
fatalIfFalse(homeDir != "", "home directory is empty")
miniooniDir := path.Join(homeDir, ".miniooni")
err = os.MkdirAll(miniooniDir, 0700)
err := os.MkdirAll(miniooniDir, 0700)
fatalOnError(err, "cannot create $HOME/.miniooni directory")
// We cleanup the assets files used by versions of ooniprobe

View File

@ -20,8 +20,8 @@ import (
"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"
"github.com/ooni/probe-cli/v3/internal/engine/netx/dialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/errorx"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
"github.com/ooni/probe-cli/v3/internal/randx"
)
@ -319,11 +319,11 @@ type Dialer struct {
// DialContext dials a specific connection and arranges such that
// headers in the outgoing request are transformed.
func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
dialer := d.Dialer
if dialer == nil {
dialer = selfcensor.DefaultDialer
child := d.Dialer
if child == nil {
child = dialer.Default
}
conn, err := dialer.DialContext(ctx, network, address)
conn, err := child.DialContext(ctx, network, address)
if err != nil {
return nil, err
}

View File

@ -10,7 +10,6 @@ import (
"github.com/ooni/probe-cli/v3/internal/engine/model"
"github.com/ooni/probe-cli/v3/internal/engine/netx/dialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/resolver"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
)
type dialManager struct {
@ -36,7 +35,7 @@ func newDialManager(ndt7URL string, logger model.Logger, userAgent string) dialM
func (mgr dialManager) dialWithTestName(ctx context.Context, testName string) (*websocket.Conn, error) {
var reso resolver.Resolver = resolver.SystemResolver{}
reso = resolver.LoggingResolver{Resolver: reso, Logger: mgr.logger}
var dlr dialer.Dialer = selfcensor.SystemDialer{}
var dlr dialer.Dialer = dialer.Default
dlr = dialer.TimeoutDialer{Dialer: dlr}
dlr = dialer.ErrorWrapperDialer{Dialer: dlr}
dlr = dialer.LoggingDialer{Dialer: dlr, Logger: mgr.logger}

View File

@ -0,0 +1,24 @@
package dialer
import (
"context"
"net"
"time"
)
// defaultNetDialer is the net.Dialer we use by default.
var defaultNetDialer = &net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 15 * time.Second,
}
// SystemDialer is the system dialer.
type SystemDialer struct{}
// DialContext implements Dialer.DialContext
func (d SystemDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return defaultNetDialer.DialContext(ctx, network, address)
}
// Default is the dialer we use by default.
var Default = SystemDialer{}

View File

@ -0,0 +1,20 @@
package dialer
import (
"strings"
"testing"
"github.com/ooni/psiphon/oopsi/golang.org/x/net/context"
)
func TestSystemDialer(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
conn, err := Default.DialContext(ctx, "tcp", "8.8.8.8:853")
if err == nil || !strings.HasSuffix(err.Error(), "operation was canceled") {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("expected nil conn here")
}
}

View File

@ -10,8 +10,8 @@ import (
"testing"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/engine/netx/dialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
)
type MockQUICDialer struct{}
@ -42,7 +42,7 @@ func TestHTTP3TransportSNI(t *testing.T) {
namech := make(chan string, 1)
sni := "sni.org"
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockSNIQUICDialer{namech: namech}, TLSConfig: &tls.Config{ServerName: sni}})
Dialer: dialer.Default, QUICDialer: MockSNIQUICDialer{namech: namech}, TLSConfig: &tls.Config{ServerName: sni}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
@ -67,7 +67,7 @@ func TestHTTP3TransportSNINoVerify(t *testing.T) {
namech := make(chan string, 1)
sni := "sni.org"
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockSNIQUICDialer{namech: namech}, TLSConfig: &tls.Config{ServerName: sni, InsecureSkipVerify: true}})
Dialer: dialer.Default, QUICDialer: MockSNIQUICDialer{namech: namech}, TLSConfig: &tls.Config{ServerName: sni, InsecureSkipVerify: true}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
@ -89,7 +89,7 @@ func TestHTTP3TransportCABundle(t *testing.T) {
certch := make(chan *x509.CertPool, 1)
certpool := x509.NewCertPool()
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockCertQUICDialer{certch: certch}, TLSConfig: &tls.Config{RootCAs: certpool}})
Dialer: dialer.Default, QUICDialer: MockCertQUICDialer{certch: certch}, TLSConfig: &tls.Config{RootCAs: certpool}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
t.Fatal(err)
@ -114,7 +114,7 @@ func TestHTTP3TransportCABundle(t *testing.T) {
func TestUnitHTTP3TransportSuccess(t *testing.T) {
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockQUICDialer{}})
Dialer: dialer.Default, QUICDialer: MockQUICDialer{}})
req, err := http.NewRequest("GET", "https://www.google.com", nil)
if err != nil {
@ -134,7 +134,7 @@ func TestUnitHTTP3TransportSuccess(t *testing.T) {
func TestUnitHTTP3TransportFailure(t *testing.T) {
txp := httptransport.NewHTTP3Transport(httptransport.Config{
Dialer: selfcensor.SystemDialer{}, QUICDialer: MockQUICDialer{}})
Dialer: dialer.Default, QUICDialer: MockQUICDialer{}})
ctx, cancel := context.WithCancel(context.Background())
cancel() // so that the request immediately fails

View File

@ -36,7 +36,6 @@ import (
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
"github.com/ooni/probe-cli/v3/internal/engine/netx/quicdialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/resolver"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
"github.com/ooni/probe-cli/v3/internal/engine/netx/tlsdialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/tlsx"
"github.com/ooni/probe-cli/v3/internal/engine/netx/trace"
@ -151,7 +150,7 @@ func NewDialer(config Config) Dialer {
if config.FullResolver == nil {
config.FullResolver = NewResolver(config)
}
var d Dialer = selfcensor.SystemDialer{}
var d Dialer = dialer.Default
d = dialer.TimeoutDialer{Dialer: d}
d = dialer.ErrorWrapperDialer{Dialer: d}
if config.Logger != nil {

View File

@ -13,7 +13,6 @@ import (
"github.com/ooni/probe-cli/v3/internal/engine/netx/dialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/httptransport"
"github.com/ooni/probe-cli/v3/internal/engine/netx/resolver"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
"github.com/ooni/probe-cli/v3/internal/engine/netx/tlsdialer"
"github.com/ooni/probe-cli/v3/internal/engine/netx/trace"
)
@ -245,7 +244,7 @@ func TestNewDialerVanilla(t *testing.T) {
if !ok {
t.Fatal("not the dialer we expected")
}
if _, ok := td.Dialer.(selfcensor.SystemDialer); !ok {
if _, ok := td.Dialer.(dialer.SystemDialer); !ok {
t.Fatal("not the dialer we expected")
}
}
@ -285,7 +284,7 @@ func TestNewDialerWithResolver(t *testing.T) {
if !ok {
t.Fatal("not the dialer we expected")
}
if _, ok := td.Dialer.(selfcensor.SystemDialer); !ok {
if _, ok := td.Dialer.(dialer.SystemDialer); !ok {
t.Fatal("not the dialer we expected")
}
}
@ -334,7 +333,7 @@ func TestNewDialerWithLogger(t *testing.T) {
if !ok {
t.Fatal("not the dialer we expected")
}
if _, ok := td.Dialer.(selfcensor.SystemDialer); !ok {
if _, ok := td.Dialer.(dialer.SystemDialer); !ok {
t.Fatal("not the dialer we expected")
}
}
@ -384,7 +383,7 @@ func TestNewDialerWithDialSaver(t *testing.T) {
if !ok {
t.Fatal("not the dialer we expected")
}
if _, ok := td.Dialer.(selfcensor.SystemDialer); !ok {
if _, ok := td.Dialer.(dialer.SystemDialer); !ok {
t.Fatal("not the dialer we expected")
}
}
@ -434,7 +433,7 @@ func TestNewDialerWithReadWriteSaver(t *testing.T) {
if !ok {
t.Fatal("not the dialer we expected")
}
if _, ok := td.Dialer.(selfcensor.SystemDialer); !ok {
if _, ok := td.Dialer.(dialer.SystemDialer); !ok {
t.Fatal("not the dialer we expected")
}
}
@ -480,7 +479,7 @@ func TestNewDialerWithContextByteCounting(t *testing.T) {
if !ok {
t.Fatal("not the dialer we expected")
}
if _, ok := td.Dialer.(selfcensor.SystemDialer); !ok {
if _, ok := td.Dialer.(dialer.SystemDialer); !ok {
t.Fatal("not the dialer we expected")
}
}

View File

@ -1,10 +1,29 @@
package resolver
import "github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
import (
"context"
"net"
)
// SystemResolver is the system resolver. It is implemented using
// selfcensor.SystemResolver so that we can perform integration testing
// by forcing the code to return specific responses.
type SystemResolver = selfcensor.SystemResolver
// SystemResolver is the system resolver.
type SystemResolver struct{}
// LookupHost implements Resolver.LookupHost.
func (r SystemResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) {
return net.DefaultResolver.LookupHost(ctx, hostname)
}
// Network implements Resolver.Network.
func (r SystemResolver) Network() string {
return "system"
}
// Address implements Resolver.Address.
func (r SystemResolver) Address() string {
return ""
}
// Default is the resolver we use by default.
var Default = SystemResolver{}
var _ Resolver = SystemResolver{}

View File

@ -1,230 +0,0 @@
// Package selfcensor contains code that triggers censorship. We use
// this functionality to implement integration tests.
//
// The self censoring functionality is disabled by default. To enable it,
// call Enable with a JSON-serialized Spec structure as its argument.
//
// The following example causes NXDOMAIN to be returned for `dns.google`:
//
// selfcensor.Enable(`{"PoisonSystemDNS":{"dns.google":["NXDOMAIN"]}}`)
//
// The following example blocks connecting to `8.8.8.8:443`:
//
// selfcensor.Enable(`{"BlockedEndpoints":{"8.8.8.8:443":"REJECT"}}`)
//
// The following example blocks packets containing dns.google:
//
// selfcensor.Enable(`{"BlockedFingerprints":{"dns.google":"RST"}}`)
//
// The documentation of the Spec structure contains further information on
// how to populate the JSON. Miniooni uses the `--self-censor-spec flag` to
// which you are supposed to pass a serialized JSON.
package selfcensor
import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"net"
"sync"
"time"
"github.com/ooni/probe-cli/v3/internal/atomicx"
)
// Spec indicates what self censorship techniques to implement.
type Spec struct {
// PoisonSystemDNS allows you to change the behaviour of the system
// DNS regarding specific domains. They keys are the domains and the
// values are the IP addresses to return. If you set the values for
// a domain to `[]string{"NXDOMAIN"}`, the system resolver will return
// an NXDOMAIN response. If you set the values for a domain to
// `[]string{"TIMEOUT"}` the system resolver will return "i/o timeout".
PoisonSystemDNS map[string][]string
// BlockedEndpoints allows you to block specific IP endpoints. The key is
// `IP:port` to block. The format is the same of net.JoinHostPort. If
// the value is "REJECT", then the connection attempt will fail with
// ECONNREFUSED. If the value is "TIMEOUT", then the connector will return
// claiming "i/o timeout". If the value is anything else, we will
// perform a "REJECT".
BlockedEndpoints map[string]string
// BlockedFingerprints allows you to block packets whose body contains
// specific fingerprints. Of course, the key is the fingerprint. If
// the value is "RST", then the connection will be reset. If the value
// is "TIMEOUT", then the code will return claiming "i/o timeout". If
// the value is anything else, we will perform a "RST".
BlockedFingerprints map[string]string
}
var (
attempts *atomicx.Int64 = &atomicx.Int64{}
enabled *atomicx.Int64 = &atomicx.Int64{}
mu sync.Mutex
spec *Spec
)
// Enabled returns whether self censorship is enabled
func Enabled() bool {
return enabled.Load() != 0
}
// Attempts returns the number of self censorship attempts so far. A self
// censorship attempt is defined as the code entering into the branch that
// _may_ perform self censorship. We expected to see this counter being
// equal to zero when Enabled() returns false.
func Attempts() int64 {
return attempts.Load()
}
// Enable turns on the self censorship engine. This function returns
// an error if we cannot parse a Spec from the serialized JSON inside
// data. Each time you call Enable you overwrite the previous spec.
func Enable(data string) error {
mu.Lock()
defer mu.Unlock()
s := new(Spec)
if err := json.Unmarshal([]byte(data), s); err != nil {
return err
}
spec = s
enabled.Add(1)
log.Printf("selfcensor: spec %+v", *spec)
return nil
}
// MaybeEnable is like enable except that it does nothing in case
// the string provided as argument is an empty string.
func MaybeEnable(data string) (err error) {
if data != "" {
err = Enable(data)
}
return
}
// SystemResolver is a self-censoring system resolver. This resolver does
// not censor anything unless you call selfcensor.Enable().
type SystemResolver struct{}
// errTimeout indicates that a timeout error has occurred.
var errTimeout = errors.New("i/o timeout")
// LookupHost implements Resolver.LookupHost
func (r SystemResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) {
if enabled.Load() != 0 { // jumps not taken by default
mu.Lock()
defer mu.Unlock()
attempts.Add(1)
if spec.PoisonSystemDNS != nil {
values := spec.PoisonSystemDNS[hostname]
if len(values) == 1 && values[0] == "NXDOMAIN" {
return nil, errors.New("no such host")
}
if len(values) == 1 && values[0] == "TIMEOUT" {
return nil, errTimeout
}
if len(values) > 0 {
return values, nil
}
}
// FALLTHROUGH
}
return net.DefaultResolver.LookupHost(ctx, hostname)
}
// Network implements Resolver.Network
func (r SystemResolver) Network() string {
return "system"
}
// Address implements Resolver.Address
func (r SystemResolver) Address() string {
return ""
}
// SystemDialer is a self-censoring system dialer. This dialer does
// not censor anything unless you call selfcensor.Enable().
type SystemDialer struct{}
// defaultNetDialer is the dialer we use by default.
var defaultNetDialer = &net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 15 * time.Second,
}
// DefaultDialer is the dialer you should use in code that wants
// to take advantage of selfcensor capabilities.
var DefaultDialer = SystemDialer{}
// DialContext implements Dialer.DialContext
func (d SystemDialer) DialContext(
ctx context.Context, network, address string) (net.Conn, error) {
if enabled.Load() != 0 { // jumps not taken by default
mu.Lock()
defer mu.Unlock()
attempts.Add(1)
if spec.BlockedEndpoints != nil {
action, ok := spec.BlockedEndpoints[address]
if ok && action == "TIMEOUT" {
return nil, errTimeout
}
if ok {
switch network {
case "tcp", "tcp4", "tcp6":
return nil, errors.New("connection refused")
default:
// not applicable
}
}
}
if spec.BlockedFingerprints != nil {
conn, err := defaultNetDialer.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
return connWrapper{Conn: conn, closed: make(chan interface{}, 128),
fingerprints: spec.BlockedFingerprints}, nil
}
// FALLTHROUGH
}
return defaultNetDialer.DialContext(ctx, network, address)
}
type connWrapper struct {
net.Conn
closed chan interface{}
fingerprints map[string]string
}
func (c connWrapper) Write(p []byte) (int, error) {
// TODO(bassosimone): implement reassembly to workaround the
// splitting of the ClientHello message.
if _, err := c.match(p, len(p)); err != nil {
return 0, err
}
return c.Conn.Write(p)
}
func (c connWrapper) match(p []byte, n int) (int, error) {
p = p[:n] // trim
for key, value := range c.fingerprints {
if bytes.Index(p, []byte(key)) != -1 {
if value == "TIMEOUT" {
return 0, errTimeout
}
return 0, errors.New("connection reset by peer")
}
}
return n, nil
}
func (c connWrapper) Close() error {
// Implementation note: we will block here if we attempt to close
// too many times and noone's reading. Because we have a large buffer,
// and because this is integration testing code, that's fine.
c.closed <- true
return c.Conn.Close()
}

View File

@ -1,271 +0,0 @@
package selfcensor_test
import (
"context"
"strings"
"testing"
"time"
"github.com/ooni/probe-cli/v3/internal/engine/netx"
"github.com/ooni/probe-cli/v3/internal/engine/netx/selfcensor"
)
// TestDisabled MUST be the first test in this file.
func TestDisabled(t *testing.T) {
if selfcensor.Enabled() != false {
t.Fatal("self censorship should be disabled by default")
}
if selfcensor.Attempts() != 0 {
t.Fatal("we expect no self censorship attempts at the beginning")
}
t.Run("the system resolver does not trigger selfcensor events", func(t *testing.T) {
addrs, err := selfcensor.SystemResolver{}.LookupHost(
context.Background(), "dns.google",
)
if err != nil {
t.Fatal(err)
}
if addrs == nil {
t.Fatal("expected non-nil addrs here")
}
if selfcensor.Attempts() != 0 {
t.Fatal("we expect no self censorship attempts by default")
}
})
t.Run("the system dialer does not trigger selfcensor events", func(t *testing.T) {
conn, err := selfcensor.SystemDialer{}.DialContext(
context.Background(), "tcp", "8.8.8.8:443",
)
if err != nil {
t.Fatal(err)
}
if conn == nil {
t.Fatal("expected non-nil conn here")
}
conn.Close()
if selfcensor.Attempts() != 0 {
t.Fatal("we expect no self censorship attempts by default")
}
})
}
// TestDisabled MUST be the second test in this file.
func TestEnableInvalidJSON(t *testing.T) {
if selfcensor.Enabled() != false {
t.Fatal("we need to start with self censorship not enabled")
}
err := selfcensor.Enable("{")
if err == nil || !strings.HasSuffix(err.Error(), "unexpected end of JSON input") {
t.Fatal("not the error we expectd")
}
if selfcensor.Enabled() != false {
t.Fatal("we expected self censorship to still be not enabled")
}
}
// TestMaybeEnableWorksAsIntended MUST be the second test in this file.
func TestMaybeEnableWorksAsIntended(t *testing.T) {
if selfcensor.Enabled() != false {
t.Fatal("we need to start with self censorship not enabled")
}
err := selfcensor.MaybeEnable("")
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != false {
t.Fatal("we expected self censorship to still be not enabled")
}
}
func TestResolveCauseNXDOMAIN(t *testing.T) {
err := selfcensor.MaybeEnable(`{"PoisonSystemDNS":{"dns.google":["NXDOMAIN"]}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
addrs, err := selfcensor.SystemResolver{}.LookupHost(
context.Background(), "dns.google",
)
if err == nil || !strings.HasSuffix(err.Error(), "no such host") {
t.Fatal("not the error we expected")
}
if addrs != nil {
t.Fatal("expected nil addrs here")
}
}
func TestResolveCauseTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err := selfcensor.MaybeEnable(`{"PoisonSystemDNS":{"dns.google":["TIMEOUT"]}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
addrs, err := selfcensor.SystemResolver{}.LookupHost(ctx, "dns.google")
if err == nil || err.Error() != "i/o timeout" {
t.Fatal("not the error we expected")
}
if addrs != nil {
t.Fatal("expected nil addrs here")
}
}
func TestResolveCauseBogon(t *testing.T) {
err := selfcensor.MaybeEnable(`{"PoisonSystemDNS":{"dns.google":["10.0.0.7"]}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
addrs, err := selfcensor.SystemResolver{}.LookupHost(
context.Background(), "dns.google")
if err != nil {
t.Fatal(err)
}
if len(addrs) != 1 || addrs[0] != "10.0.0.7" {
t.Fatal("not the addrs we expected")
}
}
func TestResolveCheckNetworkAndAddress(t *testing.T) {
err := selfcensor.MaybeEnable(`{"PoisonSystemDNS":{"dns.google":["10.0.0.7"]}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
reso := selfcensor.SystemResolver{}
if reso.Network() != "system" {
t.Fatal("invalid Network")
}
if reso.Address() != "" {
t.Fatal("invalid Address")
}
}
func TestDialHandlesErrorsWithBlockedFingerprints(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
cancel() // so we should fail immediately!
err := selfcensor.MaybeEnable(`{"BlockedFingerprints":{"dns.google":"TIMEOUT"}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
addrs, err := selfcensor.SystemDialer{}.DialContext(ctx, "tcp", "8.8.8.8:443")
if err == nil || !strings.HasSuffix(err.Error(), "operation was canceled") {
t.Fatal("not the error we expected")
}
if addrs != nil {
t.Fatal("expected nil addrs here")
}
}
func TestDialCauseTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err := selfcensor.MaybeEnable(`{"BlockedEndpoints":{"8.8.8.8:443":"TIMEOUT"}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
addrs, err := selfcensor.SystemDialer{}.DialContext(ctx, "tcp", "8.8.8.8:443")
if err == nil || err.Error() != "i/o timeout" {
t.Fatal("not the error we expected")
}
if addrs != nil {
t.Fatal("expected nil addrs here")
}
}
func TestDialCauseConnectionRefused(t *testing.T) {
err := selfcensor.MaybeEnable(`{"BlockedEndpoints":{"8.8.8.8:443":"REJECT"}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
addrs, err := selfcensor.SystemDialer{}.DialContext(
context.Background(), "tcp", "8.8.8.8:443")
if err == nil || !strings.HasSuffix(err.Error(), "connection refused") {
t.Fatal("not the error we expected")
}
if addrs != nil {
t.Fatal("expected nil addrs here")
}
}
func TestBlockedFingerprintsTimeout(t *testing.T) {
err := selfcensor.MaybeEnable(`{"BlockedFingerprints":{"dns.google":"TIMEOUT"}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
tlsDialer := netx.NewTLSDialer(netx.Config{
Dialer: selfcensor.SystemDialer{},
})
conn, err := tlsDialer.DialTLSContext(
context.Background(), "tcp", "dns.google:443")
if err == nil || err.Error() != "generic_timeout_error" {
t.Fatal("not the error expected")
}
if conn != nil {
t.Fatal("expected nil conn here")
}
}
func TestBlockedFingerprintsNoMatch(t *testing.T) {
err := selfcensor.MaybeEnable(`{"BlockedFingerprints":{"ooni.io":"TIMEOUT"}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
tlsDialer := netx.NewTLSDialer(netx.Config{
Dialer: selfcensor.SystemDialer{},
})
conn, err := tlsDialer.DialTLSContext(
context.Background(), "tcp", "dns.google:443")
if err != nil {
t.Fatal(err)
}
if conn == nil {
t.Fatal("expected non-nil conn here")
}
conn.Close()
}
func TestBlockedFingerprintsConnectionReset(t *testing.T) {
err := selfcensor.MaybeEnable(`{"BlockedFingerprints":{"dns.google":"RST"}}`)
if err != nil {
t.Fatal(err)
}
if selfcensor.Enabled() != true {
t.Fatal("we expected self censorship to be enabled now")
}
tlsDialer := netx.NewTLSDialer(netx.Config{
Dialer: selfcensor.SystemDialer{},
})
conn, err := tlsDialer.DialTLSContext(
context.Background(), "tcp", "dns.google:443")
if err == nil || err.Error() != "connection_reset" {
t.Fatal("not the error we expected")
}
if conn != nil {
t.Fatal("expected nil conn here")
}
}