├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── ARCHITECTURE.md ├── AUTHORS ├── LICENSE ├── Makefile ├── README.md ├── cmd └── minivpn │ ├── iface.go │ ├── log.go │ └── main.go ├── examples ├── ping.go └── urlgrabber.go ├── extras ├── doc.go ├── memoryless │ ├── README.md │ └── memoryless.go ├── ping │ ├── LICENSE │ ├── README.md │ ├── assert.go │ ├── assert_test.go │ ├── ping.go │ └── ping_test.go └── proxy.go ├── go.mod ├── go.sum ├── internal ├── bytesx │ ├── bytesx.go │ └── bytesx_test.go ├── controlchannel │ └── controlchannel.go ├── datachannel │ ├── common_test.go │ ├── controller.go │ ├── controller_test.go │ ├── crypto.go │ ├── crypto_test.go │ ├── doc.go │ ├── errors.go │ ├── read.go │ ├── read_test.go │ ├── service.go │ ├── service_test.go │ ├── state.go │ ├── write.go │ └── write_test.go ├── mocks │ ├── README.md │ ├── addr.go │ └── dialer.go ├── model │ ├── dialer.go │ ├── doc.go │ ├── logger.go │ ├── mocks.go │ ├── notification.go │ ├── packet.go │ ├── packet_test.go │ ├── session.go │ ├── session_test.go │ ├── trace.go │ └── tunnelinfo.go ├── networkio │ ├── closeonce.go │ ├── common_test.go │ ├── datagram.go │ ├── dialer.go │ ├── doc.go │ ├── framing.go │ ├── networkio_test.go │ ├── service.go │ ├── service_test.go │ └── stream.go ├── optional │ ├── optional.go │ └── optional_test.go ├── packetmuxer │ └── service.go ├── reliabletransport │ ├── common_test.go │ ├── constants.go │ ├── doc.go │ ├── model.go │ ├── packets.go │ ├── packets_test.go │ ├── receiver.go │ ├── receiver_test.go │ ├── reliable_ack_test.go │ ├── reliable_loss_test.go │ ├── reliable_reorder_test.go │ ├── sender.go │ ├── sender_test.go │ ├── service.go │ └── service_test.go ├── runtimex │ ├── runtimex.go │ └── runtimex_test.go ├── session │ ├── datachannelkey.go │ ├── datachannelkey_test.go │ ├── doc.go │ ├── keysource.go │ ├── keysource_test.go │ └── manager.go ├── tlssession │ ├── common_test.go │ ├── controlmsg.go │ ├── controlmsg_test.go │ ├── doc.go │ ├── tlsbio.go │ ├── tlsbio_test.go │ ├── tlshandshake.go │ ├── tlshandshake_test.go │ └── tlssession.go ├── tun │ ├── doc.go │ ├── setup.go │ ├── tun.go │ └── tundeadline.go ├── vpntest │ ├── addr.go │ ├── assert.go │ ├── certs.go │ ├── dialer.go │ ├── doc.go │ ├── packetio.go │ ├── packetio_test.go │ ├── vpntest.go │ └── vpntest_test.go └── workers │ └── workers.go ├── obfs4 ├── common.go ├── doc.go └── obfs4.go ├── pkg ├── README.md ├── config │ ├── config.go │ ├── config_test.go │ ├── vpnoptions.go │ └── vpnoptions_test.go ├── tracex │ ├── trace.go │ └── trace_test.go └── tunnel │ └── tunnel.go ├── scripts ├── README.md ├── bootstrap-provider ├── get_trace_from_pcap.py ├── go-coverage-check.sh ├── install-filternet ├── riseup-ca.crt ├── strangelove │ └── Makefile └── watchjson └── tests ├── integration ├── env-aes-256-cbc-sha256.env ├── env-aes-256-gcm-sha256.env ├── main.go ├── run-server-cbc.sh ├── run-server.sh └── wrap_integration_cover.sh └── qa └── run-filternet.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | 10 | [*.py] 11 | indent_style = space 12 | 13 | [*.{yml,yaml}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.java] 18 | indent_style = space 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Checklist 2 | 3 | * [ ] I have read the contribution guidelines 4 | * [ ] Iff you changed code related to services, or inter-service communication, make sure you update the diagrams in `ARCHITECTURE.md`. 5 | * [ ] Reference issue for this pull request: 6 | 7 | # Description 8 | 9 | Please, insert here a more detailed description. 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | # this action is covering internal/ tree with go1.20 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'main' 8 | pull_request: 9 | branches: 10 | - 'main' 11 | 12 | jobs: 13 | short-tests: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: setup go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.20' 21 | - name: Run short tests 22 | run: go test --short -cover ./internal/... 23 | 24 | lint: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Lint with revive action, from pre-built image 29 | uses: docker://morphy/revive-action:v2 30 | with: 31 | path: "internal/..." 32 | 33 | gosec: 34 | runs-on: ubuntu-latest 35 | env: 36 | GO111MODULE: on 37 | steps: 38 | - name: Checkout Source 39 | uses: actions/checkout@v4 40 | - name: Run Gosec security scanner 41 | uses: securego/gosec@master 42 | with: 43 | args: '-no-fail ./...' 44 | 45 | coverage-threshold: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: setup go 50 | uses: actions/setup-go@v5 51 | with: 52 | go-version: '1.20' 53 | - name: Ensure coverage threshold 54 | run: make test-coverage-threshold 55 | 56 | integration: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | - name: setup go 61 | uses: actions/setup-go@v5 62 | with: 63 | go-version: '1.20' 64 | - name: run integration tests 65 | run: go run ./tests/integration 66 | 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /minivpn 2 | .vscode 3 | *.swp 4 | *.swo 5 | *.pem 6 | *.ovpn 7 | /*.out 8 | data/* 9 | measurements/* 10 | coverage/* 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ain Ghazal 2 | Simone Basso 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROVIDER ?= calyx 2 | TARGET ?= "1.1.1.1" 3 | COUNT ?= 5 4 | TIMEOUT ?= 10 5 | LOCAL_TARGET := $(shell ip -4 addr show docker0 | grep 'inet ' | awk '{print $$2}' | cut -f 1 -d /) 6 | COVERAGE_THRESHOLD := 70 7 | FLAGS=-ldflags="-w -s -buildid=none -linkmode=external" -buildmode=pie -buildvcs=false 8 | 9 | build: 10 | @go build -o ./minivpn ${FLAGS} ./cmd/minivpn/ 11 | 12 | build-rel: 13 | @go build ${FLAGS} -o minivpn 14 | @upx --brute minivpn 15 | @GOOS=darwin go build -d ${FLAGS} -o minivpn-osx 16 | @GOOS=windows go build ${FLAGS} -o minivpn.exe 17 | 18 | build-race: 19 | @go build -race ./cmd/minivpn 20 | 21 | bootstrap: 22 | @./scripts/bootstrap-provider ${PROVIDER} 23 | 24 | test: 25 | GOFLAGS='-count=1' go test -v ./... 26 | 27 | test-unit: 28 | mkdir -p ./coverage/unit 29 | go test -cover ./internal/... -args -test.gocoverdir="`pwd`/coverage/unit" 30 | 31 | test-integration: 32 | cd tests/integration && ./wrap_integration_cover.sh 33 | 34 | test-combined-coverage: 35 | go tool covdata percent -i=./coverage/unit,./coverage/int 36 | # convert to text profile and exclude extras/integration test itself 37 | go tool covdata textfmt -i=./coverage/unit,./coverage/int -o coverage/profile 38 | cat coverage/profile| grep -v "extras/ping" | grep -v "tests/integration" > coverage/profile.out 39 | scripts/go-coverage-check.sh ./coverage/profile.out ${COVERAGE_THRESHOLD} 40 | 41 | test-coverage-threshold: 42 | go test --short -coverprofile=cov-threshold-refactor.out ./internal/... 43 | ./scripts/go-coverage-check.sh cov-threshold-refactor.out ${COVERAGE_THRESHOLD} 44 | 45 | test-short: 46 | go test -race -short -v ./... 47 | 48 | test-ping: 49 | ./minivpn -c data/${PROVIDER}/config -ping 50 | 51 | integration-server: 52 | # this needs the container from https://github.com/ainghazal/docker-openvpn 53 | cd tests/integration && ./run-server.sh 54 | 55 | test-fetch-config: 56 | rm -rf data/tests 57 | mkdir -p data/tests && curl 172.17.0.2:8080/ > data/tests/config 58 | 59 | qa: 60 | @# all the steps at once 61 | cd tests/integration && ./run-server.sh & 62 | sleep 5 # 5secs should be enough, increase this if not. 63 | @rm -rf data/tests 64 | @mkdir -p data/tests && curl 172.17.0.2:8080/ > data/tests/config 65 | @sleep 1 66 | ./minivpn -c data/tests/config -ping 67 | @docker stop ovpn1 68 | 69 | integration: 70 | go run ./tests/integration 71 | 72 | filternet-qa: 73 | cd tests/qa && ./run-filternet.sh remote-block-all 74 | 75 | backup-data: 76 | @tar cvzf ../data-vpn-`date +'%F'`.tar.gz 77 | 78 | netns-shell: 79 | # useful for development, if we're running the openvpn client in the protected namespace 80 | # see https://github.com/slingamn/namespaced-openvpn 81 | sudo ip netns exec protected sudo -u `whoami` -i 82 | 83 | .PHONY: lint 84 | lint: go-fmt go-vet go-sec go-revive 85 | 86 | go-fmt: 87 | gofmt -s -l . 88 | 89 | go-vet: 90 | go vet internal/... 91 | 92 | go-sec: 93 | gosec internal/... 94 | 95 | go-revive: 96 | revive internal/... 97 | 98 | install-linters: 99 | go install github.com/mgechev/revive@latest 100 | go install github.com/securego/gosec/v2/cmd/gosec@latest 101 | 102 | clean: 103 | @rm -f coverage.out 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minivpn 2 | 3 | A minimalistic implementation of the OpenVPN protocol in Go (client only). 4 | 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/ooni/gowl.svg)](https://pkg.go.dev/github.com/ooni/minivpn/vpn) 6 | ![Build Status](https://github.com/ooni/minivpn/workflows/build/badge.svg) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/ooni/minivpn)](https://goreportcard.com/report/github.com/ooni/minivpn) 8 | 9 | This implementation is intended for research purposes only. It has serious 10 | flaws, so please do **not** use it for any real-life situation where you need to 11 | trust it with user data. 12 | 13 | This is not a working implementation with all the properties that you need from 14 | software that can effectively protect your privacy. If you arrived here looking 15 | for such a thing, please use [misteriumnetwork/go-openvpn](https://github.com/mysteriumnetwork/go-openvpn) instead. 16 | 17 | 18 | ## License 19 | 20 | ``` 21 | SPDX-License-Identifier: GPL-3.0-or-later 22 | ``` 23 | 24 | ## OpenVPN Compatibility 25 | 26 | * Mode: Only `tls-client`. 27 | * Protocol: `UDPv4`, `TCPv4`. 28 | * Ciphers: `AES-128-CBC`, `AES-256-CBC`, `AES-128-GCM`, `AES-256-GCM`. 29 | * HMAC: `SHA1`, `SHA256`, `SHA512`. 30 | * Compression: `none`, `compress stub`, `comp-lzo no`. 31 | * tls-auth: `TODO`. 32 | * tls-crypt & [tls-crypt-v2](https://raw.githubusercontent.com/OpenVPN/openvpn/master/doc/tls-crypt-v2.txt): `TODO`. 33 | 34 | ## Additional features 35 | 36 | ### Obfuscation 37 | 38 | `obfs4` is supported. Add an additional entry in the config file, in this format: 39 | 40 | ``` 41 | proxy-obfs4 obfs4://RHOST:RPORT?cert=BASE64ENCODED_CERT&iat-mode=0 42 | ``` 43 | 44 | ## Configuration 45 | 46 | The public constructor for `vpn.Client` allows you to instantiate a `Client` from a 47 | correctly initialized `Options` object. 48 | 49 | For convenience, `minivpn` also understands how to parse a minimal subset of the 50 | configuration options that can be written in an openvpn config file. 51 | 52 | ### Inline file support 53 | 54 | Following the configuration format in the reference implementation, `minivpn` 55 | allows including files in the main configuration file, but only for the ` ca`, 56 | `cert` and `key` options. 57 | 58 | Each inline file is started by the line ``. 60 | 61 | Here is an example of an inline file usage: 62 | 63 | ``` 64 | 65 | -----BEGIN CERTIFICATE----- 66 | [...] 67 | -----END CERTIFICATE----- 68 | 69 | ``` 70 | 71 | ## Tests 72 | 73 | You can run a `connect+ping` test against a given provider (but be aware that 74 | there's very limited support for ciphersuites and compression). Place a config 75 | file in `data/provider/config`. The [bootstrap script](https://github.com/ooni/minivpn/blob/main/scripts/bootstrap-provider) 76 | can be useful. 77 | 78 | Then you can run: 79 | 80 | ``` 81 | make test-ping 82 | ``` 83 | 84 | ### Unit tests 85 | 86 | You can run the short tests: 87 | 88 | ``` 89 | go test -v --short ./... 90 | ``` 91 | 92 | ### Integration tests 93 | 94 | You will need `docker` installed to run the integration tests. They use a [fork 95 | of docker-openvpn](https://github.com/ooni/docker-openvpn) that allows us 96 | to configure some parameters at runtime (cipher and auth, for the time being). 97 | 98 | ``` 99 | cd tests/integration && go test -v . 100 | ``` 101 | 102 | The `dockertest` package will take care of everything: it starts a container 103 | that runs `openvpn`, binds it to port 1194, and exposes the config file for the 104 | test client on `localhost:8080`. 105 | 106 | However, for debugging sometimes is useful to run the container on one shell: 107 | 108 | ``` 109 | make integration-server 110 | ``` 111 | 112 | Now you can download the config file: 113 | 114 | ``` 115 | curl localhost:8080/ > config 116 | ``` 117 | 118 | That config file is valid to use it with the `openvpn` client. Pro tip: launch 119 | it in a [separated namespace](https://github.com/slingamn/namespaced-openvpn) 120 | so not to mess with your global routes. `make netns-shell` will drop you in 121 | a shell in the new namespace. 122 | 123 | To be able to use that config file with the `minivpn` client, you need to 124 | [extract](https://github.com/ooni/minivpn/blob/main/tests/integration/extract.sh) 125 | the different key blocks first. 126 | 127 | You can download the config file, split it and run integration tests with: 128 | 129 | ``` 130 | make test-local 131 | ``` 132 | 133 | ## Limitations 134 | 135 | Many, but re-keying is maybe one of the first expected to limit the usefulness 136 | in the current state. 137 | 138 | ## Security 139 | 140 | [7asecurity](https://7asecurity.com/) conducted an independent [whitebox security review against the minivpn implementation in August 2022](https://www.opentech.fund/results/security-safety-audits/minivpn-openvpn-go-client/). 141 | Please refer to their [full pentest report](https://www.opentech.fund/documents/216/MIV-01_-_minivpn_-_OpenVPN_Go_Client_Audit_Report_FINAL.pdf) for further details. 142 | 143 | Thanks to the Open Technology Fund for their support with this security audit. 144 | 145 | 146 | ## Pointers 147 | 148 | * [Security Overview](https://community.openvpn.net/openvpn/wiki/SecurityOverview) in the OpenVPN wiki. 149 | * [doc_procotocol_overview.h](https://github.com/OpenVPN/openvpn/blob/master/doc/doxygen/doc_protocol_overview.h) in OpenVPN source code. 150 | * [OpenVPN page in Wireshark wiki](https://wiki.wireshark.org/OpenVPN), with some available `pcaps`. 151 | 152 | ## References 153 | 154 | * https://github.com/OpenVPN/openvpn the reference implementation. 155 | * https://github.com/OpenVPN/openvpn3 the c++ class library for the client, protocol-compatible with the OpenVPN 2.x branch. 156 | * https://github.com/glacjay/govpn another go implementation 157 | * https://github.com/roburio/openvpn an ocaml implementation of a minimal subset of the protocol. 158 | * https://git.packetimpact.net/lvpn/ppyopenvpn a pure python implementation. 159 | 160 | ## Acknowledgements 161 | 162 | Big thanks to people that wrote other implementations. This project started as 163 | a learning exercise adapting `ppyopenvpn` to Go, and wouldn't have been 164 | possible without it. 165 | 166 | And to [Jason Donenfeld](https://www.jasondonenfeld.com/) for 167 | making [gVisor's netstack](https://gvisor.dev/docs/user_guide/networking/) more palatable. 168 | -------------------------------------------------------------------------------- /cmd/minivpn/iface.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | func getInterfaceByIP(ipAddr string) (*net.Interface, error) { 9 | interfaces, err := net.Interfaces() 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | for _, iface := range interfaces { 15 | addrs, err := iface.Addrs() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | for _, addr := range addrs { 21 | if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { 22 | if ipNet.IP.String() == ipAddr { 23 | return &iface, nil 24 | } 25 | } 26 | } 27 | } 28 | 29 | return nil, fmt.Errorf("interface with IP %s not found", ipAddr) 30 | } 31 | -------------------------------------------------------------------------------- /cmd/minivpn/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sync" 8 | "time" 9 | 10 | "github.com/apex/log" 11 | ) 12 | 13 | // Default handler outputting to stderr. 14 | var Default = NewHandler(os.Stderr) 15 | 16 | // start time. 17 | var start = time.Now() 18 | 19 | // colors. 20 | const ( 21 | none = 0 22 | red = 31 23 | green = 32 24 | yellow = 33 25 | blue = 34 26 | gray = 37 27 | ) 28 | 29 | // Colors mapping. 30 | var Colors = [...]int{ 31 | log.DebugLevel: gray, 32 | log.InfoLevel: blue, 33 | log.WarnLevel: yellow, 34 | log.ErrorLevel: red, 35 | log.FatalLevel: red, 36 | } 37 | 38 | // Strings mapping. 39 | var Strings = [...]string{ 40 | log.DebugLevel: "DEBUG", 41 | log.InfoLevel: "INFO", 42 | log.WarnLevel: "WARN", 43 | log.ErrorLevel: "ERROR", 44 | log.FatalLevel: "FATAL", 45 | } 46 | 47 | // Handler implementation. 48 | type Handler struct { 49 | mu sync.Mutex 50 | Writer io.Writer 51 | } 52 | 53 | // New handler. 54 | func NewHandler(w io.Writer) *Handler { 55 | return &Handler{ 56 | Writer: w, 57 | } 58 | } 59 | 60 | // HandleLog implements log.Handler. 61 | func (h *Handler) HandleLog(e *log.Entry) error { 62 | color := Colors[e.Level] 63 | level := Strings[e.Level] 64 | names := e.Fields.Names() 65 | 66 | h.mu.Lock() 67 | defer h.mu.Unlock() 68 | 69 | ts := time.Since(start) 70 | fmt.Fprintf(h.Writer, "\033[%dm%6s\033[0m[%10v] %-25s", color, level, ts, e.Message) 71 | 72 | for _, name := range names { 73 | fmt.Fprintf(h.Writer, " \033[%dm%s\033[0m=%v", color, name, e.Fields.Get(name)) 74 | } 75 | 76 | fmt.Fprintln(h.Writer) 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /cmd/minivpn/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "net" 9 | "os" 10 | "os/exec" 11 | "time" 12 | 13 | "github.com/Doridian/water" 14 | "github.com/apex/log" 15 | "github.com/jackpal/gateway" 16 | 17 | "github.com/ooni/minivpn/extras/ping" 18 | "github.com/ooni/minivpn/internal/runtimex" 19 | "github.com/ooni/minivpn/pkg/config" 20 | "github.com/ooni/minivpn/pkg/tracex" 21 | "github.com/ooni/minivpn/pkg/tunnel" 22 | ) 23 | 24 | func runCmd(binaryPath string, args ...string) { 25 | cmd := exec.Command(binaryPath, args...) 26 | cmd.Stderr = os.Stderr 27 | cmd.Stdout = os.Stdout 28 | cmd.Stdin = os.Stdin 29 | err := cmd.Run() 30 | if nil != err { 31 | log.WithError(err).Warn("error running /sbin/ip") 32 | } 33 | } 34 | 35 | func runIP(args ...string) { 36 | runCmd("/sbin/ip", args...) 37 | } 38 | 39 | func runRoute(args ...string) { 40 | runCmd("/sbin/route", args...) 41 | } 42 | 43 | type cmdConfig struct { 44 | configPath string 45 | doPing bool 46 | doTrace bool 47 | skipRoute bool 48 | timeout int 49 | } 50 | 51 | func main() { 52 | log.SetLevel(log.DebugLevel) 53 | 54 | cfg := &cmdConfig{} 55 | flag.StringVar(&cfg.configPath, "config", "", "config file to load") 56 | flag.BoolVar(&cfg.doPing, "ping", false, "if true, do ping and exit (for testing)") 57 | flag.BoolVar(&cfg.doTrace, "trace", false, "if true, do a trace of the handshake and exit (for testing)") 58 | flag.BoolVar(&cfg.skipRoute, "skip-route", false, "if true, exit without setting routes (for testing)") 59 | flag.IntVar(&cfg.timeout, "timeout", 60, "timeout in seconds (default=60)") 60 | flag.Parse() 61 | 62 | if cfg.configPath == "" { 63 | fmt.Println("[error] need config path") 64 | os.Exit(1) 65 | } 66 | 67 | log.SetHandler(NewHandler(os.Stderr)) 68 | log.SetLevel(log.DebugLevel) 69 | 70 | opts := []config.Option{ 71 | config.WithConfigFile(cfg.configPath), 72 | config.WithLogger(log.Log), 73 | } 74 | 75 | start := time.Now() 76 | 77 | var tracer *tracex.Tracer 78 | if cfg.doTrace { 79 | tracer = tracex.NewTracer(start) 80 | opts = append(opts, config.WithHandshakeTracer(tracer)) 81 | defer func() { 82 | trace := tracer.Trace() 83 | jsonData, err := json.MarshalIndent(trace, "", " ") 84 | runtimex.PanicOnError(err, "cannot serialize trace") 85 | fileName := fmt.Sprintf("handshake-trace-%s.json", time.Now().Format("2006-01-02-15:05:00")) 86 | os.WriteFile(fileName, jsonData, 0644) 87 | fmt.Println("trace written to", fileName) 88 | }() 89 | } 90 | 91 | // The TLS will expire in 60 seconds by default, but we can pass 92 | // a shorter timeout. 93 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.timeout)*time.Second) 94 | defer cancel() 95 | 96 | // create config from the passed options 97 | vpncfg := config.NewConfig(opts...) 98 | 99 | // create a vpn tun Device 100 | tun, err := tunnel.Start(ctx, &net.Dialer{}, vpncfg) 101 | if err != nil { 102 | log.WithError(err).Error("init error") 103 | return 104 | } 105 | log.Infof("Local IP: %s\n", tun.LocalAddr()) 106 | log.Infof("Gateway: %s\n", tun.RemoteAddr()) 107 | 108 | fmt.Println("initialization-sequence-completed") 109 | fmt.Printf("elapsed: %v\n", time.Since(start)) 110 | 111 | if cfg.doTrace { 112 | return 113 | } 114 | 115 | if cfg.doPing { 116 | pinger := ping.New("8.8.8.8", tun) 117 | count := 5 118 | pinger.Count = count 119 | 120 | err = pinger.Run(context.Background()) 121 | if err != nil { 122 | pinger.PrintStats() 123 | log.WithError(err).Fatal("ping error") 124 | } 125 | pinger.PrintStats() 126 | os.Exit(0) 127 | } 128 | 129 | if cfg.skipRoute { 130 | os.Exit(0) 131 | } 132 | 133 | // create a tun interface on the OS 134 | iface, err := water.New(water.Config{DeviceType: water.TUN}) 135 | runtimex.PanicOnError(err, "unable to open tun interface") 136 | 137 | // TODO: investigate what's the maximum working MTU, additionally get it from flag. 138 | MTU := 1420 139 | iface.SetMTU(MTU) 140 | 141 | localAddr := tun.LocalAddr().String() 142 | remoteAddr := tun.RemoteAddr().String() 143 | netMask := tun.NetMask() 144 | 145 | // discover local gateway IP, we need it to add a route to our remote via our network gw 146 | defaultGatewayIP, err := gateway.DiscoverGateway() 147 | if err != nil { 148 | log.Warn("could not discover default gateway IP, routes might be broken") 149 | } 150 | defaultInterfaceIP, err := gateway.DiscoverInterface() 151 | if err != nil { 152 | log.Warn("could not discover default route interface IP, routes might be broken") 153 | } 154 | defaultInterface, err := getInterfaceByIP(defaultInterfaceIP.String()) 155 | if err != nil { 156 | log.Warn("could not get default route interface, routes might be broken") 157 | } 158 | 159 | if defaultGatewayIP != nil && defaultInterface != nil { 160 | log.Infof("route add %s gw %v dev %s", vpncfg.Remote().IPAddr, defaultGatewayIP, defaultInterface.Name) 161 | runRoute("add", vpncfg.Remote().IPAddr, "gw", defaultGatewayIP.String(), defaultInterface.Name) 162 | } 163 | 164 | // we want the network CIDR for setting up the routes 165 | network := &net.IPNet{ 166 | IP: net.ParseIP(localAddr).Mask(netMask), 167 | Mask: netMask, 168 | } 169 | 170 | // configure the interface and bring it up 171 | runIP("addr", "add", localAddr, "dev", iface.Name()) 172 | runIP("link", "set", "dev", iface.Name(), "up") 173 | runRoute("add", remoteAddr, "gw", localAddr) 174 | runRoute("add", "-net", network.String(), "dev", iface.Name()) 175 | runIP("route", "add", "default", "via", remoteAddr, "dev", iface.Name()) 176 | 177 | go func() { 178 | for { 179 | packet := make([]byte, 2000) 180 | n, err := iface.Read(packet) 181 | if err != nil { 182 | log.WithError(err).Fatal("error reading from tun") 183 | } 184 | tun.Write(packet[:n]) 185 | } 186 | }() 187 | go func() { 188 | for { 189 | packet := make([]byte, 2000) 190 | n, err := tun.Read(packet) 191 | if err != nil { 192 | log.WithError(err).Fatal("error reading from tun") 193 | } 194 | iface.Write(packet[:n]) 195 | } 196 | }() 197 | select {} 198 | } 199 | -------------------------------------------------------------------------------- /examples/ping.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // This file is modified after tun/netstack/examples/ping_client.go in the 5 | // wireguard-go implementation. 6 | 7 | /* SPDX-License-Identifier: MIT 8 | * 9 | * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. 10 | * Copyright (C) 2022 Ain Ghazal. All Rights Reversed. 11 | */ 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "log" 17 | "math/rand" 18 | "time" 19 | 20 | "golang.org/x/net/icmp" 21 | "golang.org/x/net/ipv4" 22 | 23 | "github.com/ooni/minivpn/vpn" 24 | ) 25 | 26 | func main() { 27 | opts, err := vpn.ParseConfigFile("data/calyx/config") 28 | if err != nil { 29 | panic(err) 30 | } 31 | dialer := vpn.NewDialerFromOptions(opts) 32 | if err != nil { 33 | log.Panic(err) 34 | } 35 | 36 | socket, err := dialer.Dial("ping4", "riseup.net") 37 | if err != nil { 38 | log.Panic(err) 39 | } 40 | requestPing := icmp.Echo{ 41 | Seq: rand.Intn(1 << 16), 42 | Data: []byte("hello filternet"), // get the start ts in here, as sbasso suggested 43 | } 44 | icmpBytes, _ := (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) 45 | socket.SetReadDeadline(time.Now().Add(time.Second * 10)) 46 | start := time.Now() 47 | 48 | _, err = socket.Write(icmpBytes) 49 | if err != nil { 50 | log.Panic(err) 51 | } 52 | 53 | n, err := socket.Read(icmpBytes[:]) 54 | if err != nil { 55 | log.Panic(err) 56 | } 57 | replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) 58 | if err != nil { 59 | log.Panic(err) 60 | } 61 | replyPing, ok := replyPacket.Body.(*icmp.Echo) 62 | if !ok { 63 | log.Panicf("invalid reply type: %v", replyPacket) 64 | } 65 | if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { 66 | log.Panicf("invalid ping reply: %v", replyPing) 67 | } 68 | log.Printf("Ping latency: %v", time.Since(start)) 69 | } 70 | -------------------------------------------------------------------------------- /examples/urlgrabber.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // This file is modified after tun/netstack/examples/http_client.go in the 5 | // wireguard-go implementation. 6 | 7 | /* SPDX-License-Identifier: MIT 8 | * 9 | * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved. 10 | * Copyright (C) 2022 Ain Ghazal. All Rights Reversed. 11 | */ 12 | package main 13 | 14 | import ( 15 | "flag" 16 | "fmt" 17 | "io" 18 | "log" 19 | "net/http" 20 | "os" 21 | 22 | "github.com/ooni/minivpn/vpn" 23 | 24 | "runtime" 25 | "runtime/pprof" 26 | ) 27 | 28 | var cpuprofile = flag.String("cpuprof", "", "write cpu profile to `file`") 29 | var url = flag.String("url", "", "url to fetch") 30 | 31 | func main() { 32 | flag.Parse() 33 | if *cpuprofile != "" { 34 | runtime.SetCPUProfileRate(60) 35 | log.Println("creating cpu profile at:", *cpuprofile) 36 | f, err := os.Create(*cpuprofile) 37 | if err != nil { 38 | log.Fatal("could not create CPU profile: ", err) 39 | } 40 | defer f.Close() // error handling omitted for example 41 | if err := pprof.StartCPUProfile(f); err != nil { 42 | log.Fatal("could not start CPU profile: ", err) 43 | } 44 | defer pprof.StopCPUProfile() 45 | } 46 | provider := os.Getenv("PROVIDER") 47 | if provider == "" { 48 | log.Fatal("Export the PROVIDER variable") 49 | } 50 | opts, err := vpn.ParseConfigFile("data/" + provider + "/config") 51 | if err != nil { 52 | panic(err) 53 | } 54 | dialer := vpn.NewDialerFromOptions(opts) 55 | if err != nil { 56 | log.Panic(err) 57 | } 58 | 59 | client := http.Client{ 60 | Transport: &http.Transport{ 61 | DialContext: dialer.DialContext, 62 | }, 63 | } 64 | // BUG(ainghazal): https stalls unless I tweak the tun-mtu that the 65 | // remote announces. I might want to look at the mtu discovery that 66 | // openvpn does. 67 | //if len(os.Args) != 2 { 68 | // log.Println("Usage: get ") 69 | // os.Exit(1) 70 | //} 71 | 72 | resp, err := client.Get(*url) 73 | if err != nil { 74 | log.Panic(err) 75 | } 76 | body, err := io.ReadAll(resp.Body) 77 | if err != nil { 78 | log.Panic(err) 79 | } 80 | fmt.Println(string(body)) 81 | } 82 | -------------------------------------------------------------------------------- /extras/doc.go: -------------------------------------------------------------------------------- 1 | // Package extras contains some utilities that are not part of the OpenVPN implementation, 2 | // but that are useful for practical purposes together with the tunnel. 3 | package extras 4 | -------------------------------------------------------------------------------- /extras/memoryless/README.md: -------------------------------------------------------------------------------- 1 | LICENSE: Apache 2.0 2 | AUTHOR: Peter Boothe 3 | 4 | Code vendored from https://github.com/m-lab/go/blob/master/memoryless/README.md 5 | 6 | Functions which run a given function as a memoryless Poisson process. 7 | 8 | This is very useful if your function generates a gauge measurement or it exerts load on the system in some way. By distributing the measurement or load across time, we help ensure that our systems' data is minimally affected. 9 | 10 | -------------------------------------------------------------------------------- /extras/memoryless/memoryless.go: -------------------------------------------------------------------------------- 1 | // Package memoryless helps repeated calls to a function be distributed across 2 | // time in a memoryless fashion. 3 | // Vendored from https://github.com/m-lab/go/blob/master/memoryless/memoryless.go 4 | 5 | // SPDX-License-Identifier: Apache-2.0 6 | // (c) Peter Boothe 7 | 8 | package memoryless 9 | 10 | import ( 11 | "fmt" 12 | "log" 13 | "math/rand" 14 | "time" 15 | ) 16 | 17 | type Config struct { 18 | // Expected records the expected/mean/average amount of time between runs. 19 | Expected time.Duration 20 | // Min provides clamping of the randomly produced value. All timers will wait 21 | // at least Min time. 22 | Min time.Duration 23 | // Max provides clamping of the randomly produced value. All timers will take 24 | // at most Max time. 25 | Max time.Duration 26 | 27 | // Once is provided as a helper, because frequently for unit testing and 28 | // integration testing, you only want the "Forever" loop to run once. 29 | // 30 | // The zero value of this struct has Once set to false, which means the value 31 | // only needs to be set explicitly in codepaths where it might be true. 32 | Once bool 33 | } 34 | 35 | func (c Config) waittime() time.Duration { 36 | wt := time.Duration(rand.ExpFloat64() * float64(c.Expected)) 37 | if wt < c.Min { 38 | wt = c.Min 39 | } 40 | if c.Max != 0 && wt > c.Max { 41 | wt = c.Max 42 | } 43 | log.Println("wait time:", wt) 44 | return wt 45 | } 46 | 47 | // Check whether the config contrains sensible values. It return an error if the 48 | // config makes no mathematical sense, and nil if everything is okay. 49 | func (c Config) Check() error { 50 | if !(0 <= c.Min && c.Min <= c.Expected && (c.Max == 0 || c.Expected <= c.Max)) { 51 | return fmt.Errorf( 52 | "The arguments to Run make no sense. It should be true that Min <= Expected <= Max (or Min <= Expected and Max is 0), "+ 53 | "but that is not true for Min(%v) Expected(%v) Max(%v).", 54 | c.Min, c.Expected, c.Max) 55 | } 56 | return nil 57 | } 58 | 59 | // newTimer constructs and returns a timer. This function assumes that the 60 | // config has no errors. 61 | func newTimer(c Config) *time.Timer { 62 | return time.NewTimer(c.waittime()) 63 | } 64 | 65 | // NewTimer constructs a single-shot time.Timer that, if repeatedly used to 66 | // construct a series of timers, will ensure that the resulting events conform 67 | // to the memoryless distribution. For more on how this could and should be 68 | // used, see the comments to Ticker. It is intended to be a drop-in replacement 69 | // for time.NewTimer. 70 | func NewTimer(c Config) (*time.Timer, error) { 71 | if err := c.Check(); err != nil { 72 | return nil, err 73 | } 74 | return newTimer(c), nil 75 | } 76 | -------------------------------------------------------------------------------- /extras/ping/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Cameron Sparr and contributors. 4 | Copyright (c) 2022 Ain Ghazal. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /extras/ping/README.md: -------------------------------------------------------------------------------- 1 | `ping.go` is modified after the [go-ping](https://github.com/go-ping/ping) library. 2 | -------------------------------------------------------------------------------- /extras/ping/assert.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | func assert(assertion bool, message string) { 4 | if !assertion { 5 | panic(message) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /extras/ping/assert_test.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import "testing" 4 | 5 | func Test_assert(t *testing.T) { 6 | t.Run("assert false raises", func(t *testing.T) { 7 | defer func() { 8 | if r := recover(); r == nil { 9 | t.Errorf("a false assert should raise") 10 | return 11 | } 12 | }() 13 | assert(false, "should panic") 14 | }) 15 | t.Run("assert true does not raise", func(t *testing.T) { 16 | defer func() { 17 | if r := recover(); r != nil { 18 | t.Errorf("a true assert should not raise") 19 | return 20 | } 21 | }() 22 | assert(true, "should not panic") 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /extras/proxy.go: -------------------------------------------------------------------------------- 1 | package extras 2 | 3 | // This file contains some boilerplate to start a SOCKS5 proxy server connected 4 | // to a VPN tunnel. 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ooni/minivpn 2 | 3 | go 1.20 4 | 5 | // pinning for backwards-incompatible change 6 | // replace gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d => gitlab.com/yawning/obfs4.git v0.0.0-20210511220700-e330d1b7024b 7 | 8 | require ( 9 | git.torproject.org/pluggable-transports/goptlib.git v1.3.0 10 | github.com/Doridian/water v1.6.1 11 | github.com/apex/log v1.9.0 12 | github.com/google/go-cmp v0.5.9 13 | github.com/google/gopacket v1.1.19 14 | github.com/google/martian v2.1.0+incompatible 15 | github.com/google/uuid v1.3.0 16 | github.com/jackpal/gateway v1.0.11 // pinned to a previous version until we can use go1.21 17 | github.com/ory/dockertest/v3 v3.9.1 18 | github.com/refraction-networking/utls v1.3.1 19 | gitlab.com/yawning/obfs4.git v0.0.0-20220904064028-336a71d6e4cf 20 | golang.org/x/net v0.22.0 21 | golang.org/x/sync v0.6.0 22 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect 23 | ) 24 | 25 | require golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 26 | 27 | require ( 28 | filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20 // indirect 29 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 30 | github.com/Doridian/gopacket v1.2.1 // indirect 31 | github.com/Microsoft/go-winio v0.6.0 // indirect 32 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 33 | github.com/andybalholm/brotli v1.0.4 // indirect 34 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect 35 | github.com/containerd/continuity v0.3.0 // indirect 36 | github.com/davecgh/go-spew v1.1.1 // indirect 37 | github.com/dchest/siphash v1.2.1 // indirect 38 | github.com/docker/cli v20.10.14+incompatible // indirect 39 | github.com/docker/docker v20.10.7+incompatible // indirect 40 | github.com/docker/go-connections v0.4.0 // indirect 41 | github.com/docker/go-units v0.4.0 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/google/btree v1.1.2 // indirect 44 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 45 | github.com/imdario/mergo v0.3.12 // indirect 46 | github.com/klauspost/compress v1.15.15 // indirect 47 | github.com/kr/text v0.2.0 // indirect 48 | github.com/mitchellh/mapstructure v1.4.1 // indirect 49 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect 50 | github.com/opencontainers/go-digest v1.0.0 // indirect 51 | github.com/opencontainers/image-spec v1.0.2 // indirect 52 | github.com/opencontainers/runc v1.1.2 // indirect 53 | github.com/pkg/errors v0.9.1 // indirect 54 | github.com/pmezard/go-difflib v1.0.0 // indirect 55 | github.com/sirupsen/logrus v1.9.3 // indirect 56 | github.com/stretchr/objx v0.5.0 // indirect 57 | github.com/stretchr/testify v1.8.4 // indirect 58 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 59 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 60 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 61 | gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb // indirect 62 | golang.org/x/crypto v0.21.0 // indirect 63 | golang.org/x/mod v0.16.0 // indirect 64 | golang.org/x/sys v0.18.0 // indirect 65 | golang.org/x/time v0.3.0 // indirect 66 | golang.org/x/tools v0.19.0 // indirect 67 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 68 | gopkg.in/yaml.v2 v2.4.0 // indirect 69 | gopkg.in/yaml.v3 v3.0.1 // indirect 70 | gotest.tools/v3 v3.4.0 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /internal/bytesx/bytesx.go: -------------------------------------------------------------------------------- 1 | // Package bytesx provides functions operating on bytes. 2 | // 3 | // Specifically we implement these operations: 4 | // 5 | // 1. generating random bytes; 6 | // 7 | // 2. OpenVPN options encoding and decoding; 8 | // 9 | // 3. PKCS#7 padding and unpadding. 10 | package bytesx 11 | 12 | import ( 13 | "bytes" 14 | "crypto/rand" 15 | "encoding/binary" 16 | "errors" 17 | "fmt" 18 | "io" 19 | "math" 20 | 21 | "github.com/ooni/minivpn/internal/runtimex" 22 | ) 23 | 24 | var ( 25 | // ErrEncodeOption indicates an option encoding error occurred. 26 | ErrEncodeOption = errors.New("can't encode option") 27 | 28 | // ErrDecodeOption indicates an option decoding error occurred. 29 | ErrDecodeOption = errors.New("can't decode option") 30 | 31 | // ErrPaddingPKCS7 indicates that a PKCS#7 padding error has occurred. 32 | ErrPaddingPKCS7 = errors.New("PKCS#7 padding error") 33 | 34 | // ErrUnpaddingPKCS7 indicates that a PKCS#7 unpadding error has occurred. 35 | ErrUnpaddingPKCS7 = errors.New("PKCS#7 unpadding error") 36 | ) 37 | 38 | // genRandomBytes returns an array of bytes with the given size using 39 | // a CSRNG, on success, or an error, in case of failure. 40 | func GenRandomBytes(size int) ([]byte, error) { 41 | b := make([]byte, size) 42 | _, err := rand.Read(b) 43 | return b, err 44 | } 45 | 46 | // EncodeOptionStringToBytes is used to encode the options string, username and password. 47 | // 48 | // According to the OpenVPN protocol, options are represented as a two-byte word, 49 | // plus the byte representation of the string, null-terminated. 50 | // 51 | // See https://openvpn.net/community-resources/openvpn-protocol/. 52 | // 53 | // This function returns errEncodeOption in case of failure. 54 | func EncodeOptionStringToBytes(s string) ([]byte, error) { 55 | if len(s) >= math.MaxUint16 { // Using >= b/c we need to account for the final \0 56 | return nil, fmt.Errorf("%w:%s", ErrEncodeOption, "string too large") 57 | } 58 | data := make([]byte, 2) 59 | binary.BigEndian.PutUint16(data, uint16(len(s))+1) 60 | data = append(data, []byte(s)...) 61 | data = append(data, 0x00) 62 | return data, nil 63 | } 64 | 65 | // DecodeOptionStringFromBytes returns the string-value for the null-terminated string 66 | // returned by the server when sending remote options to us. 67 | // 68 | // This function returns errDecodeOption on failure. 69 | func DecodeOptionStringFromBytes(b []byte) (string, error) { 70 | if len(b) < 2 { 71 | return "", fmt.Errorf("%w: expected at least two bytes", ErrDecodeOption) 72 | } 73 | length := int(binary.BigEndian.Uint16(b[:2])) 74 | b = b[2:] // skip over the length 75 | // the server sends padding, so we cannot do a strict check 76 | if len(b) < length { 77 | return "", fmt.Errorf("%w: got %d, expected %d", ErrDecodeOption, len(b), length) 78 | } 79 | if len(b) <= 0 || length == 0 { 80 | return "", fmt.Errorf("%w: zero length encoded option is not possible: %s", ErrDecodeOption, 81 | "we need at least one byte for the trailing \\0") 82 | } 83 | if b[length-1] != 0x00 { 84 | return "", fmt.Errorf("%w: missing trailing \\0", ErrDecodeOption) 85 | } 86 | return string(b[:len(b)-1]), nil 87 | } 88 | 89 | // BytesUnpadPKCS7 performs the PKCS#7 unpadding of a byte array. 90 | func BytesUnpadPKCS7(b []byte, blockSize int) ([]byte, error) { 91 | // 1. check whether we can unpad at all 92 | if blockSize > math.MaxUint8 { 93 | return nil, fmt.Errorf("%w: blockSize too large", ErrUnpaddingPKCS7) 94 | } 95 | // 2. trivial case 96 | if len(b) <= 0 { 97 | return nil, fmt.Errorf("%w: passed empty buffer", ErrUnpaddingPKCS7) 98 | } 99 | // 4. read the padding size 100 | psiz := int(b[len(b)-1]) 101 | // 5. enforce padding size constraints 102 | if psiz <= 0x00 { 103 | return nil, fmt.Errorf("%w: padding size cannot be zero", ErrUnpaddingPKCS7) 104 | } 105 | if psiz > blockSize { 106 | return nil, fmt.Errorf("%w: padding size cannot be larger than blockSize", ErrUnpaddingPKCS7) 107 | } 108 | // 6. compute the padding offset 109 | off := len(b) - psiz 110 | // 7. return unpadded bytes 111 | runtimex.Assert(off >= 0 && off <= len(b), "off is out of bounds") 112 | return b[:off], nil 113 | } 114 | 115 | // bytesPadPKCS7 returns the PKCS#7 padding of a byte array. 116 | func BytesPadPKCS7(b []byte, blockSize int) ([]byte, error) { 117 | runtimex.PanicIfTrue(blockSize <= 0, "blocksize cannot be negative or zero") 118 | 119 | // If lth mod blockSize == 0, then the input gets appended a whole block size 120 | // See https://datatracker.ietf.org/doc/html/rfc5652#section-6.3 121 | if blockSize > math.MaxUint8 { 122 | // This padding method is well defined iff blockSize is less than 256. 123 | return nil, ErrPaddingPKCS7 124 | } 125 | psiz := blockSize - len(b)%blockSize 126 | padding := bytes.Repeat([]byte{byte(psiz)}, psiz) 127 | return append(b, padding...), nil 128 | } 129 | 130 | // ReadUint32 is a convenience function that reads a uint32 from a 4-byte 131 | // buffer, returning an error if the operation failed. 132 | func ReadUint32(buf *bytes.Buffer) (uint32, error) { 133 | var numBuf [4]byte 134 | _, err := io.ReadFull(buf, numBuf[:]) 135 | if err != nil { 136 | return 0, err 137 | } 138 | return binary.BigEndian.Uint32(numBuf[:]), nil 139 | } 140 | 141 | // WriteUint32 is a convenience function that appends to the given buffer 142 | // 4 bytes containing the big-endian representation of the given uint32 value. 143 | // Caller is responsible to ensure the passed value does not overflow the 144 | // maximal capacity of 4 bytes. 145 | func WriteUint32(buf *bytes.Buffer, val uint32) { 146 | var numBuf [4]byte 147 | binary.BigEndian.PutUint32(numBuf[:], val) 148 | buf.Write(numBuf[:]) 149 | } 150 | 151 | // WriteUint24 is a convenience function that appends to the given buffer 152 | // 3 bytes containing the big-endian representation of the given uint32 value. 153 | // Caller is responsible to ensure the passed value does not overflow the 154 | // maximal capacity of 3 bytes. 155 | func WriteUint24(buf *bytes.Buffer, val uint32) { 156 | b := &bytes.Buffer{} 157 | WriteUint32(b, val) 158 | buf.Write(b.Bytes()[1:]) 159 | } 160 | -------------------------------------------------------------------------------- /internal/controlchannel/controlchannel.go: -------------------------------------------------------------------------------- 1 | // Package controlchannel implements the control channel logic. The control channel sits 2 | // above the reliable transport and below the TLS layer. 3 | package controlchannel 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/ooni/minivpn/internal/model" 9 | "github.com/ooni/minivpn/internal/session" 10 | "github.com/ooni/minivpn/internal/workers" 11 | "github.com/ooni/minivpn/pkg/config" 12 | ) 13 | 14 | var ( 15 | serviceName = "controlchannel" 16 | ) 17 | 18 | // Service is the controlchannel service. Make sure you initialize 19 | // the channels before invoking [Service.StartWorkers]. 20 | type Service struct { 21 | // NotifyTLS is the channel that sends notifications up to the TLS layer. 22 | NotifyTLS *chan *model.Notification 23 | 24 | // ControlToReliable moves packets from us down to the reliable layer. 25 | ControlToReliable *chan *model.Packet 26 | 27 | // ReliableToControl moves packets up to us from the reliable layer below. 28 | ReliableToControl chan *model.Packet 29 | 30 | // TLSRecordToControl moves bytes down to us from the TLS layer above. 31 | TLSRecordToControl chan []byte 32 | 33 | // TLSRecordFromControl moves bytes from us up to the TLS layer above. 34 | TLSRecordFromControl *chan []byte 35 | } 36 | 37 | // StartWorkers starts the control-channel workers. See the [ARCHITECTURE] 38 | // file for more information about the packet-muxer workers. 39 | // 40 | // [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md 41 | func (svc *Service) StartWorkers( 42 | config *config.Config, 43 | workersManager *workers.Manager, 44 | sessionManager *session.Manager, 45 | ) { 46 | ws := &workersState{ 47 | logger: config.Logger(), 48 | notifyTLS: *svc.NotifyTLS, 49 | controlToReliable: *svc.ControlToReliable, 50 | reliableToControl: svc.ReliableToControl, 51 | tlsRecordToControl: svc.TLSRecordToControl, 52 | tlsRecordFromControl: *svc.TLSRecordFromControl, 53 | sessionManager: sessionManager, 54 | workersManager: workersManager, 55 | } 56 | workersManager.StartWorker(ws.moveUpWorker) 57 | workersManager.StartWorker(ws.moveDownWorker) 58 | } 59 | 60 | // workersState contains the control channel state. 61 | type workersState struct { 62 | logger model.Logger 63 | notifyTLS chan<- *model.Notification 64 | controlToReliable chan<- *model.Packet 65 | reliableToControl <-chan *model.Packet 66 | tlsRecordToControl <-chan []byte 67 | tlsRecordFromControl chan<- []byte 68 | sessionManager *session.Manager 69 | workersManager *workers.Manager 70 | } 71 | 72 | func (ws *workersState) moveUpWorker() { 73 | workerName := fmt.Sprintf("%s: moveUpWorker", serviceName) 74 | 75 | defer func() { 76 | ws.workersManager.OnWorkerDone(workerName) 77 | ws.workersManager.StartShutdown() 78 | }() 79 | 80 | ws.logger.Debugf("%s: started", workerName) 81 | 82 | for { 83 | // POSSIBLY BLOCK on reading the packet moving up the stack 84 | select { 85 | case packet := <-ws.reliableToControl: 86 | // route the packets depending on their opcode 87 | switch packet.Opcode { 88 | 89 | case model.P_CONTROL_SOFT_RESET_V1: 90 | // We cannot blindly accept SOFT_RESET requests. They only make sense 91 | // when we have generated keys. Note that a SOFT_RESET returns us to 92 | // the INITIAL state, therefore, we will not have concurrent resets in place, 93 | // even if after the first key generation we receive two SOFT_RESET requests 94 | // back to back. 95 | 96 | if ws.sessionManager.NegotiationState() < model.S_GENERATED_KEYS { 97 | continue 98 | } 99 | ws.sessionManager.SetNegotiationState(model.S_INITIAL) 100 | // TODO(ainghazal): revisit this step. 101 | // when we implement key rotation. OpenVPN has 102 | // the concept of a "lame duck", i.e., the 103 | // retiring key that needs to be expired a fixed time after the new 104 | // one starts its lifetime, and this might be a good place to try 105 | // to retire the old key. 106 | 107 | // notify the TLS layer that it should initiate 108 | // a TLS handshake and, if successful, generate 109 | // new keys for the data channel 110 | select { 111 | case ws.notifyTLS <- &model.Notification{Flags: model.NotificationReset}: 112 | // nothing 113 | 114 | case <-ws.workersManager.ShouldShutdown(): 115 | return 116 | } 117 | 118 | case model.P_CONTROL_V1: 119 | // send the packet to the TLS layer 120 | select { 121 | case ws.tlsRecordFromControl <- packet.Payload: 122 | // nothing 123 | 124 | case <-ws.workersManager.ShouldShutdown(): 125 | return 126 | } 127 | } 128 | 129 | case <-ws.workersManager.ShouldShutdown(): 130 | return 131 | } 132 | } 133 | } 134 | 135 | func (ws *workersState) moveDownWorker() { 136 | workerName := fmt.Sprintf("%s: moveDownWorker", serviceName) 137 | 138 | defer func() { 139 | ws.workersManager.OnWorkerDone(workerName) 140 | ws.workersManager.StartShutdown() 141 | }() 142 | 143 | ws.logger.Debugf("%s: started", workerName) 144 | 145 | for { 146 | // POSSIBLY BLOCK on reading the TLS record moving down the stack 147 | select { 148 | case record := <-ws.tlsRecordToControl: 149 | // transform the record into a control message 150 | packet, err := ws.sessionManager.NewPacket(model.P_CONTROL_V1, record) 151 | if err != nil { 152 | ws.logger.Warnf("%s: NewPacket: %s", workerName, err.Error()) 153 | return 154 | } 155 | 156 | // POSSIBLY BLOCK on sending the packet down the stack 157 | select { 158 | case ws.controlToReliable <- packet: 159 | // nothing 160 | 161 | case <-ws.workersManager.ShouldShutdown(): 162 | return 163 | } 164 | 165 | case <-ws.workersManager.ShouldShutdown(): 166 | return 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /internal/datachannel/common_test.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha1" 7 | "testing" 8 | 9 | "github.com/ooni/minivpn/internal/model" 10 | "github.com/ooni/minivpn/internal/runtimex" 11 | "github.com/ooni/minivpn/internal/session" 12 | "github.com/ooni/minivpn/internal/vpntest" 13 | "github.com/ooni/minivpn/pkg/config" 14 | ) 15 | 16 | func makeTestingSession() *session.Manager { 17 | manager, err := session.NewManager(config.NewConfig()) 18 | runtimex.PanicOnError(err, "could not get session manager") 19 | manager.SetRemoteSessionID(model.SessionID{0x01}) 20 | return manager 21 | } 22 | 23 | func makeTestingOptions(t *testing.T, cipher, auth string) *config.OpenVPNOptions { 24 | crt, _ := vpntest.WriteTestingCerts(t.TempDir()) 25 | opt := &config.OpenVPNOptions{ 26 | Cipher: cipher, 27 | Auth: auth, 28 | CertPath: crt.Cert, 29 | KeyPath: crt.Key, 30 | CAPath: crt.CA, 31 | } 32 | return opt 33 | } 34 | 35 | func makeTestingStateAEAD() *dataChannelState { 36 | dataCipher, _ := newDataCipher(cipherNameAES, 128, cipherModeGCM) 37 | st := &dataChannelState{ 38 | hash: sha1.New, 39 | cipherKeyLocal: *(*keySlot)(bytes.Repeat([]byte{0x65}, 64)), 40 | cipherKeyRemote: *(*keySlot)(bytes.Repeat([]byte{0x66}, 64)), 41 | hmacKeyLocal: *(*keySlot)(bytes.Repeat([]byte{0x67}, 64)), 42 | hmacKeyRemote: *(*keySlot)(bytes.Repeat([]byte{0x68}, 64)), 43 | } 44 | st.hmacLocal = hmac.New(st.hash, st.hmacKeyLocal[:20]) 45 | st.hmacRemote = hmac.New(st.hash, st.hmacKeyRemote[:20]) 46 | st.dataCipher = dataCipher 47 | return st 48 | } 49 | 50 | func makeTestingStateNonAEAD() *dataChannelState { 51 | dataCipher, _ := newDataCipher(cipherNameAES, 128, cipherModeCBC) 52 | st := &dataChannelState{ 53 | hash: sha1.New, 54 | cipherKeyLocal: *(*keySlot)(bytes.Repeat([]byte{0x65}, 64)), 55 | cipherKeyRemote: *(*keySlot)(bytes.Repeat([]byte{0x66}, 64)), 56 | hmacKeyLocal: *(*keySlot)(bytes.Repeat([]byte{0x67}, 64)), 57 | hmacKeyRemote: *(*keySlot)(bytes.Repeat([]byte{0x68}, 64)), 58 | } 59 | st.hmacLocal = hmac.New(st.hash, st.hmacKeyLocal[:20]) 60 | st.hmacRemote = hmac.New(st.hash, st.hmacKeyRemote[:20]) 61 | st.dataCipher = dataCipher 62 | return st 63 | } 64 | 65 | func makeTestingStateNonAEADReversed() *dataChannelState { 66 | dataCipher, _ := newDataCipher(cipherNameAES, 128, cipherModeCBC) 67 | st := &dataChannelState{ 68 | hash: sha1.New, 69 | cipherKeyRemote: *(*keySlot)(bytes.Repeat([]byte{0x65}, 64)), 70 | cipherKeyLocal: *(*keySlot)(bytes.Repeat([]byte{0x66}, 64)), 71 | hmacKeyRemote: *(*keySlot)(bytes.Repeat([]byte{0x67}, 64)), 72 | hmacKeyLocal: *(*keySlot)(bytes.Repeat([]byte{0x68}, 64)), 73 | } 74 | st.hmacLocal = hmac.New(st.hash, st.hmacKeyLocal[:20]) 75 | st.hmacRemote = hmac.New(st.hash, st.hmacKeyRemote[:20]) 76 | st.dataCipher = dataCipher 77 | return st 78 | } 79 | 80 | const ( 81 | rnd16 = "0123456789012345" 82 | rnd32 = "01234567890123456789012345678901" 83 | rnd48 = "012345678901234567890123456789012345678901234567" 84 | ) 85 | 86 | func makeTestKeys() ([32]byte, [32]byte, [48]byte) { 87 | r1 := *(*[32]byte)([]byte(rnd32)) 88 | r2 := *(*[32]byte)([]byte(rnd32)) 89 | r3 := *(*[48]byte)([]byte(rnd48)) 90 | return r1, r2, r3 91 | } 92 | 93 | func makeTestingDataChannelKey() *session.DataChannelKey { 94 | rl1, rl2, preml := makeTestKeys() 95 | rr1, rr2, premr := makeTestKeys() 96 | 97 | ksLocal := &session.KeySource{R1: rl1, R2: rl2, PreMaster: preml} 98 | ksRemote := &session.KeySource{R1: rr1, R2: rr2, PreMaster: premr} 99 | 100 | dck := &session.DataChannelKey{} 101 | dck.AddLocalKey(ksLocal) 102 | dck.AddRemoteKey(ksRemote) 103 | return dck 104 | } 105 | -------------------------------------------------------------------------------- /internal/datachannel/doc.go: -------------------------------------------------------------------------------- 1 | // Package datachannel implements packet encryption and decryption over the OpenVPN Data Channel. 2 | // Encryption Keys are derived after a successful TLS handshake, and they have a limited 3 | // lifetime. 4 | package datachannel 5 | -------------------------------------------------------------------------------- /internal/datachannel/errors.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import "errors" 4 | 5 | var ( 6 | errDataChannelKey = errors.New("bad key") 7 | errBadCompression = errors.New("bad compression") 8 | ErrReplayAttack = errors.New("replay attack") 9 | ErrBadHMAC = errors.New("bad hmac") 10 | ErrInitError = errors.New("improperly initialized") 11 | ErrExpiredKey = errors.New("key is expired") 12 | 13 | // ErrInvalidKeySize means that the key size is invalid. 14 | ErrInvalidKeySize = errors.New("invalid key size") 15 | 16 | // ErrUnsupportedCipher indicates we don't support the desired cipher. 17 | ErrUnsupportedCipher = errors.New("unsupported cipher") 18 | 19 | // ErrUnsupportedMode indicates that the mode is not uspported. 20 | ErrUnsupportedMode = errors.New("unsupported mode") 21 | 22 | // ErrBadInput indicates invalid inputs to encrypt/decrypt functions. 23 | ErrBadInput = errors.New("bad input") 24 | 25 | ErrSerialization = errors.New("cannot create packet") 26 | ErrCannotEncrypt = errors.New("cannot encrypt") 27 | ErrCannotDecrypt = errors.New("cannot decrypt") 28 | ) 29 | -------------------------------------------------------------------------------- /internal/datachannel/read.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/ooni/minivpn/internal/bytesx" 11 | "github.com/ooni/minivpn/internal/model" 12 | "github.com/ooni/minivpn/internal/runtimex" 13 | "github.com/ooni/minivpn/internal/session" 14 | "github.com/ooni/minivpn/pkg/config" 15 | ) 16 | 17 | var ( 18 | ErrTooShort = errors.New("too short") 19 | ErrBadRemoteHMAC = errors.New("bad remote hmac") 20 | ) 21 | 22 | func decodeEncryptedPayloadAEAD(log model.Logger, buf []byte, session *session.Manager, state *dataChannelState) (*encryptedData, error) { 23 | // P_DATA_V2 GCM data channel crypto format 24 | // 48000001 00000005 7e7046bd 444a7e28 cc6387b1 64a4d6c1 380275a... 25 | // [ OP32 ] [seq # ] [ auth tag ] [ payload ... ] 26 | // - means authenticated - * means encrypted * 27 | // [ - opcode/peer-id - ] [ - packet ID - ] [ TAG ] [ * packet payload * ] 28 | 29 | // preconditions 30 | runtimex.Assert(state != nil, "passed nil state") 31 | runtimex.Assert(state.dataCipher != nil, "data cipher not initialized") 32 | 33 | if len(buf) == 0 || len(buf) < 20 { 34 | return &encryptedData{}, fmt.Errorf("%w: %d bytes", ErrTooShort, len(buf)) 35 | } 36 | if len(state.hmacKeyRemote) < 8 { 37 | return &encryptedData{}, ErrBadRemoteHMAC 38 | } 39 | remoteHMAC := state.hmacKeyRemote[:8] 40 | packet_id := buf[:4] 41 | 42 | headers := &bytes.Buffer{} 43 | headers.WriteByte(opcodeAndKeyHeader(session)) 44 | bytesx.WriteUint24(headers, uint32(session.TunnelInfo().PeerID)) 45 | headers.Write(packet_id) 46 | 47 | // we need to swap because decryption expects payload|tag 48 | // but we've got tag | payload instead 49 | payload := &bytes.Buffer{} 50 | payload.Write(buf[20:]) // ciphertext 51 | payload.Write(buf[4:20]) // tag 52 | 53 | // iv := packetID | remoteHMAC 54 | iv := &bytes.Buffer{} 55 | iv.Write(packet_id) 56 | iv.Write(remoteHMAC) 57 | 58 | encrypted := &encryptedData{ 59 | iv: iv.Bytes(), 60 | ciphertext: payload.Bytes(), 61 | aead: headers.Bytes(), 62 | } 63 | return encrypted, nil 64 | } 65 | 66 | var ErrCannotDecode = errors.New("cannot decode") 67 | 68 | func decodeEncryptedPayloadNonAEAD(log model.Logger, buf []byte, session *session.Manager, state *dataChannelState) (*encryptedData, error) { 69 | runtimex.Assert(state != nil, "passed nil state") 70 | runtimex.Assert(state.dataCipher != nil, "data cipher not initialized") 71 | 72 | hashSize := uint8(state.hmacRemote.Size()) 73 | blockSize := state.dataCipher.blockSize() 74 | 75 | minLen := hashSize + blockSize 76 | 77 | if len(buf) < int(minLen) { 78 | return &encryptedData{}, fmt.Errorf("%w: too short (%d bytes)", ErrCannotDecode, len(buf)) 79 | } 80 | 81 | receivedHMAC := buf[:hashSize] 82 | iv := buf[hashSize : hashSize+blockSize] 83 | cipherText := buf[hashSize+blockSize:] 84 | 85 | state.hmacRemote.Reset() 86 | state.hmacRemote.Write(iv) 87 | state.hmacRemote.Write(cipherText) 88 | computedHMAC := state.hmacRemote.Sum(nil) 89 | 90 | if !hmac.Equal(computedHMAC, receivedHMAC) { 91 | log.Warnf("expected: %x, got: %x", computedHMAC, receivedHMAC) 92 | return &encryptedData{}, fmt.Errorf("%w: %s", ErrCannotDecrypt, ErrBadHMAC) 93 | } 94 | 95 | encrypted := &encryptedData{ 96 | iv: iv, 97 | ciphertext: cipherText, 98 | aead: []byte{}, // no AEAD data in this mode, leaving it empty to satisfy common interface 99 | } 100 | return encrypted, nil 101 | } 102 | 103 | // maybeDecompress de-serializes the data from the payload according to the framing 104 | // given by different compression methods. only the different no-compression 105 | // modes are supported at the moment, so no real decompression is done. It 106 | // returns a byte array, and an error if the operation could not be completed 107 | // successfully. 108 | func maybeDecompress(b []byte, st *dataChannelState, opt *config.OpenVPNOptions) ([]byte, error) { 109 | if st == nil || st.dataCipher == nil { 110 | return []byte{}, fmt.Errorf("%w:%s", ErrBadInput, "bad state") 111 | } 112 | if opt == nil { 113 | return []byte{}, fmt.Errorf("%w:%s", ErrBadInput, "bad options") 114 | } 115 | 116 | var compr byte // compression type 117 | var payload []byte 118 | 119 | // TODO(ainghazal): have two different decompress implementations 120 | // instead of this switch 121 | 122 | switch st.dataCipher.isAEAD() { 123 | case true: 124 | switch opt.Compress { 125 | case config.CompressionStub, config.CompressionLZONo: 126 | // these are deprecated in openvpn 2.5.x 127 | compr = b[0] 128 | payload = b[1:] 129 | default: 130 | compr = 0x00 131 | payload = b[:] 132 | } 133 | default: // non-aead 134 | remotePacketID := model.PacketID(binary.BigEndian.Uint32(b[:4])) 135 | lastKnownRemote, err := st.RemotePacketID() 136 | if err != nil { 137 | return payload, err 138 | } 139 | if remotePacketID <= lastKnownRemote { 140 | return []byte{}, ErrReplayAttack 141 | } 142 | st.SetRemotePacketID(remotePacketID) 143 | 144 | switch opt.Compress { 145 | case config.CompressionStub, config.CompressionLZONo: 146 | compr = b[4] 147 | payload = b[5:] 148 | default: 149 | compr = 0x00 150 | payload = b[4:] 151 | } 152 | } 153 | 154 | switch compr { 155 | case 0xfb: 156 | // compression stub swap: 157 | // we get the last byte and replace the compression byte 158 | // these are deprecated in openvpn 2.5.x 159 | end := payload[len(payload)-1] 160 | b := payload[:len(payload)-1] 161 | payload = append([]byte{end}, b...) 162 | case 0x00, 0xfa: 163 | // do nothing 164 | // 0x00 is compress-no, 165 | // 0xfa is the old no compression or comp-lzo no case. 166 | // http://build.openvpn.net/doxygen/comp_8h_source.html 167 | // see: https://community.openvpn.net/openvpn/ticket/952#comment:5 168 | default: 169 | return []byte{}, fmt.Errorf("%w: cannot handle compression %x", errBadCompression, compr) 170 | } 171 | return payload, nil 172 | } 173 | -------------------------------------------------------------------------------- /internal/datachannel/read_test.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "errors" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/apex/log" 11 | "github.com/ooni/minivpn/internal/session" 12 | ) 13 | 14 | func Test_decodeEncryptedPayloadAEAD(t *testing.T) { 15 | state := makeTestingStateAEAD() 16 | goodEncryptedPayload, _ := hex.DecodeString("00000000b3653a842f2b8a148de26375218fb01d31278ff328ff2fc65c4dbf9eb8e67766") 17 | goodDecodeIV, _ := hex.DecodeString("000000006868686868686868") 18 | goodDecodeCipherText, _ := hex.DecodeString("31278ff328ff2fc65c4dbf9eb8e67766b3653a842f2b8a148de26375218fb01d") 19 | goodDecodeAEAD, _ := hex.DecodeString("4800000000000000") 20 | 21 | type args struct { 22 | buf []byte 23 | session *session.Manager 24 | state *dataChannelState 25 | } 26 | tests := []struct { 27 | name string 28 | args args 29 | want *encryptedData 30 | wantErr error 31 | }{ 32 | { 33 | "empty buffer should fail", 34 | args{ 35 | []byte{}, 36 | makeTestingSession(), 37 | state, 38 | }, 39 | &encryptedData{}, 40 | ErrTooShort, 41 | }, 42 | { 43 | "too short should fail", 44 | args{ 45 | bytes.Repeat([]byte{0xff}, 19), 46 | makeTestingSession(), 47 | state, 48 | }, 49 | &encryptedData{}, 50 | ErrTooShort, 51 | }, 52 | { 53 | "good decode should not fail", 54 | args{ 55 | goodEncryptedPayload, 56 | makeTestingSession(), 57 | state, 58 | }, 59 | &encryptedData{ 60 | iv: goodDecodeIV, 61 | ciphertext: goodDecodeCipherText, 62 | aead: goodDecodeAEAD, 63 | }, 64 | nil, 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | got, err := decodeEncryptedPayloadAEAD(log.Log, tt.args.buf, tt.args.session, tt.args.state) 70 | if !errors.Is(err, tt.wantErr) { 71 | t.Errorf("decodeEncryptedPayloadAEAD() error = %v, wantErr %v", err, tt.wantErr) 72 | return 73 | } 74 | if !reflect.DeepEqual(got, tt.want) { 75 | t.Errorf("decodeEncryptedPayloadAEAD() = %v, want %v", got, tt.want) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func Test_decodeEncryptedPayloadNonAEAD(t *testing.T) { 82 | 83 | goodInput, _ := hex.DecodeString("fdf9b069b2e5a637fa7b5c9231166ea96307e4123031323334353637383930313233343581e4878c5eec602c2d2f5a95139c84af") 84 | iv, _ := hex.DecodeString("30313233343536373839303132333435") 85 | ciphertext, _ := hex.DecodeString("81e4878c5eec602c2d2f5a95139c84af") 86 | 87 | type args struct { 88 | buf []byte 89 | session *session.Manager 90 | state *dataChannelState 91 | } 92 | tests := []struct { 93 | name string 94 | args args 95 | want *encryptedData 96 | wantErr error 97 | }{ 98 | { 99 | name: "empty buffer should fail", 100 | args: args{ 101 | []byte{}, 102 | makeTestingSession(), 103 | makeTestingStateNonAEAD(), 104 | }, 105 | want: &encryptedData{}, 106 | wantErr: ErrCannotDecode, 107 | }, 108 | { 109 | name: "too short buffer should fail", 110 | args: args{ 111 | bytes.Repeat([]byte{0xff}, 27), 112 | makeTestingSession(), 113 | makeTestingStateNonAEAD(), 114 | }, 115 | want: &encryptedData{}, 116 | wantErr: ErrCannotDecode, 117 | }, 118 | { 119 | name: "good decode", 120 | args: args{ 121 | goodInput, 122 | makeTestingSession(), 123 | makeTestingStateNonAEADReversed(), 124 | }, 125 | want: &encryptedData{ 126 | iv: iv, 127 | ciphertext: ciphertext, 128 | }, 129 | wantErr: nil, 130 | }, 131 | } 132 | for _, tt := range tests { 133 | t.Run(tt.name, func(t *testing.T) { 134 | got, err := decodeEncryptedPayloadNonAEAD(log.Log, tt.args.buf, tt.args.session, tt.args.state) 135 | if !errors.Is(err, tt.wantErr) { 136 | t.Errorf("decodeEncryptedPayloadNonAEAD() error = %v, wantErr %v", err, tt.wantErr) 137 | return 138 | } 139 | if !bytes.Equal(got.iv, tt.want.iv) { 140 | t.Errorf("decodeEncryptedPayloadNonAEAD().iv = %v, want %v", got.iv, tt.want.iv) 141 | } 142 | if !bytes.Equal(got.ciphertext, tt.want.ciphertext) { 143 | t.Errorf("decodeEncryptedPayloadNonAEAD().iv = %v, want %v", got.iv, tt.want.iv) 144 | } 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/datachannel/service.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | // 4 | // OpenVPN data channel 5 | // 6 | 7 | import ( 8 | "encoding/hex" 9 | "fmt" 10 | "sync" 11 | 12 | "github.com/ooni/minivpn/internal/model" 13 | "github.com/ooni/minivpn/internal/session" 14 | "github.com/ooni/minivpn/internal/workers" 15 | "github.com/ooni/minivpn/pkg/config" 16 | ) 17 | 18 | var ( 19 | serviceName = "datachannel" 20 | ) 21 | 22 | // Service is the datachannel service. Make sure you initialize 23 | // the channels before invoking [Service.StartWorkers]. 24 | type Service struct { 25 | // MuxerToData moves packets up to us 26 | MuxerToData chan *model.Packet 27 | 28 | // DataOrControlToMuxer is a shared channel to write packets to the muxer layer below 29 | DataOrControlToMuxer *chan *model.Packet 30 | 31 | // TUNToData moves bytes down from the TUN layer above 32 | TUNToData chan []byte 33 | 34 | // DataToTUN moves bytes up from us to the TUN layer above us 35 | DataToTUN chan []byte 36 | 37 | // KeyReady is where the TLSState layer passes us any new keys 38 | KeyReady chan *session.DataChannelKey 39 | } 40 | 41 | // StartWorkers starts the data-channel workers. 42 | // 43 | // We start three workers: 44 | // 45 | // 1. moveUpWorker BLOCKS on dataPacketUp to read a packet coming from the muxer and 46 | // eventually BLOCKS on tunUp to deliver it; 47 | // 48 | // 2. moveDownWorker BLOCKS on tunDown to read a packet and 49 | // eventually BLOCKS on dataOrControlToMuxer to deliver it; 50 | // 51 | // 3. keyWorker BLOCKS on keyUp to read a dataChannelKey and 52 | // initializes the internal state with the resulting key; 53 | func (s *Service) StartWorkers( 54 | config *config.Config, 55 | workersManager *workers.Manager, 56 | sessionManager *session.Manager, 57 | ) { 58 | dc, err := NewDataChannelFromOptions(config.Logger(), config.OpenVPNOptions(), sessionManager) 59 | if err != nil { 60 | config.Logger().Warnf("cannot initialize channel %v", err) 61 | return 62 | } 63 | ws := &workersState{ 64 | dataChannel: dc, 65 | dataOrControlToMuxer: *s.DataOrControlToMuxer, 66 | dataToTUN: s.DataToTUN, 67 | keyReady: s.KeyReady, 68 | logger: config.Logger(), 69 | muxerToData: s.MuxerToData, 70 | sessionManager: sessionManager, 71 | tunToData: s.TUNToData, 72 | workersManager: workersManager, 73 | } 74 | 75 | firstKeyReady := make(chan any) 76 | 77 | workersManager.StartWorker(ws.moveUpWorker) 78 | workersManager.StartWorker(func() { ws.moveDownWorker(firstKeyReady) }) 79 | workersManager.StartWorker(func() { ws.keyWorker(firstKeyReady) }) 80 | } 81 | 82 | // workersState contains the data channel state. 83 | type workersState struct { 84 | dataChannel *DataChannel 85 | dataOrControlToMuxer chan<- *model.Packet 86 | dataToTUN chan<- []byte 87 | keyReady <-chan *session.DataChannelKey 88 | logger model.Logger 89 | muxerToData <-chan *model.Packet 90 | sessionManager *session.Manager 91 | tunToData <-chan []byte 92 | workersManager *workers.Manager 93 | } 94 | 95 | // moveDownWorker moves packets down the stack. It will BLOCK on PacketDown 96 | func (ws *workersState) moveDownWorker(firstKeyReady <-chan any) { 97 | workerName := serviceName + ":moveDownWorker" 98 | defer func() { 99 | ws.workersManager.OnWorkerDone(workerName) 100 | ws.workersManager.StartShutdown() 101 | }() 102 | 103 | ws.logger.Debugf("%s: started", workerName) 104 | 105 | select { 106 | // wait for the first key to be ready 107 | case <-firstKeyReady: 108 | for { 109 | select { 110 | case data := <-ws.tunToData: 111 | // TODO: writePacket should get the ACTIVE KEY (verify this) 112 | packet, err := ws.dataChannel.writePacket(data) 113 | if err != nil { 114 | ws.logger.Warnf("error encrypting: %v", err) 115 | continue 116 | } 117 | 118 | select { 119 | case ws.dataOrControlToMuxer <- packet: 120 | case <-ws.workersManager.ShouldShutdown(): 121 | return 122 | } 123 | 124 | case <-ws.workersManager.ShouldShutdown(): 125 | return 126 | } 127 | } 128 | case <-ws.workersManager.ShouldShutdown(): 129 | return 130 | } 131 | } 132 | 133 | // moveUpWorker moves packets up the stack 134 | func (ws *workersState) moveUpWorker() { 135 | workerName := fmt.Sprintf("%s: moveUpWorker", serviceName) 136 | 137 | defer func() { 138 | 139 | ws.workersManager.OnWorkerDone(workerName) 140 | ws.workersManager.StartShutdown() 141 | }() 142 | ws.logger.Debugf("%s: started", workerName) 143 | 144 | for { 145 | select { 146 | // TODO: opportunistically try to kill lame duck 147 | 148 | case pkt := <-ws.muxerToData: 149 | // TODO(ainghazal): factor out as handler function 150 | decrypted, err := ws.dataChannel.readPacket(pkt) 151 | if err != nil { 152 | ws.logger.Warnf("error decrypting: %v", err) 153 | continue 154 | } 155 | 156 | if len(decrypted) == 16 { 157 | // TODO: should reply to this keepalive ping 158 | // "2a 18 7b f3 64 1e b4 cb 07 ed 2d 0a 98 1f c7 48" 159 | fmt.Println(hex.Dump(decrypted)) 160 | continue 161 | } 162 | 163 | // POSSIBLY BLOCK writing up towards TUN 164 | ws.dataToTUN <- decrypted 165 | case <-ws.workersManager.ShouldShutdown(): 166 | return 167 | } 168 | } 169 | } 170 | 171 | // keyWorker receives notifications from key ready 172 | func (ws *workersState) keyWorker(firstKeyReady chan<- any) { 173 | workerName := fmt.Sprintf("%s: keyWorker", serviceName) 174 | 175 | defer func() { 176 | ws.workersManager.OnWorkerDone(workerName) 177 | ws.workersManager.StartShutdown() 178 | }() 179 | 180 | ws.logger.Debugf("%s: started", workerName) 181 | once := &sync.Once{} 182 | 183 | for { 184 | select { 185 | case key := <-ws.keyReady: 186 | // TODO(ainghazal): thread safety here - need to lock. 187 | // When we actually get to key rotation, we need to add locks. 188 | // Use RW lock, reader locks. 189 | 190 | err := ws.dataChannel.setupKeys(key) 191 | if err != nil { 192 | ws.logger.Warnf("error on key derivation: %v", err) 193 | continue 194 | } 195 | ws.sessionManager.SetNegotiationState(model.S_GENERATED_KEYS) 196 | once.Do(func() { 197 | close(firstKeyReady) 198 | }) 199 | 200 | case <-ws.workersManager.ShouldShutdown(): 201 | return 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /internal/datachannel/service_test.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/log" 7 | "github.com/ooni/minivpn/internal/model" 8 | "github.com/ooni/minivpn/internal/session" 9 | "github.com/ooni/minivpn/internal/workers" 10 | "github.com/ooni/minivpn/pkg/config" 11 | ) 12 | 13 | // test that we can start and stop the workers 14 | func TestService_StartWorkers(t *testing.T) { 15 | dataToMuxer := make(chan *model.Packet, 100) 16 | keyReady := make(chan *session.DataChannelKey) 17 | muxerToData := make(chan *model.Packet, 100) 18 | 19 | s := Service{ 20 | MuxerToData: muxerToData, 21 | DataOrControlToMuxer: &dataToMuxer, 22 | TUNToData: make(chan []byte, 100), 23 | DataToTUN: make(chan []byte, 100), 24 | KeyReady: keyReady, 25 | } 26 | workers := workers.NewManager(log.Log) 27 | session := makeTestingSession() 28 | 29 | opts := makeTestingOptions(t, "AES-128-GCM", "sha512") 30 | s.StartWorkers(config.NewConfig(config.WithOpenVPNOptions(opts)), workers, session) 31 | 32 | keyReady <- makeTestingDataChannelKey() 33 | <-session.Ready 34 | muxerToData <- &model.Packet{Opcode: model.P_DATA_V1, Payload: []byte("aaa")} 35 | muxerToData <- &model.Packet{Opcode: model.P_DATA_V1, Payload: []byte("bbb")} 36 | workers.StartShutdown() 37 | workers.WaitWorkersShutdown() 38 | } 39 | -------------------------------------------------------------------------------- /internal/datachannel/state.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | import ( 4 | "hash" 5 | "math" 6 | "sync" 7 | 8 | "github.com/ooni/minivpn/internal/model" 9 | ) 10 | 11 | // keySlot holds the different local and remote keys. 12 | type keySlot [64]byte 13 | 14 | // dataChannelState is the state of the data channel. 15 | type dataChannelState struct { 16 | dataCipher dataCipher 17 | 18 | // outgoing and incoming nomenclature is probably more adequate here. 19 | hmacLocal hash.Hash 20 | hmacRemote hash.Hash 21 | cipherKeyLocal keySlot 22 | cipherKeyRemote keySlot 23 | hmacKeyLocal keySlot 24 | hmacKeyRemote keySlot 25 | 26 | // TODO(ainghazal): we need to keep a local packetID too. It should be separated from the control channel. 27 | // TODO: move this to sessionManager perhaps? 28 | remotePacketID model.PacketID 29 | 30 | hash func() hash.Hash 31 | mu sync.Mutex 32 | 33 | // not used at the moment, paving the way for key rotation. 34 | // keyID int 35 | } 36 | 37 | // SetRemotePacketID stores the passed packetID internally. 38 | func (dcs *dataChannelState) SetRemotePacketID(id model.PacketID) { 39 | dcs.mu.Lock() 40 | defer dcs.mu.Unlock() 41 | dcs.remotePacketID = model.PacketID(id) 42 | } 43 | 44 | // RemotePacketID returns the last known remote packetID. It returns an error 45 | // if the stored packet id has reached the maximum capacity of the packetID 46 | // type. 47 | func (dcs *dataChannelState) RemotePacketID() (model.PacketID, error) { 48 | dcs.mu.Lock() 49 | defer dcs.mu.Unlock() 50 | pid := dcs.remotePacketID 51 | if pid == math.MaxUint32 { 52 | // we reached the max packetID, increment will overflow 53 | return 0, ErrExpiredKey 54 | } 55 | return pid, nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/datachannel/write.go: -------------------------------------------------------------------------------- 1 | package datachannel 2 | 3 | // 4 | // Functions for encoding & writing packets 5 | // 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "errors" 11 | "fmt" 12 | 13 | "github.com/ooni/minivpn/internal/bytesx" 14 | "github.com/ooni/minivpn/internal/model" 15 | "github.com/ooni/minivpn/internal/session" 16 | "github.com/ooni/minivpn/pkg/config" 17 | ) 18 | 19 | // encryptAndEncodePayloadAEAD peforms encryption and encoding of the payload in AEAD modes (i.e., AES-GCM). 20 | // TODO(ainghazal): for testing we can pass both the state object and the encryptFn 21 | func encryptAndEncodePayloadAEAD(log model.Logger, padded []byte, session *session.Manager, state *dataChannelState) ([]byte, error) { 22 | nextPacketID, err := session.LocalDataPacketID() 23 | if err != nil { 24 | return []byte{}, fmt.Errorf("bad packet id") 25 | } 26 | 27 | // in AEAD mode, we authenticate: 28 | // - 1 byte: opcode/key 29 | // - 3 bytes: peer-id (we're using P_DATA_V2) 30 | // - 4 bytes: packet-id 31 | aead := &bytes.Buffer{} 32 | aead.WriteByte(opcodeAndKeyHeader(session)) 33 | bytesx.WriteUint24(aead, uint32(session.TunnelInfo().PeerID)) 34 | bytesx.WriteUint32(aead, uint32(nextPacketID)) 35 | 36 | // the iv is the packetID (again) concatenated with the 8 bytes of the 37 | // key derived for local hmac (which we do not use for anything else in AEAD mode). 38 | iv := &bytes.Buffer{} 39 | bytesx.WriteUint32(iv, uint32(nextPacketID)) 40 | iv.Write(state.hmacKeyLocal[:8]) 41 | 42 | data := &plaintextData{ 43 | iv: iv.Bytes(), 44 | plaintext: padded, 45 | aead: aead.Bytes(), 46 | } 47 | 48 | encryptFn := state.dataCipher.encrypt 49 | encrypted, err := encryptFn(state.cipherKeyLocal[:], data) 50 | if err != nil { 51 | return []byte{}, err 52 | } 53 | 54 | // some reordering, because openvpn uses tag | payload 55 | boundary := len(encrypted) - 16 56 | tag := encrypted[boundary:] 57 | ciphertext := encrypted[:boundary] 58 | 59 | // we now write to the output buffer 60 | out := bytes.Buffer{} 61 | out.Write(data.aead) // opcode|peer-id|packet_id 62 | out.Write(tag) 63 | out.Write(ciphertext) 64 | return out.Bytes(), nil 65 | 66 | } 67 | 68 | // assign the random function to allow using a deterministic one in tests. 69 | var genRandomFn = bytesx.GenRandomBytes 70 | 71 | // encryptAndEncodePayloadNonAEAD peforms encryption and encoding of the payload in Non-AEAD modes (i.e., AES-CBC). 72 | func encryptAndEncodePayloadNonAEAD(log model.Logger, padded []byte, session *session.Manager, state *dataChannelState) ([]byte, error) { 73 | // For iv generation, OpenVPN uses a nonce-based PRNG that is initially seeded with 74 | // OpenSSL RAND_bytes function. I am assuming this is good enough for our current purposes. 75 | blockSize := state.dataCipher.blockSize() 76 | 77 | iv, err := genRandomFn(int(blockSize)) 78 | if err != nil { 79 | return nil, err 80 | } 81 | data := &plaintextData{ 82 | iv: iv, 83 | plaintext: padded, 84 | aead: nil, 85 | } 86 | 87 | encryptFn := state.dataCipher.encrypt 88 | ciphertext, err := encryptFn(state.cipherKeyLocal[:], data) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | state.hmacLocal.Reset() 94 | state.hmacLocal.Write(iv) 95 | state.hmacLocal.Write(ciphertext) 96 | computedMAC := state.hmacLocal.Sum(nil) 97 | 98 | out := &bytes.Buffer{} 99 | out.WriteByte(opcodeAndKeyHeader(session)) 100 | bytesx.WriteUint24(out, uint32(session.TunnelInfo().PeerID)) 101 | 102 | out.Write(computedMAC) 103 | out.Write(iv) 104 | out.Write(ciphertext) 105 | return out.Bytes(), nil 106 | } 107 | 108 | // doCompress adds compression bytes if needed by the passed compression options. 109 | // if the compression stub is on, it sends the first byte to the last position, 110 | // and it adds the compression preamble, according to the spec. compression 111 | // lzo-no also adds a preamble. It returns a byte array and an error if the 112 | // operation could not be completed. 113 | func doCompress(b []byte, compress config.Compression) ([]byte, error) { 114 | switch compress { 115 | case "stub": 116 | // compression stub: send first byte to last 117 | // and add 0xfb marker on the first byte. 118 | b = append(b, b[0]) 119 | b[0] = 0xfb 120 | case "lzo-no": 121 | // old "comp-lzo no" option 122 | b = append([]byte{0xfa}, b...) 123 | } 124 | return b, nil 125 | } 126 | 127 | var errPadding = errors.New("padding error") 128 | 129 | // doPadding does pkcs7 padding of the encryption payloads as 130 | // needed. if we're using the compression stub the padding is applied without taking the 131 | // trailing bit into account. it returns the resulting byte array, and an error 132 | // if the operatio could not be completed. 133 | func doPadding(b []byte, compress config.Compression, blockSize uint8) ([]byte, error) { 134 | if len(b) == 0 { 135 | return nil, fmt.Errorf("%w: %s", errPadding, "nothing to pad") 136 | } 137 | if compress == "stub" { 138 | // if we're using the compression stub 139 | // we need to account for a trailing byte 140 | // that we have appended in the doCompress stage. 141 | endByte := b[len(b)-1] 142 | padded, err := bytesx.BytesPadPKCS7(b[:len(b)-1], int(blockSize)) 143 | if err != nil { 144 | return nil, err 145 | } 146 | padded[len(padded)-1] = endByte 147 | return padded, nil 148 | } 149 | padded, err := bytesx.BytesPadPKCS7(b, int(blockSize)) 150 | if err != nil { 151 | return nil, err 152 | } 153 | return padded, nil 154 | } 155 | 156 | // prependPacketID returns the original buffer with the passed packetID 157 | // concatenated at the beginning. 158 | func prependPacketID(p model.PacketID, buf []byte) []byte { 159 | newbuf := &bytes.Buffer{} 160 | packetID := make([]byte, 4) 161 | binary.BigEndian.PutUint32(packetID, uint32(p)) 162 | newbuf.Write(packetID[:]) 163 | newbuf.Write(buf) 164 | return newbuf.Bytes() 165 | } 166 | 167 | // opcodeAndKeyHeader returns the header byte encoding the opcode and keyID (3 upper 168 | // and 5 lower bits, respectively) 169 | func opcodeAndKeyHeader(session *session.Manager) byte { 170 | return byte((byte(model.P_DATA_V2) << 3) | (byte(session.CurrentKeyID()) & 0x07)) 171 | } 172 | -------------------------------------------------------------------------------- /internal/mocks/README.md: -------------------------------------------------------------------------------- 1 | # Mocks 2 | 3 | Several mocks for testing. 4 | Code taken from [ooni/probe-cli/internal/model/mocks](https://github.com/ooni/probe-cli/tree/master/internal/model/mocks) 5 | 6 | ``` 7 | (c) Simone Basso, 2021. 8 | License: GPL-3.0 9 | ``` 10 | -------------------------------------------------------------------------------- /internal/mocks/addr.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import "net" 4 | 5 | // Addr allows mocking net.Addr. 6 | type Addr struct { 7 | MockString func() string 8 | MockNetwork func() string 9 | } 10 | 11 | var _ net.Addr = &Addr{} 12 | 13 | // String calls MockString. 14 | func (a *Addr) String() string { 15 | return a.MockString() 16 | } 17 | 18 | // Network calls MockNetwork. 19 | func (a *Addr) Network() string { 20 | return a.MockNetwork() 21 | } 22 | -------------------------------------------------------------------------------- /internal/mocks/dialer.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | ) 8 | 9 | // Dialer is a mockable Dialer. 10 | type Dialer struct { 11 | MockDialContext func(ctx context.Context, network, address string) (net.Conn, error) 12 | MockCloseIdleConnections func() 13 | } 14 | 15 | // DialContext calls MockDialContext. 16 | func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 17 | return d.MockDialContext(ctx, network, address) 18 | } 19 | 20 | // CloseIdleConnections calls MockCloseIdleConnections. 21 | func (d *Dialer) CloseIdleConnections() { 22 | d.MockCloseIdleConnections() 23 | } 24 | 25 | // Conn is a mockable net.Conn. 26 | type Conn struct { 27 | Count int 28 | MockRead func(b []byte) (int, error) 29 | MockWrite func(b []byte) (int, error) 30 | MockClose func() error 31 | MockLocalAddr func() net.Addr 32 | MockRemoteAddr func() net.Addr 33 | MockSetDeadline func(t time.Time) error 34 | MockSetReadDeadline func(t time.Time) error 35 | MockSetWriteDeadline func(t time.Time) error 36 | } 37 | 38 | // Read calls MockRead. 39 | func (c *Conn) Read(b []byte) (int, error) { 40 | return c.MockRead(b) 41 | } 42 | 43 | // Write calls MockWrite. 44 | func (c *Conn) Write(b []byte) (int, error) { 45 | return c.MockWrite(b) 46 | } 47 | 48 | // Close calls MockClose. 49 | func (c *Conn) Close() error { 50 | return c.MockClose() 51 | } 52 | 53 | // LocalAddr calls MockLocalAddr. 54 | func (c *Conn) LocalAddr() net.Addr { 55 | return c.MockLocalAddr() 56 | } 57 | 58 | // RemoteAddr calls MockRemoteAddr. 59 | func (c *Conn) RemoteAddr() net.Addr { 60 | return c.MockRemoteAddr() 61 | } 62 | 63 | // SetDeadline calls MockSetDeadline. 64 | func (c *Conn) SetDeadline(t time.Time) error { 65 | return c.MockSetDeadline(t) 66 | } 67 | 68 | // SetReadDeadline calls MockSetReadDeadline. 69 | func (c *Conn) SetReadDeadline(t time.Time) error { 70 | return c.MockSetReadDeadline(t) 71 | } 72 | 73 | // SetWriteDeadline calls MockSetWriteDeadline. 74 | func (c *Conn) SetWriteDeadline(t time.Time) error { 75 | return c.MockSetWriteDeadline(t) 76 | } 77 | 78 | var _ net.Conn = &Conn{} 79 | -------------------------------------------------------------------------------- /internal/model/dialer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | // Dialer is a type allowing to dial network connections. 9 | type Dialer interface { 10 | DialContext(context.Context, string, string) (net.Conn, error) 11 | } 12 | -------------------------------------------------------------------------------- /internal/model/doc.go: -------------------------------------------------------------------------------- 1 | // Package model implements common models for the vpn data structures. 2 | package model 3 | -------------------------------------------------------------------------------- /internal/model/logger.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Logger is the generic logger definition. 4 | type Logger interface { 5 | // Debug emits a debug message. 6 | Debug(msg string) 7 | 8 | // Debugf formats and emits a debug message. 9 | Debugf(format string, v ...any) 10 | 11 | // Info emits an informational message. 12 | Info(msg string) 13 | 14 | // Infof formats and emits an informational message. 15 | Infof(format string, v ...any) 16 | 17 | // Warn emits a warning message. 18 | Warn(msg string) 19 | 20 | // Warnf formats and emits a warning message. 21 | Warnf(format string, v ...any) 22 | } 23 | -------------------------------------------------------------------------------- /internal/model/mocks.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "fmt" 4 | 5 | // TestLogger is a logger that can be used whenever a test needs a logger to be passed around. 6 | type TestLogger struct { 7 | Lines []string 8 | } 9 | 10 | func (tl *TestLogger) append(msg string) { 11 | tl.Lines = append(tl.Lines, msg) 12 | } 13 | 14 | // Debug implements model.Logger 15 | func (tl *TestLogger) Debug(msg string) { 16 | tl.append(msg) 17 | } 18 | 19 | // Debugf implements model.Logger 20 | func (tl *TestLogger) Debugf(format string, v ...any) { 21 | tl.append(fmt.Sprintf(format, v...)) 22 | } 23 | 24 | // Info implements model.Logger 25 | func (tl *TestLogger) Info(msg string) { 26 | tl.append(msg) 27 | } 28 | 29 | // Infof implements model.Logger 30 | func (tl *TestLogger) Infof(format string, v ...any) { 31 | tl.append(fmt.Sprintf(format, v...)) 32 | } 33 | 34 | // Warn implements model.Logger 35 | func (tl *TestLogger) Warn(msg string) { 36 | tl.append(msg) 37 | } 38 | 39 | // Warnf implements model.Logger 40 | func (tl *TestLogger) Warnf(format string, v ...any) { 41 | tl.append(fmt.Sprintf(format, v...)) 42 | } 43 | 44 | func NewTestLogger() *TestLogger { 45 | return &TestLogger{ 46 | Lines: make([]string, 0), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/model/notification.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | const ( 4 | // NotificationReset indicates that a SOFT or HARD reset occurred. 5 | NotificationReset = 1 << iota 6 | ) 7 | 8 | // Notification is a notification for a service worker. 9 | type Notification struct { 10 | // Flags contains flags explaining what happened. 11 | Flags int64 12 | } 13 | -------------------------------------------------------------------------------- /internal/model/session.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // NegotiationState is the state of the session negotiation. 4 | type NegotiationState int 5 | 6 | const ( 7 | // S_ERROR means there was some form of protocol error. 8 | S_ERROR = NegotiationState(iota) - 1 9 | 10 | // S_UNDEF is the undefined state. 11 | S_UNDEF 12 | 13 | // S_INITIAL means we're ready to begin the three-way handshake. 14 | S_INITIAL 15 | 16 | // S_PRE_START means we're waiting for acknowledgment from the remote. 17 | S_PRE_START 18 | 19 | // S_START means we've done the three-way handshake. 20 | S_START 21 | 22 | // S_SENT_KEY means we have sent the local part of the key_source2 random material. 23 | S_SENT_KEY 24 | 25 | // S_GOT_KEY means we have got the remote part of key_source2. 26 | S_GOT_KEY 27 | 28 | // S_ACTIVE means the control channel was established. 29 | S_ACTIVE 30 | 31 | // S_GENERATED_KEYS means the data channel keys have been generated. 32 | S_GENERATED_KEYS 33 | ) 34 | 35 | // String maps a [SessionNegotiationState] to a string. 36 | func (sns NegotiationState) String() string { 37 | switch sns { 38 | case S_UNDEF: 39 | return "S_UNDEF" 40 | case S_INITIAL: 41 | return "S_INITIAL" 42 | case S_PRE_START: 43 | return "S_PRE_START" 44 | case S_START: 45 | return "S_START" 46 | case S_SENT_KEY: 47 | return "S_SENT_KEY" 48 | case S_GOT_KEY: 49 | return "S_GOT_KEY" 50 | case S_ACTIVE: 51 | return "S_ACTIVE" 52 | case S_GENERATED_KEYS: 53 | return "S_GENERATED_KEYS" 54 | case S_ERROR: 55 | return "S_ERROR" 56 | default: 57 | return "S_INVALID" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/model/session_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "testing" 4 | 5 | func TestNegotiationState_String(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | sns NegotiationState 9 | want string 10 | }{ 11 | { 12 | name: "undef", 13 | sns: S_UNDEF, 14 | want: "S_UNDEF", 15 | }, 16 | { 17 | name: "initial", 18 | sns: S_INITIAL, 19 | want: "S_INITIAL", 20 | }, 21 | { 22 | name: "pre start", 23 | sns: S_PRE_START, 24 | want: "S_PRE_START", 25 | }, 26 | { 27 | name: "start", 28 | sns: S_START, 29 | want: "S_START", 30 | }, 31 | { 32 | name: "sent key", 33 | sns: S_SENT_KEY, 34 | want: "S_SENT_KEY", 35 | }, 36 | { 37 | name: "got key", 38 | sns: S_GOT_KEY, 39 | want: "S_GOT_KEY", 40 | }, 41 | { 42 | name: "active", 43 | sns: S_ACTIVE, 44 | want: "S_ACTIVE", 45 | }, 46 | { 47 | name: "generated keys", 48 | sns: S_GENERATED_KEYS, 49 | want: "S_GENERATED_KEYS", 50 | }, 51 | { 52 | name: "error", 53 | sns: S_ERROR, 54 | want: "S_ERROR", 55 | }, 56 | { 57 | name: "unknown", 58 | sns: NegotiationState(10), 59 | want: "S_INVALID", 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | if got := tt.sns.String(); got != tt.want { 65 | t.Errorf("NegotiationState.String() = %v, want %v", got, tt.want) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/model/trace.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // HandshakeTracer allows to collect traces for a given OpenVPN handshake. A HandshakeTracer can be optionally 9 | // added to the top-level TUN constructor, and it will be propagated to any layer that needs to register an event. 10 | type HandshakeTracer interface { 11 | // TimeNow allows to inject time for deterministic tests. 12 | TimeNow() time.Time 13 | 14 | // OnStateChange is called for each transition in the state machine. 15 | OnStateChange(state NegotiationState) 16 | 17 | // OnIncomingPacket is called when a packet is received. 18 | OnIncomingPacket(packet *Packet, stage NegotiationState) 19 | 20 | // OnOutgoingPacket is called when a packet is about to be sent. 21 | OnOutgoingPacket(packet *Packet, stage NegotiationState, retries int) 22 | 23 | // OnDroppedPacket is called whenever a packet is dropped (in/out) 24 | OnDroppedPacket(direction Direction, stage NegotiationState, packet *Packet) 25 | } 26 | 27 | // Direction is one of two directions on a packet. 28 | type Direction int 29 | 30 | const ( 31 | // DirectionIncoming marks received packets. 32 | DirectionIncoming = Direction(iota) 33 | 34 | // DirectionOutgoing marks packets to be sent. 35 | DirectionOutgoing 36 | ) 37 | 38 | var _ fmt.Stringer = Direction(0) 39 | 40 | // String implements fmt.Stringer 41 | func (d Direction) String() string { 42 | switch d { 43 | case DirectionIncoming: 44 | return "read" 45 | case DirectionOutgoing: 46 | return "write" 47 | default: 48 | return "undefined" 49 | } 50 | } 51 | 52 | // DummyTracer is a no-op implementation of [model.HandshakeTracer] that does nothing 53 | // but can be safely passed as a default implementation. 54 | type DummyTracer struct{} 55 | 56 | // TimeNow allows to manipulate time for deterministic tests. 57 | func (dt DummyTracer) TimeNow() time.Time { return time.Now() } 58 | 59 | // OnStateChange is called for each transition in the state machine. 60 | func (dt DummyTracer) OnStateChange(NegotiationState) {} 61 | 62 | // OnIncomingPacket is called when a packet is received. 63 | func (dt DummyTracer) OnIncomingPacket(*Packet, NegotiationState) {} 64 | 65 | // OnOutgoingPacket is called when a packet is about to be sent. 66 | func (dt DummyTracer) OnOutgoingPacket(*Packet, NegotiationState, int) {} 67 | 68 | // OnDroppedPacket is called whenever a packet is dropped (in/out) 69 | func (dt DummyTracer) OnDroppedPacket(Direction, NegotiationState, *Packet) {} 70 | 71 | // Assert that dummyTracer implements [model.HandshakeTracer]. 72 | var _ HandshakeTracer = &DummyTracer{} 73 | -------------------------------------------------------------------------------- /internal/model/tunnelinfo.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // TunnelInfo holds state about the VPN TunnelInfo that has longer duration than a 4 | // given session. This information is gathered at different stages: 5 | // - during the handshake (mtu). 6 | // - after server pushes config options(ip, gw). 7 | type TunnelInfo struct { 8 | // GW is the Route Gateway. 9 | GW string 10 | 11 | // IP is the assigned IP. 12 | IP string 13 | 14 | // MTU is the configured MTU pushed by the remote. 15 | MTU int 16 | 17 | // NetMask is the netmask configured on the TUN interface, pushed by the ifconfig command. 18 | NetMask string 19 | 20 | // PeerID is the peer-id assigned to us by the remote. 21 | PeerID int 22 | } 23 | -------------------------------------------------------------------------------- /internal/networkio/closeonce.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | ) 7 | 8 | // closeOnceConn is a [net.Conn] where the Close method has once semantics. 9 | // 10 | // The zero value is invalid; use [newCloseOnceConn]. 11 | type closeOnceConn struct { 12 | // once ensures we close just once. 13 | once sync.Once 14 | 15 | // Conn is the underlying conn. 16 | net.Conn 17 | } 18 | 19 | var _ net.Conn = &closeOnceConn{} 20 | 21 | // newCloseOnceConn creates a [closeOnceConn]. 22 | func newCloseOnceConn(conn net.Conn) *closeOnceConn { 23 | return &closeOnceConn{ 24 | once: sync.Once{}, 25 | Conn: conn, 26 | } 27 | } 28 | 29 | // Close implements net.Conn 30 | func (c *closeOnceConn) Close() (err error) { 31 | c.once.Do(func() { 32 | err = c.Conn.Close() 33 | }) 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /internal/networkio/common_test.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | 8 | "github.com/ooni/minivpn/internal/vpntest" 9 | ) 10 | 11 | type mockedConn struct { 12 | conn *vpntest.Conn 13 | dataIn [][]byte 14 | dataOut [][]byte 15 | } 16 | 17 | func (mc *mockedConn) NetworkReads() [][]byte { 18 | return mc.dataOut 19 | } 20 | 21 | func (mc *mockedConn) NetworkWrites() [][]byte { 22 | return mc.dataIn 23 | } 24 | 25 | func newDialer(underlying *mockedConn) *vpntest.Dialer { 26 | dialer := &vpntest.Dialer{ 27 | MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { 28 | return underlying.conn, nil 29 | }, 30 | } 31 | return dialer 32 | } 33 | 34 | func newMockedConn(network string, dataIn, dataOut [][]byte) *mockedConn { 35 | conn := &mockedConn{ 36 | dataIn: dataIn, 37 | dataOut: dataOut, 38 | } 39 | conn.conn = &vpntest.Conn{ 40 | MockLocalAddr: func() net.Addr { 41 | addr := &vpntest.Addr{ 42 | MockString: func() string { return "1.2.3.4" }, 43 | MockNetwork: func() string { return network }, 44 | } 45 | return addr 46 | }, 47 | MockRead: func(b []byte) (int, error) { 48 | if len(conn.dataOut) > 0 { 49 | copy(b[:], conn.dataOut[0]) 50 | ln := len(conn.dataOut[0]) 51 | conn.dataOut = conn.dataOut[1:] 52 | return ln, nil 53 | } 54 | return 0, errors.New("EOF") 55 | }, 56 | MockWrite: func(b []byte) (int, error) { 57 | conn.dataIn = append(conn.dataIn, b) 58 | return len(b), nil 59 | }, 60 | } 61 | return conn 62 | } 63 | -------------------------------------------------------------------------------- /internal/networkio/datagram.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "math" 5 | "net" 6 | ) 7 | 8 | // datagramConn wraps a datagram socket and implements OpenVPN framing. 9 | type datagramConn struct { 10 | net.Conn 11 | } 12 | 13 | var _ FramingConn = &datagramConn{} 14 | 15 | // ReadRawPacket implements FramingConn 16 | func (c *datagramConn) ReadRawPacket() ([]byte, error) { 17 | buffer := make([]byte, math.MaxUint16) // maximum UDP datagram size 18 | count, err := c.Read(buffer) 19 | if err != nil { 20 | return nil, err 21 | } 22 | pkt := buffer[:count] 23 | return pkt, nil 24 | } 25 | 26 | // WriteRawPacket implements FramingConn 27 | func (c *datagramConn) WriteRawPacket(pkt []byte) error { 28 | if len(pkt) > math.MaxUint16 { 29 | return ErrPacketTooLarge 30 | } 31 | _, err := c.Conn.Write(pkt) 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /internal/networkio/dialer.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ooni/minivpn/internal/model" 7 | ) 8 | 9 | // Dialer dials network connections. The zero value of this structure is 10 | // invalid; please, use the [NewDialer] constructor. 11 | type Dialer struct { 12 | // dialer is the underlying [DialerContext] we use to dial. 13 | dialer model.Dialer 14 | 15 | // logger is the [Logger] with which we log. 16 | logger model.Logger 17 | } 18 | 19 | // NewDialer creates a new [Dialer] instance. 20 | func NewDialer(logger model.Logger, dialer model.Dialer) *Dialer { 21 | return &Dialer{ 22 | dialer: dialer, 23 | logger: logger, 24 | } 25 | } 26 | 27 | // DialContext establishes a connection and, on success, automatically wraps the 28 | // returned connection to implement OpenVPN framing when not using UDP. 29 | func (d *Dialer) DialContext(ctx context.Context, network, address string) (FramingConn, error) { 30 | // dial with the underlying dialer 31 | conn, err := d.dialer.DialContext(ctx, network, address) 32 | if err != nil { 33 | d.logger.Warnf("networkio: dial failed: %s", err.Error()) 34 | return nil, err 35 | } 36 | 37 | d.logger.Debugf("networkio: connected to %s/%s", address, network) 38 | 39 | // make sure the conn has close once semantics 40 | conn = newCloseOnceConn(conn) 41 | 42 | // wrap the conn and return 43 | switch conn.LocalAddr().Network() { 44 | case "udp", "udp4", "udp6": 45 | return &datagramConn{conn}, nil 46 | default: 47 | return &streamConn{conn}, nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/networkio/doc.go: -------------------------------------------------------------------------------- 1 | // Package networkio implements raw packets network I/O. 2 | package networkio 3 | -------------------------------------------------------------------------------- /internal/networkio/framing.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | // FramingConn is an OpenVPN network connection that knows about 9 | // the framing used by OpenVPN to read and write raw packets. 10 | type FramingConn interface { 11 | // ReadRawPacket reads and return a raw OpenVPN packet. 12 | ReadRawPacket() ([]byte, error) 13 | 14 | // WriteRawPacket writes a raw OpenVPN packet. 15 | WriteRawPacket(pkt []byte) error 16 | 17 | // SetReadDeadline is like net.Conn.SetReadDeadline. 18 | SetReadDeadline(t time.Time) error 19 | 20 | // SetWriteDeadline is like net.Conn.SetWriteDeadline. 21 | SetWriteDeadline(t time.Time) error 22 | 23 | // LocalAddr is like net.Conn.LocalAddr. 24 | LocalAddr() net.Addr 25 | 26 | // RemoteAddr is like net.Conn.RemoteAddr. 27 | RemoteAddr() net.Addr 28 | 29 | // Close is like net.Conn.Close. 30 | Close() error 31 | } 32 | -------------------------------------------------------------------------------- /internal/networkio/networkio_test.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net" 7 | "testing" 8 | 9 | "github.com/apex/log" 10 | "github.com/ooni/minivpn/internal/vpntest" 11 | ) 12 | 13 | func Test_TCPLikeConn(t *testing.T) { 14 | t.Run("A tcp-like conn implements the openvpn size framing", func(t *testing.T) { 15 | dataIn := make([][]byte, 0) 16 | dataOut := make([][]byte, 0) 17 | // write size 18 | dataOut = append(dataOut, []byte{0, 8}) 19 | // write payload 20 | want := []byte("deadbeef") 21 | dataOut = append(dataOut, want) 22 | 23 | underlying := newMockedConn("tcp", dataIn, dataOut) 24 | testDialer := newDialer(underlying) 25 | dialer := NewDialer(log.Log, testDialer) 26 | framingConn, err := dialer.DialContext(context.Background(), "tcp", "1.1.1.1") 27 | 28 | if err != nil { 29 | t.Errorf("should not error getting a framingConn") 30 | } 31 | got, err := framingConn.ReadRawPacket() 32 | if err != nil { 33 | t.Errorf("should not error: err = %v", err) 34 | } 35 | if !bytes.Equal(got, want) { 36 | t.Errorf("got = %v, want = %v", got, want) 37 | } 38 | 39 | written := []byte("ingirumimusnocteetconsumimurigni") 40 | framingConn.WriteRawPacket(written) 41 | gotWritten := underlying.NetworkWrites() 42 | if !bytes.Equal(gotWritten[0], append([]byte{0, byte(len(written))}, written...)) { 43 | t.Errorf("got = %v, want = %v", gotWritten, written) 44 | } 45 | }) 46 | } 47 | 48 | func Test_UDPLikeConn(t *testing.T) { 49 | t.Run("A udp-like conn returns the packets directly", func(t *testing.T) { 50 | dataIn := make([][]byte, 0) 51 | dataOut := make([][]byte, 0) 52 | // write payload 53 | want := []byte("deadbeef") 54 | dataOut = append(dataOut, want) 55 | 56 | underlying := newMockedConn("udp", dataIn, dataOut) 57 | testDialer := newDialer(underlying) 58 | dialer := NewDialer(log.Log, testDialer) 59 | framingConn, err := dialer.DialContext(context.Background(), "udp", "1.1.1.1") 60 | if err != nil { 61 | t.Errorf("should not error getting a framingConn") 62 | } 63 | got, err := framingConn.ReadRawPacket() 64 | if err != nil { 65 | t.Errorf("should not error: err = %v", err) 66 | } 67 | if !bytes.Equal(got, want) { 68 | t.Errorf("got = %v, want = %v", got, want) 69 | } 70 | written := []byte("ingirumimusnocteetconsumimurigni") 71 | framingConn.WriteRawPacket(written) 72 | gotWritten := underlying.NetworkWrites() 73 | if !bytes.Equal(gotWritten[0], written) { 74 | t.Errorf("got = %v, want = %v", gotWritten, written) 75 | } 76 | }) 77 | } 78 | 79 | func Test_CloseOnceConn(t *testing.T) { 80 | t.Run("A conn can be closed more than once", func(t *testing.T) { 81 | ctr := 0 82 | testDialer := &vpntest.Dialer{ 83 | MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { 84 | conn := &vpntest.Conn{ 85 | MockClose: func() error { 86 | ctr++ 87 | return nil 88 | }, 89 | MockLocalAddr: func() net.Addr { 90 | addr := &vpntest.Addr{ 91 | MockString: func() string { return "1.2.3.4" }, 92 | MockNetwork: func() string { return network }, 93 | } 94 | return addr 95 | }, 96 | } 97 | return conn, nil 98 | }, 99 | } 100 | 101 | dialer := NewDialer(log.Log, testDialer) 102 | framingConn, err := dialer.DialContext(context.Background(), "tcp", "1.1.1.1") 103 | if err != nil { 104 | t.Errorf("should not error getting a framingConn") 105 | } 106 | framingConn.Close() 107 | framingConn.Close() 108 | if ctr != 1 { 109 | t.Errorf("close function should be called only once") 110 | } 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /internal/networkio/service.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ooni/minivpn/internal/model" 7 | "github.com/ooni/minivpn/internal/workers" 8 | "github.com/ooni/minivpn/pkg/config" 9 | ) 10 | 11 | var ( 12 | serviceName = "networkio" 13 | ) 14 | 15 | // Service is the network I/O service. Make sure you initialize 16 | // the channels before invoking [Service.StartWorkers]. 17 | type Service struct { 18 | // MuxerToNetwork moves bytes down from the muxer to the network IO layer 19 | MuxerToNetwork chan []byte 20 | 21 | // NetworkToMuxer moves bytes up from the network IO layer to the muxer 22 | NetworkToMuxer *chan []byte 23 | } 24 | 25 | // StartWorkers starts the network I/O workers. See the [ARCHITECTURE] 26 | // file for more information about the network I/O workers. 27 | // 28 | // [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md 29 | func (svc *Service) StartWorkers( 30 | config *config.Config, 31 | manager *workers.Manager, 32 | conn FramingConn, 33 | ) { 34 | ws := &workersState{ 35 | conn: conn, 36 | logger: config.Logger(), 37 | manager: manager, 38 | muxerToNetwork: svc.MuxerToNetwork, 39 | networkToMuxer: *svc.NetworkToMuxer, 40 | } 41 | 42 | manager.StartWorker(ws.moveUpWorker) 43 | manager.StartWorker(ws.moveDownWorker) 44 | } 45 | 46 | // workersState contains the service workers state 47 | type workersState struct { 48 | // conn is the connection to use 49 | conn FramingConn 50 | 51 | // logger is the logger to use 52 | logger model.Logger 53 | 54 | // manager controls the workers lifecycle 55 | manager *workers.Manager 56 | 57 | // muxerToNetwork is the channel for reading outgoing packets 58 | // that are coming down to us 59 | muxerToNetwork <-chan []byte 60 | 61 | // networkToMuxer is the channel for writing incoming packets 62 | // that are coming up to us from the net 63 | networkToMuxer chan<- []byte 64 | } 65 | 66 | // moveUpWorker moves packets up the stack. 67 | func (ws *workersState) moveUpWorker() { 68 | workerName := fmt.Sprintf("%s: moveUpWorker", serviceName) 69 | 70 | defer func() { 71 | // make sure the manager knows we're done 72 | ws.manager.OnWorkerDone(workerName) 73 | 74 | // tear down everything else because a workers exited 75 | ws.manager.StartShutdown() 76 | }() 77 | 78 | ws.logger.Debug("networkio: moveUpWorker: started") 79 | 80 | for { 81 | // POSSIBLY BLOCK on the connection to read a new packet 82 | pkt, err := ws.conn.ReadRawPacket() 83 | if err != nil { 84 | ws.logger.Debugf("%s: ReadRawPacket: %s", workerName, err.Error()) 85 | return 86 | } 87 | 88 | // POSSIBLY BLOCK on the channel to deliver the packet 89 | select { 90 | case ws.networkToMuxer <- pkt: 91 | case <-ws.manager.ShouldShutdown(): 92 | return 93 | } 94 | } 95 | } 96 | 97 | // moveDownWorker moves packets down the stack 98 | func (ws *workersState) moveDownWorker() { 99 | workerName := fmt.Sprintf("%s: moveDownWorker", serviceName) 100 | 101 | defer func() { 102 | // make sure the manager knows we're done 103 | ws.manager.OnWorkerDone(workerName) 104 | 105 | // tear down everything else because a worker exited 106 | ws.manager.StartShutdown() 107 | }() 108 | 109 | ws.logger.Debugf("%s: started", workerName) 110 | 111 | for { 112 | // POSSIBLY BLOCK when receiving from channel. 113 | select { 114 | case pkt := <-ws.muxerToNetwork: 115 | // POSSIBLY BLOCK on the connection to write the packet 116 | if err := ws.conn.WriteRawPacket(pkt); err != nil { 117 | ws.logger.Infof("%s: WriteRawPacket: %s", workerName, err.Error()) 118 | return 119 | } 120 | 121 | case <-ws.manager.ShouldShutdown(): 122 | return 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /internal/networkio/service_test.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/apex/log" 9 | "github.com/ooni/minivpn/internal/runtimex" 10 | "github.com/ooni/minivpn/internal/workers" 11 | "github.com/ooni/minivpn/pkg/config" 12 | ) 13 | 14 | // test that we can initialize, start and stop the networkio workers. 15 | func TestService_StartStopWorkers(t *testing.T) { 16 | if testing.Verbose() { 17 | log.SetLevel(log.DebugLevel) 18 | } 19 | workersManager := workers.NewManager(log.Log) 20 | 21 | wantToRead := []byte("deadbeef") 22 | 23 | dataIn := make([][]byte, 0) 24 | 25 | // out is out of the network (i.e., incoming data, reads) 26 | dataOut := make([][]byte, 0) 27 | dataOut = append(dataOut, wantToRead) 28 | 29 | underlying := newMockedConn("udp", dataIn, dataOut) 30 | testDialer := newDialer(underlying) 31 | dialer := NewDialer(log.Log, testDialer) 32 | 33 | framingConn, err := dialer.DialContext(context.Background(), "udp", "1.1.1.1") 34 | runtimex.PanicOnError(err, "should not error on getting new context") 35 | 36 | muxerToNetwork := make(chan []byte, 1024) 37 | networkToMuxer := make(chan []byte, 1024) 38 | muxerToNetwork <- []byte("AABBCCDD") 39 | 40 | s := Service{ 41 | MuxerToNetwork: muxerToNetwork, 42 | NetworkToMuxer: &networkToMuxer, 43 | } 44 | 45 | s.StartWorkers(config.NewConfig(config.WithLogger(log.Log)), workersManager, framingConn) 46 | got := <-networkToMuxer 47 | 48 | //time.Sleep(time.Millisecond * 10) 49 | workersManager.StartShutdown() 50 | workersManager.WaitWorkersShutdown() 51 | 52 | if !bytes.Equal(got, wantToRead) { 53 | t.Errorf("expected word %s in networkToMuxer, got %s", wantToRead, got) 54 | } 55 | 56 | networkWrites := underlying.NetworkWrites() 57 | if len(networkWrites) == 0 { 58 | t.Errorf("expected network writes") 59 | return 60 | } 61 | if !bytes.Equal(networkWrites[0], []byte("AABBCCDD")) { 62 | t.Errorf("network writes do not match") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/networkio/stream.go: -------------------------------------------------------------------------------- 1 | package networkio 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "math" 8 | "net" 9 | ) 10 | 11 | // streamConn wraps a stream socket and implements OpenVPN framing. 12 | type streamConn struct { 13 | net.Conn 14 | } 15 | 16 | var _ FramingConn = &streamConn{} 17 | 18 | // ReadRawPacket implements FramingConn 19 | func (c *streamConn) ReadRawPacket() ([]byte, error) { 20 | lenbuf := make([]byte, 2) 21 | if _, err := io.ReadFull(c.Conn, lenbuf); err != nil { 22 | return nil, err 23 | } 24 | length := binary.BigEndian.Uint16(lenbuf) 25 | buf := make([]byte, length) 26 | if _, err := io.ReadFull(c.Conn, buf); err != nil { 27 | return nil, err 28 | } 29 | return buf, nil 30 | } 31 | 32 | // ErrPacketTooLarge means that a packet is larger than [math.MaxUint16]. 33 | var ErrPacketTooLarge = errors.New("openvpn: packet too large") 34 | 35 | // WriteRawPacket implements FramingConn 36 | func (c *streamConn) WriteRawPacket(pkt []byte) error { 37 | if len(pkt) > math.MaxUint16 { 38 | return ErrPacketTooLarge 39 | } 40 | length := make([]byte, 2) 41 | binary.BigEndian.PutUint16(length, uint16(len(pkt))) 42 | pkt = append(length, pkt...) 43 | _, err := c.Conn.Write(pkt) 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /internal/optional/optional.go: -------------------------------------------------------------------------------- 1 | // Package optional contains safer code to handle optional values. 2 | // This package is taken from probe-cli/internal/optional. 3 | // Copyright 2024, Simone Basso 4 | package optional 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "reflect" 10 | 11 | "github.com/ooni/minivpn/internal/runtimex" 12 | ) 13 | 14 | // Value is an optional value. The zero value of this structure 15 | // is equivalent to the one you get when calling [None]. 16 | type Value[T any] struct { 17 | // indirect is the indirect pointer to the value. 18 | indirect *T 19 | } 20 | 21 | // None constructs an empty value. 22 | func None[T any]() Value[T] { 23 | return Value[T]{nil} 24 | } 25 | 26 | // Some constructs a some value unless T is a pointer and points to 27 | // nil, in which case [Some] is equivalent to [None]. 28 | func Some[T any](value T) Value[T] { 29 | v := Value[T]{} 30 | maybeSetFromValue(&v, value) 31 | return v 32 | } 33 | 34 | // maybeSetFromValue sets the underlying value unless T is a pointer 35 | // and points to nil in which case we set the Value to be empty. 36 | func maybeSetFromValue[T any](v *Value[T], value T) { 37 | rv := reflect.ValueOf(value) 38 | if rv.Type().Kind() == reflect.Pointer && rv.IsNil() { 39 | v.indirect = nil 40 | return 41 | } 42 | v.indirect = &value 43 | } 44 | 45 | var _ json.Unmarshaler = &Value[int]{} 46 | 47 | // UnmarshalJSON implements json.Unmarshaler. Note that a `null` JSON 48 | // value always leads to an empty Value. 49 | func (v *Value[T]) UnmarshalJSON(data []byte) error { 50 | // A `null` underlying value should always be equivalent to 51 | // invoking the None constructor of for T. While this is not 52 | // what the [json] package recommends doing for this case, 53 | // it is consistent with initializing an optional. 54 | if bytes.Equal(data, []byte(`null`)) { 55 | v.indirect = nil 56 | return nil 57 | } 58 | 59 | // Otherwise, let's try to unmarshal into a real value 60 | var value T 61 | if err := json.Unmarshal(data, &value); err != nil { 62 | return err 63 | } 64 | 65 | // Enforce the same semantics of the Some constructor: treat 66 | // pointer types specially to avoid the case where we have 67 | // a Value that is wrapping a nil pointer but for which the 68 | // IsNone check actually returns false. (Maybe this check is 69 | // redundant but it seems better to enforce it anyway.) 70 | maybeSetFromValue(v, value) 71 | return nil 72 | } 73 | 74 | var _ json.Marshaler = Value[int]{} 75 | 76 | // MarshalJSON implements json.Marshaler. An empty value serializes 77 | // to `null` and otherwise we serialize the underluing value. 78 | func (v Value[T]) MarshalJSON() ([]byte, error) { 79 | if v.indirect == nil { 80 | return json.Marshal(nil) 81 | } 82 | return json.Marshal(*v.indirect) 83 | } 84 | 85 | // IsNone returns whether this [Value] is empty. 86 | func (v Value[T]) IsNone() bool { 87 | return v.indirect == nil 88 | } 89 | 90 | // Unwrap returns the underlying value or panics. In case of 91 | // panic, the value passed to panic is an error. 92 | func (v Value[T]) Unwrap() T { 93 | runtimex.Assert(!v.IsNone(), "is none") 94 | return *v.indirect 95 | } 96 | 97 | // UnwrapOr returns the fallback if the [Value] is empty. 98 | func (v Value[T]) UnwrapOr(fallback T) T { 99 | if v.IsNone() { 100 | return fallback 101 | } 102 | return v.Unwrap() 103 | } 104 | -------------------------------------------------------------------------------- /internal/reliabletransport/common_test.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "github.com/apex/log" 5 | "github.com/ooni/minivpn/internal/bytesx" 6 | "github.com/ooni/minivpn/internal/model" 7 | "github.com/ooni/minivpn/internal/runtimex" 8 | "github.com/ooni/minivpn/internal/session" 9 | "github.com/ooni/minivpn/internal/vpntest" 10 | "github.com/ooni/minivpn/internal/workers" 11 | "github.com/ooni/minivpn/pkg/config" 12 | ) 13 | 14 | // 15 | // Common utilities for tests in this package. 16 | // 17 | 18 | // initManagers initializes a workers manager and a session manager. 19 | func initManagers() (*workers.Manager, *session.Manager) { 20 | w := workers.NewManager(log.Log) 21 | s, err := session.NewManager(config.NewConfig(config.WithLogger(log.Log))) 22 | runtimex.PanicOnError(err, "cannot create session manager") 23 | return w, s 24 | } 25 | 26 | // newRandomSessionID returns a random session ID to initialize mock sessions. 27 | func newRandomSessionID() model.SessionID { 28 | b, err := bytesx.GenRandomBytes(8) 29 | if err != nil { 30 | panic(err) 31 | } 32 | return model.SessionID(b) 33 | } 34 | 35 | func ackSetFromInts(s []int) *ackSet { 36 | acks := make([]model.PacketID, 0) 37 | for _, i := range s { 38 | acks = append(acks, model.PacketID(i)) 39 | } 40 | return newACKSet(acks...) 41 | } 42 | 43 | func ackSetFromRange(start, total int) *ackSet { 44 | acks := make([]model.PacketID, 0) 45 | for i := 0; i < total; i++ { 46 | acks = append(acks, model.PacketID(start+i)) 47 | } 48 | return newACKSet(acks...) 49 | } 50 | 51 | func initializeSessionIDForWriter(writer *vpntest.PacketWriter, session *session.Manager) { 52 | peerSessionID := newRandomSessionID() 53 | writer.RemoteSessionID = model.SessionID(session.LocalSessionID()) 54 | writer.LocalSessionID = peerSessionID 55 | session.SetRemoteSessionID(peerSessionID) 56 | } 57 | -------------------------------------------------------------------------------- /internal/reliabletransport/constants.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | const ( 4 | // Capacity for the array of packets that we're tracking at any given moment (outgoing). 5 | // This is defined by OpenVPN in ssl_pkt.h 6 | RELIABLE_SEND_BUFFER_SIZE = 6 7 | 8 | // Capacity for the array of packets that we're tracking at any given moment (incoming). 9 | // This is defined by OpenVPN in ssl_pkt.h 10 | RELIABLE_RECV_BUFFER_SIZE = 12 11 | 12 | // The maximum numbers of ACKs that we put in an array for an outgoing packet. 13 | MAX_ACKS_PER_OUTGOING_PACKET = 4 14 | 15 | // How many IDs pending to be acked can we store. 16 | ACK_SET_CAPACITY = 8 17 | 18 | // Initial timeout for TLS retransmission, in seconds. 19 | INITIAL_TLS_TIMEOUT_SECONDS = 2 20 | 21 | // Maximum backoff interval, in seconds. 22 | MAX_BACKOFF_SECONDS = 60 23 | 24 | // Default sender ticker period, in milliseconds. 25 | SENDER_TICKER_MS = 1000 * 60 26 | ) 27 | -------------------------------------------------------------------------------- /internal/reliabletransport/doc.go: -------------------------------------------------------------------------------- 1 | // Package reliabletransport implements the reliable transport module for OpenVPN. 2 | // See [the official documentation](https://community.openvpn.net/openvpn/wiki/SecurityOverview) for a detailed explanation 3 | // of why this is needed, and how it relates to the requirements of the control channel. 4 | // It is worth to mention that, even though the original need is to have a reliable control channel 5 | // on top of UDP, this is also used when tunneling over TCP. 6 | package reliabletransport 7 | -------------------------------------------------------------------------------- /internal/reliabletransport/model.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "github.com/ooni/minivpn/internal/model" 5 | ) 6 | 7 | type outgoingPacketWriter interface { 8 | // TryInsertOutgoingPacket attempts to insert a packet into the 9 | // inflight queue. If return value is false, insertion was not successful (e.g., too many 10 | // packets in flight). 11 | TryInsertOutgoingPacket(*model.Packet) bool 12 | } 13 | 14 | type seenPacketHandler interface { 15 | // OnIncomingPacketSeen processes a notification received in the shared lateral channel where receiver 16 | // notifies sender of incoming packets. There are two side-effects expected from this call: 17 | // 1. The ID in incomingPacketSeen needs to be appended to the array of packets pending to be acked, if not already 18 | // there. This insertion needs to be reflected by NextPacketIDsToACK() 19 | // 2. Any ACK values in the incomingPacketSeen need to: 20 | // a) evict the matching packet, if existing in the in flight queue, and 21 | // b) increment the counter of acks-with-higher-pid for each packet with a lesser 22 | // packet id (used for fast retransmission) 23 | OnIncomingPacketSeen(incomingPacketSeen) 24 | } 25 | 26 | type outgoingPacketHandler interface { 27 | // NextPacketIDsToACK returns an array of pending IDs to ACK to 28 | // our remote. The length of this array MUST NOT be larger than CONTROL_SEND_ACK_MAX. 29 | // This is used to append it to the ACK array of the outgoing packet. 30 | NextPacketIDsToACK() []model.PacketID 31 | } 32 | 33 | // incomingPacketHandler knows how to deal with incoming packets (going up). 34 | type incomingPacketHandler interface { 35 | // MaybeInsertIncoming will insert a given packet in the reliable 36 | // incoming queue if it passes a series of sanity checks. 37 | MaybeInsertIncoming(*model.Packet) bool 38 | 39 | // NextIncomingSequence gets the largest sequence of packets ready to be passed along 40 | // to the control channel above us. 41 | NextIncomingSequence() incomingSequence 42 | } 43 | -------------------------------------------------------------------------------- /internal/reliabletransport/packets.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ooni/minivpn/internal/model" 7 | "github.com/ooni/minivpn/internal/optional" 8 | ) 9 | 10 | // inFlightPacket wraps a [model.Packet] with metadata for retransmission. 11 | type inFlightPacket struct { 12 | // deadline is a moment in time when is this packet scheduled for the next retransmission. 13 | deadline time.Time 14 | 15 | // how many acks we've received for packets with higher PID. 16 | higherACKs int 17 | 18 | // packet is the underlying packet being sent. 19 | packet *model.Packet 20 | 21 | // retries is a monotonically increasing counter for retransmission. 22 | retries int 23 | } 24 | 25 | func newInFlightPacket(p *model.Packet) *inFlightPacket { 26 | return &inFlightPacket{ 27 | deadline: time.Time{}, 28 | higherACKs: 0, 29 | packet: p, 30 | retries: 0, 31 | } 32 | } 33 | 34 | // ACKForHigherPacket increments the number of acks received for a higher pid than this packet. This will influence the fast rexmit selection algorithm. 35 | func (p *inFlightPacket) ACKForHigherPacket() { 36 | p.higherACKs++ 37 | } 38 | 39 | func (p *inFlightPacket) ScheduleForRetransmission(t time.Time) { 40 | p.retries++ 41 | p.deadline = t.Add(p.backoff()) 42 | } 43 | 44 | // backoff will calculate the next retransmission interval. 45 | func (p *inFlightPacket) backoff() time.Duration { 46 | backoff := time.Duration(1< maxBackoff { 49 | backoff = maxBackoff 50 | } 51 | return backoff 52 | } 53 | 54 | // inflightSequence is a sequence of inFlightPackets. 55 | // A inflightSequence MUST be sorted (since the controlchannel has assigned sequential packet IDs when creating the 56 | // packet) 57 | type inflightSequence []*inFlightPacket 58 | 59 | // nearestDeadlineTo returns the lower deadline to a passed reference time for all the packets in the inFlight queue. Used to re-arm the Ticker. We need to be careful and not pass a 60 | func (seq inflightSequence) nearestDeadlineTo(t time.Time) time.Time { 61 | // we default to a long wakeup 62 | timeout := t.Add(time.Duration(SENDER_TICKER_MS) * time.Millisecond) 63 | 64 | for _, p := range seq { 65 | if p.deadline.Before(timeout) { 66 | timeout = p.deadline 67 | } 68 | } 69 | 70 | // what's past is past and we need to move on. 71 | if timeout.Before(t) { 72 | timeout = t.Add(time.Nanosecond) 73 | } 74 | return timeout 75 | } 76 | 77 | // readyToSend returns the subset of this sequence that has a expired deadline or 78 | // is suitable for fast retransmission. 79 | func (seq inflightSequence) readyToSend(t time.Time) inflightSequence { 80 | expired := make([]*inFlightPacket, 0) 81 | for _, p := range seq { 82 | if p.higherACKs >= 3 { 83 | expired = append(expired, p) 84 | continue 85 | } 86 | if p.deadline.Before(t) { 87 | expired = append(expired, p) 88 | } 89 | } 90 | return expired 91 | } 92 | 93 | // implement sort.Interface 94 | func (seq inflightSequence) Len() int { 95 | return len(seq) 96 | } 97 | 98 | // implement sort.Interface 99 | func (seq inflightSequence) Swap(i, j int) { 100 | seq[i], seq[j] = seq[j], seq[i] 101 | } 102 | 103 | // implement sort.Interface 104 | func (seq inflightSequence) Less(i, j int) bool { 105 | return seq[i].packet.ID < seq[j].packet.ID 106 | } 107 | 108 | // An incomingSequence is an array of [model.Packet]. 109 | // An incomingSequence can be sorted. 110 | type incomingSequence []*model.Packet 111 | 112 | // implement sort.Interface 113 | func (seq incomingSequence) Len() int { 114 | return len(seq) 115 | } 116 | 117 | // implement sort.Interface 118 | func (seq incomingSequence) Swap(i, j int) { 119 | seq[i], seq[j] = seq[j], seq[i] 120 | } 121 | 122 | // implement sort.Interface 123 | func (seq incomingSequence) Less(i, j int) bool { 124 | return seq[i].ID < seq[j].ID 125 | } 126 | 127 | // incomingPacketSeen is a struct that the receiver sends us when a new packet is seen. 128 | type incomingPacketSeen struct { 129 | id optional.Value[model.PacketID] 130 | acks optional.Value[[]model.PacketID] 131 | } 132 | -------------------------------------------------------------------------------- /internal/reliabletransport/receiver.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/ooni/minivpn/internal/model" 9 | "github.com/ooni/minivpn/internal/optional" 10 | "github.com/ooni/minivpn/internal/session" 11 | ) 12 | 13 | // moveUpWorker moves packets up the stack (receiver). 14 | // The sender and receiver data structures lack mutexes because they are 15 | // intended to be confined to a single goroutine (one for each worker), and 16 | // the workers SHOULD ONLY communicate via message passing. 17 | func (ws *workersState) moveUpWorker() { 18 | workerName := fmt.Sprintf("%s: moveUpWorker", serviceName) 19 | 20 | defer func() { 21 | ws.workersManager.OnWorkerDone(workerName) 22 | ws.workersManager.StartShutdown() 23 | }() 24 | 25 | ws.logger.Debugf("%s: started", workerName) 26 | 27 | receiver := newReliableReceiver(ws.logger, ws.incomingSeen) 28 | 29 | for { 30 | // POSSIBLY BLOCK reading a packet to move up the stack 31 | // or POSSIBLY BLOCK waiting for notifications 32 | select { 33 | case packet := <-ws.muxerToReliable: 34 | ws.tracer.OnIncomingPacket(packet, ws.sessionManager.NegotiationState()) 35 | 36 | if packet.Opcode != model.P_CONTROL_HARD_RESET_SERVER_V2 { 37 | // the hard reset has already been logged by the layer below 38 | packet.Log(ws.logger, model.DirectionIncoming) 39 | } 40 | 41 | // TODO: are we handling a HARD_RESET_V2 while we're doing a handshake? 42 | // I'm not sure that's a valid behavior for a server. 43 | // We should be able to deterministically test how this affects the state machine. 44 | 45 | // sanity check incoming packet 46 | if ok := incomingSanityChecks(ws.logger, workerName, packet, ws.sessionManager); !ok { 47 | continue 48 | } 49 | 50 | // notify seen packet to the sender using the lateral channel. 51 | seen := receiver.newIncomingPacketSeen(packet) 52 | ws.incomingSeen <- seen 53 | 54 | // TODO(ainghazal): drop a packet that is a replay (id <= lastConsumed, but != ACK...?) 55 | 56 | // we only want to insert control packets going to the tls layer 57 | if packet.Opcode != model.P_CONTROL_V1 { 58 | continue 59 | } 60 | 61 | if inserted := receiver.MaybeInsertIncoming(packet); !inserted { 62 | // this packet was not inserted in the queue: we drop it 63 | // TODO: add reason 64 | ws.tracer.OnDroppedPacket( 65 | model.DirectionIncoming, 66 | ws.sessionManager.NegotiationState(), 67 | packet) 68 | ws.logger.Debugf("Dropping packet: %v", packet.ID) 69 | continue 70 | } 71 | 72 | ready := receiver.NextIncomingSequence() 73 | for _, nextPacket := range ready { 74 | // POSSIBLY BLOCK delivering to the upper layer 75 | select { 76 | case ws.reliableToControl <- nextPacket: 77 | case <-ws.workersManager.ShouldShutdown(): 78 | return 79 | } 80 | } 81 | 82 | case <-ws.workersManager.ShouldShutdown(): 83 | return 84 | } 85 | } 86 | } 87 | 88 | func incomingSanityChecks(logger model.Logger, workerName string, packet *model.Packet, session *session.Manager) bool { 89 | // drop a packet from a remote session we don't know about. 90 | if !bytes.Equal(packet.LocalSessionID[:], session.RemoteSessionID()) { 91 | logger.Warnf( 92 | "%s: packet with invalid LocalSessionID: got %x; expected %x", 93 | workerName, 94 | packet.LocalSessionID, 95 | session.RemoteSessionID(), 96 | ) 97 | return false 98 | } 99 | 100 | if len(packet.ACKs) == 0 { 101 | return true 102 | } 103 | 104 | // only if we get incoming ACKs we can also check that the remote session id matches our own 105 | // (packets with no ack array do not include remoteSessionID) 106 | if !bytes.Equal(packet.RemoteSessionID[:], session.LocalSessionID()) { 107 | logger.Warnf( 108 | "%s: packet with invalid RemoteSessionID: got %x; expected %x", 109 | workerName, 110 | packet.RemoteSessionID, 111 | session.LocalSessionID(), 112 | ) 113 | return false 114 | } 115 | return true 116 | } 117 | 118 | // 119 | // incomingPacketHandler implementation. 120 | // 121 | 122 | // reliableReceiver is the receiver part that sees incoming packets moving up the stack. 123 | // Please use the constructor `newReliableReceiver()` 124 | type reliableReceiver struct { 125 | // logger is the logger to use 126 | logger model.Logger 127 | 128 | // incomingPackets are packets to process (reorder) before they are passed to TLS layer. 129 | incomingPackets incomingSequence 130 | 131 | // incomingSeen is a channel where we send notifications for incoming packets seen by us. 132 | incomingSeen chan<- incomingPacketSeen 133 | 134 | // lastConsumed is the last [model.PacketID] that we have passed to the control layer above us. 135 | lastConsumed model.PacketID 136 | } 137 | 138 | func newReliableReceiver(logger model.Logger, ch chan incomingPacketSeen) *reliableReceiver { 139 | return &reliableReceiver{ 140 | logger: logger, 141 | incomingPackets: make([]*model.Packet, 0), 142 | incomingSeen: ch, 143 | lastConsumed: 0, 144 | } 145 | } 146 | 147 | func (r *reliableReceiver) MaybeInsertIncoming(p *model.Packet) bool { 148 | // we drop if at capacity, by default double the size of the outgoing buffer 149 | if len(r.incomingPackets) >= RELIABLE_RECV_BUFFER_SIZE { 150 | r.logger.Warnf("dropping packet, buffer full with len %v", len(r.incomingPackets)) 151 | return false 152 | } 153 | 154 | // insert this one in the queue to pass to TLS. 155 | r.incomingPackets = append(r.incomingPackets, p) 156 | return true 157 | } 158 | 159 | func (r *reliableReceiver) NextIncomingSequence() incomingSequence { 160 | last := r.lastConsumed 161 | ready := make([]*model.Packet, 0, RELIABLE_RECV_BUFFER_SIZE) 162 | 163 | // sort them so that we begin with lower model.PacketID 164 | sort.Sort(r.incomingPackets) 165 | var keep incomingSequence 166 | 167 | for i, p := range r.incomingPackets { 168 | if p.ID-last == 1 { 169 | ready = append(ready, p) 170 | last++ 171 | } else if p.ID > last { 172 | // here we broke sequentiality, but we want 173 | // to drop anything that is below lastConsumed 174 | keep = append(keep, r.incomingPackets[i:]...) 175 | break 176 | } 177 | } 178 | r.lastConsumed = last 179 | r.incomingPackets = keep 180 | return ready 181 | } 182 | 183 | func (r *reliableReceiver) newIncomingPacketSeen(p *model.Packet) incomingPacketSeen { 184 | incomingPacket := incomingPacketSeen{} 185 | if p.Opcode == model.P_ACK_V1 { 186 | incomingPacket.acks = optional.Some(p.ACKs) 187 | } else { 188 | incomingPacket.id = optional.Some(p.ID) 189 | incomingPacket.acks = optional.Some(p.ACKs) 190 | } 191 | 192 | return incomingPacket 193 | } 194 | 195 | // assert that reliableReceiver implements incomingPacketHandler 196 | var _ incomingPacketHandler = &reliableReceiver{} 197 | -------------------------------------------------------------------------------- /internal/reliabletransport/receiver_test.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/apex/log" 8 | "github.com/ooni/minivpn/internal/model" 9 | ) 10 | 11 | // 12 | // tests for reliableReceiver 13 | // 14 | 15 | func Test_newReliableReceiver(t *testing.T) { 16 | rr := newReliableReceiver(log.Log, make(chan incomingPacketSeen)) 17 | if rr.logger == nil { 18 | t.Errorf("newReliableReceiver() should not have nil logger") 19 | } 20 | if rr.incomingPackets == nil { 21 | t.Errorf("newReliableReceiver() should not have nil incomingPackets ch") 22 | } 23 | if rr.lastConsumed != 0 { 24 | t.Errorf("newReliableReceiver() should have lastConsumed == 0") 25 | } 26 | } 27 | 28 | func Test_reliableQueue_MaybeInsertIncoming(t *testing.T) { 29 | if testing.Verbose() { 30 | log.SetLevel(log.DebugLevel) 31 | } 32 | 33 | type fields struct { 34 | incomingPackets incomingSequence 35 | } 36 | type args struct { 37 | p *model.Packet 38 | } 39 | tests := []struct { 40 | name string 41 | fields fields 42 | args args 43 | want bool 44 | }{ 45 | { 46 | name: "empty incoming, insert one", 47 | fields: fields{ 48 | incomingPackets: make([]*model.Packet, 0), 49 | }, 50 | args: args{ 51 | &model.Packet{ID: 1}, 52 | }, 53 | want: true, 54 | }, 55 | { 56 | name: "almost full incoming, insert one", 57 | fields: fields{ 58 | incomingPackets: []*model.Packet{ 59 | {ID: 1}, {ID: 2}, {ID: 3}, {ID: 4}, 60 | {ID: 5}, {ID: 6}, {ID: 7}, {ID: 8}, 61 | {ID: 9}, {ID: 10}, {ID: 11}, 62 | }, 63 | }, 64 | args: args{&model.Packet{ID: 12}}, 65 | want: true, 66 | }, 67 | { 68 | name: "full incoming, cannot insert", 69 | fields: fields{ 70 | incomingPackets: []*model.Packet{ 71 | {ID: 1}, {ID: 2}, {ID: 3}, {ID: 4}, 72 | {ID: 5}, {ID: 6}, {ID: 7}, {ID: 8}, 73 | {ID: 9}, {ID: 10}, {ID: 11}, {ID: 12}, 74 | }, 75 | }, 76 | args: args{ 77 | &model.Packet{ID: 13}, 78 | }, 79 | want: false, 80 | }, 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | r := &reliableReceiver{ 85 | logger: log.Log, 86 | incomingPackets: tt.fields.incomingPackets, 87 | } 88 | if got := r.MaybeInsertIncoming(tt.args.p); got != tt.want { 89 | t.Errorf("reliableQueue.MaybeInsertIncoming() = %v, want %v", got, tt.want) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func Test_reliableQueue_NextIncomingSequence(t *testing.T) { 96 | if testing.Verbose() { 97 | log.SetLevel(log.DebugLevel) 98 | } 99 | 100 | type fields struct { 101 | lastConsumed model.PacketID 102 | incomingPackets incomingSequence 103 | } 104 | tests := []struct { 105 | name string 106 | fields fields 107 | want incomingSequence 108 | }{ 109 | { 110 | name: "empty sequence", 111 | fields: fields{ 112 | incomingPackets: []*model.Packet{}, 113 | lastConsumed: model.PacketID(0), 114 | }, 115 | want: []*model.Packet{}, 116 | }, 117 | { 118 | name: "single packet", 119 | fields: fields{ 120 | lastConsumed: model.PacketID(0), 121 | incomingPackets: []*model.Packet{ 122 | {ID: 1}, 123 | }, 124 | }, 125 | want: []*model.Packet{ 126 | {ID: 1}, 127 | }, 128 | }, 129 | { 130 | name: "series of sequential packets", 131 | fields: fields{ 132 | lastConsumed: model.PacketID(0), 133 | incomingPackets: []*model.Packet{{ID: 1}, {ID: 2}, {ID: 3}}, 134 | }, 135 | want: []*model.Packet{{ID: 1}, {ID: 2}, {ID: 3}}, 136 | }, 137 | { 138 | name: "series of sequential packets with hole", 139 | fields: fields{ 140 | lastConsumed: model.PacketID(0), 141 | incomingPackets: []*model.Packet{{ID: 1}, {ID: 2}, {ID: 3}, {ID: 5}}, 142 | }, 143 | want: []*model.Packet{{ID: 1}, {ID: 2}, {ID: 3}}, 144 | }, 145 | { 146 | name: "series of sequential packets with hole, lastConsumed higher", 147 | fields: fields{ 148 | lastConsumed: model.PacketID(10), 149 | incomingPackets: []*model.Packet{{ID: 1}, {ID: 2}, {ID: 3}, {ID: 5}}, 150 | }, 151 | want: []*model.Packet{}, 152 | }, 153 | { 154 | name: "series of sequential packets with hole, lastConsumed higher, some above", 155 | fields: fields{ 156 | lastConsumed: model.PacketID(10), 157 | incomingPackets: []*model.Packet{{ID: 1}, {ID: 2}, {ID: 10}, {ID: 11}, {ID: 12}, {ID: 20}}, 158 | }, 159 | want: []*model.Packet{{ID: 11}, {ID: 12}}, 160 | }, 161 | } 162 | for _, tt := range tests { 163 | t.Run(tt.name, func(t *testing.T) { 164 | r := &reliableReceiver{ 165 | lastConsumed: tt.fields.lastConsumed, 166 | incomingPackets: tt.fields.incomingPackets, 167 | } 168 | if got := r.NextIncomingSequence(); !reflect.DeepEqual(got, tt.want) { 169 | t.Errorf("reliableQueue.NextIncomingSequence() = %v, want %v", got, tt.want) 170 | } 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /internal/reliabletransport/reliable_ack_test.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/ooni/minivpn/internal/model" 9 | "github.com/ooni/minivpn/internal/vpntest" 10 | "github.com/ooni/minivpn/pkg/config" 11 | 12 | // TODO: replace with stlib slices after 1.21 13 | "golang.org/x/exp/slices" 14 | ) 15 | 16 | // test that everything that is received from below is eventually ACKed to the sender. 17 | /* 18 | 19 | ┌────┐id ┌────┐ 20 | │sndr│◄──┤rcvr│ 21 | └─┬──┘ └──▲─┘ 22 | │ │ 23 | │ │ 24 | │ │ 25 | ▼ send 26 | ack 27 | */ 28 | func TestReliable_ACK(t *testing.T) { 29 | if testing.Verbose() { 30 | log.SetLevel(log.DebugLevel) 31 | } 32 | 33 | type args struct { 34 | inputSequence []string 35 | start int 36 | wantacks int 37 | } 38 | 39 | tests := []struct { 40 | name string 41 | args args 42 | }{ 43 | { 44 | name: "ten ordered packets in", 45 | args: args{ 46 | inputSequence: []string{ 47 | "[1] CONTROL_V1 +1ms", 48 | "[2] CONTROL_V1 +1ms", 49 | "[3] CONTROL_V1 +1ms", 50 | "[4] CONTROL_V1 +1ms", 51 | "[5] CONTROL_V1 +1ms", 52 | "[6] CONTROL_V1 +1ms", 53 | "[7] CONTROL_V1 +1ms", 54 | "[8] CONTROL_V1 +1ms", 55 | "[9] CONTROL_V1 +1ms", 56 | "[10] CONTROL_V1 +1ms", 57 | }, 58 | start: 1, 59 | wantacks: 10, 60 | }, 61 | }, 62 | { 63 | name: "five ordered packets with offset", 64 | args: args{ 65 | inputSequence: []string{ 66 | "[100] CONTROL_V1 +1ms", 67 | "[101] CONTROL_V1 +1ms", 68 | "[102] CONTROL_V1 +1ms", 69 | "[103] CONTROL_V1 +1ms", 70 | "[104] CONTROL_V1 +1ms", 71 | }, 72 | start: 100, 73 | wantacks: 5, 74 | }, 75 | }, 76 | { 77 | name: "five reversed packets", 78 | args: args{ 79 | inputSequence: []string{ 80 | "[5] CONTROL_V1 +1ms", 81 | "[4] CONTROL_V1 +1ms", 82 | "[3] CONTROL_V1 +1ms", 83 | "[2] CONTROL_V1 +1ms", 84 | "[1] CONTROL_V1 +1ms", 85 | }, 86 | start: 1, 87 | wantacks: 5, 88 | }, 89 | }, 90 | { 91 | name: "ten unordered packets with duplicates", 92 | args: args{ 93 | inputSequence: []string{ 94 | "[5] CONTROL_V1 +1ms", 95 | "[1] CONTROL_V1 +1ms", 96 | "[5] CONTROL_V1 +1ms", 97 | "[2] CONTROL_V1 +1ms", 98 | "[1] CONTROL_V1 +1ms", 99 | "[4] CONTROL_V1 +1ms", 100 | "[2] CONTROL_V1 +1ms", 101 | "[3] CONTROL_V1 +1ms", 102 | "[3] CONTROL_V1 +1ms", 103 | "[4] CONTROL_V1 +1ms", 104 | }, 105 | start: 1, 106 | wantacks: 5, 107 | }, 108 | }, 109 | } 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | s := &Service{} 113 | 114 | // just to properly initialize it, we don't care about these 115 | s.ControlToReliable = make(chan *model.Packet) 116 | // this one up to control/tls also needs to be buffered because otherwise 117 | // we'll block on the receiver when delivering up. 118 | reliableToControl := make(chan *model.Packet, 1024) 119 | s.ReliableToControl = &reliableToControl 120 | 121 | // the only two channels we're going to be testing on this test 122 | // we want to buffer enough to be safe writing to them. 123 | dataIn := make(chan *model.Packet, 1024) 124 | dataOut := make(chan *model.Packet, 1024) 125 | 126 | s.MuxerToReliable = dataIn // up 127 | s.DataOrControlToMuxer = &dataOut // down 128 | 129 | workers, session := initManagers() 130 | 131 | t0 := time.Now() 132 | 133 | // let the workers pump up the jam! 134 | s.StartWorkers(config.NewConfig(config.WithLogger(log.Log)), workers, session) 135 | 136 | writer := vpntest.NewPacketWriter(dataIn) 137 | 138 | // initialize a mock session ID for our peer 139 | initializeSessionIDForWriter(writer, session) 140 | 141 | go writer.WriteSequence(tt.args.inputSequence) 142 | 143 | reader := vpntest.NewPacketReader(dataOut) 144 | witness := vpntest.NewWitness(reader) 145 | 146 | if ok := witness.VerifyNumberOfACKs(tt.args.wantacks, t0); !ok { 147 | got := len(witness.Log().ACKs()) 148 | t.Errorf("TestACK: got = %v, want %v", got, tt.args.wantacks) 149 | } 150 | gotAckSet := ackSetFromInts(witness.Log().ACKs()).sorted() 151 | wantAckSet := ackSetFromRange(tt.args.start, tt.args.wantacks).sorted() 152 | 153 | if !slices.Equal(gotAckSet, wantAckSet) { 154 | t.Errorf("TestACK: got = %v, want %v", gotAckSet, wantAckSet) 155 | 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /internal/reliabletransport/reliable_reorder_test.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/ooni/minivpn/internal/model" 9 | "github.com/ooni/minivpn/internal/vpntest" 10 | "github.com/ooni/minivpn/pkg/config" 11 | ) 12 | 13 | // test that we're able to reorder (towards TLS) whatever is received (from the muxer). 14 | // 15 | // dataOut 16 | // ▲ 17 | // │ 18 | // ┌────┐ ┌──┴─┐ 19 | // │sndr│ │rcvr│ 20 | // └────┘ └────┘ 21 | // ▲ 22 | // | 23 | // dataIn 24 | func TestReliable_Reordering_UP(t *testing.T) { 25 | if testing.Verbose() { 26 | log.SetLevel(log.DebugLevel) 27 | } 28 | 29 | type args struct { 30 | inputSequence []string 31 | outputSequence []int 32 | } 33 | 34 | tests := []struct { 35 | name string 36 | args args 37 | }{ 38 | { 39 | name: "test with a well-ordered input sequence", 40 | args: args{ 41 | inputSequence: []string{ 42 | "[1] CONTROL_V1 +1ms", 43 | "[2] CONTROL_V1 +1ms", 44 | "[3] CONTROL_V1 +1ms", 45 | "[4] CONTROL_V1 +1ms", 46 | }, 47 | outputSequence: []int{1, 2, 3, 4}, 48 | }, 49 | }, 50 | { 51 | name: "test reordering for input sequence", 52 | args: args{ 53 | inputSequence: []string{ 54 | "[2] CONTROL_V1 +1ms", 55 | "[4] CONTROL_V1 +1ms", 56 | "[3] CONTROL_V1 +1ms", 57 | "[1] CONTROL_V1 +1ms", 58 | }, 59 | outputSequence: []int{1, 2, 3, 4}, 60 | }, 61 | }, 62 | { 63 | name: "test reordering for input sequence, longer waits", 64 | args: args{ 65 | inputSequence: []string{ 66 | "[2] CONTROL_V1 +5ms", 67 | "[4] CONTROL_V1 +10ms", 68 | "[3] CONTROL_V1 +1ms", 69 | "[1] CONTROL_V1 +50ms", 70 | }, 71 | outputSequence: []int{1, 2, 3, 4}, 72 | }, 73 | }, 74 | { 75 | name: "test reordering for input sequence, with duplicates", 76 | args: args{ 77 | inputSequence: []string{ 78 | "[2] CONTROL_V1 +1ms", 79 | "[2] CONTROL_V1 +1ms", 80 | "[4] CONTROL_V1 +1ms", 81 | "[4] CONTROL_V1 +1ms", 82 | "[4] CONTROL_V1 +1ms", 83 | "[1] CONTROL_V1 +1ms", 84 | "[3] CONTROL_V1 +1ms", 85 | "[1] CONTROL_V1 +1ms", 86 | }, 87 | outputSequence: []int{1, 2, 3, 4}, 88 | }, 89 | }, 90 | { 91 | name: "reordering with acks interspersed", 92 | args: args{ 93 | inputSequence: []string{ 94 | "[2] CONTROL_V1 +5ms", 95 | "[4] CONTROL_V1 +2ms", 96 | "[0] ACK_V1 +1ms", 97 | "[3] CONTROL_V1 +1ms", 98 | "[0] ACK_V1 +1ms", 99 | "[1] CONTROL_V1 +2ms", 100 | }, 101 | outputSequence: []int{1, 2, 3, 4}, 102 | }, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | s := &Service{} 108 | 109 | // just to properly initialize it, we don't care about these 110 | s.ControlToReliable = make(chan *model.Packet) 111 | dataToMuxer := make(chan *model.Packet) 112 | s.DataOrControlToMuxer = &dataToMuxer 113 | 114 | // the only two channels we're going to be testing on this test 115 | // we want to buffer enough to be safe writing to them. 116 | dataIn := make(chan *model.Packet, 1024) 117 | dataOut := make(chan *model.Packet, 1024) 118 | 119 | s.MuxerToReliable = dataIn 120 | s.ReliableToControl = &dataOut 121 | 122 | workers, session := initManagers() 123 | 124 | t0 := time.Now() 125 | 126 | // let the workers pump up the jam! 127 | s.StartWorkers(config.NewConfig(config.WithLogger(log.Log)), workers, session) 128 | 129 | writer := vpntest.NewPacketWriter(dataIn) 130 | initializeSessionIDForWriter(writer, session) 131 | 132 | go writer.WriteSequence(tt.args.inputSequence) 133 | 134 | reader := vpntest.NewPacketReader(dataOut) 135 | if ok := reader.WaitForSequence(tt.args.outputSequence, t0); !ok { 136 | got := reader.Log().IDSequence() 137 | t.Errorf("Reordering: got = %v, want %v", got, tt.args.outputSequence) 138 | } 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /internal/reliabletransport/service.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "github.com/ooni/minivpn/internal/model" 5 | "github.com/ooni/minivpn/internal/session" 6 | "github.com/ooni/minivpn/internal/workers" 7 | "github.com/ooni/minivpn/pkg/config" 8 | ) 9 | 10 | var ( 11 | serviceName = "reliabletransport" 12 | ) 13 | 14 | // Service is the reliable service. Make sure you initialize 15 | // the channels before invoking [Service.StartWorkers]. 16 | type Service struct { 17 | // DataOrControlToMuxer is a shared channel that moves packets down to the muxer 18 | DataOrControlToMuxer *chan *model.Packet 19 | 20 | // ControlToReliable moves packets down to us 21 | ControlToReliable chan *model.Packet 22 | 23 | // MuxerToReliable moves packets up to us 24 | MuxerToReliable chan *model.Packet 25 | 26 | // ReliableToControl moves packets up from us to the control layer above 27 | ReliableToControl *chan *model.Packet 28 | } 29 | 30 | // StartWorkers starts the reliable-transport workers. See the [ARCHITECTURE] 31 | // file for more information about the reliable-transport workers. 32 | // 33 | // [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md 34 | func (s *Service) StartWorkers( 35 | config *config.Config, 36 | workersManager *workers.Manager, 37 | sessionManager *session.Manager, 38 | ) { 39 | // incomingSeen is a buffered channel to avoid losing packets if we're busy 40 | // processing in the sender goroutine. 41 | ws := &workersState{ 42 | controlToReliable: s.ControlToReliable, 43 | dataOrControlToMuxer: *s.DataOrControlToMuxer, 44 | incomingSeen: make(chan incomingPacketSeen, 100), 45 | logger: config.Logger(), 46 | muxerToReliable: s.MuxerToReliable, 47 | reliableToControl: *s.ReliableToControl, 48 | sessionManager: sessionManager, 49 | tracer: config.Tracer(), 50 | workersManager: workersManager, 51 | } 52 | workersManager.StartWorker(ws.moveUpWorker) 53 | workersManager.StartWorker(ws.moveDownWorker) 54 | } 55 | 56 | // workersState contains the reliable workers state 57 | type workersState struct { 58 | // controlToReliable is the channel from which we read packets going down the stack. 59 | controlToReliable <-chan *model.Packet 60 | 61 | // dataOrControlToMuxer is the channel where we write packets going down the stack. 62 | dataOrControlToMuxer chan<- *model.Packet 63 | 64 | // incomingSeen is the shared channel to connect sender and receiver goroutines. 65 | incomingSeen chan incomingPacketSeen 66 | 67 | // logger is the logger to use 68 | logger model.Logger 69 | 70 | // muxerToReliable is the channel from which we read packets going up the stack. 71 | muxerToReliable <-chan *model.Packet 72 | 73 | // reliableToControl is the channel where we write packets going up the stack. 74 | reliableToControl chan<- *model.Packet 75 | 76 | // sessionManager manages the OpenVPN session. 77 | sessionManager *session.Manager 78 | 79 | // tracer is a handshake tracer. 80 | tracer model.HandshakeTracer 81 | 82 | // workersManager controls the workers lifecycle. 83 | workersManager *workers.Manager 84 | } 85 | -------------------------------------------------------------------------------- /internal/reliabletransport/service_test.go: -------------------------------------------------------------------------------- 1 | package reliabletransport 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/apex/log" 7 | "github.com/ooni/minivpn/internal/model" 8 | "github.com/ooni/minivpn/internal/session" 9 | "github.com/ooni/minivpn/internal/workers" 10 | "github.com/ooni/minivpn/pkg/config" 11 | ) 12 | 13 | // test that we can start and stop the workers 14 | func TestService_StartWorkers(t *testing.T) { 15 | type fields struct { 16 | DataOrControlToMuxer *chan *model.Packet 17 | ControlToReliable chan *model.Packet 18 | MuxerToReliable chan *model.Packet 19 | ReliableToControl *chan *model.Packet 20 | } 21 | type args struct { 22 | config *config.Config 23 | workersManager *workers.Manager 24 | sessionManager *session.Manager 25 | } 26 | tests := []struct { 27 | name string 28 | fields fields 29 | args args 30 | }{ 31 | { 32 | name: "call startworkers with properly initialized channels", 33 | fields: fields{ 34 | DataOrControlToMuxer: func() *chan *model.Packet { 35 | ch := make(chan *model.Packet) 36 | return &ch 37 | }(), 38 | ControlToReliable: make(chan *model.Packet), 39 | MuxerToReliable: make(chan *model.Packet), 40 | ReliableToControl: func() *chan *model.Packet { 41 | ch := make(chan *model.Packet) 42 | return &ch 43 | }(), 44 | }, 45 | args: args{ 46 | config: config.NewConfig(config.WithLogger(log.Log)), 47 | workersManager: workers.NewManager(log.Log), 48 | sessionManager: func() *session.Manager { 49 | m, _ := session.NewManager(config.NewConfig(config.WithLogger(log.Log))) 50 | return m 51 | }(), 52 | }, 53 | }, 54 | } 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(_ *testing.T) { 57 | s := &Service{ 58 | DataOrControlToMuxer: tt.fields.DataOrControlToMuxer, 59 | ControlToReliable: tt.fields.ControlToReliable, 60 | MuxerToReliable: tt.fields.MuxerToReliable, 61 | ReliableToControl: tt.fields.ReliableToControl, 62 | } 63 | s.StartWorkers(tt.args.config, tt.args.workersManager, tt.args.sessionManager) 64 | tt.args.workersManager.StartShutdown() 65 | tt.args.workersManager.WaitWorkersShutdown() 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/runtimex/runtimex.go: -------------------------------------------------------------------------------- 1 | // Package runtimex contains [runtime] extensions. 2 | package runtimex 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // PanicIfFalse calls panic with the given message if the given statement is false. 10 | func PanicIfFalse(stmt bool, message string) { 11 | if !stmt { 12 | panic(errors.New(message)) 13 | } 14 | } 15 | 16 | // PanicIfTrue calls panic with the given message if the given statement is true. 17 | func PanicIfTrue(stmt bool, message string) { 18 | if stmt { 19 | panic(errors.New(message)) 20 | } 21 | } 22 | 23 | // Assert calls panic with the given message if the given statement is false. 24 | var Assert = PanicIfFalse 25 | 26 | // PanicOnError calls panic() if err is not nil. The type passed to panic 27 | // is an error type wrapping the original error. 28 | func PanicOnError(err error, message string) { 29 | if err != nil { 30 | panic(fmt.Errorf("%s: %w", message, err)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/runtimex/runtimex_test.go: -------------------------------------------------------------------------------- 1 | // Package runtimex contains [runtime] extensions. 2 | package runtimex 3 | 4 | import ( 5 | "errors" 6 | "testing" 7 | ) 8 | 9 | func TestPanicIfFalse(t *testing.T) { 10 | t.Run("expect a panic for a false statement", func(t *testing.T) { 11 | assertPanic(t, func() { PanicIfFalse(true == false, "should panic") }) 12 | }) 13 | t.Run("do not expect a panic for a true statement", func(t *testing.T) { 14 | PanicIfFalse(1 == 0+1, "should not panic") 15 | }) 16 | } 17 | 18 | func TestPanicIfTrue(t *testing.T) { 19 | t.Run("expect a panic for a true statement", func(t *testing.T) { 20 | assertPanic(t, func() { PanicIfTrue(1 == 0+1, "should panic") }) 21 | }) 22 | t.Run("do not expect a panic for a false statement", func(t *testing.T) { 23 | PanicIfTrue(1 == 0, "should not panic") 24 | }) 25 | } 26 | 27 | func TestAssert(t *testing.T) { 28 | t.Run("expect a panic for a false statement", func(t *testing.T) { 29 | assertPanic(t, func() { Assert(true == false, "should panic") }) 30 | }) 31 | t.Run("do not expect a panic for a true statement", func(t *testing.T) { 32 | Assert(1 == 0+1, "should not panic") 33 | }) 34 | } 35 | 36 | func TestPanicOnError(t *testing.T) { 37 | t.Run("expect a panic for a non-null error", func(t *testing.T) { 38 | assertPanic(t, func() { PanicOnError(errors.New("bad thing"), "should panic") }) 39 | }) 40 | t.Run("do not expect a panic for a false statement", func(t *testing.T) { 41 | PanicOnError(nil, "should not panic") 42 | }) 43 | } 44 | 45 | func assertPanic(t *testing.T, f func()) { 46 | defer func() { 47 | if r := recover(); r == nil { 48 | t.Errorf("expected code to panic") 49 | } 50 | }() 51 | f() 52 | } 53 | -------------------------------------------------------------------------------- /internal/session/datachannelkey.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | // ErrDataChannelKey is a [DataChannelKey] error. 11 | ErrDataChannelKey = errors.New("bad data-channel key") 12 | ) 13 | 14 | // DataChannelKey represents a pair of key sources that have been negotiated 15 | // over the control channel, and from which we will derive local and remote 16 | // keys for encryption and decrption over the data channel. The index refers to 17 | // the short key_id that is passed in the lower 3 bits if a packet header. 18 | // The setup of the keys for a given data channel (that is, for every key_id) 19 | // is made by expanding the keysources using the prf function. 20 | // 21 | // Do note that we are not yet implementing key renegotiation - but the index 22 | // is provided for convenience when/if we support that in the future. 23 | type DataChannelKey struct { 24 | index uint32 25 | ready bool 26 | local *KeySource 27 | remote *KeySource 28 | mu sync.Mutex 29 | } 30 | 31 | // Local returns the local [KeySource] 32 | func (dck *DataChannelKey) Local() *KeySource { 33 | return dck.local 34 | } 35 | 36 | // Remote returns the local [KeySource] 37 | func (dck *DataChannelKey) Remote() *KeySource { 38 | return dck.remote 39 | } 40 | 41 | // AddRemoteKey adds the server keySource to our dataChannelKey. This makes the 42 | // dataChannelKey ready to be used. 43 | func (dck *DataChannelKey) AddRemoteKey(k *KeySource) error { 44 | dck.mu.Lock() 45 | defer dck.mu.Unlock() 46 | if dck.ready { 47 | return fmt.Errorf("%w: %s", ErrDataChannelKey, "cannot overwrite remote key slot") 48 | } 49 | dck.remote = k 50 | dck.ready = true 51 | return nil 52 | } 53 | 54 | // AddLocalKey adds the local keySource to our dataChannelKey. 55 | func (dck *DataChannelKey) AddLocalKey(k *KeySource) error { 56 | dck.mu.Lock() 57 | defer dck.mu.Unlock() 58 | dck.local = k 59 | return nil 60 | } 61 | 62 | // Ready returns whether the [DataChannelKey] is ready. 63 | func (dck *DataChannelKey) Ready() bool { 64 | dck.mu.Lock() 65 | defer dck.mu.Unlock() 66 | return dck.ready 67 | } 68 | -------------------------------------------------------------------------------- /internal/session/datachannelkey_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "testing" 4 | 5 | func Test_dataChannelKey_addRemoteKey(t *testing.T) { 6 | type fields struct { 7 | ready bool 8 | remote *KeySource 9 | } 10 | type args struct { 11 | k *KeySource 12 | } 13 | tests := []struct { 14 | name string 15 | fields fields 16 | args args 17 | wantErr bool 18 | }{ 19 | { 20 | "adding a keysource should make it ready", 21 | fields{false, &KeySource{}}, 22 | args{&KeySource{}}, 23 | false, 24 | }, 25 | { 26 | "adding when ready should fail", 27 | fields{true, &KeySource{}}, 28 | args{&KeySource{}}, 29 | true, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | dck := &DataChannelKey{ 35 | ready: tt.fields.ready, 36 | remote: tt.fields.remote, 37 | } 38 | if err := dck.AddRemoteKey(tt.args.k); (err != nil) != tt.wantErr { 39 | t.Errorf("dataChannelKey.AddRemoteKey() error = %v, wantErr %v", err, tt.wantErr) 40 | } 41 | if !dck.Ready() { 42 | t.Errorf("should be ready") 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/session/doc.go: -------------------------------------------------------------------------------- 1 | // Package session keeps state for the application, including internal state 2 | // transitions for the OpenVPN protocol, data channel keys, and all the state 3 | // pertaining to the different packet counters. 4 | package session 5 | -------------------------------------------------------------------------------- /internal/session/keysource.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/ooni/minivpn/internal/bytesx" 9 | ) 10 | 11 | // randomFn mocks the function to generate random bytes. 12 | var randomFn = bytesx.GenRandomBytes 13 | 14 | // errRandomBytes is the error returned when we cannot generate random bytes. 15 | var errRandomBytes = errors.New("error generating random bytes") 16 | 17 | // KeySource contains random data to generate keys. 18 | type KeySource struct { 19 | R1 [32]byte 20 | R2 [32]byte 21 | PreMaster [48]byte 22 | } 23 | 24 | // Bytes returns the byte representation of a [KeySource]. 25 | func (k *KeySource) Bytes() []byte { 26 | buf := &bytes.Buffer{} 27 | buf.Write(k.PreMaster[:]) 28 | buf.Write(k.R1[:]) 29 | buf.Write(k.R2[:]) 30 | return buf.Bytes() 31 | } 32 | 33 | // NewKeySource constructs a new [KeySource]. 34 | func NewKeySource() (*KeySource, error) { 35 | random1, err := randomFn(32) 36 | if err != nil { 37 | return nil, fmt.Errorf("%w: %s", errRandomBytes, err.Error()) 38 | } 39 | 40 | var r1, r2 [32]byte 41 | var preMaster [48]byte 42 | copy(r1[:], random1) 43 | 44 | random2, err := randomFn(32) 45 | if err != nil { 46 | return nil, fmt.Errorf("%w: %s", errRandomBytes, err.Error()) 47 | } 48 | copy(r2[:], random2) 49 | 50 | random3, err := randomFn(48) 51 | if err != nil { 52 | return nil, fmt.Errorf("%w: %s", errRandomBytes, err.Error()) 53 | } 54 | copy(preMaster[:], random3) 55 | return &KeySource{ 56 | R1: r1, 57 | R2: r2, 58 | PreMaster: preMaster, 59 | }, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/session/keysource_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | rnd16 = "0123456789012345" 11 | rnd32 = "01234567890123456789012345678901" 12 | rnd48 = "012345678901234567890123456789012345678901234567" 13 | ) 14 | 15 | func makeTestKeys() ([32]byte, [32]byte, [48]byte) { 16 | r1 := *(*[32]byte)([]byte(rnd32)) 17 | r2 := *(*[32]byte)([]byte(rnd32)) 18 | r3 := *(*[48]byte)([]byte(rnd48)) 19 | return r1, r2, r3 20 | } 21 | 22 | // getDeterministicRandomKeySize returns a sequence of integers 23 | // using the map in the closure. we use this to construct a deterministic 24 | // random function to replace the random function used in the real client. 25 | func getDeterministicRandomKeySizeFn() func() int { 26 | var rndSeq = map[int]int{ 27 | 1: 32, 28 | 2: 32, 29 | 3: 48, 30 | } 31 | i := 1 32 | f := func() int { 33 | v := rndSeq[i] 34 | i += 1 35 | return v 36 | } 37 | return f 38 | } 39 | 40 | func TestNewKeySource(t *testing.T) { 41 | 42 | genKeySizeFn := getDeterministicRandomKeySizeFn() 43 | 44 | // we replace the global random function used in the constructor 45 | randomFn = func(int) ([]byte, error) { 46 | switch genKeySizeFn() { 47 | case 48: 48 | return []byte(rnd48), nil 49 | default: 50 | return []byte(rnd32), nil 51 | } 52 | } 53 | 54 | r1, r2, premaster := makeTestKeys() 55 | ks := &KeySource{r1, r2, premaster} 56 | 57 | tests := []struct { 58 | name string 59 | want *KeySource 60 | }{ 61 | { 62 | name: "test generation of a new key with mocked random data", 63 | want: ks, 64 | }, 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | if got, _ := NewKeySource(); !reflect.DeepEqual(got, tt.want) { 69 | t.Errorf("newKeySource() = %v, want %v", got, tt.want) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func Test_keySource_Bytes(t *testing.T) { 76 | r1, r2, premaster := makeTestKeys() 77 | goodSerialized := append(premaster[:], r1[:]...) 78 | goodSerialized = append(goodSerialized, r2[:]...) 79 | 80 | type fields struct { 81 | r1 [32]byte 82 | r2 [32]byte 83 | preMaster [48]byte 84 | } 85 | tests := []struct { 86 | name string 87 | fields fields 88 | want []byte 89 | }{ 90 | { 91 | name: "good keysource", 92 | fields: fields{ 93 | r1: r1, 94 | r2: r2, 95 | preMaster: premaster, 96 | }, 97 | want: goodSerialized, 98 | }, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | k := &KeySource{ 103 | R1: tt.fields.r1, 104 | R2: tt.fields.r2, 105 | PreMaster: tt.fields.preMaster, 106 | } 107 | if got := k.Bytes(); !bytes.Equal(got, tt.want) { 108 | t.Errorf("keySource.Bytes() = %v, want %v", got, tt.want) 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /internal/tlssession/common_test.go: -------------------------------------------------------------------------------- 1 | package tlssession 2 | 3 | import ( 4 | "github.com/ooni/minivpn/internal/model" 5 | "github.com/ooni/minivpn/internal/runtimex" 6 | "github.com/ooni/minivpn/internal/session" 7 | "github.com/ooni/minivpn/pkg/config" 8 | ) 9 | 10 | func makeTestingSession() *session.Manager { 11 | manager, err := session.NewManager(config.NewConfig()) 12 | runtimex.PanicOnError(err, "could not get session manager") 13 | manager.SetRemoteSessionID(model.SessionID{0x01}) 14 | return manager 15 | } 16 | -------------------------------------------------------------------------------- /internal/tlssession/controlmsg_test.go: -------------------------------------------------------------------------------- 1 | package tlssession 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/ooni/minivpn/internal/model" 10 | ) 11 | 12 | func Test_NewTunnelInfoFromRemoteOptionsString(t *testing.T) { 13 | type args struct { 14 | remoteOpts remoteOptions 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want *model.TunnelInfo 20 | }{ 21 | { 22 | name: "get route", 23 | args: args{ 24 | remoteOptions{ 25 | "route": []string{"1.1.1.1"}, 26 | }, 27 | }, 28 | want: &model.TunnelInfo{ 29 | GW: "1.1.1.1", 30 | }, 31 | }, 32 | { 33 | name: "get route from gw", 34 | args: args{ 35 | remoteOptions{ 36 | "route-gateway": []string{"1.1.2.2"}, 37 | }, 38 | }, 39 | want: &model.TunnelInfo{ 40 | GW: "1.1.2.2", 41 | }, 42 | }, 43 | { 44 | name: "get ip", 45 | args: args{ 46 | remoteOptions{ 47 | "ifconfig": []string{"1.1.3.3", "255.255.255.0"}, 48 | }, 49 | }, 50 | want: &model.TunnelInfo{ 51 | IP: "1.1.3.3", 52 | NetMask: "255.255.255.0", 53 | }, 54 | }, 55 | { 56 | name: "get ip and route", 57 | args: args{ 58 | remoteOptions{ 59 | "ifconfig": []string{"10.0.8.1", "255.255.255.0"}, 60 | "route": []string{"1.1.3.3"}, 61 | "route-gateway": []string{"1.1.2.2"}, 62 | }, 63 | }, 64 | want: &model.TunnelInfo{ 65 | IP: "10.0.8.1", 66 | NetMask: "255.255.255.0", 67 | GW: "1.1.3.3", 68 | }, 69 | }, 70 | { 71 | name: "empty map", 72 | args: args{ 73 | remoteOpts: remoteOptions{}, 74 | }, 75 | want: &model.TunnelInfo{}, 76 | }, 77 | { 78 | name: "entries with nil value field", 79 | args: args{ 80 | remoteOpts: remoteOptions{"bad": nil}, 81 | }, 82 | want: &model.TunnelInfo{}, 83 | }, 84 | } 85 | for _, tt := range tests { 86 | t.Run(tt.name, func(t *testing.T) { 87 | diff := cmp.Diff(newTunnelInfoFromPushedOptions(tt.args.remoteOpts), tt.want) 88 | if diff != "" { 89 | t.Error(diff) 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func Test_pushedOptionsAsMap(t *testing.T) { 96 | type args struct { 97 | pushedOptions []byte 98 | } 99 | tests := []struct { 100 | name string 101 | args args 102 | want remoteOptions 103 | }{ 104 | { 105 | name: "do parse tunnel ip", 106 | args: args{[]byte("foo bar,ifconfig 10.0.0.3,")}, 107 | want: remoteOptions{ 108 | "foo": []string{"bar"}, 109 | "ifconfig": []string{"10.0.0.3"}, 110 | }, 111 | }, 112 | { 113 | name: "empty string", 114 | args: args{[]byte{}}, 115 | want: remoteOptions{}, 116 | }, 117 | } 118 | for _, tt := range tests { 119 | t.Run(tt.name, func(t *testing.T) { 120 | if diff := cmp.Diff(pushedOptionsAsMap(tt.args.pushedOptions), tt.want); diff != "" { 121 | t.Error(cmp.Diff(pushedOptionsAsMap(tt.args.pushedOptions), tt.want)) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | func Test_parseServerControlMessage(t *testing.T) { 128 | serverRespHex := "0000000002a490a20a83086e255b4d6c2a10ee9c488d683d1a1337bd4b32b24196a49c98632f00fddcab2c261cb6efae333eed9e1a7f83f3095a0da79b7a6f4709fe1ae040008856342c6465762d747970652074756e2c6c696e6b2d6d747520313535312c74756e2d6d747520313530302c70726f746f2054435076345f5345525645522c636970686572204145532d3235362d47434d2c61757468205b6e756c6c2d6469676573745d2c6b657973697a65203235362c6b65792d6d6574686f6420322c746c732d73657276657200" 129 | wantOptions := "V4,dev-type tun,link-mtu 1551,tun-mtu 1500,proto TCPv4_SERVER,cipher AES-256-GCM,auth [null-digest],keysize 256,key-method 2,tls-server" 130 | wantRandom1, _ := hex.DecodeString("a490a20a83086e255b4d6c2a10ee9c488d683d1a1337bd4b32b24196a49c9863") 131 | wantRandom2, _ := hex.DecodeString("2f00fddcab2c261cb6efae333eed9e1a7f83f3095a0da79b7a6f4709fe1ae040") 132 | 133 | msg, _ := hex.DecodeString(serverRespHex) 134 | gotKeySource, gotOptions, err := parseServerControlMessage(msg) 135 | if err != nil { 136 | t.Errorf("expected null error, got %v", err) 137 | } 138 | if wantOptions != gotOptions { 139 | t.Errorf("parseServerControlMessage(). got options = %v, want options %v", gotOptions, wantOptions) 140 | } 141 | if !bytes.Equal(wantRandom1, gotKeySource.R1[:]) { 142 | t.Errorf("parseServerControlMessage(). got R1 = %v, want %v", gotKeySource.R1, wantRandom1) 143 | } 144 | if !bytes.Equal(wantRandom2, gotKeySource.R2[:]) { 145 | t.Errorf("parseServerControlMessage(). got R2 = %v, want %v", gotKeySource.R2, wantRandom2) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/tlssession/doc.go: -------------------------------------------------------------------------------- 1 | // Package tlssession performs a TLS handshake over the control channel, and then it 2 | // exchanges keys with the server over this secure channel. 3 | package tlssession 4 | -------------------------------------------------------------------------------- /internal/tlssession/tlsbio.go: -------------------------------------------------------------------------------- 1 | package tlssession 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/ooni/minivpn/internal/model" 10 | ) 11 | 12 | // tlsBio allows to use channels to read and write 13 | type tlsBio struct { 14 | closeOnce sync.Once 15 | directionDown chan<- []byte 16 | directionUp <-chan []byte 17 | hangup chan any 18 | logger model.Logger 19 | readBuffer *bytes.Buffer 20 | } 21 | 22 | // newTLSBio creates a new tlsBio 23 | func newTLSBio(logger model.Logger, directionUp <-chan []byte, directionDown chan<- []byte) *tlsBio { 24 | return &tlsBio{ 25 | closeOnce: sync.Once{}, 26 | directionDown: directionDown, 27 | directionUp: directionUp, 28 | hangup: make(chan any), 29 | logger: logger, 30 | readBuffer: &bytes.Buffer{}, 31 | } 32 | } 33 | 34 | func (t *tlsBio) Close() error { 35 | t.closeOnce.Do(func() { 36 | close(t.hangup) 37 | }) 38 | return nil 39 | } 40 | 41 | func (t *tlsBio) Read(data []byte) (int, error) { 42 | for { 43 | count, _ := t.readBuffer.Read(data) 44 | if count > 0 { 45 | t.logger.Debugf("[tlsbio] received %d bytes", len(data)) 46 | return count, nil 47 | } 48 | select { 49 | case extra := <-t.directionUp: 50 | t.readBuffer.Write(extra) 51 | case <-t.hangup: 52 | return 0, net.ErrClosed 53 | } 54 | } 55 | } 56 | 57 | func (t *tlsBio) Write(data []byte) (int, error) { 58 | t.logger.Debugf("[tlsbio] requested to write %d bytes", len(data)) 59 | select { 60 | case t.directionDown <- data: 61 | return len(data), nil 62 | case <-t.hangup: 63 | return 0, net.ErrClosed 64 | } 65 | } 66 | 67 | func (t *tlsBio) LocalAddr() net.Addr { 68 | return &tlsBioAddr{} 69 | } 70 | 71 | func (t *tlsBio) RemoteAddr() net.Addr { 72 | return &tlsBioAddr{} 73 | } 74 | 75 | func (t *tlsBio) SetDeadline(tt time.Time) error { 76 | return nil 77 | } 78 | 79 | func (t *tlsBio) SetReadDeadline(tt time.Time) error { 80 | return nil 81 | } 82 | 83 | func (t *tlsBio) SetWriteDeadline(tt time.Time) error { 84 | return nil 85 | } 86 | 87 | // tlsBioAddr is the type of address returned by [Conn] 88 | type tlsBioAddr struct{} 89 | 90 | var _ net.Addr = &tlsBioAddr{} 91 | 92 | // Network implements net.Addr 93 | func (*tlsBioAddr) Network() string { 94 | return "tlsBioAddr" 95 | } 96 | 97 | // String implements net.Addr 98 | func (*tlsBioAddr) String() string { 99 | return "tlsBioAddr" 100 | } 101 | -------------------------------------------------------------------------------- /internal/tlssession/tlsbio_test.go: -------------------------------------------------------------------------------- 1 | package tlssession 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/apex/log" 8 | "github.com/ooni/minivpn/internal/runtimex" 9 | ) 10 | 11 | func Test_tlsBio(t *testing.T) { 12 | t.Run("can close tlsbio more than once", func(t *testing.T) { 13 | up := make(chan []byte, 10) 14 | down := make(chan []byte, 10) 15 | tls := newTLSBio(log.Log, up, down) 16 | tls.Close() 17 | tls.Close() 18 | }) 19 | 20 | t.Run("read less than in buffer", func(t *testing.T) { 21 | up := make(chan []byte, 10) 22 | down := make(chan []byte, 10) 23 | up <- []byte("abcd") 24 | tls := newTLSBio(log.Log, up, down) 25 | buf := []byte{1} 26 | n, err := tls.Read(buf) 27 | if err != nil { 28 | t.Error("expected error nil") 29 | } 30 | if n != 1 { 31 | t.Error("expected 1 byte read") 32 | } 33 | if string(buf) != "a" { 34 | t.Error("expected to read 'a'") 35 | } 36 | }) 37 | 38 | t.Run("write sends bytes down", func(t *testing.T) { 39 | up := make(chan []byte, 10) 40 | down := make(chan []byte, 10) 41 | up <- []byte("abcd") 42 | tls := newTLSBio(log.Log, up, down) 43 | buf := []byte("abcd") 44 | n, err := tls.Write(buf) 45 | if err != nil { 46 | t.Error("should not fail") 47 | } 48 | if n != 4 { 49 | t.Error("expected 4 bytes written") 50 | } 51 | got := <-down 52 | if string(got) != "abcd" { 53 | t.Errorf("did not write what expected") 54 | } 55 | }) 56 | 57 | t.Run("exercise net.Conn implementation", func(t *testing.T) { 58 | up := make(chan []byte, 10) 59 | down := make(chan []byte, 10) 60 | tls := newTLSBio(log.Log, up, down) 61 | runtimex.Assert(tls.LocalAddr().Network() == "tlsBioAddr", "bad network") 62 | runtimex.Assert(tls.LocalAddr().String() == "tlsBioAddr", "bad addr") 63 | tls.RemoteAddr() 64 | tls.SetReadDeadline(time.Now()) 65 | tls.SetWriteDeadline(time.Now()) 66 | tls.SetDeadline(time.Now()) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /internal/tun/doc.go: -------------------------------------------------------------------------------- 1 | // Package tun is the public interface for the minivpn application. It exposes a tun device interface 2 | // where the user of the application can write to and read from. 3 | package tun 4 | -------------------------------------------------------------------------------- /internal/tun/setup.go: -------------------------------------------------------------------------------- 1 | package tun 2 | 3 | import ( 4 | "github.com/ooni/minivpn/internal/controlchannel" 5 | "github.com/ooni/minivpn/internal/datachannel" 6 | "github.com/ooni/minivpn/internal/model" 7 | "github.com/ooni/minivpn/internal/networkio" 8 | "github.com/ooni/minivpn/internal/packetmuxer" 9 | "github.com/ooni/minivpn/internal/reliabletransport" 10 | "github.com/ooni/minivpn/internal/runtimex" 11 | "github.com/ooni/minivpn/internal/session" 12 | "github.com/ooni/minivpn/internal/tlssession" 13 | "github.com/ooni/minivpn/internal/workers" 14 | "github.com/ooni/minivpn/pkg/config" 15 | ) 16 | 17 | // connectChannel connects an existing channel (a "signal" in Qt terminology) 18 | // to a nil pointer to channel (a "slot" in Qt terminology). 19 | func connectChannel[T any](signal chan T, slot **chan T) { 20 | runtimex.Assert(signal != nil, "signal is nil") 21 | runtimex.Assert(slot == nil || *slot == nil, "slot or *slot aren't nil") 22 | *slot = &signal 23 | } 24 | 25 | // startWorkers starts all the workers. See the [ARCHITECTURE] 26 | // file for more information about the workers. 27 | // 28 | // [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md 29 | func startWorkers(config *config.Config, conn networkio.FramingConn, 30 | sessionManager *session.Manager, tunDevice *TUN) *workers.Manager { 31 | 32 | // create a workers manager 33 | workersManager := workers.NewManager(config.Logger()) 34 | 35 | // create the networkio service. 36 | nio := &networkio.Service{ 37 | MuxerToNetwork: make(chan []byte), 38 | NetworkToMuxer: nil, 39 | } 40 | 41 | // create the packetmuxer service. 42 | muxer := &packetmuxer.Service{ 43 | MuxerToReliable: nil, 44 | MuxerToData: nil, 45 | NotifyTLS: nil, 46 | HardReset: make(chan any, 1), 47 | DataOrControlToMuxer: make(chan *model.Packet), 48 | MuxerToNetwork: nil, 49 | NetworkToMuxer: make(chan []byte), 50 | } 51 | 52 | // connect networkio and packetmuxer 53 | connectChannel(nio.MuxerToNetwork, &muxer.MuxerToNetwork) 54 | connectChannel(muxer.NetworkToMuxer, &nio.NetworkToMuxer) 55 | 56 | // create the datachannel service. 57 | datach := &datachannel.Service{ 58 | MuxerToData: make(chan *model.Packet), 59 | DataOrControlToMuxer: nil, 60 | KeyReady: make(chan *session.DataChannelKey, 1), 61 | TUNToData: tunDevice.tunDown, 62 | DataToTUN: tunDevice.tunUp, 63 | } 64 | 65 | // connect the packetmuxer and the datachannel 66 | connectChannel(datach.MuxerToData, &muxer.MuxerToData) 67 | connectChannel(muxer.DataOrControlToMuxer, &datach.DataOrControlToMuxer) 68 | 69 | // create the reliabletransport service. 70 | rel := &reliabletransport.Service{ 71 | DataOrControlToMuxer: nil, 72 | ControlToReliable: make(chan *model.Packet), 73 | MuxerToReliable: make(chan *model.Packet), 74 | ReliableToControl: nil, 75 | } 76 | 77 | // connect reliable service and packetmuxer. 78 | connectChannel(rel.MuxerToReliable, &muxer.MuxerToReliable) 79 | connectChannel(muxer.DataOrControlToMuxer, &rel.DataOrControlToMuxer) 80 | 81 | // create the controlchannel service. 82 | ctrl := &controlchannel.Service{ 83 | NotifyTLS: nil, 84 | ControlToReliable: nil, 85 | ReliableToControl: make(chan *model.Packet), 86 | TLSRecordToControl: make(chan []byte), 87 | TLSRecordFromControl: nil, 88 | } 89 | 90 | // connect the reliable service and the controlchannel service 91 | connectChannel(rel.ControlToReliable, &ctrl.ControlToReliable) 92 | connectChannel(ctrl.ReliableToControl, &rel.ReliableToControl) 93 | 94 | // create the tlssession service 95 | tlsx := &tlssession.Service{ 96 | NotifyTLS: make(chan *model.Notification, 1), 97 | KeyUp: nil, 98 | TLSRecordUp: make(chan []byte), 99 | TLSRecordDown: nil, 100 | } 101 | 102 | // connect the tlsstate service and the controlchannel service 103 | connectChannel(tlsx.NotifyTLS, &ctrl.NotifyTLS) 104 | connectChannel(tlsx.TLSRecordUp, &ctrl.TLSRecordFromControl) 105 | connectChannel(ctrl.TLSRecordToControl, &tlsx.TLSRecordDown) 106 | 107 | // connect tlsstate service and the datachannel service 108 | connectChannel(datach.KeyReady, &tlsx.KeyUp) 109 | 110 | // connect the muxer and the tlsstate service 111 | connectChannel(tlsx.NotifyTLS, &muxer.NotifyTLS) 112 | 113 | // start all the workers 114 | nio.StartWorkers(config, workersManager, conn) 115 | muxer.StartWorkers(config, workersManager, sessionManager) 116 | rel.StartWorkers(config, workersManager, sessionManager) 117 | ctrl.StartWorkers(config, workersManager, sessionManager) 118 | datach.StartWorkers(config, workersManager, sessionManager) 119 | tlsx.StartWorkers(config, workersManager, sessionManager) 120 | 121 | // tell the packetmuxer that it should handshake ASAP 122 | muxer.HardReset <- true 123 | 124 | return workersManager 125 | } 126 | -------------------------------------------------------------------------------- /internal/tun/tundeadline.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tun 6 | 7 | // 8 | // This file adapts code from net.Pipe in the Go standard library. 9 | // 10 | 11 | import ( 12 | "sync" 13 | "time" 14 | ) 15 | 16 | // tunDeadline is an abstraction for handling timeouts. 17 | type tunDeadline struct { 18 | mu sync.Mutex // Guards timer and cancel 19 | timer *time.Timer 20 | cancel chan struct{} // Must be non-nil 21 | } 22 | 23 | func makeTUNDeadline() tunDeadline { 24 | return tunDeadline{cancel: make(chan struct{})} 25 | } 26 | 27 | // set sets the point in time when the deadline will time out. 28 | // A timeout event is signaled by closing the channel returned by waiter. 29 | // Once a timeout has occurred, the deadline can be refreshed by specifying a 30 | // t value in the future. 31 | // 32 | // A zero value for t prevents timeout. 33 | func (d *tunDeadline) set(t time.Time) { 34 | d.mu.Lock() 35 | defer d.mu.Unlock() 36 | 37 | if d.timer != nil && !d.timer.Stop() { 38 | <-d.cancel // Wait for the timer callback to finish and close cancel 39 | } 40 | d.timer = nil 41 | 42 | // Time is zero, then there is no deadline. 43 | closed := isClosedChan(d.cancel) 44 | if t.IsZero() { 45 | if closed { 46 | d.cancel = make(chan struct{}) 47 | } 48 | return 49 | } 50 | 51 | // Time in the future, setup a timer to cancel in the future. 52 | if dur := time.Until(t); dur > 0 { 53 | if closed { 54 | d.cancel = make(chan struct{}) 55 | } 56 | d.timer = time.AfterFunc(dur, func() { 57 | close(d.cancel) 58 | }) 59 | return 60 | } 61 | 62 | // Time in the past, so close immediately. 63 | if !closed { 64 | close(d.cancel) 65 | } 66 | } 67 | 68 | // wait returns a channel that is closed when the deadline is exceeded. 69 | func (d *tunDeadline) wait() chan struct{} { 70 | d.mu.Lock() 71 | defer d.mu.Unlock() 72 | return d.cancel 73 | } 74 | 75 | func isClosedChan(c <-chan struct{}) bool { 76 | select { 77 | case <-c: 78 | return true 79 | default: 80 | return false 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/vpntest/addr.go: -------------------------------------------------------------------------------- 1 | package vpntest 2 | 3 | import "net" 4 | 5 | // Addr allows mocking net.Addr. 6 | type Addr struct { 7 | MockString func() string 8 | MockNetwork func() string 9 | } 10 | 11 | var _ net.Addr = &Addr{} 12 | 13 | // String calls MockString. 14 | func (a *Addr) String() string { 15 | return a.MockString() 16 | } 17 | 18 | // Network calls MockNetwork. 19 | func (a *Addr) Network() string { 20 | return a.MockNetwork() 21 | } 22 | -------------------------------------------------------------------------------- /internal/vpntest/assert.go: -------------------------------------------------------------------------------- 1 | package vpntest 2 | 3 | import "testing" 4 | 5 | func AssertPanic(t *testing.T, f func()) { 6 | defer func() { 7 | if r := recover(); r == nil { 8 | t.Errorf("expected code to panic") 9 | } 10 | }() 11 | f() 12 | } 13 | -------------------------------------------------------------------------------- /internal/vpntest/certs.go: -------------------------------------------------------------------------------- 1 | package vpntest 2 | 3 | import "os" 4 | 5 | var pemTestingKey = []byte(`-----BEGIN PRIVATE KEY----- 6 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/vw0YScdbP2wg 7 | 3M+N6BlsCQePUVFlyLh3faPtfqKTeWfyMYhGMeUE4fMcO1H0l7b/+zfwfA85AhlT 8 | dU152AXvizBidnaQXwVxsxzLPiPxn3qH5KxD72vkMHMyUrRh/tdJzIj1bqlCiLcw 9 | SK5EDPMwuUSAIk7evRzLUdGu1JkUxi7xox03R5rvC8ZohAPSRxFAg6rajkk7HlUi 10 | BepNz5PRlPGJ0Kfn0oa/BF+5F3Y4WU+75r9tK+H691eRL65exTGrYIOZE9Rd6i8C 11 | S3WoFNmlO6tv0HMAh/GYR6/mrekOkSZdjNIbDfcNiFsvNtMIO9jztd7g/3BcQg/3 12 | eFydHplrAgMBAAECggEAM8lBnCGw+e/zIB0C4WyiEQ+PPyHTPg4r4/nG4EmnVvUf 13 | IcZG685l8B+mLSXISKsA/bm3rfeTlO4AMQ4pUpMJZ1zMQIuGEg/XxJF/YVTzGDre 14 | OP2FmQN8vDBprFmx5hWRx5i6FK9Cf3m1IBFBH5fvxmUDHygk7PteX3tFilZY0ccM 15 | TpK8nOOpbbK/8S8dC6ePXYgjamLotAnKdgKnpmxQjiprsRAWiOr7DFdjMLCUyZkC 16 | NYwRszVNX84wLOFNzFdU653gFKNcJ/8NI2MBQ5EaBMWOcxNgdfBtCXE9GwQVNzp2 17 | tjTt2QYbTdaw6LAMKgrWgaZBp0VSK4WTlYLifwrSQQKBgQD4Ah39r/l+QyTLwr6d 18 | AkMp/rgpOYzvaRzuUcZnObvi8yfFlJJ6EM4zfNICXNexdqeL+WTaSV1yuc4/rsRx 19 | nAgXklgz2UpATccLJ7JrCDsWgZm71tfUWQM5IbMgkyVixwGYiTsW+kMxFD0n2sNK 20 | sPkEgr2IiSEDfjzTf0LPr7sLyQKBgQDF7NCTTEp92FSz5OcKNSI7iH+lsVgV+U88 21 | Widc/thn/vRnyRqpvyjUvl9D9jMTz2/9DiV06lCYfN8KpknCb3jCWY5cjmOSZQTs 22 | oHQQX145Exe8cj2z+66QK6CsE1tlUC99Y684hn+eDlLMIQGMtRz8aSYb8oZo68sM 23 | hcTaP8CtkwKBgQDK0RhrrWyQWCKQS9uMFRyODFPYysq5wzE4qEFji3BeodFFoEHF 24 | d1bZ/lrUOc7evxU3wCU86kB0oQTNSYQ3EI4BkNl21V0Gh1Seh8E+DIYd2rC5T3JD 25 | ouOi5i9SFWO+itaAQsHDAbjPOyjkHeAVhfKvQKf1L4eDDsp5f5pItAJ4GQKBgDvF 26 | EwuYW1p7jMCynG7Bsu/Ffb68unwQSLRSCVcVAqcNICODYJDoUF1GjCBK5gvSdeA2 27 | eGtBI0uZUgW2R8n2vcH7J3md6kXYSc9neQVEt4CG2oEnAqkqlQGmmyO7yLrkpyK3 28 | ir+IJlvFuY05Xm1ueC1lV4PTDnH62tuSPesmm3oPAoGBANsj/l6xgcMZK6VKZHGV 29 | gG59FoMudCvMP1pITJh+TQPIJbD4TgYnDUG7z14zrYhxChWHYysVrIT35Iuu7k6S 30 | JlkPybAiLmv2nulx9fRkTzcGgvPtG3iHS/WQLvr9umWrfmQYMMW1Udr0IdflS1Sk 31 | fIeuXWkQrCE24uKSInkRupLO 32 | -----END PRIVATE KEY-----`) 33 | 34 | var pemTestingCertificate = []byte(`-----BEGIN CERTIFICATE----- 35 | MIIDjTCCAnUCFGb3X7au5DHHCSd8n6e5vG1/HGtyMA0GCSqGSIb3DQEBCwUAMIGB 36 | MQswCQYDVQQGEwJOWjELMAkGA1UECAwCTk8xEjAQBgNVBAcMCUludGVybmV0ejEN 37 | MAsGA1UECgwEQW5vbjENMAsGA1UECwwEcm9vdDESMBAGA1UEAwwJbG9jYWxob3N0 38 | MR8wHQYJKoZIhvcNAQkBFhB1c2VyQGV4YW1wbGUuY29tMB4XDTIyMDUyMDE4Mzk0 39 | N1oXDTIyMDYxOTE4Mzk0N1owgYMxCzAJBgNVBAYTAk5aMQswCQYDVQQIDAJOTzES 40 | MBAGA1UEBwwJSW50ZXJuZXR6MQ0wCwYDVQQKDARBbm9uMQ8wDQYDVQQLDAZzZXJ2 41 | ZXIxEjAQBgNVBAMMCWxvY2FsaG9zdDEfMB0GCSqGSIb3DQEJARYQdXNlckBleGFt 42 | cGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+/DRhJx1s/ 43 | bCDcz43oGWwJB49RUWXIuHd9o+1+opN5Z/IxiEYx5QTh8xw7UfSXtv/7N/B8DzkC 44 | GVN1TXnYBe+LMGJ2dpBfBXGzHMs+I/GfeofkrEPva+QwczJStGH+10nMiPVuqUKI 45 | tzBIrkQM8zC5RIAiTt69HMtR0a7UmRTGLvGjHTdHmu8LxmiEA9JHEUCDqtqOSTse 46 | VSIF6k3Pk9GU8YnQp+fShr8EX7kXdjhZT7vmv20r4fr3V5Evrl7FMatgg5kT1F3q 47 | LwJLdagU2aU7q2/QcwCH8ZhHr+at6Q6RJl2M0hsN9w2IWy820wg72PO13uD/cFxC 48 | D/d4XJ0emWsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGt+m0kwuULOVEr7QvbOI 49 | 6pxEd9AysxWxGzGBM6G9jrhlgch10wWuhDZq0LqahlWQ8DK9Kjg+pHEYYN8B1m0L 50 | 2lloFpXb+AXJR9RKsBr4iU2HdJkPIAwYlDhPUTeskfWP61JGGQC6oem3UXCbLldE 51 | VxcY3vSifP9/pIyjHVULa83FQwwsseavav3NvBgYIyglz+BLl6azMdFLXyzGzEUv 52 | iiN6MdNrJ34iDKHCYSlNvJktJY91eTsQ1GLYD6O9C5KrCJRp0ibQ1keSE7vdhnTY 53 | doKeoNOwq224DcktFdFAYnOM/q3dKxz3m8TsM5OLel4kebqDovPt0hJl2Wwwx43k 54 | 0A== 55 | -----END CERTIFICATE-----`) 56 | 57 | var pemTestingCa = []byte(`-----BEGIN CERTIFICATE----- 58 | MIID5TCCAs2gAwIBAgIUecMREJYMxFeQEWNBRSCM1x/pAEIwDQYJKoZIhvcNAQEL 59 | BQAwgYExCzAJBgNVBAYTAk5aMQswCQYDVQQIDAJOTzESMBAGA1UEBwwJSW50ZXJu 60 | ZXR6MQ0wCwYDVQQKDARBbm9uMQ0wCwYDVQQLDARyb290MRIwEAYDVQQDDAlsb2Nh 61 | bGhvc3QxHzAdBgkqhkiG9w0BCQEWEHVzZXJAZXhhbXBsZS5jb20wHhcNMjIwNTIw 62 | MTgzOTQ3WhcNMjIwNjE5MTgzOTQ3WjCBgTELMAkGA1UEBhMCTloxCzAJBgNVBAgM 63 | Ak5PMRIwEAYDVQQHDAlJbnRlcm5ldHoxDTALBgNVBAoMBEFub24xDTALBgNVBAsM 64 | BHJvb3QxEjAQBgNVBAMMCWxvY2FsaG9zdDEfMB0GCSqGSIb3DQEJARYQdXNlckBl 65 | eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMxO6abV 66 | xOy/2VuekAAvJnM2bFIpqSoWK1uMDHJc7NRWVPy2UFaDvCL2g+CSqEyqMN0NI0El 67 | J2cIAgUYOa0+wHJWQhAL60veR6ew9JfIDk3S7YNeKzUGgrRzKvTLdms5mL8fZpT+ 68 | GFwHprx58EZwg2TDQ6bGdThsSYNbx72PRngIOl5k6NWdIgd0wiAAYIpNQQUc8rDC 69 | IG4VvoitbpzYcAFCxCVGivodLP02pk2hokbidnLyTj5wIVTccA3u9FeEq2+IIAfr 70 | OW+3LjCpH9SC+3qPjA0UHv2bCLMVzIp86lUsbx6Qcoy0RPh5qC28cLk19wQj5+pw 71 | XtOeL90d2Hokf40CAwEAAaNTMFEwHQYDVR0OBBYEFNuQwyljbQs208ZCI5NFuzvo 72 | 1ez8MB8GA1UdIwQYMBaAFNuQwyljbQs208ZCI5NFuzvo1ez8MA8GA1UdEwEB/wQF 73 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHPkGlDDq79rdxFfbt0dMKm1dWZtPlZl 74 | iIY9Pcet/hgf69OKXwb4h3E0IjFW7JHwo4Bfr4mqrTQLTC1qCRNEMC9XUyc4neQy 75 | 3r2LRk+D7XAN1zwL6QPw550ukbLk4R4I1xQr+9Sap9h0QUaJj5tts6XSzhZ1AylJ 76 | HgmkOnPOpcIWm+yUMEDESGnhE8hfXR1nhb5lLrg2HIqp9qRRH1w/wc7jG3bYV3jg 77 | S5nL4GaRzx84PB1HWONlh0Wp7KBk2j6Lp0acoJwI2mHJcJoOPpaYiWWYNNTjMv2/ 78 | XXNUizTI136liavLslSMoYkjYAun+5HOux/keA1L+lm2XeG06Ew1qS4= 79 | -----END CERTIFICATE-----`) 80 | 81 | // TestingCert holds key, cert and ca to pass to tests needing to mock certificates. 82 | type TestingCert struct { 83 | Cert string 84 | Key string 85 | CA string 86 | } 87 | 88 | // WriteTEestingCerts will write valid certificates in the passed dir, and return a [TestingCert] and any error. 89 | func WriteTestingCerts(dir string) (TestingCert, error) { 90 | certFile, err := os.CreateTemp(dir, "tmpfile-") 91 | if err != nil { 92 | return TestingCert{}, err 93 | } 94 | certFile.Write(pemTestingCertificate) 95 | 96 | keyFile, err := os.CreateTemp(dir, "tmpfile-") 97 | if err != nil { 98 | return TestingCert{}, err 99 | } 100 | keyFile.Write(pemTestingKey) 101 | 102 | caFile, err := os.CreateTemp(dir, "tmpfile-") 103 | if err != nil { 104 | return TestingCert{}, err 105 | } 106 | caFile.Write(pemTestingCa) 107 | 108 | testingCert := TestingCert{ 109 | Cert: certFile.Name(), 110 | Key: keyFile.Name(), 111 | CA: caFile.Name(), 112 | } 113 | return testingCert, nil 114 | } 115 | -------------------------------------------------------------------------------- /internal/vpntest/dialer.go: -------------------------------------------------------------------------------- 1 | package vpntest 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | ) 8 | 9 | // Dialer is a mockable Dialer. 10 | type Dialer struct { 11 | MockDialContext func(ctx context.Context, network, address string) (net.Conn, error) 12 | } 13 | 14 | // DialContext calls MockDialContext. 15 | func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 16 | return d.MockDialContext(ctx, network, address) 17 | } 18 | 19 | // Conn is a mockable net.Conn. 20 | type Conn struct { 21 | MockRead func(b []byte) (int, error) 22 | MockWrite func(b []byte) (int, error) 23 | MockClose func() error 24 | MockLocalAddr func() net.Addr 25 | MockRemoteAddr func() net.Addr 26 | MockSetDeadline func(t time.Time) error 27 | MockSetReadDeadline func(t time.Time) error 28 | MockSetWriteDeadline func(t time.Time) error 29 | } 30 | 31 | // Read calls MockRead. 32 | func (c *Conn) Read(b []byte) (int, error) { 33 | return c.MockRead(b) 34 | } 35 | 36 | // Write calls MockWrite. 37 | func (c *Conn) Write(b []byte) (int, error) { 38 | return c.MockWrite(b) 39 | } 40 | 41 | // Close calls MockClose. 42 | func (c *Conn) Close() error { 43 | return c.MockClose() 44 | } 45 | 46 | // LocalAddr calls MockLocalAddr. 47 | func (c *Conn) LocalAddr() net.Addr { 48 | return c.MockLocalAddr() 49 | } 50 | 51 | // RemoteAddr calls MockRemoteAddr. 52 | func (c *Conn) RemoteAddr() net.Addr { 53 | return c.MockRemoteAddr() 54 | } 55 | 56 | // SetDeadline calls MockSetDeadline. 57 | func (c *Conn) SetDeadline(t time.Time) error { 58 | return c.MockSetDeadline(t) 59 | } 60 | 61 | // SetReadDeadline calls MockSetReadDeadline. 62 | func (c *Conn) SetReadDeadline(t time.Time) error { 63 | return c.MockSetReadDeadline(t) 64 | } 65 | 66 | // SetWriteDeadline calls MockSetWriteDeadline. 67 | func (c *Conn) SetWriteDeadline(t time.Time) error { 68 | return c.MockSetWriteDeadline(t) 69 | } 70 | 71 | var _ net.Conn = &Conn{} 72 | -------------------------------------------------------------------------------- /internal/vpntest/doc.go: -------------------------------------------------------------------------------- 1 | // Package vpntest provides utitities to facilitate testing different minivpn packages. 2 | package vpntest 3 | -------------------------------------------------------------------------------- /internal/vpntest/vpntest.go: -------------------------------------------------------------------------------- 1 | // Package vpntest provides utilities for minivpn testing. 2 | package vpntest 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/ooni/minivpn/internal/model" 12 | ) 13 | 14 | // TestPacket is used to simulate incoming packets over the network. The goal is to be able to 15 | // have a compact representation of a sequence of packets, their type, and extra properties like 16 | // inter-arrival time. 17 | type TestPacket struct { 18 | // Opcode is the OpenVPN packet opcode. 19 | Opcode model.Opcode 20 | 21 | // ID is the packet sequence 22 | ID int 23 | 24 | // ACKs is the ack array in this packet 25 | ACKs []int 26 | 27 | // IAT is the inter-arrival time until the next packet is received. 28 | IAT time.Duration 29 | } 30 | 31 | // NewTestPacketFromString constructs a new TestPacket. The input 32 | // representation for the test packet string is in the form: 33 | // "[ID] OPCODE (ack:) +42ms" 34 | // where the ack array is optional. 35 | func NewTestPacketFromString(s string) (*TestPacket, error) { 36 | parts := strings.Split(s, " +") 37 | 38 | // Extracting id, opcode and ack parts 39 | head := strings.Split(parts[0], " ") 40 | if len(head) < 2 || len(head) > 3 { 41 | return nil, fmt.Errorf("invalid format for ID-op-acks: %s", parts[0]) 42 | } 43 | 44 | id, err := strconv.Atoi(strings.Trim(head[0], "[]")) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to parse id: %v", err) 47 | } 48 | 49 | opcode, err := model.NewOpcodeFromString(head[1]) 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to parse opcode: %v", err) 52 | } 53 | 54 | acks := []int{} 55 | 56 | if len(head) == 3 { 57 | acks, err = parseACKs(strings.Trim(head[2], "()")) 58 | fmt.Println("acks:", acks) 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to parse opcode: %v", err) 61 | } 62 | } 63 | 64 | // Parsing duration part 65 | iatStr := parts[1] 66 | iat, err := time.ParseDuration(iatStr) 67 | if err != nil { 68 | return nil, fmt.Errorf("failed to parse duration: %v", err) 69 | } 70 | 71 | return &TestPacket{ID: id, Opcode: opcode, ACKs: acks, IAT: iat}, nil 72 | } 73 | 74 | var errBadACK = errors.New("wrong ack string") 75 | 76 | func parseACKs(s string) ([]int, error) { 77 | acks := []int{} 78 | h := strings.Split(s, "ack:") 79 | if len(h) != 2 { 80 | return acks, errBadACK 81 | } 82 | values := strings.Split(h[1], ",") 83 | for _, v := range values { 84 | n, err := strconv.Atoi(v) 85 | if err == nil { 86 | acks = append(acks, n) 87 | } 88 | } 89 | return acks, nil 90 | } 91 | -------------------------------------------------------------------------------- /internal/vpntest/vpntest_test.go: -------------------------------------------------------------------------------- 1 | // Package vpntest provides utilities for minivpn testing. 2 | package vpntest 3 | 4 | import ( 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/ooni/minivpn/internal/model" 10 | ) 11 | 12 | func TestNewTestPacketFromString(t *testing.T) { 13 | type args struct { 14 | s string 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want *TestPacket 20 | wantErr bool 21 | }{ 22 | { 23 | name: "parse a correct testpacket string", 24 | args: args{"[1] CONTROL_V1 +42ms"}, 25 | want: &TestPacket{ 26 | ID: 1, 27 | Opcode: model.P_CONTROL_V1, 28 | ACKs: []int{}, 29 | IAT: time.Millisecond * 42, 30 | }, 31 | wantErr: false, 32 | }, 33 | { 34 | name: "parse a testpacket with acks", 35 | args: args{"[1] CONTROL_V1 (ack:0,1) +42ms"}, 36 | want: &TestPacket{ 37 | ID: 1, 38 | Opcode: model.P_CONTROL_V1, 39 | ACKs: []int{0, 1}, 40 | IAT: time.Millisecond * 42, 41 | }, 42 | wantErr: false, 43 | }, 44 | { 45 | name: "empty acks part", 46 | args: args{"[1] CONTROL_V1 (ack:) +42ms"}, 47 | want: &TestPacket{ 48 | ID: 1, 49 | Opcode: model.P_CONTROL_V1, 50 | ACKs: []int{}, 51 | IAT: time.Millisecond * 42, 52 | }, 53 | wantErr: false, 54 | }, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | got, err := NewTestPacketFromString(tt.args.s) 59 | if (err != nil) != tt.wantErr { 60 | t.Errorf("NewTestPacketFromString() error = %v, wantErr %v", err, tt.wantErr) 61 | return 62 | } 63 | if !reflect.DeepEqual(got, tt.want) { 64 | t.Errorf("NewTestPacketFromString() = %v, want %v", got, tt.want) 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/workers/workers.go: -------------------------------------------------------------------------------- 1 | // Package workers contains code to manage workers. 2 | // 3 | // A worker is a goroutine running in the background that performs some 4 | // activity related to implementing the OpenVPN protocol. 5 | package workers 6 | 7 | import ( 8 | "errors" 9 | "sync" 10 | 11 | "github.com/ooni/minivpn/internal/model" 12 | ) 13 | 14 | // ErrShutdown is the error returned by a worker that is shutting down. 15 | var ErrShutdown = errors.New("worker is shutting down") 16 | 17 | // Manager coordinates the lifeycles of the workers implementing the OpenVPN 18 | // protocol. The zero value is invalid; use [NewManager]. 19 | type Manager struct { 20 | // logger logs events 21 | logger model.Logger 22 | 23 | // shouldShutdown is closed to signal all workers to shut down. 24 | shouldShutdown chan any 25 | 26 | // shutdownOnce ensures we close shutdownSignal once. 27 | shutdownOnce sync.Once 28 | 29 | // wg tracks the running workers. 30 | wg *sync.WaitGroup 31 | } 32 | 33 | // NewManager creates a new [*Manager]. 34 | func NewManager(logger model.Logger) *Manager { 35 | return &Manager{ 36 | logger: logger, 37 | shouldShutdown: make(chan any), 38 | shutdownOnce: sync.Once{}, 39 | wg: &sync.WaitGroup{}, 40 | } 41 | } 42 | 43 | // StartWorker starts a worker in a background goroutine. 44 | func (m *Manager) StartWorker(fx func()) { 45 | m.wg.Add(1) 46 | go fx() 47 | } 48 | 49 | // OnWorkerDone MUST be called when a worker goroutine terminates. 50 | func (m *Manager) OnWorkerDone(name string) { 51 | m.logger.Debugf("%s: worker done", name) 52 | m.wg.Done() 53 | } 54 | 55 | // StartShutdown initiates the shutdown of all workers. 56 | func (m *Manager) StartShutdown() { 57 | m.shutdownOnce.Do(func() { 58 | close(m.shouldShutdown) 59 | }) 60 | } 61 | 62 | // ShouldShutdown returns the channel closed when workers should shut down. 63 | func (m *Manager) ShouldShutdown() <-chan any { 64 | return m.shouldShutdown 65 | } 66 | 67 | // WaitWorkersShutdown blocks until all workers have shut down. 68 | func (m *Manager) WaitWorkersShutdown() { 69 | m.wg.Wait() 70 | } 71 | -------------------------------------------------------------------------------- /obfs4/common.go: -------------------------------------------------------------------------------- 1 | package obfs4 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "net/url" 8 | ) 9 | 10 | // Node is a proxy node, that can be used to construct a proxy chain. 11 | type Node struct { 12 | Addr string // ag: I'm guessing this is used like ip:port 13 | Host string // ... but then this is redundant 14 | Protocol string // obfs4 in this case 15 | url *url.URL // url 16 | Values url.Values // contains the cert and iat-mode parameters 17 | //Transport string // this only makes sense if/when we do use different transporters for obfs4. for the time being this can be removed, or perhaps denoted as "raw" 18 | } 19 | 20 | // NewNodeNewNodeFromURI returns a configured proxy node. It accepts a string with all the parameters 21 | // needed to establish a connection to the obfs4 proxy, in the form: 22 | // obfs4://:?cert=&iat-mode= 23 | func NewNodeFromURI(uri string) (Node, error) { 24 | u, err := url.Parse(uri) 25 | if err != nil { 26 | return Node{}, err 27 | } 28 | log.Printf("Using %s proxy at %s:%s", u.Scheme, u.Hostname(), u.Port()) 29 | // q, err := url.ParseQuery(u.RawQuery) 30 | // log.Println("cert:", url.QueryEscape(q["cert"][0])) 31 | 32 | if u.Scheme != "obfs4" { 33 | return Node{}, fmt.Errorf("expected obfs4:// uri") 34 | } 35 | 36 | return Node{ 37 | Protocol: u.Scheme, 38 | Addr: net.JoinHostPort(u.Hostname(), u.Port()), 39 | Host: u.Hostname(), 40 | url: u, 41 | Values: u.Query(), 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /obfs4/doc.go: -------------------------------------------------------------------------------- 1 | // Package obsf4 allows to initialize an obfs4 connection to a remote obfs4 2 | // proxy (that is configured to redirect traffic to a OpenVPN gateway belonging 3 | // to the gateway pool for a given provider). The raw vpn connection can be 4 | // then composed through the obfs4 pluggable transport. 5 | 6 | package obfs4 7 | -------------------------------------------------------------------------------- /obfs4/obfs4.go: -------------------------------------------------------------------------------- 1 | // obfs4 connection wrappers 2 | // 3 | // SPDX-License-Identifier: MIT 4 | // (c) 2015-2022 rhui zheng 5 | // (c) 2015-2022 ginuerzh and gost contributors 6 | // (c) 2022 Ain Ghazal 7 | 8 | // Code in this package is derived from: 9 | // https://github.com/ginuerzh/gost 10 | 11 | package obfs4 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "log" 17 | "net" 18 | 19 | pt "git.torproject.org/pluggable-transports/goptlib.git" 20 | 21 | "gitlab.com/yawning/obfs4.git/transports/base" 22 | "gitlab.com/yawning/obfs4.git/transports/obfs4" 23 | "golang.org/x/net/proxy" 24 | ) 25 | 26 | // The server certificate given to the client is in the following format: 27 | // obfs4://server_ip:443?cert=4UbQjIfjJEQHPOs8vs5sagrSXx1gfrDCGdVh2hpIPSKH0nklv1e4f29r7jb91VIrq4q5Jw&iat-mode=0' 28 | // be sure to urlencode the certificate you obtain from obfs4proxy or other software. 29 | 30 | type obfs4Context struct { 31 | cf base.ClientFactory 32 | cargs interface{} // type obfs4ClientArgs 33 | } 34 | 35 | var obfs4Map = make(map[string]obfs4Context) 36 | 37 | type Dialer struct { 38 | node Node 39 | } 40 | 41 | func NewDialer(node Node) *Dialer { 42 | return &Dialer{node} 43 | } 44 | 45 | func (d *Dialer) DialContext(ctx context.Context, network string, address string) (net.Conn, error) { 46 | // TODO(ainghazal): honor ctx 47 | dialFn := dialer(d.node.Addr) 48 | return dialFn(network, address) 49 | } 50 | 51 | // Obfs4ClientInit initializes the obfs4 client 52 | func Obfs4ClientInit(node Node) error { 53 | if _, ok := obfs4Map[node.Addr]; ok { 54 | return fmt.Errorf("obfs4 context already initialized") 55 | } 56 | 57 | t := new(obfs4.Transport) 58 | 59 | stateDir := node.Values.Get("state-dir") 60 | if stateDir == "" { 61 | stateDir = "." 62 | } 63 | 64 | ptArgs := pt.Args(node.Values) 65 | 66 | // we're only dealing with the client side here, we assume 67 | // server side is running obfs4proxy or the likes. in the future it would perhaps be 68 | // nice to support other obfs4-based transporters as gost is doing. 69 | cf, err := t.ClientFactory(stateDir) 70 | if err != nil { 71 | log.Println("obfs4: error on clientFactory") 72 | return err 73 | } 74 | 75 | cargs, err := cf.ParseArgs(&ptArgs) 76 | if err != nil { 77 | log.Println("error on parseArgs:", err.Error()) 78 | return err 79 | } 80 | 81 | obfs4Map[node.Addr] = obfs4Context{cf: cf, cargs: cargs} 82 | return nil 83 | } 84 | 85 | type DialFunc func(string, string) (net.Conn, error) 86 | 87 | func dialer(nodeAddr string) DialFunc { 88 | oc := obfs4Map[nodeAddr] 89 | // From the documentation of the ClientFactory interface: 90 | // https://github.com/Yawning/obfs4/blob/master/transports/base/base.go#L42 91 | // Dial creates an outbound net.Conn, and does whatever is required 92 | // (eg: handshaking) to get the connection to the point where it is 93 | // ready to relay data. 94 | // Dial(network, address string, dialFn DialFunc, args interface{}) (net.Conn, error) 95 | dialFn := proxy.Direct.Dial 96 | return func(network, address string) (net.Conn, error) { 97 | return oc.cf.Dial(network, nodeAddr, dialFn, oc.cargs) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | This folder contains public go packages. 2 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/apex/log" 7 | "github.com/ooni/minivpn/internal/model" 8 | "github.com/ooni/minivpn/internal/runtimex" 9 | ) 10 | 11 | // Config contains options to initialize the OpenVPN tunnel. 12 | type Config struct { 13 | // openVPNOptions contains options related to openvpn. 14 | openvpnOptions *OpenVPNOptions 15 | 16 | // logger will be used to log events. 17 | logger model.Logger 18 | 19 | // if a tracer is provided, it will be used to trace the openvpn handshake. 20 | tracer model.HandshakeTracer 21 | } 22 | 23 | // NewConfig returns a Config ready to intialize a vpn tunnel. 24 | func NewConfig(options ...Option) *Config { 25 | cfg := &Config{ 26 | openvpnOptions: &OpenVPNOptions{}, 27 | logger: log.Log, 28 | tracer: &model.DummyTracer{}, 29 | } 30 | for _, opt := range options { 31 | opt(cfg) 32 | } 33 | return cfg 34 | } 35 | 36 | // Option is an option you can pass to initialize minivpn. 37 | type Option func(config *Config) 38 | 39 | // WithLogger configures the passed [Logger]. 40 | func WithLogger(logger model.Logger) Option { 41 | return func(config *Config) { 42 | config.logger = logger 43 | } 44 | } 45 | 46 | // Logger returns the configured logger. 47 | func (c *Config) Logger() model.Logger { 48 | return c.logger 49 | } 50 | 51 | // WithHandshakeTracer configures the passed [HandshakeTracer]. 52 | func WithHandshakeTracer(tracer model.HandshakeTracer) Option { 53 | return func(config *Config) { 54 | config.tracer = tracer 55 | } 56 | } 57 | 58 | // Tracer returns the handshake tracer. 59 | func (c *Config) Tracer() model.HandshakeTracer { 60 | return c.tracer 61 | } 62 | 63 | // WithConfigFile configures OpenVPNOptions parsed from the given file. 64 | func WithConfigFile(configPath string) Option { 65 | return func(config *Config) { 66 | openvpnOpts, err := ReadConfigFile(configPath) 67 | runtimex.PanicOnError(err, "cannot parse config file") 68 | runtimex.PanicIfFalse(openvpnOpts.HasAuthInfo(), "missing auth info") 69 | config.openvpnOptions = openvpnOpts 70 | } 71 | } 72 | 73 | // WithOpenVPNOptions configures the passed OpenVPN options. 74 | func WithOpenVPNOptions(openvpnOptions *OpenVPNOptions) Option { 75 | return func(config *Config) { 76 | config.openvpnOptions = openvpnOptions 77 | } 78 | } 79 | 80 | // OpenVPNOptions returns the configured openvpn options. 81 | func (c *Config) OpenVPNOptions() *OpenVPNOptions { 82 | return c.openvpnOptions 83 | } 84 | 85 | // Remote has info about the OpenVPN remote, useful to pass to the external dialer. 86 | type Remote struct { 87 | // IPAddr is the IP Address for the remote. 88 | IPAddr string 89 | 90 | // Endpoint is in the form ip:port. 91 | Endpoint string 92 | 93 | // Protocol is either "tcp" or "udp" 94 | Protocol string 95 | } 96 | 97 | // Remote returns the OpenVPN remote. 98 | func (c *Config) Remote() *Remote { 99 | return &Remote{ 100 | IPAddr: c.openvpnOptions.Remote, 101 | Endpoint: net.JoinHostPort(c.openvpnOptions.Remote, c.openvpnOptions.Port), 102 | Protocol: c.openvpnOptions.Proto.String(), 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | fp "path/filepath" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/ooni/minivpn/internal/model" 10 | ) 11 | 12 | func TestNewConfig(t *testing.T) { 13 | t.Run("default constructor does not fail", func(t *testing.T) { 14 | c := NewConfig() 15 | if c.logger == nil { 16 | t.Errorf("logger should not be nil") 17 | } 18 | if c.tracer == nil { 19 | t.Errorf("tracer should not be nil") 20 | } 21 | }) 22 | t.Run("WithLogger sets the logger", func(t *testing.T) { 23 | testLogger := model.NewTestLogger() 24 | c := NewConfig(WithLogger(testLogger)) 25 | if c.Logger() != testLogger { 26 | t.Errorf("expected logger to be set to the configured one") 27 | } 28 | }) 29 | t.Run("WithTracer sets the tracer", func(t *testing.T) { 30 | testTracer := model.HandshakeTracer(model.DummyTracer{}) 31 | c := NewConfig(WithHandshakeTracer(testTracer)) 32 | if c.Tracer() != testTracer { 33 | t.Errorf("expected tracer to be set to the configured one") 34 | } 35 | }) 36 | 37 | t.Run("WithConfigFile sets OpenVPNOptions after parsing the configured file", func(t *testing.T) { 38 | configFile := writeValidConfigFile(t.TempDir()) 39 | c := NewConfig(WithConfigFile(configFile)) 40 | opts := c.OpenVPNOptions() 41 | if opts.Proto.String() != "udp" { 42 | t.Error("expected proto udp") 43 | } 44 | wantRemote := &Remote{ 45 | IPAddr: "2.3.4.5", 46 | Endpoint: "2.3.4.5:1194", 47 | Protocol: "udp", 48 | } 49 | if diff := cmp.Diff(c.Remote(), wantRemote); diff != "" { 50 | t.Error(diff) 51 | } 52 | }) 53 | 54 | } 55 | 56 | var sampleConfigFile = ` 57 | remote 2.3.4.5 1194 58 | proto udp 59 | cipher AES-256-GCM 60 | auth SHA512 61 | ca ca.crt 62 | cert cert.pem 63 | key cert.pem 64 | ` 65 | 66 | func writeValidConfigFile(dir string) string { 67 | cfg := fp.Join(dir, "config") 68 | os.WriteFile(cfg, []byte(sampleConfigFile), 0600) 69 | os.WriteFile(fp.Join(dir, "ca.crt"), []byte("dummy"), 0600) 70 | os.WriteFile(fp.Join(dir, "cert.pem"), []byte("dummy"), 0600) 71 | os.WriteFile(fp.Join(dir, "key.pem"), []byte("dummy"), 0600) 72 | return cfg 73 | } 74 | -------------------------------------------------------------------------------- /pkg/tracex/trace_test.go: -------------------------------------------------------------------------------- 1 | // Package tracex implements a handshake tracer that can be passed to the TUN constructor to 2 | // observe handshake events. 3 | package tracex 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/ooni/minivpn/internal/model" 9 | ) 10 | 11 | func Test_maybeAddTagsFromPacket(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | packetPayload []byte 15 | expectedTags []string 16 | }{ 17 | { 18 | name: "Empty payload", 19 | packetPayload: []byte{}, 20 | expectedTags: []string{}, 21 | }, 22 | { 23 | name: "Payload too short", 24 | packetPayload: []byte{0x16, 0x00, 0x00, 0x00, 0x00}, 25 | expectedTags: []string{}, 26 | }, 27 | { 28 | name: "Client Hello", 29 | packetPayload: []byte{0x16, 0x00, 0x00, 0x00, 0x00, 0x01}, 30 | expectedTags: []string{"client_hello"}, 31 | }, 32 | { 33 | name: "Server Hello", 34 | packetPayload: []byte{0x16, 0x00, 0x00, 0x00, 0x00, 0x02}, 35 | expectedTags: []string{"server_hello"}, 36 | }, 37 | { 38 | name: "No tag matching", 39 | packetPayload: []byte{0x17, 0x00, 0x00, 0x00, 0x00, 0x01}, 40 | expectedTags: []string{}, 41 | }, 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | event := &Event{Tags: []string{}} 47 | packet := &model.Packet{Payload: tt.packetPayload} 48 | 49 | maybeAddTagsFromPacket(event, packet) 50 | 51 | // Check if tags are as expected 52 | if len(event.Tags) != len(tt.expectedTags) { 53 | t.Fatalf("Expected %v tags, but got %v", len(tt.expectedTags), len(event.Tags)) 54 | } 55 | 56 | for i, tag := range tt.expectedTags { 57 | if event.Tags[i] != tag { 58 | t.Errorf("Expected tag %v, but got %v", tag, event.Tags[i]) 59 | } 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/tunnel/tunnel.go: -------------------------------------------------------------------------------- 1 | // Package tunnel contains the public tunnel API. 2 | package tunnel 3 | 4 | import ( 5 | "context" 6 | "net" 7 | 8 | "github.com/apex/log" 9 | "github.com/ooni/minivpn/internal/networkio" 10 | "github.com/ooni/minivpn/internal/tun" 11 | "github.com/ooni/minivpn/pkg/config" 12 | ) 13 | 14 | // SimpleDialer establishes network connections. 15 | type SimpleDialer interface { 16 | DialContext(ctx context.Context, network, endpoint string) (net.Conn, error) 17 | } 18 | 19 | // We're creating a type alias to expose the internal TUN implementation on the public API. 20 | type TUN = tun.TUN 21 | 22 | // Start starts a VPN tunnel initialized with the passed dialer and config, and returns a TUN device 23 | // that can later be stopped. In case there was any error during the initialization of the tunnel, 24 | // they will also be returned by this function. 25 | func Start(ctx context.Context, underlyingDialer SimpleDialer, cfg *config.Config) (*TUN, error) { 26 | dialer := networkio.NewDialer(cfg.Logger(), underlyingDialer) 27 | conn, err := dialer.DialContext(ctx, cfg.Remote().Protocol, cfg.Remote().Endpoint) 28 | if err != nil { 29 | log.WithError(err).Error("dialer.DialContext") 30 | return nil, err 31 | } 32 | return tun.StartTUN(ctx, conn, cfg) 33 | } 34 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Bootstrapping a provider 2 | 3 | From the parent folder: 4 | 5 | ``` 6 | make bootstrap 7 | make test 8 | ``` 9 | -------------------------------------------------------------------------------- /scripts/bootstrap-provider: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import urllib.request 6 | 7 | PROVIDERS=("calyx", "riseup") 8 | 9 | APIS = { 10 | "calyx": "https://api.vpn.calyx.dev/", 11 | "riseup": "https://api.float.hexacab.org/" 12 | } 13 | 14 | CAURI = "ca.crt" 15 | CERTURI = "3/cert" 16 | 17 | IPS = { 18 | "calyx": "185.220.103.44", 19 | "riseup": "204.13.164.252" 20 | } 21 | 22 | def getConfig(p): 23 | ip = IPS.get(p) 24 | return f"""remote {ip} 1194 25 | proto udp 26 | cipher AES-256-GCM 27 | auth SHA512 28 | ca ca.crt 29 | cert cert.pem 30 | key cert.pem 31 | """ 32 | 33 | def check_args(): 34 | if len(sys.argv) != 2: 35 | print("Usage: bootstrap-provider ") 36 | sys.exit(1) 37 | if sys.argv[1] not in PROVIDERS: 38 | print("Invalid provider") 39 | sys.exit(1) 40 | 41 | def getPath(provider): 42 | return os.path.join(os.getcwd(), "data", provider) 43 | 44 | 45 | def downloadFile(uri, path): 46 | with urllib.request.urlopen(uri) as resp, open(path, 'wb') as out: 47 | data = resp.read() 48 | out.write(data) 49 | 50 | 51 | def fetchCa(p): 52 | if not os.path.isfile(path:=os.path.join(getPath(p), "ca.crt")): 53 | downloadFile(APIS[p] + CAURI, path) 54 | 55 | 56 | def fetchCert(p): 57 | if not os.path.isfile(path:=os.path.join(getPath(p), "cert.pem")): 58 | downloadFile(APIS[p] + CERTURI, path) 59 | 60 | 61 | def writeConfig(p): 62 | path = os.path.join(getPath(p), "config") 63 | config = getConfig(p) 64 | with open(path, 'wb') as out: 65 | out.write(bytes(config, 'utf-8')) 66 | 67 | 68 | if __name__ == "__main__": 69 | check_args() 70 | p = sys.argv[1] 71 | print("[+] Bootstrapping provider:", p) 72 | os.makedirs(getPath(p), exist_ok=True) 73 | fetchCa(p) 74 | fetchCert(p) 75 | writeConfig(p) 76 | print("[+] Done") 77 | -------------------------------------------------------------------------------- /scripts/get_trace_from_pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Parse an OpenVPN handshake pcap file, extract relevant fields from the json, 4 | # and return a compact representation of the packets in the handshake that can be 5 | # used for testing minivpn's implementation. 6 | # 7 | # This script depends on tshark. 8 | # 9 | # Usage: 10 | # 11 | # There are two subcommands: json | sequence 12 | # - json dumps the json representation of a subset of the handshake 13 | # - sequence outputs a test sequence that can be used to write unit tests. 14 | # 15 | # Examples: 16 | # 17 | # python3 get_trace_from_pcap.py good-handshake.pcapng json | jq 18 | # python3 get_trace_from_pcap.py good-handshake.pcapng sequence | nl 19 | # 20 | 21 | import json 22 | import subprocess 23 | import sys 24 | 25 | opcodes = { 26 | '0x01': 'CONTROL_HARD_RESET_CLIENT_V1', 27 | '0x02': 'CONTROL_HARD_RESET_SERVER_V1', 28 | '0x03': 'CONTROL_SOFT_RESET_V1', 29 | '0x04': 'CONTROL_V1', 30 | '0x05': 'ACK_V1', 31 | '0x06': 'DATA_V1', 32 | '0x07': 'CONTROL_HARD_RESET_CLIENT_V2', 33 | '0x08': 'CONTROL_HARD_RESET_SERVER_V2', 34 | '0x09': 'DATA_V2' 35 | } 36 | 37 | def process_tshark_output(data): 38 | packets = [] 39 | ips = {} 40 | 41 | for packet in data: 42 | ip = packet['_source']['layers']['ip'] 43 | udp = packet['_source']['layers']['udp'] 44 | 45 | # TODO(ainghazal): do sanity check here and verify all of them belong to the same UDP stream. 46 | 47 | openvpn = packet['_source']['layers']['openvpn'] 48 | 49 | time_relative = udp['Timestamps']['udp.time_relative'] 50 | time_delta = udp['Timestamps']['udp.time_delta'] 51 | 52 | ip_src = ip['ip.src'] 53 | if len(ips) == 0: 54 | ips[ip_src] = 'client' 55 | 56 | ip_dst = ip['ip.dst'] 57 | if len(ips) == 1: 58 | ips[ip_dst] = 'server' 59 | 60 | packets.append({ 61 | 'time_relative': time_relative, 62 | 'time_delta': time_delta, 63 | 'from': ips[ip_src], 64 | 'to': ips[ip_dst], 65 | 'openvpn': openvpn, 66 | }) 67 | 68 | return packets 69 | 70 | 71 | def sequence_from_packets(packets): 72 | for i, packet in enumerate(packets): 73 | if packet['from'] == 'client': 74 | dir = '>' 75 | else: 76 | dir = '<' 77 | 78 | packet_id = packet['openvpn'].get('openvpn.mpid', 0) 79 | opcode = opcodes[packet['openvpn']['openvpn.type_tree']['openvpn.opcode']] 80 | 81 | acks = [] 82 | ack_len = packet['openvpn'].get('openvpn.mpidarraylength') 83 | if ack_len is not None and int(ack_len) != 0: 84 | acks = packet['openvpn']['Packet-ID Array']['openvpn.mpidarrayelement'] 85 | 86 | if len(acks) > 0: 87 | ack_str = ','.join([str(ack) for ack in acks]) 88 | else: 89 | ack_str = '' 90 | 91 | try: 92 | # get the inter-arrival time until the next packet in the 93 | # handshake arrives. in the unit tests, we specify this as IAT 94 | # for a TestPacket, since we want the packet writer to sleep 95 | # for this amount of time. 96 | next_packet_ts = float(packets[i+1].get('time_delta')) * 1000 97 | except IndexError: 98 | next_packet_ts = 0 99 | 100 | print(f"{dir} [{packet_id}] {opcode} (acks:{ack_str}) +{next_packet_ts:.8f}ms") 101 | 102 | 103 | if __name__ == "__main__": 104 | pcap = sys.argv[1] 105 | subcmd = sys.argv[2] 106 | 107 | command = f"tshark -r {pcap} -T json --no-duplicate-keys" 108 | out = subprocess.check_output(command, shell=True) 109 | output_str = out.decode('utf-8') 110 | data = json.loads(output_str) 111 | packets = process_tshark_output(data) 112 | 113 | if subcmd == "json": 114 | print(json.dumps(packets)) 115 | sys.exit(0) 116 | 117 | if subcmd == "sequence": 118 | sequence_from_packets(packets) 119 | sys.exit(0) 120 | -------------------------------------------------------------------------------- /scripts/go-coverage-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Ref: 4 | # - https://pretzelhands.com/posts/command-line-flags 5 | 6 | # Usage: 7 | # go test -race -v -coverprofile=coverage.out 8 | # ./go-coverage-check.sh coverage.out 70 9 | 10 | 11 | PROFILE=$1 12 | THRESHOLD=$2 13 | COVERAGE=$(go tool cover -func=$PROFILE | grep total|awk '{print substr($3, 1, length($3) - 1)}') 14 | echo "$COVERAGE $THRESHOLD" | awk '{if (!($1 >= $2)) { print "Coverage: " $1 "%" ", Expected threshold: " $2 "%"; exit 1 } }' 15 | -------------------------------------------------------------------------------- /scripts/install-filternet: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | go install github.com/bassosimone/monorepo/tools/filternet@latest 3 | -------------------------------------------------------------------------------- /scripts/riseup-ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl 3 | dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE 4 | AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw 5 | NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM 6 | Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv 7 | b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m 8 | TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a 9 | 7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE 10 | LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY 11 | iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK 12 | 5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx 13 | HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58 14 | m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF 15 | PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q 16 | hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj 17 | shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k 18 | ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu 19 | f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD 20 | VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB 21 | AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v 22 | qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/ 23 | 3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ 24 | 4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7 25 | 3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch 26 | Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf 27 | Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg 28 | tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF 29 | tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ 30 | UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp 31 | 0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO 32 | -----END CERTIFICATE----- 33 | -----BEGIN CERTIFICATE----- 34 | MIIBYjCCAQigAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxMRUFQIFJv 35 | b3QgQ0EwHhcNMjExMTAyMTkwNTM3WhcNMjYxMTAyMTkxMDM3WjAXMRUwEwYDVQQD 36 | EwxMRUFQIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQxOXBGu+gf 37 | pjHzVteGTWL6XnFxtEnKMFpKaJkA/VOHmESzoLsZRQxt88GssxaqC01J17idQiqv 38 | zgNpedmtvFtyo0UwQzAOBgNVHQ8BAf8EBAMCAqQwEgYDVR0TAQH/BAgwBgEB/wIB 39 | ATAdBgNVHQ4EFgQUZdoUlJrCIUNFrpffAq+LQjnwEz4wCgYIKoZIzj0EAwIDSAAw 40 | RQIgfr3w4tnRG+NdI3LsGPlsRktGK20xHTzsB3orB0yC6cICIQCB+/9y8nmSStfN 41 | VUMUyk2hNd7/kC8nL222TTD7VZUtsg== 42 | -----END CERTIFICATE----- 43 | -------------------------------------------------------------------------------- /scripts/strangelove/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # strangelove: using DPI for the common good. 3 | # 4 | # This file, for now, is just a bunch of snippets using the super-awesome netfilter module for nDPI. 5 | # thanks to vmon for some documentation of what's possible: 6 | # https://internetfreedomfestival.org/wiki/index.php/Playing_cat_and_mouse_with_Deep_Packet_Inspection 7 | # 8 | # and to vel21ripn for maintaining an up-to-date fork: https://github.com/vel21ripn/nDPI 9 | # 10 | # In the future this could be integrated into filternet/jafar. 11 | # Before doing that, working on a dkms version of xt_ndpi is probably a good idea. 12 | # 13 | VPNBLOCK=-m ndpi --proto openvpn 14 | LIMIT=-m limit --limit 15 | 5_SEC=5/sec 16 | 10_SEC=10/sec 17 | PACKETLIMIT=-m connbytes --connbytes 20:20 --connbytes-dir both --connbytes-mode packets 18 | 19 | throttle-5: 20 | # throttles at 5 packets/s 21 | iptables -I OUTPUT ${VPNBLOCK} -j DROP 22 | iptables -I OUTPUT ${VPNBLOCK} ${LIMIT} ${5_SEC} -m state --state ESTABLISHED -j ACCEPT 23 | 24 | throttle-10: 25 | # throttles at 10 packets/s 26 | iptables -I OUTPUT ${VPNBLOCK} -j DROP 27 | iptables -I OUTPUT ${VPNBLOCK} ${LIMIT} ${10_SEC} -m state --state ESTABLISHED -j ACCEPT 28 | 29 | packetlimit: 30 | # not sure what this is doing, seems a rate? 31 | iptables -A OUTPUT ${VPNBLOCK} ${PACKETLIMIT} -j DROP 32 | 33 | quota: 34 | # quota: ~2.7 MB 35 | iptables -A OUTPUT ${VPNBLOCK} -m quota --quota 200000 -j ACCEPT -c 0 0 36 | iptables -A OUTPUT ${VPNBLOCK} -m quota --quota 200000 -j LOG --log-prefix "quota-over: " --log-level 4 37 | iptables -A OUTPUT ${VPNBLOCK} -j DROP 38 | 39 | stopwatch: 40 | # drop connection after 10 seconds 41 | iptables -A OUTPUT ${VPNBLOCK} -m state --state NEW -m recent --set 42 | iptables -A OUTPUT ${VPNBLOCK} -m state --state ESTABLISHED -m recent --rcheck --seconds 10 -j ACCEPT 43 | iptables -A OUTPUT ${VPNBLOCK} -m state --state ESTABLISHED -j LOG 44 | iptables -A OUTPUT ${VPNBLOCK} -m state --state ESTABLISHED -j DROP 45 | 46 | clean: 47 | @echo "[+] Flushing all rules" 48 | @iptables -F 49 | -------------------------------------------------------------------------------- /scripts/watchjson: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | tail -f "$1" | jq 3 | -------------------------------------------------------------------------------- /tests/integration/env-aes-256-cbc-sha256.env: -------------------------------------------------------------------------------- 1 | OPENVPN_CIPHER=AES-256-CBC 2 | OPENVPN_AUTH=SHA256 3 | -------------------------------------------------------------------------------- /tests/integration/env-aes-256-gcm-sha256.env: -------------------------------------------------------------------------------- 1 | OPENVPN_CIPHER=AES-256-GCM 2 | OPENVPN_AUTH=SHA256 3 | -------------------------------------------------------------------------------- /tests/integration/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/apex/log" 14 | "github.com/ory/dockertest/v3" 15 | dc "github.com/ory/dockertest/v3/docker" 16 | 17 | "github.com/ooni/minivpn/extras/ping" 18 | "github.com/ooni/minivpn/internal/networkio" 19 | "github.com/ooni/minivpn/internal/tun" 20 | "github.com/ooni/minivpn/pkg/config" 21 | ) 22 | 23 | const ( 24 | dockerImage = "ainghazal/openvpn" 25 | dockerTag = "latest" 26 | ) 27 | 28 | var ( 29 | target = "172.17.0.1" 30 | count = 3 31 | ) 32 | 33 | func copyFile(src, dst string) error { 34 | input, err := os.ReadFile(src) 35 | if err != nil { 36 | fmt.Println(err) 37 | return nil 38 | } 39 | 40 | dstFile := filepath.Join(dst, src) 41 | err = os.WriteFile(dstFile, input, 0744) 42 | if err != nil { 43 | fmt.Println("Error creating", dstFile) 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | func launchDocker(cipher, auth string) ([]byte, *dockertest.Pool, *dockertest.Resource, error) { 50 | pool, err := dockertest.NewPool("") 51 | if err != nil { 52 | log.Fatalf("Could not connect to docker: %s", err) 53 | } 54 | 55 | options := &dockertest.RunOptions{ 56 | Repository: dockerImage, 57 | Tag: dockerTag, 58 | PortBindings: map[dc.Port][]dc.PortBinding{ 59 | "1194/udp": {{HostPort: "1194"}}, 60 | "8080/tcp": {{HostPort: "8080"}}, 61 | }, 62 | Env: []string{"OPENVPN_CIPHER=" + cipher, "OPENVPN_AUTH=" + auth}, 63 | CapAdd: []string{"NET_ADMIN"}, 64 | } 65 | resource, err := pool.RunWithOptions(options) 66 | if err != nil { 67 | log.Fatalf("Could not start resource: %s", err) 68 | } 69 | // exponential backoff-retry, because the application in the container might not be ready to accept connections yet 70 | // the minio client does not do service discovery for you (i.e. it does not check if connection can be established), so we have to use the health check 71 | var config []byte 72 | if err := pool.Retry(func() error { 73 | url := "http://localhost:8080/" 74 | resp, err := http.Get(url) 75 | if err != nil { 76 | return err 77 | } 78 | if resp.StatusCode != http.StatusOK { 79 | return fmt.Errorf("status code not OK") 80 | } 81 | config, err = io.ReadAll(resp.Body) 82 | if err != nil { 83 | panic(err) 84 | } 85 | fmt.Println("Got OpenVPN client config") 86 | return nil 87 | }); err != nil { 88 | log.Fatalf("Could not connect to docker: %s", err) 89 | } 90 | return config, pool, resource, nil 91 | } 92 | 93 | func stopContainer(p *dockertest.Pool, res *dockertest.Resource) { 94 | fmt.Println("[!] Stopping container") 95 | if err := p.Purge(res); err != nil { 96 | log.Warnf("Could not purge resource: %s\n", err) 97 | } 98 | } 99 | 100 | func readLines(f string) ([]string, error) { 101 | var ll []string 102 | rf, err := os.Open(f) 103 | if err != nil { 104 | return ll, err 105 | } 106 | defer rf.Close() 107 | fs := bufio.NewScanner(rf) 108 | fs.Split(bufio.ScanLines) 109 | for fs.Scan() { 110 | ll = append(ll, fs.Text()) 111 | } 112 | return ll, nil 113 | } 114 | 115 | // This main function exercises AES256GCM 116 | func main() { 117 | tmp, err := os.MkdirTemp("", "minivpn-integration-test") 118 | defer os.RemoveAll(tmp) // clean up 119 | 120 | fmt.Println("launching docker") 121 | configData, pool, resource, err := launchDocker("AES-256-GCM", "SHA256") 122 | if err != nil { 123 | log.WithError(err).Fatal("cannot start docker") 124 | } 125 | // when all test done, time to kill and remove the container 126 | defer stopContainer(pool, resource) 127 | 128 | cfgFile, err := os.CreateTemp(tmp, "minivpn-e2e-") 129 | if err != nil { 130 | log.WithError(err).Fatal("Cannot create temporary file") 131 | } 132 | defer cfgFile.Close() 133 | fmt.Println("Config written to: " + cfgFile.Name()) 134 | 135 | if _, err = cfgFile.Write(configData); err != nil { 136 | log.WithError(err).Fatal("Failed to write config to temporary file") 137 | } 138 | 139 | // actual test begins 140 | vpnConfig := config.NewConfig(config.WithConfigFile(cfgFile.Name())) 141 | 142 | dialer := networkio.NewDialer(log.Log, &net.Dialer{}) 143 | conn, err := dialer.DialContext(context.TODO(), vpnConfig.Remote().Protocol, vpnConfig.Remote().Endpoint) 144 | if err != nil { 145 | log.WithError(err).Fatal("dial error") 146 | } 147 | 148 | tunnel, err := tun.StartTUN(context.Background(), conn, vpnConfig) 149 | if err != nil { 150 | log.WithError(err).Fatal("cannot start tunnel") 151 | } 152 | 153 | pinger := ping.New(target, tunnel) 154 | pinger.Count = count 155 | err = pinger.Run(context.Background()) 156 | defer pinger.Stop() 157 | if err != nil { 158 | log.WithError(err).Fatalf("VPN Error") 159 | } 160 | if pinger.PacketLoss() != 0 { 161 | log.Fatalf("packet loss is not zero") 162 | } 163 | // let's assert something wise about the pings 164 | // can we parse the logs? get initialization etc 165 | } 166 | -------------------------------------------------------------------------------- /tests/integration/run-server-cbc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker run --cap-add=NET_ADMIN \ 3 | -p 1194:1194/udp -p 8080:8080/tcp \ 4 | --rm \ 5 | --env-file=env-aes-256-cbc-sha256.env \ 6 | --name=ovpn1 \ 7 | ainghazal/openvpn 8 | -------------------------------------------------------------------------------- /tests/integration/run-server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker run --cap-add=NET_ADMIN \ 3 | -p 1194:1194/udp -p 8080:8080/tcp \ 4 | --rm \ 5 | --env-file=env-aes-256-gcm-sha256.env \ 6 | --name=ovpn1 \ 7 | ainghazal/openvpn 8 | -------------------------------------------------------------------------------- /tests/integration/wrap_integration_cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | COVDATA=../../coverage/int 4 | 5 | # 6 | # Setup 7 | # 8 | rm -rf $COVDATA 9 | mkdir -p $COVDATA 10 | 11 | # 12 | # Pass in "-cover" to the script to build for coverage, then 13 | # run with GOCOVERDIR set. 14 | # 15 | go build -cover . 16 | GOCOVERDIR=$COVDATA ./integration 17 | 18 | # 19 | # Post-process the resulting profiles. 20 | # 21 | go tool covdata percent -i=$COVDATA 22 | 23 | # 24 | # Remove the binary 25 | # 26 | rm ./integration 27 | -------------------------------------------------------------------------------- /tests/qa/run-filternet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "Running minivpn with filternet" 3 | 4 | USER=`whoami` 5 | 6 | REMOTE_DOCKER=172.17.0.2 # can grep it from the config file instead 7 | REMOTE=185.220.103.44 8 | TARGET=`ip -4 addr show docker0 | grep 'inet ' | awk '{print $2}' | cut -f 1 -d /` 9 | FILTERNET=`which filternet` 10 | TIMEOUT=5 11 | 12 | #DROP_RULE="-d ${REMOTE} -p udp --dport 1194 -j DROP" 13 | DROP_RULE="-p udp --dport 1194 -j DROP" 14 | 15 | remote_block_all() { 16 | echo "Using drop rule:" $DROP_RULE 17 | sudo ${FILTERNET} \ 18 | --firewall-rule "${DROP_RULE}" \ 19 | --user $USER \ 20 | --workdir ../.. TIMEOUT=${TIMEOUT} make test-ping 21 | # TODO it would be nice to store the exit code somewhere, and check for it 22 | if [ "$?" -ne "1" ]; then 23 | echo "[!] remote-block-all ==> test failed (expected exit code: 2)" 24 | exit 1 25 | else 26 | echo "[+] remote-block-all ==> test ok." 27 | exit 0 28 | fi 29 | } 30 | 31 | local_block_all() { 32 | echo "Using drop rule:" $DROP_RULE 33 | sudo ${FILTERNET} \ 34 | --firewall-rule "${DROP_RULE}" \ 35 | --user $USER \ 36 | --workdir ../.. MINIVPN_HANDSHAKE_TIMEOUT=${TIMEOUT} make test-local 37 | echo "exit code:" $? 38 | [ "$?" -eq 1 ] && echo "Test OK" && exit 0 39 | exit 1 40 | } 41 | 42 | OPTION=$1 43 | case $OPTION in 44 | remote-block-all) 45 | remote_block_all 46 | ;; 47 | local-block-all) 48 | local_block_all 49 | ;; 50 | 51 | *) 52 | echo Unknown test scenario: $OPTION 53 | exit 1 54 | 55 | esac 56 | 57 | # --workdir ../.. make test-local 58 | # --workdir ../.. make qa 59 | --------------------------------------------------------------------------------