Skip to content

Commit

Permalink
webconnectivityqa: import test cases using proxies (#1239)
Browse files Browse the repository at this point in the history
## Checklist

- [x] I have read the [contribution
guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md)
- [x] reference issue for this pull request:
ooni/probe#1803
- [x] if you changed anything related to how experiments work and you
need to reflect these changes in the ooni/spec repository, please link
to the related ooni/spec pull request: N/A
- [x] if you changed code inside an experiment, make sure you bump its
version number: N/A


## Description

This diff imports into webconnectivityqa test cases using proxies that
are currently also implemented by ./QA/webconnectivity.py.

The ./QA/webconnectivity.py contains three test cases. Two of them deal
with using transparent proxies without DNS lies, which was easy to do
using iptables, and much harder now. However, it's doubtful whether
those two cases are actually very useful, since there is no measurement
feature which allows us to distinguish them from what we would otherwise
get (perhaps, possibly latency?).

The third case, instead, is interesting and deals with the DNS serving
to users the IP addresses of transparent HTTP and TLS proxies. To make
this test case more similar to what it was in Python, and considering
that LTE uses many resolvers, here I have chosen to use DNS spoofing,
which may or may not be the best choice for LTE in the long term.

Yet, since the objective currently is to be able to check v0.4 against
webconnectivityqa and the A/B comparison and the focus to LTE will come
at a later stage, this seems good enough for now.
  • Loading branch information
bassosimone authored Sep 5, 2023
1 parent f374d21 commit a379ecd
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 83 deletions.
83 changes: 0 additions & 83 deletions QA/webconnectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,86 +47,6 @@ def assert_status_flags_are(ooni_exe, tk, desired):
assert tk["x_status"] == desired


def webconnectivity_transparent_http_proxy(ooni_exe, outfile):
"""Test case where we pass through a transparent HTTP proxy"""
args = []
args.append("-iptables-hijack-http-to")
args.append("127.0.0.1:80")
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i http://example.org web_connectivity",
"webconnectivity_transparent_http_proxy",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "consistent"
assert tk["control_failure"] == None
assert tk["http_experiment_failure"] == None
assert tk["body_length_match"] == True
assert tk["body_proportion"] == 1
assert tk["status_code_match"] == True
assert tk["headers_match"] == True
assert tk["title_match"] == True
assert tk["blocking"] == False
assert tk["accessible"] == True
assert_status_flags_are(ooni_exe, tk, 2)


def webconnectivity_transparent_https_proxy(ooni_exe, outfile):
"""Test case where we pass through a transparent HTTPS proxy"""
args = []
args.append("-iptables-hijack-https-to")
args.append("127.0.0.1:443")
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i https://example.org web_connectivity",
"webconnectivity_transparent_https_proxy",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "consistent"
assert tk["control_failure"] == None
assert tk["http_experiment_failure"] == None
assert tk["body_length_match"] == True
assert tk["body_proportion"] == 1
assert tk["status_code_match"] == True
assert tk["headers_match"] == True
assert tk["title_match"] == True
assert tk["blocking"] == False
assert tk["accessible"] == True
assert_status_flags_are(ooni_exe, tk, 1)


def webconnectivity_dns_hijacking(ooni_exe, outfile):
"""Test case where there is DNS hijacking towards a transparent proxy."""
args = []
args.append("-iptables-hijack-dns-to")
args.append("127.0.0.1:53")
args.append("-dns-proxy-hijack")
args.append("example.org")
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i https://example.org web_connectivity",
"webconnectivity_dns_hijacking",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "inconsistent"
assert tk["control_failure"] == None
assert tk["http_experiment_failure"] == None
assert tk["body_length_match"] == True
assert tk["body_proportion"] == 1
assert tk["status_code_match"] == True
assert tk["headers_match"] == True
assert tk["title_match"] == True
assert tk["blocking"] == False
assert tk["accessible"] == True
assert_status_flags_are(ooni_exe, tk, 1)


def webconnectivity_http_connection_refused_with_consistent_dns(ooni_exe, outfile):
"""Test case where there's TCP/IP blocking w/ consistent DNS that occurs
while we're following the chain of redirects."""
Expand Down Expand Up @@ -576,9 +496,6 @@ def main():
outfile = "webconnectivity.jsonl"
ooni_exe = sys.argv[1]
tests = [
webconnectivity_transparent_http_proxy,
webconnectivity_transparent_https_proxy,
webconnectivity_dns_hijacking,
webconnectivity_http_connection_refused_with_consistent_dns,
webconnectivity_http_connection_reset_with_consistent_dns,
webconnectivity_http_nxdomain_with_consistent_dns,
Expand Down
80 changes: 80 additions & 0 deletions internal/experiment/webconnectivityqa/dnshijacking.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package webconnectivityqa

import (
"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/netemx"
)

// dnsHijackingToProxyWithHTTPURL is the case where an ISP rule forces clients to always
// use an explicity passthrough proxy for a given domain.
func dnsHijackingToProxyWithHTTPURL() *TestCase {
// TODO(bassosimone): it's debateable whether this case is actually WAI but the
// transparent TLS proxy really makes our analysis a bit more complex
return &TestCase{
Name: "dnsHijackingToProxyWithHTTPURL",
Flags: TestCaseFlagNoLTE, // BUG: LTE thinks the DNS is consistent
Input: "http://www.example.com/",
Configure: func(env *netemx.QAEnv) {

// add DPI rule to force all the cleartext DNS queries to
// point the client to used the ISPProxyAddress
env.DPIEngine().AddRule(&netem.DPISpoofDNSResponse{
Addresses: []string{netemx.ISPProxyAddress},
Logger: env.Logger(),
Domain: "www.example.com",
})

},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSConsistency: "inconsistent",
BodyLengthMatch: true,
BodyProportion: 1,
StatusCodeMatch: true,
HeadersMatch: true,
TitleMatch: true,
XStatus: 2, // StatusSuccessCleartext
XDNSFlags: 0,
XBlockingFlags: 32, // analysisFlagSuccess
Accessible: true,
Blocking: false,
},
}
}

// dnsHijackingToProxyWithHTTPSURL is the case where an ISP rule forces clients to always
// use an explicity passthrough proxy for a given domain.
func dnsHijackingToProxyWithHTTPSURL() *TestCase {
// TODO(bassosimone): it's debateable whether this case is actually WAI but the
// transparent TLS proxy really makes our analysis a bit more complex
return &TestCase{
Name: "dnsHijackingToProxyWithHTTPSURL",
Flags: TestCaseFlagNoLTE, // BUG: LTE thinks the DNS is consistent
Input: "https://www.example.com/",
Configure: func(env *netemx.QAEnv) {

// add DPI rule to force all the cleartext DNS queries to
// point the client to used the ISPProxyAddress
env.DPIEngine().AddRule(&netem.DPISpoofDNSResponse{
Addresses: []string{netemx.ISPProxyAddress},
Logger: env.Logger(),
Domain: "www.example.com",
})

},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSConsistency: "inconsistent",
BodyLengthMatch: true,
BodyProportion: 1,
StatusCodeMatch: true,
HeadersMatch: true,
TitleMatch: true,
XStatus: 1, // StatusSuccessSecure
XDNSFlags: 0,
XBlockingFlags: 32, // analysisFlagSuccess
Accessible: true,
Blocking: false,
},
}
}
3 changes: 3 additions & 0 deletions internal/experiment/webconnectivityqa/testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ func AllTestCases() []*TestCase {
dnsBlockingAndroidDNSCacheNoData(),
dnsBlockingNXDOMAIN(),

dnsHijackingToProxyWithHTTPURL(),
dnsHijackingToProxyWithHTTPSURL(),

sucessWithHTTP(),
sucessWithHTTPS(),

Expand Down
3 changes: 3 additions & 0 deletions internal/netemx/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ const AddressDNSGoogle8888 = "8.8.8.8"
// blockpages to its users. As of 2023-09-04, this is the IP address resolving for thepiratebay.com when
// you're attempting to access this website from Italy.
const AddressPublicBlockpage = "83.224.65.41"

// ISPProxyAddress is the IP address of the ISP's HTTP transparent proxy.
const ISPProxyAddress = "130.192.182.17"
33 changes: 33 additions & 0 deletions internal/netemx/scenario.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package netemx

import (
"net/http"

"github.com/apex/log"
"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/testingx"
)

const (
// ScenarioRolePublicDNS means we should create DNS-over-HTTPS and DNS-over-UDP servers.
ScenarioRolePublicDNS = iota
Expand All @@ -18,6 +27,9 @@ const (

// ScenarioRoleBlockpageServer means we should serve a blockpage using HTTP.
ScenarioRoleBlockpageServer

// ScenarioRoleProxy means the host is a transparent proxy.
ScenarioRoleProxy
)

// ScenarioDomainAddresses describes a domain and address used in a scenario.
Expand Down Expand Up @@ -111,6 +123,13 @@ var InternetScenario = []*ScenarioDomainAddresses{{
},
Role: ScenarioRoleBlockpageServer,
WebServerFactory: BlockpageHandlerFactory(),
}, {
Domains: []string{},
Addresses: []string{
ISPProxyAddress,
},
Role: ScenarioRoleProxy,
WebServerFactory: nil,
}}

// MustNewScenario constructs a complete testing scenario using the domains and IP
Expand Down Expand Up @@ -175,6 +194,20 @@ func MustNewScenario(config []*ScenarioDomainAddresses) *QAEnv {
Ports: []int{80},
}))
}

