-
-
Save bassosimone/3ce52a37fc7394f7cce82391685c5477 to your computer and use it in GitHub Desktop.
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
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