Skip to content

Instantly share code, notes, and snippets.

@bassosimone
Last active February 28, 2023 10:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bassosimone/3ce52a37fc7394f7cce82391685c5477 to your computer and use it in GitHub Desktop.
Save bassosimone/3ce52a37fc7394f7cce82391685c5477 to your computer and use it in GitHub Desktop.
diff --git a/go.mod b/go.mod
index a2f47e9b..fe20d3f1 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
github.com/cretz/bine v0.2.0
github.com/fatih/color v1.13.0
github.com/google/go-cmp v0.5.9
+ github.com/google/gopacket v1.1.19-0.20200831200443-df1bbd09a561
github.com/google/martian/v3 v3.3.2
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.3.0
@@ -38,9 +39,11 @@ require (
github.com/upper/db/v4 v4.6.0
gitlab.com/yawning/obfs4.git v0.0.0-20220904064028-336a71d6e4cf
gitlab.com/yawning/utls.git v0.0.12-1
- golang.org/x/crypto v0.5.0
+ golang.org/x/crypto v0.6.0
golang.org/x/net v0.7.0
golang.org/x/sys v0.5.0
+ golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd
+ gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0
)
require (
@@ -52,6 +55,7 @@ require (
github.com/Psiphon-Labs/tls-tris v0.0.0-20210713133851-676a693d51ad // indirect
github.com/andybalholm/brotli v1.0.5-0.20220518190645-786ec621f618 // indirect
github.com/golang/mock v1.6.0 // indirect
+ github.com/google/btree v1.0.1 // indirect
github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.15.14 // indirect
@@ -60,6 +64,8 @@ require (
github.com/segmentio/fasthash v1.0.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 // indirect
+ golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
+ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
)
require (
diff --git a/go.sum b/go.sum
index 87b4802b..c89021e8 100644
--- a/go.sum
+++ b/go.sum
@@ -52,8 +52,8 @@ github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
@@ -301,6 +301,8 @@ github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -317,6 +319,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19-0.20200831200443-df1bbd09a561 h1:VB5cLlMqQWruyqG6OW/EHDLUawT/hel1I3ElBE4iHg0=
+github.com/google/gopacket v1.1.19-0.20200831200443-df1bbd09a561/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 h1:OL2d27ueTKnlQJoqLW2fc9pWYulFnJYLWzomGV7HqZo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -972,8 +975,9 @@ golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1217,6 +1221,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1291,6 +1296,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
+golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
+golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd h1:thMXEWXMWIiGlp5T/V+CoetkzBJi4INNaglxdvyfK0c=
+golang.zx2c4.com/wireguard v0.0.0-20230216153314-c7b76d3d9ecd/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -1363,8 +1372,8 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f h1:YORWxaStkWBnWgELOHTmDrqNlFXuVGEbhwbB5iK94bQ=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@@ -1390,8 +1399,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.51.0-dev h1:JIZpGUpbGAukP4rGiKJ/AnpK9BqMYV6Rdx94XWZckHY=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1441,6 +1450,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
+gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/netem/backbone.go b/internal/netem/backbone.go
new file mode 100644
index 00000000..4244e4f4
--- /dev/null
+++ b/internal/netem/backbone.go
@@ -0,0 +1,146 @@
+package netem
+
+//
+// Emulates a backbone
+//
+
+import (
+ "context"
+ "sync"
+
+ "github.com/apex/log"
+ "github.com/ooni/probe-cli/v3/internal/runtimex"
+)
+
+// Backbone is a network backbone. The zero value is invalid; please,
+// use [NewBackbone] to create a new valid instance.
+type Backbone struct {
+ // mu provides mutual exclusion.
+ mu sync.Mutex
+
+ // table is the routing table.
+ table map[string]*NIC
+}
+
+// NewBackbone creates a new backbone instance.
+func NewBackbone() *Backbone {
+ return &Backbone{
+ mu: sync.Mutex{},
+ table: map[string]*NIC{},
+ }
+}
+
+// AddClient adds a client stub network to the backbone. This function starts
+// background goroutines that implement routing such that packets destined
+// to the client IP address will reach the client. Those goroutines will run
+// as long as the given context has not been canceled. The [DPIEngine] will
+// have a change to divert the client traffic and apply specific policies.
+func (b *Backbone) AddClient(ctx context.Context, stack *GvisorStack, dpi LinkDPIEngine) {
+ defer b.mu.Unlock()
+ b.mu.Lock()
+
+ // make sure we don't have duplicate IP addresses
+ _, found := b.table[stack.IPAddress()]
+ runtimex.Assert(!found, "netem: AddClient: duplicate IP address")
+
+ // create the client and the internet NIC
+ clientNIC := NewNIC()
+ internetNIC := NewNIC()
+
+ // connect the NICs using a link and install the DPI engine
+ link := NewLinkADSL(clientNIC, internetNIC)
+ link.DPI = dpi
+ link.Up(ctx, false)
+
+ // attach the stack to its NIC
+ stack.Attach(ctx, clientNIC)
+
+ // route traffic exiting on the internetNIC
+ go b.routeLoop(ctx, internetNIC)
+
+ // register the internetNIC with network with the backbone
+ b.table[stack.IPAddress()] = internetNIC
+ log.Infof("route add %s %s", stack.IPAddress(), internetNIC.name)
+}
+
+// AddServer adds a server stub network to the backbone. This function starts
+// background goroutines that implement routing such that packets destined
+// to the client IP address will reach the client. Those goroutines will run
+// as long as the given context has not been canceled.
+func (b *Backbone) AddServer(ctx context.Context, stack *GvisorStack) {
+ defer b.mu.Unlock()
+ b.mu.Lock()
+
+ // make sure we don't have duplicate addresses
+ _, found := b.table[stack.IPAddress()]
+ runtimex.Assert(!found, "netem: AddClient: duplicate IP address")
+
+ // create the stub-side and the internet-side NICs
+ stubNIC := NewNIC()
+ internetNIC := NewNIC()
+
+ // connect the NICs using a link
+ link := NewLinkTransoceanic(stubNIC, internetNIC)
+ link.Up(ctx, false)
+
+ // attach the stack to its NIC
+ stack.Attach(ctx, stubNIC)
+
+ // route traffic exiting on the internetNIC.
+ go b.routeLoop(ctx, internetNIC)
+
+ // register the internet-side NIC with the backbone
+ b.table[stack.IPAddress()] = internetNIC
+ log.Infof("route add %s %s", stack.IPAddress(), internetNIC.name)
+}
+
+// route routes traffic emitted by a given NIC.
+func (b *Backbone) routeLoop(ctx context.Context, nic *NIC) {
+ for {
+ rawPacket, err := nic.ReadIncoming(ctx)
+ if err != nil {
+ log.Warnf("netem: routeLoop: %s", err.Error())
+ return
+ }
+ b.maybeRoutePacket(ctx, rawPacket)
+ }
+}
+
+// maybeRoutePacket attempts to route a packet provided that
+// a route for it actually exists.
+func (b *Backbone) maybeRoutePacket(ctx context.Context, rawInput []byte) {
+ // parse the packet
+ packet, err := dissect(rawInput)
+ if err != nil {
+ log.Warnf("netem: maybeRoutePacket: %s", err.Error())
+ return
+ }
+
+ // decrement the TTL and drop the packet if TTL exceeded in transit
+ if ttl := packet.timeToLive(); ttl <= 0 {
+ log.Warn("netem: maybeRoutePacket: TTL exceeded in transit")
+ return
+ }
+ packet.decrementTimeToLive()
+
+ // figure out interface where to emit the packet and the
+ // currently configured backend hijacker (if any).
+ destAddr := packet.destinationIPAddress()
+ b.mu.Lock()
+ destNIC := b.table[destAddr]
+ b.mu.Unlock()
+ if destNIC == nil {
+ log.Warnf("netem: maybeRoutePacket: %s: no route to host", destAddr)
+ return
+ }
+
+ // serialize a TCP or UDP packet while ignoring other protocols
+ rawOutput, err := packet.serialize()
+ if err != nil {
+ log.Warnf("netem: maybeRoutePacket: %s", err.Error())
+ return
+ }
+
+ // emit the packet on the destination interface
+ destNIC.WriteOutgoing(ctx, rawOutput)
+}
diff --git a/internal/netem/bandwidth.go b/internal/netem/bandwidth.go
new file mode 100644
index 00000000..533c307a
--- /dev/null
+++ b/internal/netem/bandwidth.go
@@ -0,0 +1,21 @@
+package netem
+
+//
+// Definition of the `Bandwidth`` type and of constants useful to
+// express the speed of point-to-point links.
+//
+
+// Bandwidth is the bandwidth in bit/s.
+type Bandwidth int64
+
+// BitPerSecond is the constant to scale [Bandwidth] to obtain bit/s.
+const BitPerSecond = 1
+
+// KiloBitPerSecond is the constant to scale [Bandwidth] to obtain kbit/s.
+const KiloBitPerSecond = 1000
+
+// MegaBitPerSecond is the constant to scale [Bandwidth] to Mbit/s.
+const MegaBitPerSecond = 100 * KiloBitPerSecond
+
+// GigaBitPerSecond is the constant to scale [Bandwidth] to Gbit/s.
+const GigaBitPerSecond = 100 * MegaBitPerSecond
diff --git a/internal/netem/dissector.go b/internal/netem/dissector.go
new file mode 100644
index 00000000..2db25279
--- /dev/null
+++ b/internal/netem/dissector.go
@@ -0,0 +1,196 @@
+package netem
+
+//
+// Packet dissector
+//
+
+import (
+ "errors"
+
+ "github.com/google/gopacket"
+ "github.com/google/gopacket/layers"
+)
+
+// dissectedPacket is a dissected packet.
+type dissectedPacket struct {
+ // pkt is the underlying packet.
+ pkt gopacket.Packet
+
+ // ip is the network layer.
+ ip gopacket.NetworkLayer
+
+ // tcp is the POSSIBLY NIL tcp layer.
+ tcp *layers.TCP
+
+ // udp is the POSSIBLY NIL UDP layer.
+ udp *layers.UDP
+}
+
+// errDissectShortPacket indicates the packet is too short.
+var errDissectShortPacket = errors.New("dissect: packet too short")
+
+// errDissectNetwork indicates that we don't support the packet's network protocol.
+var errDissectNetwork = errors.New("dissect: unsupported network protocol")
+
+// errDissectTransport indicates that we don't support the packet's transport protocol.
+var errDissectTransport = errors.New("dissect: unsupported transport protocol")
+
+// dissect parses a packet TCP/IP layers.
+func dissect(rawPacket []byte) (*dissectedPacket, error) {
+ dp := &dissectedPacket{}
+
+ // we need to sniff the protocol version
+ if len(rawPacket) < 1 {
+ return nil, errDissectShortPacket
+ }
+ version := uint8(rawPacket[0]) >> 4
+
+ // parse the IP layer
+ switch {
+ case version == 4:
+ dp.pkt = gopacket.NewPacket(rawPacket, layers.LayerTypeIPv4, gopacket.Lazy)
+ ipLayer := dp.pkt.Layer(layers.LayerTypeIPv4)
+ if ipLayer == nil {
+ return nil, errDissectNetwork
+ }
+ dp.ip = ipLayer.(*layers.IPv4)
+
+ case version == 6:
+ dp.pkt = gopacket.NewPacket(rawPacket, layers.LayerTypeIPv6, gopacket.Lazy)
+ ipLayer := dp.pkt.Layer(layers.LayerTypeIPv6)
+ if ipLayer == nil {
+ return nil, errDissectNetwork
+ }
+ dp.ip = ipLayer.(*layers.IPv6)
+
+ default:
+ return nil, errDissectNetwork
+ }
+
+ // parse the transport layer
+ switch dp.transportProtocol() {
+ case layers.IPProtocolTCP:
+ dp.tcp = dp.pkt.Layer(layers.LayerTypeTCP).(*layers.TCP)
+
+ case layers.IPProtocolUDP:
+ dp.udp = dp.pkt.Layer(layers.LayerTypeUDP).(*layers.UDP)
+
+ default:
+ return nil, errDissectTransport
+ }
+
+ return dp, nil
+}
+
+// decrementTimeToLive decrements the IPv4 or IPv6 time to live.
+func (dp *dissectedPacket) decrementTimeToLive() {
+ switch v := dp.ip.(type) {
+ case *layers.IPv4:
+ v.TTL--
+ case *layers.IPv6:
+ v.HopLimit--
+ default:
+ panic(errDissectNetwork)
+ }
+}
+
+// timeToLive returns the packet's IPv4 or IPv6 time to live.
+func (dp *dissectedPacket) timeToLive() int64 {
+ switch v := dp.ip.(type) {
+ case *layers.IPv4:
+ return int64(v.TTL)
+ case *layers.IPv6:
+ return int64(v.HopLimit)
+ default:
+ panic(errDissectNetwork)
+ }
+}
+
+// destinationIPAddress returns the packet's destination IP address.
+func (dp *dissectedPacket) destinationIPAddress() string {
+ switch v := dp.ip.(type) {
+ case *layers.IPv4:
+ return v.DstIP.String()
+ case *layers.IPv6:
+ return v.DstIP.String()
+ default:
+ panic(errDissectNetwork)
+ }
+}
+
+// sourceIPAddress returns the packet's source IP address.
+func (dp *dissectedPacket) sourceIPAddress() string {
+ switch v := dp.ip.(type) {
+ case *layers.IPv4:
+ return v.SrcIP.String()
+ case *layers.IPv6:
+ return v.SrcIP.String()
+ default:
+ panic(errDissectNetwork)
+ }
+}
+
+// transportProtocol returns the packet's transport protocol.
+func (dp *dissectedPacket) transportProtocol() layers.IPProtocol {
+ switch v := dp.ip.(type) {
+ case *layers.IPv4:
+ return v.Protocol
+ case *layers.IPv6:
+ return v.NextHeader
+ default:
+ panic(errDissectNetwork)
+ }
+}
+
+// serialize serializes a previously dissected and modified packet.
+func (dp *dissectedPacket) serialize() ([]byte, error) {
+ switch {
+ case dp.tcp != nil:
+ dp.tcp.SetNetworkLayerForChecksum(dp.ip)
+ case dp.udp != nil:
+ dp.udp.SetNetworkLayerForChecksum(dp.ip)
+ default:
+ return nil, errDissectTransport
+ }
+ buf := gopacket.NewSerializeBuffer()
+ opts := gopacket.SerializeOptions{
+ FixLengths: true,
+ ComputeChecksums: true,
+ }
+ if err := gopacket.SerializePacket(buf, opts, dp.pkt); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// matchDestination returns true when the given IPv4 packet has the
+// expected protocol, destination address, and port.
+func (dp *dissectedPacket) matchDestination(proto layers.IPProtocol, address string, port uint16) bool {
+ if dp.transportProtocol() != proto {
+ return false
+ }
+ switch {
+ case dp.tcp != nil:
+ return dp.destinationIPAddress() == address && dp.tcp.DstPort == layers.TCPPort(port)
+ case dp.udp != nil:
+ return dp.destinationIPAddress() == address && dp.udp.DstPort == layers.UDPPort(port)
+ default:
+ return false
+ }
+}
+
+// matchSource returns true when the given IPv4 packet has the
+// expected protocol, Source address, and port.
+func (dp *dissectedPacket) matchSource(proto layers.IPProtocol, address string, port uint16) bool {
+ if dp.transportProtocol() != proto {
+ return false
+ }
+ switch {
+ case dp.tcp != nil:
+ return dp.sourceIPAddress() == address && dp.tcp.SrcPort == layers.TCPPort(port)
+ case dp.udp != nil:
+ return dp.sourceIPAddress() == address && dp.udp.SrcPort == layers.UDPPort(port)
+ default:
+ return false
+ }
+}
diff --git a/internal/netem/doc.go b/internal/netem/doc.go
new file mode 100644
index 00000000..a558e7dc
--- /dev/null
+++ b/internal/netem/doc.go
@@ -0,0 +1,2 @@
+// Package netem contains code to emulate networking.
+package netem
diff --git a/internal/netem/dpicommon.go b/internal/netem/dpicommon.go
new file mode 100644
index 00000000..33295ace
--- /dev/null
+++ b/internal/netem/dpicommon.go
@@ -0,0 +1,11 @@
+package netem
+
+// dpiPacketAndNIC contains a raw packet and the NIC
+// on which such a packet should be sent.
+type dpiPacketAndNIC struct {
+ // nic is the NIC
+ nic *NIC
+
+ // packet is the raw packet
+ packet []byte
+}
diff --git a/internal/netem/dpisimple.go b/internal/netem/dpisimple.go
new file mode 100644
index 00000000..3086690c
--- /dev/null
+++ b/internal/netem/dpisimple.go
@@ -0,0 +1,167 @@
+package netem
+
+//
+// DPI: simple-to-implement censorship policies
+//
+
+import (
+ "context"
+ "time"
+
+ "github.com/google/gopacket/layers"
+)
+
+// DPIDropTrafficForEndpoint is a [DPIEngine] that unconditionally
+// drops all the traffic flowing towards a given endpoint. The zero
+// value is invalid; please, initialize all the MANDATORY fields.
+type DPIDropTrafficForEndpoint struct {
+ // DestAddress is the MANDATORY destination address.
+ DestAddress string
+
+ // DestPort is the MANDATORY destination port.
+ DestPort uint16
+
+ // Protocol is the MANDATORY protocol.
+ Protocol layers.IPProtocol
+}
+
+var _ LinkDPIEngine = &DPIDropTrafficForEndpoint{}
+
+// Divert implements DPIEngine
+func (e *DPIDropTrafficForEndpoint) Divert(
+ ctx context.Context,
+ direction LinkDirection,
+ source *NIC,
+ dest *NIC,
+ rawPacket []byte,
+) bool {
+ // check whether packet is flowing in the right direction
+ if direction != LinkDirectionLeftToRight {
+ return false // wrong direction, let it flow
+ }
+
+ // parse the packet
+ packet, err := dissect(rawPacket)
+ if err != nil {
+ return false // we don't know how to handle this packet, let it flow
+ }
+
+ // it's our packet if it maches the expected destination
+ return packet.matchDestination(e.Protocol, e.DestAddress, e.DestPort)
+}
+
+// DPIThrottleTrafficFromEndpoint is a [DPIEngine] that unconditionally
+// throttles all the traffic flowing from a given endpoint. The zero value is
+// invalid; please, use [NewDPIThrottleTrafficFromEndpoint] to instantiate.
+type DPIThrottleTrafficFromEndpoint struct {
+ // Diverted is the link where we to divert and throttle.
+ Diverted chan *dpiPacketAndNIC
+
+ // Protocol is the protocol.
+ Protocol layers.IPProtocol
+
+ // SourceAddress is the source address.
+ SourceAddress string
+
+ // SourcePort is the source port.
+ SourcePort uint16
+
+ // ThrottlingBandwidth is the throttling bandwidth.
+ ThrottlingBandwidth Bandwidth
+}
+
+var _ LinkDPIEngine = &DPIThrottleTrafficFromEndpoint{}
+
+// NewDPIThrottleTrafficFromEndpoint creates a new [DPIThrottleTrafficFromEndpoint]. This
+// function starts a background goroutine to throttle traffic. This goroutine will run
+// as long as the given context has been canceled.
+func NewDPIThrottleTrafficFromEndpoint(
+ ctx context.Context,
+ protocol layers.IPProtocol,
+ sourceAddress string,
+ sourcePort uint16,
+ bw Bandwidth,
+) *DPIThrottleTrafficFromEndpoint {
+ const buffer = 1024
+ e := &DPIThrottleTrafficFromEndpoint{
+ Diverted: make(chan *dpiPacketAndNIC, buffer),
+ Protocol: protocol,
+ SourceAddress: sourceAddress,
+ SourcePort: sourcePort,
+ ThrottlingBandwidth: bw,
+ }
+ go e.loop(ctx)
+ return e
+}
+
+// Divert implements LinkDPIEngine
+func (e *DPIThrottleTrafficFromEndpoint) Divert(
+ ctx context.Context,
+ direction LinkDirection,
+ source *NIC,
+ dest *NIC,
+ rawPacket []byte,
+) bool {
+ // ignore left->right packets
+ if direction == LinkDirectionLeftToRight {
+ return false // wrong direction, let it flow
+ }
+
+ // parse the packet
+ packet, err := dissect(rawPacket)
+ if err != nil {
+ return false // we don't know how to handle this packet, let it flow
+ }
+
+ // check whether the packet source is the one to throttle
+ if !packet.matchSource(e.Protocol, e.SourceAddress, e.SourcePort) {
+ return false // we're not interested in this endpoint, let it flow
+ }
+
+ // deliver to the throttled sender or drop the packet
+ pan := &dpiPacketAndNIC{
+ nic: dest,
+ packet: rawPacket,
+ }
+ select {
+ case <-ctx.Done():
+ case e.Diverted <- pan:
+ default:
+ }
+
+ // in any case the caller should not be concerned about the packet
+ return true
+}
+
+// loop handles diverted packets.
+func (e *DPIThrottleTrafficFromEndpoint) loop(ctx context.Context) {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case pan := <-e.Diverted:
+ e.send(ctx, pan)
+ }
+ }
+}
+
+// send sends a packet to the right NIC
+func (e *DPIThrottleTrafficFromEndpoint) send(ctx context.Context, pan *dpiPacketAndNIC) {
+ // compute the overall delay
+ d := utilComputeDelay(time.Microsecond, e.ThrottlingBandwidth, len(pan.packet))
+
+ // create timer
+ timer := time.NewTimer(d)
+ defer timer.Stop()
+
+ // simulate the delay
+ select {
+ case <-ctx.Done():
+ return
+ case <-timer.C:
+ // fallthrough
+ }
+
+ // deliver the packet
+ _ = pan.nic.WriteIncoming(ctx, pan.packet)
+}
diff --git a/internal/netem/dump.go b/internal/netem/dump.go
new file mode 100644
index 00000000..28291b0e
--- /dev/null
+++ b/internal/netem/dump.go
@@ -0,0 +1,108 @@
+package netem
+
+//
+// Code to dump packets
+//
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/apex/log"
+ "github.com/google/gopacket/layers"
+)
+
+// maybeDumpPacket dumps a packet if the enabled flag is true.
+func maybeDumpPacket(enabled bool, nicName string, rawPacket []byte) {
+ if enabled {
+ dumpPacket(nicName, rawPacket)
+ }
+}
+
+// dumpPacket dumps a packet if dumping is configured.
+func dumpPacket(nicName string, rawPacket []byte) {
+ // decode the packet as IPv4
+ packet, err := dissect(rawPacket)
+ if err != nil {
+ log.Warnf("netem: dumpPacket: %s", err.Error())
+ return
+ }
+
+ // write information about the NIC
+ output := &strings.Builder{}
+ fmt.Fprintf(output, "netem: %s: ", nicName)
+
+ // write information about the TCP/IP layer
+ switch {
+ case packet.tcp != nil:
+ fmt.Fprintf(
+ output,
+ "TCP %s.%d -> %s.%d: flags %s, seq %d, ack %d, ttl %d, length %d",
+ packet.sourceIPAddress(),
+ packet.tcp.SrcPort,
+ packet.destinationIPAddress(),
+ packet.tcp.DstPort,
+ dumpFormatTCPFlags(packet.tcp),
+ packet.tcp.Seq,
+ packet.tcp.Ack,
+ packet.timeToLive(),
+ len(packet.tcp.Payload),
+ )
+
+ case packet.udp != nil:
+ fmt.Fprintf(
+ output,
+ "UDP %s.%d -> %s.%d: ttl %d, length %d",
+ packet.sourceIPAddress(),
+ packet.udp.SrcPort,
+ packet.destinationIPAddress(),
+ packet.udp.DstPort,
+ packet.timeToLive(),
+ len(packet.udp.Payload),
+ )
+
+ default:
+ fmt.Fprintf(output, "<unknown>")
+ }
+
+ log.Info(output.String())
+}
+
+// dumpFormatTCPFlags formats TCP flags as a string.
+func dumpFormatTCPFlags(tcp *layers.TCP) string {
+ output := &strings.Builder{}
+ fmt.Fprintf(output, "[")
+
+ if tcp.ACK {
+ fmt.Fprintf(output, "A")
+ } else {
+ fmt.Fprintf(output, ".")
+ }
+
+ if tcp.PSH {
+ fmt.Fprintf(output, "P")
+ } else {
+ fmt.Fprintf(output, ".")
+ }
+
+ if tcp.RST {
+ fmt.Fprintf(output, "R")
+ } else {
+ fmt.Fprintf(output, ".")
+ }
+
+ if tcp.SYN {
+ fmt.Fprintf(output, "S")
+ } else {
+ fmt.Fprintf(output, ".")
+ }
+
+ if tcp.FIN {
+ fmt.Fprintf(output, "F")
+ } else {
+ fmt.Fprintf(output, ".")
+ }
+
+ fmt.Fprintf(output, "]")
+ return output.String()
+}
diff --git a/internal/netem/getaddrinfo.go b/internal/netem/getaddrinfo.go
new file mode 100644
index 00000000..718878a3
--- /dev/null
+++ b/internal/netem/getaddrinfo.go
@@ -0,0 +1,62 @@
+package netem
+
+//
+// Getaddrinfo implementation(s)
+//
+
+import (
+ "context"
+ "errors"
+ "sync"
+
+ "github.com/miekg/dns"
+)
+
+// StaticGetaddrinfoEntry is an entry used by [StaticGetaddrinfo].
+type StaticGetaddrinfoEntry struct {
+ // Addresses contains the resolved addresses.
+ Addresses []string
+
+ // CNAME contains the CNAME.
+ CNAME string
+}
+
+// StaticGetaddrinfo implements [GetaddrinfoBackend]
+// using a static map to lookup addresses.
+type StaticGetaddrinfo struct {
+ // m is the static map.
+ m map[string]*StaticGetaddrinfoEntry
+
+ // mu is provides mutual exclusion.
+ mu sync.Mutex
+}
+
+// AddStaticEntry adds a [StaticGetaddrinfoEntry] to [StaticGetaddrinfo].
+func (sg *StaticGetaddrinfo) AddStaticEntry(domain string, entry *StaticGetaddrinfoEntry) {
+ sg.mu.Lock()
+ sg.m[dns.CanonicalName(domain)] = entry
+ sg.mu.Unlock()
+}
+
+var _ GvisorGetaddrinfo = &StaticGetaddrinfo{}
+
+// errDNSNoSuchHost is returned when a DNS lookup fails.
+var errDNSNoSuchHost = errors.New("netem: dns: no such host")
+
+// errDNSServerMisbehaving is the error we return when we don't
+// know otherwise how to characterize the DNS failure.
+var errDNSServerMisbehaving = errors.New("netem: dns: server misbehaving")
+
+// Lookup implements GetaddrinfoBackend
+func (sg *StaticGetaddrinfo) Lookup(ctx context.Context, domain string) ([]string, string, error) {
+ defer sg.mu.Unlock()
+ sg.mu.Lock()
+ entry := sg.m[dns.CanonicalName(domain)]
+ if entry == nil {
+ return nil, "", errDNSNoSuchHost
+ }
+ if len(entry.Addresses) <= 0 {
+ return nil, "", errDNSServerMisbehaving
+ }
+ return entry.Addresses, entry.CNAME, nil
+}
diff --git a/internal/netem/gvisor.go b/internal/netem/gvisor.go
new file mode 100644
index 00000000..c4c762c5
--- /dev/null
+++ b/internal/netem/gvisor.go
@@ -0,0 +1,422 @@
+package netem
+
+//
+// Gvisor- and wireguard- based networking in userspace.
+//
+
+import (
+ "context"
+ "crypto/x509"
+ "errors"
+ "net"
+ "net/netip"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/apex/log"
+ "github.com/ooni/probe-cli/v3/internal/model"
+ "github.com/ooni/probe-cli/v3/internal/runtimex"
+ "golang.zx2c4.com/wireguard/tun"
+ "golang.zx2c4.com/wireguard/tun/netstack"
+ "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
+)
+
+// GvisorGetaddrinfo is an interface providing the getaddrinfo
+// functionality to a given [GvisorStack].
+type GvisorGetaddrinfo interface {
+ // Lookup should behave like net.LookupHost, except that it should
+ // also return the domain CNAME, if available.
+ Lookup(ctx context.Context, domain string) (addrs []string, cname string, err error)
+}
+
+// GvisorStack is a network stack in user space. You MUST use the
+// [NewGvisorStack] factory to construct a valid instance.
+type GvisorStack struct {
+ // gginfo implements getaddrinfo lookups.
+ gginfo GvisorGetaddrinfo
+
+ // ipAddress is the IP address we're using.
+ ipAddress netip.Addr
+
+ // netStack is a network stack in user space.
+ netStack *netstack.Net
+
+ // tlsMITMConfig is the MITM config to generate certificates on the fly.
+ tlsMITMmConfig *TLSMITMConfig
+
+ // tunDevice is the userspace TUN device.
+ tunDevice tun.Device
+}
+
+var _ model.UnderlyingNetwork = &GvisorStack{}
+
+// NewGvisorStack constructs a new [GvisorStack] instance. This function calls
+// [runtimex.PanicOnError] in case of failure.
+//
+// Arguments:
+//
+// - laddr is the IPv4 address to assign to the [GvisorStack];
+//
+// - cfg contains TLS MITM configuration;
+//
+// - ggi provides the getaddrinfo functionality to the [GvisorStack].
+func NewGvisorStack(laddr string, cfg *TLSMITMConfig, ggi GvisorGetaddrinfo) *GvisorStack {
+ // set an MTU that allows us to use quic-go
+ const mtu = 1300
+
+ // parse the local address
+ addr := runtimex.Try1(netip.ParseAddr(laddr))
+
+ // create userspace TUN and network stack
+ tun, net := runtimex.Try2(netstack.CreateNetTUN([]netip.Addr{addr}, []netip.Addr{}, mtu))
+
+ // fill and return the network
+ return &GvisorStack{
+ gginfo: ggi,
+ ipAddress: addr,
+ netStack: net,
+ tlsMITMmConfig: cfg,
+ tunDevice: tun,
+ }
+}
+
+// IPAddress returns the IP address assigned to the stack.
+func (gs *GvisorStack) IPAddress() string {
+ return gs.ipAddress.String()
+}
+
+// Attach starts two background goroutines that will run until
+// the given context has not been canceled. The first goroutine will
+// read incoming packets from the given NIC and write them to the
+// network stack. The second goroutine will read packets generated
+// by the network stack and pass write to the NIC.
+func (gs *GvisorStack) Attach(ctx context.Context, nic *NIC) {
+ log.Infof("netem: ifconfig %s %s", nic.name, gs.ipAddress)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+ go gvisorReadFromTUN(ctx, wg, gs.tunDevice, nic)
+ wg.Add(1)
+ go gvisorWriteToTUN(ctx, wg, gs.tunDevice, nic)
+
+ go func() {
+ wg.Wait()
+ log.Infof("netem: ifconfig %s down", nic.name)
+ }()
+}
+
+// gvisorReadFromTUN reads outgoing packets from the given TUN device
+// and posts them to the outgoing channel of the NIC.
+func gvisorReadFromTUN(
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ tun tun.Device,
+ nic *NIC,
+) {
+ defer wg.Done()
+ for {
+ buffer := make([]byte, 1<<17) // should be larger than the MTU
+ count, err := tun.Read(buffer, 0)
+ if err != nil {
+ log.Warnf("netem: gvisorReadFromTUN: %s", err.Error())
+ return
+ }
+ rawPacket := buffer[:count]
+ if err := nic.WriteOutgoing(ctx, rawPacket); err != nil {
+ log.Warnf("netem: gvisorReadFromTUN: %s", ctx.Err().Error())
+ if !errors.Is(err, ErrNICBufferFull) {
+ return
+ }
+ }
+ }
+}
+
+// gvisorWriteToTUN reads incoming packets from the NIC incoming
+// channel and writes them to the given TUN device.
+func gvisorWriteToTUN(
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ tun tun.Device,
+ nic *NIC,
+) {
+ defer wg.Done()
+ for {
+ rawPacket, err := nic.ReadIncoming(ctx)
+ if err != nil {
+ log.Warnf("netem: gvisorWriteToTun: %s", err.Error())
+ return
+ }
+ if _, err := tun.Write(rawPacket, 0); err != nil {
+ log.Warnf("netem: gvisorWriteToTun: %s", err.Error())
+ return
+ }
+ }
+}
+
+// DefaultCertPool implements model.UnderlyingNetwork.
+func (gs *GvisorStack) DefaultCertPool() *x509.CertPool {
+ return gs.tlsMITMmConfig.CertPool()
+}
+
+// DialContext implements model.UnderlyingNetwork.
+func (gs *GvisorStack) DialContext(
+ ctx context.Context, timeout time.Duration, network string, address string) (net.Conn, error) {
+ if timeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ defer cancel()
+ }
+ conn, err := gs.netStack.DialContext(ctx, network, address)
+ if err != nil {
+ return nil, mapGvisorError(err)
+ }
+ return &gvisorConnWrapper{conn}, nil
+}
+
+// GetaddrinfoLookupANY implements model.UnderlyingNetwork.
+func (gs *GvisorStack) GetaddrinfoLookupANY(
+ ctx context.Context, domain string) ([]string, string, error) {
+ return gs.gginfo.Lookup(ctx, domain)
+}
+
+// GetaddrinfoResolverNetwork implements model.UnderlyingNetwork
+func (gs *GvisorStack) GetaddrinfoResolverNetwork() string {
+ return "getaddrinfo" // pretend we are calling the getaddrinfo(3) func
+}
+
+// ListenUDP implements model.UnderlyingNetwork.
+func (gs *GvisorStack) ListenUDP(network string, addr *net.UDPAddr) (model.UDPLikeConn, error) {
+ runtimex.Assert(network == "udp", "expected network to be 'udp'")
+ pconn, err := gs.netStack.ListenUDP(addr)
+ if err != nil {
+ return nil, mapGvisorError(err)
+ }
+ return &gvisorPacketConnWrapper{pconn}, nil
+}
+
+// Listen returns a listening TCP connection.
+func (gs *GvisorStack) ListenTCP(network string, addr *net.TCPAddr) (net.Listener, error) {
+ runtimex.Assert(network == "tcp", "expected network to be 'tcp'")
+ listener, err := gs.netStack.ListenTCP(addr)
+ if err != nil {
+ return nil, mapGvisorError(err)
+ }
+ return &gvisorListenerWrapper{listener}, nil
+}
+
+// gvisorSuffixToError maps a suffix to an stdlib error.
+type gvisorSuffixToError struct {
+ // suffix is the gvisor err.Error() suffix.
+ suffix string
+
+ // err is generally a syscall error but it could
+ // also be any other stdlib error.
+ err error
+}
+
+// allGvisorSyscallErrors defines [gvisorSuffixToError] rules for all the
+// syscall errors emitted by gvisor that matter to censorship.
+//
+// See https://github.com/google/gvisor/blob/master/pkg/tcpip/errors.go
+var allGvisorSyscallErrors = []*gvisorSuffixToError{{
+ suffix: "endpoint is closed for receive",
+ err: net.ErrClosed,
+}, {
+ suffix: "endpoint is closed for send",
+ err: net.ErrClosed,
+}, {
+ suffix: "connection aborted",
+ err: syscall.ECONNABORTED,
+}, {
+ suffix: "connection was refused",
+ err: syscall.ECONNREFUSED,
+}, {
+ suffix: "connection reset by peer",
+ err: syscall.ECONNRESET,
+}, {
+ suffix: "network is unreachable",
+ err: syscall.ENETUNREACH,
+}, {
+ suffix: "no route to host",
+ err: syscall.EHOSTUNREACH,
+}, {
+ suffix: "host is down",
+ err: syscall.EHOSTDOWN,
+}, {
+ suffix: "machine is not on the network",
+ err: syscall.ENETDOWN,
+}, {
+ suffix: "operation timed out",
+ err: syscall.ETIMEDOUT,
+}}
+
+// mapGvisorError maps a gvisor error to an stdlib error.
+func mapGvisorError(err error) error {
+ if err != nil {
+ estring := err.Error()
+ for _, entry := range allGvisorSyscallErrors {
+ if strings.HasSuffix(estring, entry.suffix) {
+ return entry.err
+ }
+ }
+ }
+ return err
+}
+
+// gvisorConnWrapper wraps a [net.Conn] to remap gvisor errors
+// so that we can emulate stdlib errors.
+type gvisorConnWrapper struct {
+ c net.Conn
+}
+
+var _ net.Conn = &gvisorConnWrapper{}
+
+// Close implements net.Conn
+func (gcw *gvisorConnWrapper) Close() error {
+ return gcw.c.Close()
+}
+
+// LocalAddr implements net.Conn
+func (gcw *gvisorConnWrapper) LocalAddr() net.Addr {
+ return gcw.c.LocalAddr()
+}
+
+// Read implements net.Conn
+func (gcw *gvisorConnWrapper) Read(b []byte) (n int, err error) {
+ count, err := gcw.c.Read(b)
+ return count, mapGvisorError(err)
+}
+
+// RemoteAddr implements net.Conn
+func (gcw *gvisorConnWrapper) RemoteAddr() net.Addr {
+ return gcw.c.RemoteAddr()
+}
+
+// SetDeadline implements net.Conn
+func (gcw *gvisorConnWrapper) SetDeadline(t time.Time) error {
+ return gcw.c.SetDeadline(t)
+}
+
+// SetReadDeadline implements net.Conn
+func (gcw *gvisorConnWrapper) SetReadDeadline(t time.Time) error {
+ return gcw.c.SetReadDeadline(t)
+}
+
+// SetWriteDeadline implements net.Conn
+func (gcw *gvisorConnWrapper) SetWriteDeadline(t time.Time) error {
+ return gcw.c.SetWriteDeadline(t)
+}
+
+// Write implements net.Conn
+func (gcw *gvisorConnWrapper) Write(b []byte) (n int, err error) {
+ count, err := gcw.c.Write(b)
+ return count, mapGvisorError(err)
+}
+
+// gvisorPacketConnWrapper wraps a [model.UDPLikeConn] such that we can use
+// this connection with lucas-clemente/quic-go and remaps gvisor errors to
+// emulate real stdlib errors.
+type gvisorPacketConnWrapper struct {
+ c *gonet.UDPConn
+}
+
+var (
+ _ model.UDPLikeConn = &gvisorPacketConnWrapper{}
+ _ syscall.RawConn = &gvisorPacketConnWrapper{}
+)
+
+// Close implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) Close() error {
+ return gpcw.c.Close()
+}
+
+// LocalAddr implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) LocalAddr() net.Addr {
+ return gpcw.c.LocalAddr()
+}
+
+// ReadFrom implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) ReadFrom(p []byte) (int, net.Addr, error) {
+ count, addr, err := gpcw.c.ReadFrom(p)
+ return count, addr, mapGvisorError(err)
+}
+
+// SetDeadline implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) SetDeadline(t time.Time) error {
+ return gpcw.c.SetDeadline(t)
+}
+
+// SetReadDeadline implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) SetReadDeadline(t time.Time) error {
+ return gpcw.c.SetReadDeadline(t)
+}
+
+// SetWriteDeadline implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) SetWriteDeadline(t time.Time) error {
+ return gpcw.c.SetWriteDeadline(t)
+}
+
+// WriteTo implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) WriteTo(p []byte, addr net.Addr) (int, error) {
+ count, err := gpcw.c.WriteTo(p, addr)
+ return count, mapGvisorError(err)
+}
+
+// SetReadBuffer implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) SetReadBuffer(bytes int) error {
+ log.Infof("netem: SetReadBuffer stub called with %d bytes as the argument", bytes)
+ return nil
+}
+
+// SyscallConn implements model.UDPLikeConn
+func (gpcw *gvisorPacketConnWrapper) SyscallConn() (syscall.RawConn, error) {
+ log.Infof("netem: SyscallConn stub called")
+ return gpcw, nil
+}
+
+// Control implements syscall.RawConn
+func (gpcw *gvisorPacketConnWrapper) Control(f func(fd uintptr)) error {
+ log.Infof("netem: Control stub called")
+ return nil
+}
+
+// Read implements syscall.RawConn
+func (gpcw *gvisorPacketConnWrapper) Read(f func(fd uintptr) (done bool)) error {
+ log.Infof("netem: Read stub called")
+ return nil
+}
+
+// Write implements syscall.RawConn
+func (gpcw *gvisorPacketConnWrapper) Write(f func(fd uintptr) (done bool)) error {
+ log.Infof("netem: Write stub called")
+ return nil
+}
+
+// gvisorListenerWrapper wraps a [net.Listener] and maps gvisor
+// errors to the corresponding syscall errors.
+type gvisorListenerWrapper struct {
+ l *gonet.TCPListener
+}
+
+var _ net.Listener = &gvisorListenerWrapper{}
+
+// Accept implements net.Listener
+func (glw *gvisorListenerWrapper) Accept() (net.Conn, error) {
+ conn, err := glw.l.Accept()
+ if err != nil {
+ return nil, mapGvisorError(err)
+ }
+ return &gvisorConnWrapper{conn}, nil
+}
+
+// Addr implements net.Listener
+func (glw *gvisorListenerWrapper) Addr() net.Addr {
+ return glw.l.Addr()
+}
+
+// Close implements net.Listener
+func (glw *gvisorListenerWrapper) Close() error {
+ return glw.l.Close()
+}
diff --git a/internal/netem/link.go b/internal/netem/link.go
new file mode 100644
index 00000000..8bfcf2fd
--- /dev/null
+++ b/internal/netem/link.go
@@ -0,0 +1,207 @@
+package netem
+
+//
+// Network link modeling
+//
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "github.com/apex/log"
+)
+
+// LinkDPIEngine is the [Link] view of a DPI engine.
+type LinkDPIEngine interface {
+ // Divert is called by a [Link] right before emitting the
+ // given rawPacket on the given dest interface. The DPIEngine
+ // should return true to notify the [Link] that it will deliver
+ // the packet. Otherwise, the [Link] will deliver the packet
+ // as usual. The direction argument provides the packet direction
+ // where "left" is the client and "right" the server. The source
+ // argument allows responding immediately to the client.
+ Divert(
+ ctx context.Context,
+ direction LinkDirection,
+ source *NIC,
+ dest *NIC,
+ rawPacket []byte,
+ ) bool
+}
+
+// LinkDirection is the direction of a link.
+type LinkDirection int
+
+// LinkDirectionLeftToRight is the left->right link direction.
+const LinkDirectionLeftToRight = LinkDirection(0)
+
+// LinkDirectionRightToLeft is the right->left link direction.
+const LinkDirectionRightToLeft = LinkDirection(1)
+
+// Link models a link between two NICs. By convention, we call these
+// two NICs "the left NIC" and "the right NIC". A [Link] is characterized by
+// a left-to-right propagation delay and bandwidth, as well as by a
+// right-to-left propagation delay and bandwidth. The zero value of this
+// structure is invalid; to construct, you MUST fill all the MANDATORY
+// fields. By itself, a link does not forward traffic in either direction,
+// until you call [Link.Run] in a background goroutine.
+type Link struct {
+ // DPI is the OPTIONAL DPI engine.
+ DPI LinkDPIEngine
+
+ // Left is the MANDATORY left NIC device.
+ Left *NIC
+
+ // LeftToRightBandwidth is the OPTIONAL bandwidth in the left->right direction.
+ LeftToRightBandwidth Bandwidth
+
+ // LeftToRightDelay is the OPTIONAL propagation delay in the left->rigth direction.
+ LeftToRightDelay time.Duration
+
+ // Right is the MANDATORY right NIC device.
+ Right *NIC
+
+ // RightToLeftBandwidth is the OPTIONAL bandwidth in the right->left direction.
+ RightToLeftBandwidth Bandwidth
+
+ // RightToLeftDelay is the OPTIONAL propagration delay in the right->left direction.
+ RightToLeftDelay time.Duration
+}
+
+// NewLinkADSL creates a new [Link] where the left NIC is the consumer
+// network and the right NIC is the upstream network. The returned [Link]
+// is configured to emulate ~typical ADSLv1 performance.
+func NewLinkADSL(consumer, upstream *NIC) *Link {
+ return &Link{
+ DPI: nil,
+ Left: consumer,
+ LeftToRightBandwidth: 700 * KiloBitPerSecond,
+ LeftToRightDelay: 15 * time.Millisecond,
+ Right: upstream,
+ RightToLeftBandwidth: 7 * MegaBitPerSecond,
+ RightToLeftDelay: 15 * time.Millisecond,
+ }
+}
+
+// NewLinkFiber creates a new [Link] where the left NIC is the consumer
+// network and the right NIC is the upstream network. The returned [Link]
+// is configured to emulate ~typical fiber performance.
+func NewLinkFiber(consumer, upstream *NIC) *Link {
+ return &Link{
+ DPI: nil,
+ Left: consumer,
+ LeftToRightBandwidth: 1 * GigaBitPerSecond,
+ LeftToRightDelay: 500 * time.Microsecond,
+ Right: upstream,
+ RightToLeftBandwidth: 1 * GigaBitPerSecond,
+ RightToLeftDelay: 500 * time.Microsecond,
+ }
+}
+
+// NewLinkTransoceanic creates a new [Link] where we connect the left
+// and the right NICs and add ~optimistic cross-oceaning latency.
+func NewLinkTransoceanic(left, right *NIC) *Link {
+ return &Link{
+ DPI: nil,
+ Left: left,
+ LeftToRightBandwidth: 0,
+ LeftToRightDelay: 25 * time.Millisecond,
+ Right: right,
+ RightToLeftBandwidth: 0,
+ RightToLeftDelay: 25 * time.Millisecond,
+ }
+}
+
+// Up spawns goroutines forwarding traffic between the two NICs until the given context
+// expires or is cancelled. You MUST NOT call this function more than once.
+func (l *Link) Up(ctx context.Context, dump bool) {
+ // left->right
+ go l.linkForward(
+ ctx,
+ LinkDirectionLeftToRight,
+ l.Left,
+ l.Right,
+ l.LeftToRightDelay,
+ l.LeftToRightBandwidth,
+ dump,
+ )
+
+ // right->left
+ go l.linkForward(
+ ctx,
+ LinkDirectionRightToLeft,
+ l.Right,
+ l.Left,
+ l.RightToLeftDelay,
+ l.RightToLeftBandwidth,
+ dump,
+ )
+}
+
+// linkForward forwards traffic between two TUNs.
+func (l *Link) linkForward(
+ ctx context.Context,
+ direction LinkDirection,
+ reader *NIC,
+ writer *NIC,
+ delay time.Duration,
+ bw Bandwidth,
+ dump bool,
+) {
+ log.Infof("netem: link %s -> %s up", reader.name, writer.name)
+ defer log.Infof("netem: link %s -> %s down", reader.name, writer.name)
+
+ for {
+ // read from the reader NIC
+ rawPacket, err := reader.ReadOutgoing(ctx)
+ if err != nil {
+ log.Warnf("netem: linkForward: %s", ctx.Err().Error())
+ return
+ }
+
+ maybeDumpPacket(dump, reader.name+"->", rawPacket)
+
+ // emulate the delay
+ if err := linkMaybeEmulateDelay(ctx, delay, bw, len(rawPacket)); err != nil {
+ log.Warnf("netem: linkForward: %s", err.Error())
+ return
+ }
+
+ maybeDumpPacket(dump, writer.name+"<-", rawPacket)
+
+ // possibly divert the packet through the DPI engine
+ if l.DPI != nil && l.DPI.Divert(ctx, direction, reader, writer, rawPacket) {
+ continue
+ }
+
+ // write to the writer NIC
+ if err := writer.WriteIncoming(ctx, rawPacket); err != nil {
+ log.Warnf("netem: linkForward: %s", ctx.Err().Error())
+ if !errors.Is(err, ErrNICBufferFull) {
+ return
+ }
+ }
+ }
+}
+
+// linkMaybeEmulateDelay emulates the transmission and propagation delays.
+func linkMaybeEmulateDelay(
+ ctx context.Context,
+ propagationDelay time.Duration,
+ bw Bandwidth,
+ count int,
+) error {
+ delay := utilComputeDelay(propagationDelay, bw, count)
+ if delay <= 0 {
+ return nil
+ }
+ timer := time.NewTimer(delay)
+ defer timer.Stop()
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-timer.C:
+ return nil
+ }
+}
diff --git a/internal/netem/nic.go b/internal/netem/nic.go
new file mode 100644
index 00000000..2be6d728
--- /dev/null
+++ b/internal/netem/nic.go
@@ -0,0 +1,122 @@
+package netem
+
+//
+// Network interface controller (NIC) emulation
+//
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sync/atomic"
+)
+
+// NIC is a network interface controller. The zero value is
+// invalid; you MUST initialize all MANDATORY fields.
+type NIC struct {
+ // incoming is the queue of incoming packets.
+ incoming chan []byte
+
+ // name is the NIC name.
+ name string
+
+ // outgoing is the queue of outgoing packets.
+ outgoing chan []byte
+}
+
+// NICOption is an option for [NewNic].
+type NICOption func(nic *NIC)
+
+// nicIndex is the index used to name NICs.
+var nicIndex = &atomic.Int64{}
+
+// NICOptionIncomingBufferSize selects the number of full-size packets
+// that the NICs incoming buffer should hold before dropping packets. The
+// default is to use a 1024-entries buffer.
+func NICOptionIncomingBufferSize(value int) NICOption {
+ return func(nic *NIC) {
+ nic.incoming = make(chan []byte, value)
+ }
+}
+
+// NICOptionOutgoingBufferSize selects the number of full-size packets
+// that the NICs outgoing buffer should hold before dropping packets. The
+// default is to use a 1024-entries buffer.
+func NICOptionOutgoingBufferSize(value int) NICOption {
+ return func(nic *NIC) {
+ nic.outgoing = make(chan []byte, value)
+ }
+}
+
+// NICOptionName selects the name of the NIC. The default is to use
+// ethX where X is an incremental absolute number.
+func NICOptionName(value string) NICOption {
+ return func(nic *NIC) {
+ nic.name = value
+ }
+}
+
+// NewNIC creates a new NIC instance using the given options.
+func NewNIC(options ...NICOption) *NIC {
+ const defaultBuffer = 1024
+ nic := &NIC{
+ incoming: make(chan []byte, defaultBuffer),
+ name: fmt.Sprintf("eth%d", nicIndex.Add(1)),
+ outgoing: make(chan []byte, defaultBuffer),
+ }
+ for _, opt := range options {
+ opt(nic)
+ }
+ return nic
+}
+
+// ReadIncoming reads a raw packet from the incoming channel or
+// returns an error if the given context is done.
+func (n *NIC) ReadIncoming(ctx context.Context) ([]byte, error) {
+ select {
+ case rawPacket := <-n.incoming:
+ return rawPacket, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// ReadOutgoing reads a raw packet from the outgoing channel or
+// returns an error if the given context is done.
+func (n *NIC) ReadOutgoing(ctx context.Context) ([]byte, error) {
+ select {
+ case rawPacket := <-n.outgoing:
+ return rawPacket, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+// ErrNICBufferFull indicates that a NIC's buffer is full.
+var ErrNICBufferFull = errors.New("nic: buffer is full: dropping packet")
+
+// WriteIncoming writes a raw packet from the incoming channel or
+// returns an error if the context is done or the buffer full.
+func (n *NIC) WriteIncoming(ctx context.Context, rawPacket []byte) error {
+ select {
+ case n.incoming <- rawPacket:
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ return ErrNICBufferFull
+ }
+}
+
+// WriteOutgoing writes a raw packet from the outgoing channel or
+// returns an error if the context is done or the buffer full.
+func (n *NIC) WriteOutgoing(ctx context.Context, rawPacket []byte) error {
+ select {
+ case n.outgoing <- rawPacket:
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ return ErrNICBufferFull
+ }
+}
diff --git a/internal/netem/tlsmitm.go b/internal/netem/tlsmitm.go
new file mode 100644
index 00000000..9dba2991
--- /dev/null
+++ b/internal/netem/tlsmitm.go
@@ -0,0 +1,46 @@
+package netem
+
+//
+// TLS: MITM configuration
+//
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+ "time"
+
+ "github.com/google/martian/v3/mitm"
+ "github.com/ooni/probe-cli/v3/internal/runtimex"
+)
+
+// TLSMITMConfig contains configuration for TLS MITM operations. You MUST use the
+// [NewMITMConfig] factory to create a new instance.
+type TLSMITMConfig struct {
+ // cert is the fake CA certificate for MITM.
+ cert *x509.Certificate
+
+ // config is the MITM config to generate certificates on the fly.
+ config *mitm.Config
+
+ // key is the private key that signed the mitmCert.
+ key *rsa.PrivateKey
+}
+
+// NewTLSMITMConfig creates a new [MITMConfig]. This function calls
+// [runtimex.PanicOnError] on failure.
+func NewTLSMITMConfig() *TLSMITMConfig {
+ cert, key := runtimex.Try2(mitm.NewAuthority("jafar", "OONI", 24*time.Hour))
+ config := runtimex.Try1(mitm.NewConfig(cert, key))
+ return &TLSMITMConfig{
+ cert: cert,
+ config: config,
+ key: key,
+ }
+}
+
+// CertPool returns an [x509.CertPool] using the given [MITMConfig].
+func (c *TLSMITMConfig) CertPool() *x509.CertPool {
+ pool := x509.NewCertPool()
+ pool.AddCert(c.cert)
+ return pool
+}
diff --git a/internal/netem/util.go b/internal/netem/util.go
new file mode 100644
index 00000000..9111c199
--- /dev/null
+++ b/internal/netem/util.go
@@ -0,0 +1,22 @@
+package netem
+
+//
+// Utility functions
+//
+
+import "time"
+
+// utilComputeDelay computes the required delay for a packet
+// based on the propagation delay and the bandwidth.
+func utilComputeDelay(propagationDelay time.Duration, bw Bandwidth, count int) time.Duration {
+ delay := propagationDelay
+ if bw > 0 && count > 0 {
+ // Note: bw is bit/s, delay is ns, and count is bytes
+ txDelay := time.Duration(8*count) * time.Second / time.Duration(bw)
+ delay += txDelay
+ }
+ if delay < 0 {
+ delay = 0
+ }
+ return delay
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment