-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(netemx): HTTP3 server implementing NetStackServerFactory (#1224)
This diff continues improving and refactoring netemx with the objective of unifying how we create all kind of servers. Here, specifically, we modify the HTTPS server implementing NetStackServerFactory implemented in the previous commit and obtain an HTTP3 server honouring NetStackServerFactory. Crucially, this diff also adds support for overriding the TLS config passed to the server, which enables us to test for expired certificates, self-signed certificates, and so forth. While working on this diff, I noticed a weird behavior with HTTP/3 tests using the same address, which is documented at ooni/probe#2527. I modified the tests to make them pass. To this end, I changed the IP addresses used by HTTP/3 tests to avoid reusing www.example.com's IP address. It seems fine, for now, to merge this code, because HTTP/3 is not a cornerstone of how we measure, for now. But we should investigate further in the future! ## 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
- Loading branch information
1 parent
02730b5
commit 6b59b92
Showing
2 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package netemx | ||
|
||
import ( | ||
"crypto/tls" | ||
"io" | ||
"net" | ||
"net/http" | ||
"sync" | ||
|
||
"github.com/ooni/netem" | ||
"github.com/ooni/probe-cli/v3/internal/runtimex" | ||
"github.com/quic-go/quic-go/http3" | ||
) | ||
|
||
// HTTP3ServerFactory implements [NetStackServerFactory] for HTTP-over-TLS (i.e., HTTPS). | ||
type HTTP3ServerFactory struct { | ||
// Factory is the MANDATORY factory for creating the [http.Handler]. | ||
Factory HTTPHandlerFactory | ||
|
||
// Ports is the MANDATORY list of ports where to listen. | ||
Ports []int | ||
|
||
// TLSConfig is the OPTIONAL TLS config to use. | ||
TLSConfig *tls.Config | ||
} | ||
|
||
var _ NetStackServerFactory = &HTTP3ServerFactory{} | ||
|
||
// MustNewServer implements NetStackServerFactory. | ||
func (f *HTTP3ServerFactory) MustNewServer(stack *netem.UNetStack) NetStackServer { | ||
return &http3Server{ | ||
closers: []io.Closer{}, | ||
factory: f.Factory, | ||
mu: sync.Mutex{}, | ||
ports: f.Ports, | ||
tlsConfig: f.TLSConfig, | ||
unet: stack, | ||
} | ||
} | ||
|
||
type http3Server struct { | ||
closers []io.Closer | ||
factory HTTPHandlerFactory | ||
mu sync.Mutex | ||
ports []int | ||
tlsConfig *tls.Config | ||
unet *netem.UNetStack | ||
} | ||
|
||
// Close implements NetStackServer. | ||
func (srv *http3Server) Close() error { | ||
// make the method locked as requested by the documentation | ||
defer srv.mu.Unlock() | ||
srv.mu.Lock() | ||
|
||
// close each of the closers | ||
for _, closer := range srv.closers { | ||
_ = closer.Close() | ||
} | ||
|
||
// be idempotent | ||
srv.closers = []io.Closer{} | ||
return nil | ||
} | ||
|
||
// MustStart implements NetStackServer. | ||
func (srv *http3Server) MustStart() { | ||
// make the method locked as requested by the documentation | ||
defer srv.mu.Unlock() | ||
srv.mu.Lock() | ||
|
||
// create the handler | ||
handler := srv.factory.NewHandler() | ||
|
||
// create the listening address | ||
ipAddr := net.ParseIP(srv.unet.IPAddress()) | ||
runtimex.Assert(ipAddr != nil, "expected valid IP address") | ||
|
||
for _, port := range srv.ports { | ||
srv.mustListenPortLocked(handler, ipAddr, port) | ||
} | ||
} | ||
|
||
func (srv *http3Server) mustListenPortLocked(handler http.Handler, ipAddr net.IP, port int) { | ||
// create the listening socket | ||
addr := &net.UDPAddr{IP: ipAddr, Port: port} | ||
listener := runtimex.Try1(srv.unet.ListenUDP("udp", addr)) | ||
|
||
// use the netstack TLS config or the custom one configured by the user | ||
tlsConfig := srv.tlsConfig | ||
if tlsConfig == nil { | ||
tlsConfig = srv.unet.ServerTLSConfig() | ||
} else { | ||
tlsConfig = tlsConfig.Clone() | ||
} | ||
|
||
// serve requests in a background goroutine | ||
srvr := &http3.Server{ | ||
TLSConfig: tlsConfig, | ||
Handler: handler, | ||
} | ||
go srvr.Serve(listener) | ||
|
||
// make sure we track the server (the .Serve method will close the | ||
// listener once we close the server itself) | ||
srv.closers = append(srv.closers, srvr) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package netemx | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/apex/log" | ||
"github.com/google/go-cmp/cmp" | ||
"github.com/ooni/netem" | ||
"github.com/ooni/probe-cli/v3/internal/netxlite" | ||
"github.com/ooni/probe-cli/v3/internal/runtimex" | ||
) | ||
|
||
func TestHTTP3ServerFactory(t *testing.T) { | ||
t.Run("when using the TLSConfig provided by netem", func(t *testing.T) { | ||
/* | ||
__ ________________________ | ||
/ \ / \__ ___/\_ _____/ | ||
\ \/\/ / | | | __) | ||
\ / | | | \ | ||
\__/\ / |____| \___ / | ||
\/ \/ | ||
I originally wrote this test to use AddressWwwExampleCom and the test | ||
failed with generic_timeout_error. Now, instead, if I change it to use | ||
10.55.56.57, the test is working as intended. I am wondering whether | ||
I am not fully understanding how quic-go/quic-go works. | ||
My (limited?) understanding: just a single test can use AddressWwwExampleCom | ||
and, if I use it in other tests, there are issues leading to timeouts. | ||
See https://github.com/ooni/probe/issues/2527. | ||
*/ | ||
|
||
env := MustNewQAEnv( | ||
QAEnvOptionNetStack("10.55.56.57", &HTTP3ServerFactory{ | ||
Factory: HTTPHandlerFactoryFunc(func() http.Handler { | ||
return ExampleWebPageHandler() | ||
}), | ||
Ports: []int{443}, | ||
TLSConfig: nil, // explicitly nil, let's use netem's config | ||
}), | ||
) | ||
defer env.Close() | ||
|
||
env.AddRecordToAllResolvers("www.example.com", "", "10.55.56.57") | ||
|
||
env.Do(func() { | ||
client := netxlite.NewHTTP3ClientWithResolver(log.Log, netxlite.NewStdlibResolver(log.Log)) | ||
req := runtimex.Try1(http.NewRequest("GET", "https://www.example.com/", nil)) | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer resp.Body.Close() | ||
if resp.StatusCode != 200 { | ||
t.Fatal("unexpected StatusCode", resp.StatusCode) | ||
} | ||
data, err := netxlite.ReadAllContext(req.Context(), resp.Body) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if diff := cmp.Diff(ExampleWebPage, string(data)); diff != "" { | ||
t.Fatal(diff) | ||
} | ||
}) | ||
}) | ||
|
||
t.Run("when using an incompatible TLS config", func(t *testing.T) { | ||
/* | ||
__ ________________________ | ||
/ \ / \__ ___/\_ _____/ | ||
\ \/\/ / | | | __) | ||
\ / | | | \ | ||
\__/\ / |____| \___ / | ||
\/ \/ | ||
I originally wrote this test to use AddressWwwExampleCom and the test | ||
failed with generic_timeout_error. Now, instead, if I change it to use | ||
10.55.56.100, the test is working as intended. I am wondering whether | ||
I am not fully understanding how quic-go/quic-go works. | ||
My (limited?) understanding: just a single test can use AddressWwwExampleCom | ||
and, if I use it in other tests, there are issues leading to timeouts. | ||
See https://github.com/ooni/probe/issues/2527. | ||
*/ | ||
|
||
// we're creating a distinct MITM TLS config and we're using it, so we expect | ||
// that we're not able to verify certificates in client code | ||
mitmConfig := runtimex.Try1(netem.NewTLSMITMConfig()) | ||
|
||
env := MustNewQAEnv( | ||
QAEnvOptionNetStack("10.55.56.100", &HTTP3ServerFactory{ | ||
Factory: HTTPHandlerFactoryFunc(func() http.Handler { | ||
return ExampleWebPageHandler() | ||
}), | ||
Ports: []int{443}, | ||
TLSConfig: mitmConfig.TLSConfig(), // custom! | ||
}), | ||
) | ||
defer env.Close() | ||
|
||
env.AddRecordToAllResolvers("www.example.com", "", "10.55.56.100") | ||
|
||
env.Do(func() { | ||
client := netxlite.NewHTTP3ClientWithResolver(log.Log, netxlite.NewStdlibResolver(log.Log)) | ||
req := runtimex.Try1(http.NewRequest("GET", "https://www.example.com/", nil)) | ||
resp, err := client.Do(req) | ||
if err == nil || err.Error() != netxlite.FailureSSLInvalidCertificate { | ||
t.Fatal("unexpected error", err) | ||
} | ||
if resp != nil { | ||
t.Fatal("expected nil resp") | ||
} | ||
}) | ||
}) | ||
} |