case ScenarioRoleProxy:
for _, addr := range sad.Addresses {
opts = append(opts, QAEnvOptionNetStack(addr,
&HTTPCleartextServerFactory{
Factory: HTTPHandlerFactoryFunc(func(env NetStackServerFactoryEnv, stack *netem.UNetStack) http.Handler {
return testingx.HTTPHandlerProxy(env.Logger(), &netxlite.Netx{
Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: stack}})
}),
Ports: []int{80},
},
NewTLSProxyServerFactory(log.Log, 443),
))
}
}
}

Expand Down
88 changes: 88 additions & 0 deletions internal/netemx/tlsproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package netemx

import (
"io"
"net"
"sync"

"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
"github.com/ooni/probe-cli/v3/internal/testingx"
)

// NewTLSProxyServerFactory is a [NetStackServerFactory] for the TCP echo service.
func NewTLSProxyServerFactory(logger model.Logger, ports ...uint16) NetStackServerFactory {
return &tlsProxyServerFactory{
logger: logger,
ports: ports,
}
}

type tlsProxyServerFactory struct {
logger model.Logger
ports []uint16
}

// MustNewServer implements NetStackServerFactory.
func (f *tlsProxyServerFactory) MustNewServer(_ NetStackServerFactoryEnv, stack *netem.UNetStack) NetStackServer {
return &tlsProxyServer{
closers: []io.Closer{},
logger: f.logger,
mu: sync.Mutex{},
ports: f.ports,
unet: stack,
}
}

