Netem allows writing integration tests in Go where networking code uses Gvisor-based networking. Netem also includes primitives to emulate link latency, losses, and internet censorship (null routing, SNI-based blocking, throttling). Using netem, one can easily simulate complex integration testing scenarios involving difficult or adversarial networks.
We currently support go1.20.
To add netem as a dependency, run:
go get -u -v -d github.com/ooni/netem
This command will download netem and update your go.mod
and go.sum
.
You probably also want to manually force using the Gvisor version we're using in this library with:
go get -u -v -d gvisor.dev/gvisor@COMMIT_HASH
because Gvisor's default branch is not
ready to be used with Go tools and go get
would misbehave.
When updating Gvisor in this library, make sure you pin to a commit from the go branch, which is the Gvisor branch supporting go tools.
go test .
To enable the race detector, run:
go test -race .
Note: we notice that the race detector would be very slow under macOS and many tests will fail; it still seems to be fine under Linux.
TODO(bassosimone): this section needs to be updated because we have
recently removed the stdlib.go
file and functionality, since we have
much better functionality inside of ooni/probe-cli.
Existing Go code needs to be adjusted to support netem.
Suppose you have this Go code:
func yourCode(ctx context.Context) error {
addrs, err := net.DefaultResolver.LookupHost(ctx, "www.example.com")
// ...
}
You need to convert this code to use netem:
func yourCode(ctx context.Context, nn *netem.Net) error {
addrs, err := nn.LookupHost(ctx, "www.example.com")
// ...
}
Normally, you would create a netem.Net like this:
nn := &netem.Net{
Stack: &netem.Stdlib{},
}
Your code will still work as intended. But, now you have the
option to replace the Net
underlying stack with an userspace
TCP/IP network stack, for writing integration tests.
Let us do that. We start by creating a StarTopology:
topology, err := netem.NewStarTopology(&netem.NullLogger{})
if err != nil { /* ... */ }
defer topology.Close()
Then, we use AddHost to add two userspace network stacks to such a topology:
clientStack, err := netem.AddHost(
"1.2.3.4", // stack IPv4 address
"5.4.3.2", // resolver IPv4 address
&netem.LinkConfig{}, // link with no delay, losses, or DPI
)
if err != nil { /* ... */ }
serverStack, err := netem.AddHost(
"5.4.3.2", // stack IPv4 address
"5.4.3.2", // resolver IPv4 address
&netem.LinkConfig{}, // link with no delay, losses, or DPI
)
if err != nil { /* ... */ }
We now have the following topology:
graph TD
client[clientStack<br>1.2.3.4]---router{Router}
server[serverStack<br>5.4.3.2]---router
Now, we can create a DNSServer
on 5.4.3.2
as follows:
dnsCfg := netem.NewDNSConfig()
dnsCfg.AddRecord(
"www.example.com",
"", // empty CNAME
"5.6.7.8",
)
dnsServer, err := netem.NewDNSServer(
&netem.NullLogger{},
serverStack,
"5.4.3.2",
dnsCfg,
)
if err != nil { /* ... */ }
Finally, we create a netem.Net as follows:
nn2 := &netem.Net{
Stack: clientStack,
}
and we can test yourCode
as follows:
func TestYourCode(t *testing.T) {
// ... create nn2 ...
err := yourCode(context.Background(), nn2)
if err != nil {
t.Fatal(err)
}
}
This test will test your code using the above network stacks and topology.