Skip to content

Commit

Permalink
feat: add the experimental netsim package (#13)
Browse files Browse the repository at this point in the history
Like https://github.com/ooni/netem, this package allows writing
integration tests and simulating censorship conditions.

However, netsim, which has been written from scratch, does not depend on
gvisor, thus significantly reducing the burden of integrating the
package itself.

Additionally, netsim has much less emphasis on simulating delays and
losses and, more pragmatically, just focuses on allowing to simulate
cases of censorship involving blocking.

The intention is to use this package in rbmk-project/rbmk to write
integration tests for censorship conditions.
  • Loading branch information
bassosimone authored Nov 21, 2024
1 parent 22d5fa1 commit 9b55346
Show file tree
Hide file tree
Showing 22 changed files with 1,845 additions and 0 deletions.
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
module github.com/rbmk-project/x

go 1.23.3

require (
github.com/miekg/dns v1.1.62
github.com/rbmk-project/common v0.3.0
github.com/rogpeppe/go-internal v1.13.1
golang.org/x/sys v0.27.0
)

require (
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/rbmk-project/common v0.3.0 h1:g9iX/lg5kvzmgxgr4xbkcCjA2jfazM1zAyLKzLD/3iU=
github.com/rbmk-project/common v0.3.0/go.mod h1:uzrFIJl8SEOpgS2pSeBFLUgqc4D1lIcGk/EYuxkFO0U=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
34 changes: 34 additions & 0 deletions netsim/addr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// net.Addr implementation.
//

package netsim

import (
"net"
"net/netip"
)

// Addr represents a TCP/UDP address.
type Addr struct {
// AddrPort is the endpoint address and port.
AddrPort netip.AddrPort

// Protocol is the endpoint protocol.
Protocol IPProtocol
}

// Ensure [*Addr] implements [net.Addr].
var _ net.Addr = &Addr{}

// Network implements [net.Addr].
func (sa *Addr) Network() string {
return sa.Protocol.String()
}

// String implements [net.Addr].
func (sa *Addr) String() string {
return sa.AddrPort.String()
}
84 changes: 84 additions & 0 deletions netsim/deadline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// SPDX-License-Identifier: BSD-3-Clause
//
// Adapted from: https://go.dev/src/net/pipe.go
//
// Deadline management.
//

package netsim

import (
"sync"
"time"
)

// deadline is an abstraction for handling timeouts.
type deadline struct {
mu sync.Mutex // Guards timer and cancel
timer *time.Timer
cancel chan struct{} // Must be non-nil
}

// newDeadline creates a new [*deadline] instance.
func newDeadline() *deadline {
return &deadline{cancel: make(chan struct{})}
}

// Set sets the point in time when the deadline will time out.
// A timeout event is signaled by closing the channel returned by waiter.
// Once a timeout has occurred, the deadline can be refreshed by specifying a
// t value in the future.
//
// A zero value for t prevents timeout.
func (d *deadline) Set(t time.Time) {
d.mu.Lock()
defer d.mu.Unlock()

if d.timer != nil && !d.timer.Stop() {
<-d.cancel // Wait for the timer callback to finish and close cancel
}
d.timer = nil

// Time is zero, then there is no deadline.
closed := isClosedChan(d.cancel)
if t.IsZero() {
if closed {
d.cancel = make(chan struct{})
}
return
}

// Time in the future, setup a timer to cancel in the future.
if dur := time.Until(t); dur > 0 {
if closed {
d.cancel = make(chan struct{})
}
d.timer = time.AfterFunc(dur, func() {
close(d.cancel)
})
return
}

// Time in the past, so close immediately.
if !closed {
close(d.cancel)
}
}

// wait returns a channel that is closed when the deadline is exceeded.
func (d *deadline) Wait() chan struct{} {
d.mu.Lock()
defer d.mu.Unlock()
return d.cancel
}

// isClosedChan returns whether a channel is closed.
func isClosedChan(c <-chan struct{}) bool {
select {
case <-c:
return true
default:
return false
}
}
49 changes: 49 additions & 0 deletions netsim/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-3.0-or-later

/*
Package netsim provides a simple network simulation framework
that developers can use to write integration tests.
# Usage and Features
The [NewStack] function creates a new, simulated network stack
using a given IP address. You can invoke usual functions on the
stack, such as:
- DialContext
- Listen
- ListenPacket
These functions return simulated [net.Conn], [net.Listener], and
[net.PacketConn] respectively.
When a connection sends data, the data is wrapped inside a [*Packet]
emitted on the channel returned by [*Stack.Output]. The [*Link]
type allows connecting two [*Stack] such that they can send [*Packet]
to each other. To send a [*Packet] to a [*Stack], you need to post
the packet on the channel returned by [*Stack.Input]. You don't need
to use a [*Link] as long as you correctly forward packets. In fact,
for simulating complex censorship scenarios, you probably want to
write custom code to forward or drop [*Packet]. In the future, there
will be subpackages of [netsim] providing this functionality.
Subpackages of this package contain extensions. For example, the
[netsim/simpki] package code helps to simulate a PKI.
The implementation of [net.Conn], [net.Listener], and [net.PacketConn] are
[*TCPConn], [*UDPConn], and [*UDPListener]. These types, which can also
be created manually, are tiny wrappers around [*Port], which contains most
of the common implementation code. These types are public to enable writing
more complex tests (e.g., the sending of unexpected TCP flags).
The errors returned by these types are the same [syscall.Errno] the
standard library and the kernel would generate in similar cases (we use
the [x/sys] repository to pull system-dependent error values).
This package contains comprehensive examples showing how to use it.
# Design Documents
This package is experimental and has no design documents for now.
*/
package netsim
40 changes: 40 additions & 0 deletions netsim/errno_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//go:build unix

//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// UNIX errno definitions.
//

package netsim

import "golang.org/x/sys/unix"

const (
// EADDRINUSE is the address in use error.
EADDRINUSE = unix.EADDRINUSE

// ECONNABORTED is the connection aborted error.
ECONNABORTED = unix.ECONNABORTED

// ECONNRESET is the connection reset by peer error.
ECONNRESET = unix.ECONNRESET

// EHOSTUNREACH is the host unreachable error.
EHOSTUNREACH = unix.EHOSTUNREACH

// EINVAL is the invalid argument error.
EINVAL = unix.EINVAL

// ENETDOWN is the network is down error.
ENETDOWN = unix.ENETDOWN

// ENOBUFS is the no buffer space available error.
ENOBUFS = unix.ENOBUFS

// ENOTCONN is the not connected error.
ENOTCONN = unix.ENOTCONN

// EPROTONOSUPPORT is the protocol not supported error.
EPROTONOSUPPORT = unix.EPROTONOSUPPORT
)
40 changes: 40 additions & 0 deletions netsim/errno_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//go:build windows

//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Windows errno definitions.
//

package netsim

import "golang.org/x/sys/windows"

const (
// EADDRINUSE is the address in use error.
EADDRINUSE = windows.WSAEADDRINUSE

// ECONNABORTED is the connection aborted error.
ECONNABORTED = windows.WSAECONNABORTED

// ECONNRESET is the connection reset by peer error.
ECONNRESET = windows.WSAECONNRESET

// EHOSTUNREACH is the host unreachable error.
EHOSTUNREACH = windows.WSAEHOSTUNREACH

// EINVAL is the invalid argument error.
EINVAL = windows.WSAEINVAL

// ENETDOWN is the network is down error.
ENETDOWN = windows.WSAENETDOWN

// ENOBUFS is the no buffer space available error.
ENOBUFS = windows.WSAENOBUFS

// ENOTCONN is the not connected error.
ENOTCONN = windows.WSAENOTCONN

// EPROTONOSUPPORT is the protocol not supported error.
EPROTONOSUPPORT = windows.WSAEPROTONOSUPPORT
)
111 changes: 111 additions & 0 deletions netsim/example_dns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package netsim_test

import (
"context"
"fmt"
"log"
"net"
"net/netip"
"time"

"github.com/miekg/dns"
"github.com/rbmk-project/x/connpool"
"github.com/rbmk-project/x/netsim"
)

// This example shows how to use [netsim] to simulate a DNS
// server that listens for incoming requests over UDP.
func Example_dnsOverUDP() {
// Create a pool to close resources when done.
cpool := connpool.New()
defer cpool.Close()

// Create the server stack.
serverAddr := netip.MustParseAddr("8.8.8.8")
serverStack := netsim.NewStack(serverAddr)
cpool.Add(serverStack)

// Create the client stack.
clientAddr := netip.MustParseAddr("130.192.91.211")
clientStack := netsim.NewStack(clientAddr)
cpool.Add(clientStack)

// Link the client and the server stacks.
link := netsim.NewLink(clientStack, serverStack)
cpool.Add(link)

// Create a context with a watchdog timeout.
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

// Create the server UDP listener.
serverEndpoint := netip.AddrPortFrom(serverAddr, 53)
serverConn, err := serverStack.ListenPacket(ctx, "udp", serverEndpoint.String())
if err != nil {
log.Fatal(err)
}
cpool.Add(serverConn)

// Start the server in the background.
serverDNS := &dns.Server{
PacketConn: serverConn,
Handler: dns.HandlerFunc(func(rw dns.ResponseWriter, query *dns.Msg) {
resp := &dns.Msg{}
resp.SetReply(query)
resp.Answer = append(resp.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: "dns.google.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 3600,
Rdlength: 0,
},
A: net.IPv4(8, 8, 8, 8),
})
if err := rw.WriteMsg(resp); err != nil {
log.Fatal(err)
}
}),
}
go serverDNS.ActivateAndServe()
defer serverDNS.Shutdown()

// Create the client connection with the DNS server.
conn, err := clientStack.DialContext(ctx, "udp", serverEndpoint.String())
if err != nil {
log.Fatal(err)
}
cpool.Add(conn)

// Create the query to send
query := new(dns.Msg)
query.Id = dns.Id()
query.RecursionDesired = true
query.Question = []dns.Question{{
Name: "dns.google.",
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}}

// Perform the DNS round trip
clientDNS := &dns.Client{}
resp, _, err := clientDNS.ExchangeWithConnContext(ctx, query, &dns.Conn{Conn: conn})
if err != nil {
log.Fatal(err)
}

// Print the responses
for _, ans := range resp.Answer {
if a, ok := ans.(*dns.A); ok {
fmt.Printf("%s\n", a.A.String())
}
}

// Explicitly close the connections
cpool.Close()

// Output:
// 8.8.8.8
}
Loading

0 comments on commit 9b55346

Please sign in to comment.