type tlsProxyServer struct {
closers []io.Closer
logger model.Logger
mu sync.Mutex
ports []uint16
unet *netem.UNetStack
}

// Close implements NetStackServer.
func (srv *tlsProxyServer) Close() error {
// "this method MUST be CONCURRENCY SAFE"
defer srv.mu.Unlock()
srv.mu.Lock()

// make sure we close all the child listeners
for _, closer := range srv.closers {
_ = closer.Close()
}

// "this method MUST be IDEMPOTENT"
srv.closers = []io.Closer{}

return nil
}

// MustStart implements NetStackServer.
func (srv *tlsProxyServer) MustStart() {
// "this method MUST be CONCURRENCY SAFE"
defer srv.mu.Unlock()
srv.mu.Lock()

// for each port of interest - note that here we panic liberally because we are
// allowed to do so by the [NetStackServer] documentation.
for _, port := range srv.ports {
// create the endpoint address
ipAddr := net.ParseIP(srv.unet.IPAddress())
runtimex.Assert(ipAddr != nil, "invalid IP address")
epnt := &net.TCPAddr{IP: ipAddr, Port: int(port)}

server := testingx.MustNewTLSSNIProxyEx(
srv.logger,
&netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: srv.unet}},
epnt,
srv.unet,
)

// track this server as something to close later
srv.closers = append(srv.closers, server)
}
}

0 comments on commit a379ecd

Please sign in to comment.