Skip to content

Commit e146d99

Browse files
authored
feat: support for testing socks5 clients (#1280)
This diff imports a fork of github.com/armon/go-socks5 that has been adapted to use netem and simplified to suit our testing needs. With this functionality in tree, we can start thinking about writing better netem based tests for the ooniprobe bootstrap. The overall idea is to be well positioned to improve the bootstrap and introduce dynamic support for beacons. While there, discover that we were using `log.SetLevel(log.DebugLevel)` in a racy way in tests, so remove all the instances of this call from tests given that we can always add it when needed and we don't want to keep commented out code as a general policy anyway. Reference issue: ooni/probe#2531
1 parent b7bd9f0 commit e146d99

24 files changed

+1606
-22
lines changed

internal/legacy/netx/integration_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ func TestHTTPTransportWorkingAsIntended(t *testing.T) {
1515
if testing.Short() {
1616
t.Skip("skip test in short mode")
1717
}
18-
log.SetLevel(log.DebugLevel)
1918
counter := bytecounter.New()
2019
config := Config{
2120
BogonIsError: true,

internal/netemx/oohelperd_test.go

-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ func TestOOHelperDHandler(t *testing.T) {
3131
}
3232
thReqRaw := runtimex.Try1(json.Marshal(thReq))
3333

34-
//log.SetLevel(log.DebugLevel)
35-
3634
// TODO(https://github.com/ooni/probe/issues/2534): NewHTTPClientStdlib has QUIRKS but they're not needed here
3735
httpClient := netxlite.NewHTTPClientStdlib(log.Log)
3836

internal/testingproxy/hosthttp.go

-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ func (tc *hostNetworkTestCaseWithHTTP) Run(t *testing.T) {
4848
proxyServer := testingx.MustNewHTTPServer(testingx.NewHTTPProxyHandler(log.Log, netx))
4949
defer proxyServer.Close()
5050

51-
//log.SetLevel(log.DebugLevel)
52-
5351
// create an HTTP client configured to use the given proxy
5452
//
5553
// note how we use a dialer that asserts that we're using the proxy IP address

internal/testingproxy/hosthttps.go

-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ func (tc *hostNetworkTestCaseWithHTTPWithTLS) Run(t *testing.T) {
4949
proxyServer := testingx.MustNewHTTPServerTLS(testingx.NewHTTPProxyHandler(log.Log, netx))
5050
defer proxyServer.Close()
5151

52-
//log.SetLevel(log.DebugLevel)
53-
5452
// extend the default cert pool with the proxy's own CA
5553
pool := netxlite.NewMozillaCertPool()
5654
pool.AddCert(proxyServer.CACert)

internal/testingproxy/netemhttp.go

-2
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ func (tc *netemTestCaseWithHTTP) Run(t *testing.T) {
104104
// create the netx instance for the client
105105
netx := &netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: clientStack}}
106106

107-
//log.SetLevel(log.DebugLevel)
108-
109107
// create an HTTP client configured to use the given proxy
110108
//
111109
// note how we use a dialer that asserts that we're using the proxy IP address

internal/testingproxy/netemhttps.go

-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ func (tc *netemTestCaseWithHTTPWithTLS) Run(t *testing.T) {
105105
// create the netx instance for the client
106106
netx := &netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: clientStack}}
107107

108-
//log.SetLevel(log.DebugLevel)
109-
110108
// create an HTTP client configured to use the given proxy
111109
//
112110
// note how we use a dialer that asserts that we're using the proxy IP address

internal/testingproxy/qa_test.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,20 @@ import (
66
"github.com/ooni/probe-cli/v3/internal/testingproxy"
77
)
88

9-
func TestWorkingAsIntended(t *testing.T) {
10-
for _, testCase := range testingproxy.AllTestCases {
9+
func TestHTTPProxy(t *testing.T) {
10+
for _, testCase := range testingproxy.HTTPTestCases {
11+
t.Run(testCase.Name(), func(t *testing.T) {
12+
short := testCase.Short()
13+
if !short && testing.Short() {
14+
t.Skip("skip test in short mode")
15+
}
16+
testCase.Run(t)
17+
})
18+
}
19+
}
20+
21+
func TestSOCKSProxy(t *testing.T) {
22+
for _, testCase := range testingproxy.SOCKSTestCases {
1123
t.Run(testCase.Name(), func(t *testing.T) {
1224
short := testCase.Short()
1325
if !short && testing.Short() {

internal/testingproxy/sockshost.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package testingproxy
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/apex/log"
10+
"github.com/ooni/probe-cli/v3/internal/netxlite"
11+
"github.com/ooni/probe-cli/v3/internal/testingsocks5"
12+
)
13+
14+
// WithHostNetworkSOCKSProxyAndURL returns a [TestCase] where:
15+
//
16+
// - we fetch a URL;
17+
//
18+
// - using the host network;
19+
//
20+
// - and a SOCKS5 proxy.
21+
//
22+
// Because this [TestCase] uses the host network, it does not run in -short mode.
23+
func WithHostNetworkSOCKSProxyAndURL(URL string) TestCase {
24+
return &hostNetworkTestCaseWithSOCKS{
25+
TargetURL: URL,
26+
}
27+
}
28+
29+
type hostNetworkTestCaseWithSOCKS struct {
30+
TargetURL string
31+
}
32+
33+
var _ TestCase = &hostNetworkTestCaseWithSOCKS{}
34+
35+
// Name implements TestCase.
36+
func (tc *hostNetworkTestCaseWithSOCKS) Name() string {
37+
return fmt.Sprintf("fetching %s using the host network and a SOCKS5 proxy", tc.TargetURL)
38+
}
39+
40+
// Run implements TestCase.
41+
func (tc *hostNetworkTestCaseWithSOCKS) Run(t *testing.T) {
42+
// create an instance of Netx where the underlying network is nil,
43+
// which means we're using the host's network
44+
netx := &netxlite.Netx{Underlying: nil}
45+
46+
// create the proxy server using the host network
47+
endpoint := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}
48+
proxyServer := testingsocks5.MustNewServer(log.Log, netx, endpoint)
49+
defer proxyServer.Close()
50+
51+
// create an HTTP client configured to use the given proxy
52+
//
53+
// note how we use a dialer that asserts that we're using the proxy IP address
54+
// rather than the host address, so we're sure that we're using the proxy
55+
dialer := &dialerWithAssertions{
56+
ExpectAddress: "127.0.0.1",
57+
Dialer: netx.NewDialerWithResolver(log.Log, netx.NewStdlibResolver(log.Log)),
58+
}
59+
tlsDialer := netxlite.NewTLSDialer(dialer, netxlite.NewTLSHandshakerStdlib(log.Log))
60+
txp := netxlite.NewHTTPTransportWithOptions(log.Log, dialer, tlsDialer,
61+
netxlite.HTTPTransportOptionProxyURL(proxyServer.URL()))
62+
client := &http.Client{Transport: txp}
63+
defer client.CloseIdleConnections()
64+
65+
// get the homepage and assert we're getting a succesful response
66+
httpCheckResponse(t, client, tc.TargetURL)
67+
}
68+
69+
// Short implements TestCase.
70+
func (tc *hostNetworkTestCaseWithSOCKS) Short() bool {
71+
return false
72+
}

internal/testingproxy/socksnetem.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package testingproxy
2+
3+
import (
4+
"crypto/tls"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"testing"
9+
10+
"github.com/apex/log"
11+
"github.com/ooni/netem"
12+
"github.com/ooni/probe-cli/v3/internal/netxlite"
13+
"github.com/ooni/probe-cli/v3/internal/runtimex"
14+
"github.com/ooni/probe-cli/v3/internal/testingsocks5"
15+
"github.com/ooni/probe-cli/v3/internal/testingx"
16+
)
17+
18+
// WithNetemSOCKSProxyAndURL returns a [TestCase] where:
19+
//
20+
// - we fetch a URL;
21+
//
22+
// - using the github.com/ooni.netem;
23+
//
24+
// - and a SOCKS5 proxy.
25+
//
26+
// Because this [TestCase] uses netem, it also runs in -short mode.
27+
func WithNetemSOCKSProxyAndURL(URL string) TestCase {
28+
return &netemTestCaseWithSOCKS{
29+
TargetURL: URL,
30+
}
31+
}
32+
33+
type netemTestCaseWithSOCKS struct {
34+
TargetURL string
35+
}
36+
37+
var _ TestCase = &netemTestCaseWithSOCKS{}
38+
39+
// Name implements TestCase.
40+
func (tc *netemTestCaseWithSOCKS) Name() string {
41+
return fmt.Sprintf("fetching %s using netem and a SOCKS5 proxy", tc.TargetURL)
42+
}
43+
44+
// Run implements TestCase.
45+
func (tc *netemTestCaseWithSOCKS) Run(t *testing.T) {
46+
topology := runtimex.Try1(netem.NewStarTopology(log.Log))
47+
defer topology.Close()
48+
49+
const (
50+
wwwIPAddr = "93.184.216.34"
51+
proxyIPAddr = "10.0.0.1"
52+
clientIPAddr = "10.0.0.2"
53+
)
54+
55+
// create:
56+
//
57+
// - a www stack modeling www.example.com
58+
//
59+
// - a proxy stack
60+
//
61+
// - a client stack
62+
//
63+
// Note that www.example.com's IP address is also the resolver used by everyone
64+
wwwStack := runtimex.Try1(topology.AddHost(wwwIPAddr, wwwIPAddr, &netem.LinkConfig{}))
65+
proxyStack := runtimex.Try1(topology.AddHost(proxyIPAddr, wwwIPAddr, &netem.LinkConfig{}))
66+
clientStack := runtimex.Try1(topology.AddHost(clientIPAddr, wwwIPAddr, &netem.LinkConfig{}))
67+
68+
// configure the wwwStack as the DNS resolver with proper configuration
69+
dnsConfig := netem.NewDNSConfig()
70+
dnsConfig.AddRecord("www.example.com.", "", wwwIPAddr)
71+
dnsServer := runtimex.Try1(netem.NewDNSServer(log.Log, wwwStack, wwwIPAddr, dnsConfig))
72+
defer dnsServer.Close()
73+
74+
// configure the wwwStack to respond to HTTP requests on port 80
75+
wwwServer80 := testingx.MustNewHTTPServerEx(
76+
&net.TCPAddr{IP: net.ParseIP(wwwIPAddr), Port: 80},
77+
wwwStack,
78+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
79+
w.Write([]byte("Bonsoir, Elliot!\r\n"))
80+
}),
81+
)
82+
defer wwwServer80.Close()
83+
84+
// configure the wwwStack to respond to HTTPS requests on port 443
85+
wwwServer443 := testingx.MustNewHTTPServerTLSEx(
86+
&net.TCPAddr{IP: net.ParseIP(wwwIPAddr), Port: 443},
87+
wwwStack,
88+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
89+
w.Write([]byte("Bonsoir, Elliot!\r\n"))
90+
}),
91+
wwwStack,
92+
)
93+
defer wwwServer443.Close()
94+
95+
// configure the proxyStack to implement the SOCKS proxy on port 9050
96+
proxyServer := testingsocks5.MustNewServer(
97+
log.Log,
98+
&netxlite.Netx{
99+
Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: proxyStack}},
100+
&net.TCPAddr{IP: net.ParseIP(proxyIPAddr), Port: 9050},
101+
)
102+
defer proxyServer.Close()
103+
104+
// create the netx instance for the client
105+
netx := &netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: clientStack}}
106+
107+
// create an HTTP client configured to use the given proxy
108+
//
109+
// note how we use a dialer that asserts that we're using the proxy IP address
110+
// rather than the host address, so we're sure that we're using the proxy
111+
dialer := &dialerWithAssertions{
112+
ExpectAddress: proxyIPAddr,
113+
Dialer: netx.NewDialerWithResolver(log.Log, netx.NewStdlibResolver(log.Log)),
114+
}
115+
tlsDialer := netxlite.NewTLSDialer(dialer, netx.NewTLSHandshakerStdlib(log.Log))
116+
txp := netxlite.NewHTTPTransportWithOptions(log.Log, dialer, tlsDialer,
117+
netxlite.HTTPTransportOptionProxyURL(proxyServer.URL()),
118+
119+
// TODO(https://github.com/ooni/probe/issues/2536)
120+
netxlite.HTTPTransportOptionTLSClientConfig(&tls.Config{
121+
RootCAs: runtimex.Try1(clientStack.DefaultCertPool()),
122+
}),
123+
)
124+
client := &http.Client{Transport: txp}
125+
defer client.CloseIdleConnections()
126+
127+
// get the homepage and assert we're getting a succesful response
128+
httpCheckResponse(t, client, tc.TargetURL)
129+
}
130+
131+
// Short implements TestCase.
132+
func (tc *netemTestCaseWithSOCKS) Short() bool {
133+
return true
134+
}

internal/testingproxy/testcase.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,28 @@ type TestCase interface {
1414
Short() bool
1515
}
1616

17-
// AllTestCases contains all the test cases.
18-
var AllTestCases = []TestCase{
19-
// host network and HTTP proxy
17+
// SOCKSTestCases contains the SOCKS test cases.
18+
var SOCKSTestCases = []TestCase{
19+
// with host network
20+
WithHostNetworkSOCKSProxyAndURL("http://www.example.com/"),
21+
WithHostNetworkSOCKSProxyAndURL("https://www.example.com/"),
22+
23+
// with netem
24+
WithNetemSOCKSProxyAndURL("http://www.example.com/"),
25+
WithNetemSOCKSProxyAndURL("https://www.example.com/"),
26+
27+
// with netem and IPv4 addresses so we test another SOCKS5 dialing mode
28+
WithNetemSOCKSProxyAndURL("http://93.184.216.34/"),
29+
WithNetemSOCKSProxyAndURL("https://93.184.216.34/"),
30+
}
31+
32+
// HTTPTestCases contains the HTTP test cases.
33+
var HTTPTestCases = []TestCase{
34+
// with host network and HTTP proxy
2035
WithHostNetworkHTTPProxyAndURL("http://www.example.com/"),
2136
WithHostNetworkHTTPProxyAndURL("https://www.example.com/"),
2237

23-
// host network and HTTPS proxy
38+
// with host network and HTTPS proxy
2439
WithHostNetworkHTTPWithTLSProxyAndURL("http://www.example.com/"),
2540
WithHostNetworkHTTPWithTLSProxyAndURL("https://www.example.com/"),
2641

internal/testingsocks5/LICENSE

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2014 Armon Dadgar
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

0 commit comments

Comments
 (0)