-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add the experimental netsim package (#13)
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
1 parent
22d5fa1
commit 9b55346
Showing
22 changed files
with
1,845 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 |
---|---|---|
@@ -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 | ||
) |
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,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= |
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,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() | ||
} |
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,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 | ||
} | ||
} |
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,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 |
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,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 | ||
) |
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,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 | ||
) |
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,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 | ||
} |
Oops, something went wrong.