feat(oohelperd): measure TLS for :443 endpoints (#886)
This diff improves oohelperd to measure :443 endpoints with TLS. Part of https://github.com/ooni/probe/issues/2237.
This commit is contained in:
parent
df0e099b73
commit
1e7384d1cc
|
@ -50,7 +50,11 @@ func dnsDo(ctx context.Context, config *dnsConfig) {
|
|||
addrs = []string{} // fix: the old test helper did that
|
||||
}
|
||||
failure := dnsMapFailure(newfailure(err))
|
||||
config.Out <- ctrlDNSResult{Failure: failure, Addrs: addrs}
|
||||
config.Out <- ctrlDNSResult{
|
||||
Failure: failure,
|
||||
Addrs: addrs,
|
||||
ASNs: []int64{}, // unused by the TH and not serialized
|
||||
}
|
||||
}
|
||||
|
||||
// dnsMapFailure attempts to map netxlite failures to the strings
|
||||
|
|
|
@ -29,6 +29,9 @@ type handler struct {
|
|||
|
||||
// NewResolver is the MANDATORY factory for creating a new resolver.
|
||||
NewResolver func() model.Resolver
|
||||
|
||||
// NewTLSHandshaker is the MANDATORY factory for creating a new TLS handshaker.
|
||||
NewTLSHandshaker func() model.TLSHandshaker
|
||||
}
|
||||
|
||||
var _ http.Handler = &handler{}
|
||||
|
|
|
@ -63,6 +63,9 @@ func TestHandlerWorkingAsIntended(t *testing.T) {
|
|||
NewResolver: func() model.Resolver {
|
||||
return netxlite.NewUnwrappedStdlibResolver()
|
||||
},
|
||||
NewTLSHandshaker: func() model.TLSHandshaker {
|
||||
return netxlite.NewTLSHandshakerStdlib(model.DiscardLogger)
|
||||
},
|
||||
}
|
||||
srv := httptest.NewServer(handler)
|
||||
defer srv.Close()
|
||||
|
@ -78,23 +81,35 @@ func TestHandlerWorkingAsIntended(t *testing.T) {
|
|||
expectations := []expectationSpec{{
|
||||
name: "check for invalid method",
|
||||
reqMethod: "GET",
|
||||
reqContentType: "",
|
||||
reqBody: "",
|
||||
respStatusCode: 400,
|
||||
respContentType: "",
|
||||
parseBody: false,
|
||||
}, {
|
||||
name: "check for invalid content-type",
|
||||
reqMethod: "POST",
|
||||
reqContentType: "",
|
||||
reqBody: "",
|
||||
respStatusCode: 400,
|
||||
respContentType: "",
|
||||
parseBody: false,
|
||||
}, {
|
||||
name: "check for invalid request body",
|
||||
reqMethod: "POST",
|
||||
reqContentType: "application/json",
|
||||
reqBody: "{",
|
||||
respStatusCode: 400,
|
||||
respContentType: "",
|
||||
parseBody: false,
|
||||
}, {
|
||||
name: "with measurement failure",
|
||||
reqMethod: "POST",
|
||||
reqContentType: "application/json",
|
||||
reqBody: `{"http_request": "http://[::1]aaaa"}`,
|
||||
respStatusCode: 400,
|
||||
respContentType: "",
|
||||
parseBody: false,
|
||||
}, {
|
||||
name: "with reasonably good request",
|
||||
reqMethod: "POST",
|
||||
|
|
|
@ -18,6 +18,9 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/tracex"
|
||||
)
|
||||
|
||||
// TODO(bassosimone): we should refactor the TH to use step-by-step such that we
|
||||
// can use an existing connection for the HTTP-measuring task
|
||||
|
||||
// ctrlHTTPResponse is the result of the HTTP check performed by
|
||||
// the Web Connectivity test helper.
|
||||
type ctrlHTTPResponse = webconnectivity.ControlHTTPRequestResult
|
||||
|
@ -51,11 +54,13 @@ func httpDo(ctx context.Context, config *httpConfig) {
|
|||
defer config.Wg.Done()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", config.URL, nil)
|
||||
if err != nil {
|
||||
config.Out <- ctrlHTTPResponse{ // fix: emit -1 like the old test helper does
|
||||
// fix: emit -1 like the old test helper does
|
||||
config.Out <- ctrlHTTPResponse{
|
||||
BodyLength: -1,
|
||||
Failure: httpMapFailure(err),
|
||||
StatusCode: -1,
|
||||
Title: "",
|
||||
Headers: map[string]string{},
|
||||
StatusCode: -1,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -73,11 +78,13 @@ func httpDo(ctx context.Context, config *httpConfig) {
|
|||
defer clnt.CloseIdleConnections()
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
config.Out <- ctrlHTTPResponse{ // fix: emit -1 like old test helper does
|
||||
// fix: emit -1 like the old test helper does
|
||||
config.Out <- ctrlHTTPResponse{
|
||||
BodyLength: -1,
|
||||
Failure: httpMapFailure(err),
|
||||
StatusCode: -1,
|
||||
Title: "",
|
||||
Headers: map[string]string{},
|
||||
StatusCode: -1,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -52,6 +52,9 @@ type endpointInfo struct {
|
|||
|
||||
// Epnt is the endpoint to measure
|
||||
Epnt string
|
||||
|
||||
// TLS indicates whether we should try using TLS
|
||||
TLS bool
|
||||
}
|
||||
|
||||
// ipInfoToEndpoints takes in input the [ipinfo] returned by newIPInfo
|
||||
|
@ -86,6 +89,7 @@ func ipInfoToEndpoints(URL *url.URL, ipinfo map[string]*webconnectivity.ControlI
|
|||
out = append(out, endpointInfo{
|
||||
Addr: addr,
|
||||
Epnt: epnt,
|
||||
TLS: port == "443",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,15 +142,19 @@ func Test_ipInfoToEndpoints(t *testing.T) {
|
|||
want: []endpointInfo{{
|
||||
Addr: "8.8.4.4",
|
||||
Epnt: "8.8.4.4:443",
|
||||
TLS: true,
|
||||
}, {
|
||||
Addr: "8.8.4.4",
|
||||
Epnt: "8.8.4.4:80",
|
||||
TLS: false,
|
||||
}, {
|
||||
Addr: "8.8.8.8",
|
||||
Epnt: "8.8.8.8:443",
|
||||
TLS: true,
|
||||
}, {
|
||||
Addr: "8.8.8.8",
|
||||
Epnt: "8.8.8.8:80",
|
||||
TLS: false,
|
||||
}},
|
||||
}, {
|
||||
name: "with bogons and explicit port",
|
||||
|
@ -176,9 +180,11 @@ func Test_ipInfoToEndpoints(t *testing.T) {
|
|||
want: []endpointInfo{{
|
||||
Addr: "8.8.4.4",
|
||||
Epnt: "8.8.4.4:5432",
|
||||
TLS: false,
|
||||
}, {
|
||||
Addr: "8.8.8.8",
|
||||
Epnt: "8.8.8.8:5432",
|
||||
TLS: false,
|
||||
}},
|
||||
}, {
|
||||
name: "with addresses and some bogons, no port, and unknown scheme",
|
||||
|
@ -224,9 +230,11 @@ func Test_ipInfoToEndpoints(t *testing.T) {
|
|||
want: []endpointInfo{{
|
||||
Addr: "8.8.4.4",
|
||||
Epnt: "8.8.4.4:443",
|
||||
TLS: true,
|
||||
}, {
|
||||
Addr: "8.8.8.8",
|
||||
Epnt: "8.8.8.8:443",
|
||||
TLS: true,
|
||||
}},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -62,6 +62,9 @@ func main() {
|
|||
return netxlite.NewDialerWithResolver(log.Log, newResolver())
|
||||
},
|
||||
NewResolver: newResolver,
|
||||
NewTLSHandshaker: func() model.TLSHandshaker {
|
||||
return netxlite.NewTLSHandshakerStdlib(log.Log)
|
||||
},
|
||||
})
|
||||
srv := &http.Server{Addr: *endpoint, Handler: mux}
|
||||
listener, err := net.Listen("tcp", *endpoint)
|
||||
|
|
|
@ -47,7 +47,13 @@ func measure(ctx context.Context, config *handler, creq *ctrlRequest) (*ctrlResp
|
|||
wg.Wait()
|
||||
|
||||
// start assembling the response
|
||||
cresp := &ctrlResponse{}
|
||||
cresp := &ctrlResponse{
|
||||
TCPConnect: map[string]webconnectivity.ControlTCPConnectResult{},
|
||||
TLSHandshake: map[string]webconnectivity.ControlTLSHandshakeResult{},
|
||||
HTTPRequest: webconnectivity.ControlHTTPRequestResult{},
|
||||
DNS: webconnectivity.ControlDNSResult{},
|
||||
IPInfo: map[string]*webconnectivity.ControlIPInfo{},
|
||||
}
|
||||
select {
|
||||
case cresp.DNS = <-dnsch:
|
||||
default:
|
||||
|
@ -56,6 +62,7 @@ func measure(ctx context.Context, config *handler, creq *ctrlRequest) (*ctrlResp
|
|||
cresp.DNS = ctrlDNSResult{
|
||||
Failure: nil,
|
||||
Addrs: []string{},
|
||||
ASNs: []int64{}, // unused by the TH and not serialized
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,8 +76,11 @@ func measure(ctx context.Context, config *handler, creq *ctrlRequest) (*ctrlResp
|
|||
wg.Add(1)
|
||||
go tcpDo(ctx, &tcpConfig{
|
||||
Address: endpoint.Addr,
|
||||
EnableTLS: endpoint.TLS,
|
||||
Endpoint: endpoint.Epnt,
|
||||
NewDialer: config.NewDialer,
|
||||
NewTSLHandshaker: config.NewTLSHandshaker,
|
||||
URLHostname: URL.Hostname(),
|
||||
Out: tcpconnch,
|
||||
Wg: wg,
|
||||
})
|
||||
|
@ -93,12 +103,17 @@ func measure(ctx context.Context, config *handler, creq *ctrlRequest) (*ctrlResp
|
|||
|
||||
// continue assembling the response
|
||||
cresp.HTTPRequest = <-httpch
|
||||
cresp.TCPConnect = make(map[string]ctrlTCPResult)
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case tcpconn := <-tcpconnch:
|
||||
cresp.TCPConnect[tcpconn.Endpoint] = tcpconn.TCP
|
||||
if tcpconn.TLS != nil {
|
||||
cresp.TLSHandshake[tcpconn.Endpoint] = *tcpconn.TLS
|
||||
if info := cresp.IPInfo[tcpconn.Address]; info != nil && tcpconn.TLS.Failure == nil {
|
||||
info.Flags |= webconnectivity.ControlIPInfoFlagValidForDomain
|
||||
}
|
||||
}
|
||||
default:
|
||||
break Loop
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package main
|
||||
|
||||
//
|
||||
// TCP connect measurements
|
||||
// TCP connect (and optionally TLS handshake) measurements
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -18,6 +19,9 @@ import (
|
|||
// ctrlTCPResult is the result of the TCP check performed by the test helper.
|
||||
type ctrlTCPResult = webconnectivity.ControlTCPConnectResult
|
||||
|
||||
// ctrlTLSResult is the result of the TLS check performed by the test helper.
|
||||
type ctrlTLSResult = webconnectivity.ControlTLSHandshakeResult
|
||||
|
||||
// tcpResultPair contains the endpoint and the corresponding result.
|
||||
type tcpResultPair struct {
|
||||
// Address is the IP address we measured.
|
||||
|
@ -28,6 +32,9 @@ type tcpResultPair struct {
|
|||
|
||||
// TCP contains the TCP results.
|
||||
TCP ctrlTCPResult
|
||||
|
||||
// TLS contains the TLS results
|
||||
TLS *ctrlTLSResult
|
||||
}
|
||||
|
||||
// tcpConfig configures the TCP connect check.
|
||||
|
@ -35,22 +42,31 @@ type tcpConfig struct {
|
|||
// Address is the MANDATORY address to measure.
|
||||
Address string
|
||||
|
||||
// EnableTLS OPTIONALLY enables TLS.
|
||||
EnableTLS bool
|
||||
|
||||
// Endpoint is the MANDATORY endpoint to connect to.
|
||||
Endpoint string
|
||||
|
||||
// NewDialer is the MANDATORY factory for creating a new dialer.
|
||||
NewDialer func() model.Dialer
|
||||
|
||||
// NewTSLHandshaker is the MANDATORY factory for creating a new handshaker.
|
||||
NewTSLHandshaker func() model.TLSHandshaker
|
||||
|
||||
// Out is the MANDATORY where we'll post the TCP measurement results.
|
||||
Out chan *tcpResultPair
|
||||
|
||||
// URLHostname is the MANDATORY URL.Hostname() to use.
|
||||
URLHostname string
|
||||
|
||||
// Wg is MANDATORY and is used to sync with the parent.
|
||||
Wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// tcpDo performs the TCP check.
|
||||
func tcpDo(ctx context.Context, config *tcpConfig) {
|
||||
const timeout = 10 * time.Second
|
||||
const timeout = 15 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
defer config.Wg.Done()
|
||||
|
@ -58,6 +74,7 @@ func tcpDo(ctx context.Context, config *tcpConfig) {
|
|||
Address: config.Address,
|
||||
Endpoint: config.Endpoint,
|
||||
TCP: webconnectivity.ControlTCPConnectResult{},
|
||||
TLS: nil, // means: not measured
|
||||
}
|
||||
defer func() {
|
||||
config.Out <- out
|
||||
|
@ -67,7 +84,23 @@ func tcpDo(ctx context.Context, config *tcpConfig) {
|
|||
conn, err := dialer.DialContext(ctx, "tcp", config.Endpoint)
|
||||
out.TCP.Failure = tcpMapFailure(newfailure(err))
|
||||
out.TCP.Status = err == nil
|
||||
measurexlite.MaybeClose(conn)
|
||||
defer measurexlite.MaybeClose(conn)
|
||||
if err != nil || !config.EnableTLS {
|
||||
return
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
RootCAs: netxlite.NewDefaultCertPool(),
|
||||
ServerName: config.URLHostname,
|
||||
}
|
||||
thx := config.NewTSLHandshaker()
|
||||
tlsConn, _, err := thx.Handshake(ctx, conn, tlsConfig)
|
||||
out.TLS = &ctrlTLSResult{
|
||||
ServerName: config.URLHostname,
|
||||
Status: err == nil,
|
||||
Failure: newfailure(err),
|
||||
}
|
||||
measurexlite.MaybeClose(tlsConn)
|
||||
}
|
||||
|
||||
// tcpMapFailure attempts to map netxlite failures to the strings
|
||||
|
|
|
@ -9,6 +9,9 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||
)
|
||||
|
||||
// TODO(bassosimone): these struct definitions should be moved outside the
|
||||
// specific implementation of Web Connectivity v0.4.
|
||||
|
||||
// ControlRequest is the request that we send to the control
|
||||
type ControlRequest struct {
|
||||
HTTPRequest string `json:"http_request"`
|
||||
|
@ -23,6 +26,14 @@ type ControlTCPConnectResult struct {
|
|||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
// ControlTLSHandshakeResult is the result of the TLS handshake
|
||||
// attempt performed by the control vantage point.
|
||||
type ControlTLSHandshakeResult struct {
|
||||
ServerName string `json:"server_name"`
|
||||
Status bool `json:"status"`
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
// ControlHTTPRequestResult is the result of the HTTP request
|
||||
// performed by the control vantage point.
|
||||
type ControlHTTPRequestResult struct {
|
||||
|
@ -67,11 +78,16 @@ const (
|
|||
|
||||
// ControlIPInfoFlagIsBogon indicates that the address is a bogon
|
||||
ControlIPInfoFlagIsBogon
|
||||
|
||||
// ControlIPInfoFlagValidForDomain indicates that an IP address
|
||||
// is valid for the domain because it works with TLS
|
||||
ControlIPInfoFlagValidForDomain
|
||||
)
|
||||
|
||||
// ControlResponse is the response from the control service.
|
||||
type ControlResponse struct {
|
||||
TCPConnect map[string]ControlTCPConnectResult `json:"tcp_connect"`
|
||||
TLSHandshake map[string]ControlTLSHandshakeResult `json:"tls_handshake"`
|
||||
HTTPRequest ControlHTTPRequestResult `json:"http_request"`
|
||||
DNS ControlDNSResult `json:"dns"`
|
||||
IPInfo map[string]*ControlIPInfo `json:"ip_info"`
|
||||
|
|
Loading…
Reference in New Issue
Block a user