Skip to content

Commit

Permalink
feat(netemx): HTTP3 server implementing NetStackServerFactory (#1224)
Browse files Browse the repository at this point in the history
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
bassosimone committed Sep 4, 2023
1 parent 02730b5 commit 6b59b92
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 0 deletions.
107 changes: 107 additions & 0 deletions internal/netemx/http3.go
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)
}
118 changes: 118 additions & 0 deletions internal/netemx/http3_test.go
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")
}
})
})
}

0 comments on commit 6b59b92

Please sign in to comment.