diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..28afb69c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = true + +[*.py] +indent_style = space + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.java] +indent_style = space diff --git a/.gitignore b/.gitignore index 90c93439..6b0e6c86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -minivpn -./obfs4vpn -./vpnping -./geturl -ndt7 +/minivpn +/vpnping +/obfs4vpn +/geturl +/ndt7 +.vscode *.swp *.swo *.pem diff --git a/go.mod b/go.mod index 266b6af7..3b12fd46 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ooni/minivpn -go 1.18 +go 1.20 // pinning for backwards-incompatible change // replace gitlab.com/yawning/obfs4.git v0.0.0-20220204003609-77af0cba934d => gitlab.com/yawning/obfs4.git v0.0.0-20210511220700-e330d1b7024b @@ -19,15 +19,15 @@ require ( github.com/pborman/getopt/v2 v2.1.0 github.com/refraction-networking/utls v1.3.1 gitlab.com/yawning/obfs4.git v0.0.0-20220904064028-336a71d6e4cf - golang.org/x/net v0.8.0 - golang.org/x/sync v0.1.0 - golang.zx2c4.com/wireguard v0.0.0-20230313165553-0ad14a89f5f9 + golang.org/x/net v0.17.0 + golang.org/x/sync v0.4.0 + golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 ) require ( filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20 // indirect github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect - github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 // indirect @@ -39,7 +39,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/btree v1.0.1 // indirect + github.com/google/btree v1.1.2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/klauspost/compress v1.15.15 // indirect @@ -53,15 +53,18 @@ require ( github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.14.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - gopkg.in/yaml.v2 v2.3.0 // indirect - gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect ) diff --git a/go.sum b/go.sum index 1ae4137a..34049e97 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,8 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -148,8 +148,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -298,9 +298,9 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= @@ -311,7 +311,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= @@ -348,8 +350,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -380,6 +382,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -405,8 +409,8 @@ golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -420,8 +424,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -462,8 +466,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -472,8 +477,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -512,14 +518,16 @@ golang.org/x/tools v0.0.0-20200409170454-77362c5149f0/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200422205258-72e4a01eba43/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20230313165553-0ad14a89f5f9 h1:33IsKfBTQLVYccOgWKaE7X+lVqZVN9EjcDSNfI10Lmc= -golang.zx2c4.com/wireguard v0.0.0-20230313165553-0ad14a89f5f9/go.mod h1:KNrjddgin1zD9sfQawwoXCUwWboceZH78ASVHkXu6GM= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -584,8 +592,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -601,15 +609,16 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY= -gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/bytesx/bytesx.go b/internal/bytesx/bytesx.go new file mode 100644 index 00000000..fbaa75d6 --- /dev/null +++ b/internal/bytesx/bytesx.go @@ -0,0 +1,157 @@ +// Package bytesx provides functions operating on bytes. +// +// Specifically we implement these operations: +// +// 1. generating random bytes; +// +// 2. OpenVPN options encoding and decoding; +// +// 3. PKCS#7 padding and unpadding. +package bytesx + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + + "github.com/ooni/minivpn/internal/runtimex" +) + +var ( + // ErrEncodeOption indicates an option encoding error occurred. + ErrEncodeOption = errors.New("can't encode option") + + // ErrDecodeOption indicates an option decoding error occurred. + ErrDecodeOption = errors.New("can't decode option") + + // ErrPaddingPKCS7 indicates that a PKCS#7 padding error has occurred. + ErrPaddingPKCS7 = errors.New("PKCS#7 padding error") + + // ErrUnpaddingPKCS7 indicates that a PKCS#7 unpadding error has occurred. + ErrUnpaddingPKCS7 = errors.New("PKCS#7 unpadding error") +) + +// genRandomBytes returns an array of bytes with the given size using +// a CSRNG, on success, or an error, in case of failure. +func GenRandomBytes(size int) ([]byte, error) { + b := make([]byte, size) + _, err := rand.Read(b) + return b, err +} + +// EncodeOptionStringToBytes is used to encode the options string, username and password. +// +// According to the OpenVPN protocol, options are represented as a two-byte word, +// plus the byte representation of the string, null-terminated. +// +// See https://openvpn.net/community-resources/openvpn-protocol/. +// +// This function returns errEncodeOption in case of failure. +func EncodeOptionStringToBytes(s string) ([]byte, error) { + if len(s) >= math.MaxUint16 { // Using >= b/c we need to account for the final \0 + return nil, fmt.Errorf("%w:%s", ErrEncodeOption, "string too large") + } + data := make([]byte, 2) + binary.BigEndian.PutUint16(data, uint16(len(s))+1) + data = append(data, []byte(s)...) + data = append(data, 0x00) + return data, nil +} + +// DecodeOptionStringFromBytes returns the string-value for the null-terminated string +// returned by the server when sending remote options to us. +// +// This function returns errDecodeOption on failure. +func DecodeOptionStringFromBytes(b []byte) (string, error) { + if len(b) < 2 { + return "", fmt.Errorf("%w: expected at least two bytes", ErrDecodeOption) + } + length := int(binary.BigEndian.Uint16(b[:2])) + b = b[2:] // skip over the length + // the server sends padding, so we cannot do a strict check + if len(b) < length { + return "", fmt.Errorf("%w: got %d, expected %d", ErrDecodeOption, len(b), length) + } + if len(b) <= 0 || length == 0 { + return "", fmt.Errorf("%w: zero length encoded option is not possible: %s", ErrDecodeOption, + "we need at least one byte for the trailing \\0") + } + if b[length-1] != 0x00 { + return "", fmt.Errorf("%w: missing trailing \\0", ErrDecodeOption) + } + return string(b[:len(b)-1]), nil +} + +// BytesUnpadPKCS7 performs the PKCS#7 unpadding of a byte array. +func BytesUnpadPKCS7(b []byte, blockSize int) ([]byte, error) { + // 1. check whether we can unpad at all + if blockSize > math.MaxUint8 { + return nil, fmt.Errorf("%w: blockSize too large", ErrUnpaddingPKCS7) + } + // 2. trivial case + if len(b) <= 0 { + return nil, fmt.Errorf("%w: passed empty buffer", ErrUnpaddingPKCS7) + } + // 4. read the padding size + psiz := int(b[len(b)-1]) + // 5. enforce padding size constraints + if psiz <= 0x00 { + return nil, fmt.Errorf("%w: padding size cannot be zero", ErrUnpaddingPKCS7) + } + if psiz > blockSize { + return nil, fmt.Errorf("%w: padding size cannot be larger than blockSize", ErrUnpaddingPKCS7) + } + // 6. compute the padding offset + off := len(b) - psiz + // 7. return unpadded bytes + runtimex.Assert(off >= 0 && off <= len(b), "off is out of bounds") + return b[:off], nil +} + +// bytesPadPKCS7 returns the PKCS#7 padding of a byte array. +func BytesPadPKCS7(b []byte, blockSize int) ([]byte, error) { + runtimex.PanicIfTrue(blockSize <= 0, "blocksize cannot be negative or zero") + + // If lth mod blockSize == 0, then the input gets appended a whole block size + // See https://datatracker.ietf.org/doc/html/rfc5652#section-6.3 + if blockSize > math.MaxUint8 { + // This padding method is well defined iff blockSize is less than 256. + return nil, ErrPaddingPKCS7 + } + psiz := blockSize - len(b)%blockSize + padding := bytes.Repeat([]byte{byte(psiz)}, psiz) + return append(b, padding...), nil +} + +// ReadUint32 is a convenience function that reads a uint32 from a 4-byte +// buffer, returning an error if the operation failed. +func ReadUint32(buf *bytes.Buffer) (uint32, error) { + var numBuf [4]byte + _, err := io.ReadFull(buf, numBuf[:]) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint32(numBuf[:]), nil +} + +// WriteUint32 is a convenience function that appends to the given buffer +// 4 bytes containing the big-endian representation of the given uint32 value. +func WriteUint32(buf *bytes.Buffer, val uint32) { + var numBuf [4]byte + binary.BigEndian.PutUint32(numBuf[:], val) + buf.Write(numBuf[:]) +} + +// WriteUint24 is a convenience function that appends to the given buffer +// 3 bytes containing the big-endian representation of the given uint32 value. +// Caller is responsible to ensure the passed value does not overflow the +// maximal capacity of 3 bytes. +func WriteUint24(buf *bytes.Buffer, val uint32) { + b := &bytes.Buffer{} + WriteUint32(b, val) + buf.Write(b.Bytes()[1:]) +} diff --git a/internal/model/dialer.go b/internal/model/dialer.go new file mode 100644 index 00000000..90915329 --- /dev/null +++ b/internal/model/dialer.go @@ -0,0 +1,11 @@ +package model + +import ( + "context" + "net" +) + +// Dialer is a type allowing to dial network connections. +type Dialer interface { + DialContext(context.Context, string, string) (net.Conn, error) +} diff --git a/internal/model/doc.go b/internal/model/doc.go new file mode 100644 index 00000000..4232563e --- /dev/null +++ b/internal/model/doc.go @@ -0,0 +1,2 @@ +// Package model implements common models for the vpn data structures. +package model diff --git a/internal/model/logger.go b/internal/model/logger.go new file mode 100644 index 00000000..f008b2b3 --- /dev/null +++ b/internal/model/logger.go @@ -0,0 +1,23 @@ +// Package model contains common data models. +package model + +// Logger is the generic logger definition. +type Logger interface { + // Debug emits a debug message. + Debug(msg string) + + // Debugf formats and emits a debug message. + Debugf(format string, v ...any) + + // Info emits an informational message. + Info(msg string) + + // Infof formats and emits an informational message. + Infof(format string, v ...any) + + // Warn emits a warning message. + Warn(msg string) + + // Warnf formats and emits a warning message. + Warnf(format string, v ...any) +} diff --git a/internal/model/notification.go b/internal/model/notification.go new file mode 100644 index 00000000..629b4315 --- /dev/null +++ b/internal/model/notification.go @@ -0,0 +1,12 @@ +package model + +const ( + // NotificationReset indicates that a SOFT or HARD reset occurred. + NotificationReset = 1 << iota +) + +// Notification is a notification for a service worker. +type Notification struct { + // Flags contains flags explaining what happened. + Flags int64 +} diff --git a/internal/model/options.go b/internal/model/options.go new file mode 100644 index 00000000..9a344a33 --- /dev/null +++ b/internal/model/options.go @@ -0,0 +1,649 @@ +package model + +// +// Parse VPN options. +// +// Mostly, this file conforms to the format in the reference implementation. +// However, there are some additions that are specific. To avoid feature creep +// and fat dependencies, the main `vpn` module only supports mainline +// capabilities. It is still useful to carry all options in a single type, +// so it's up to the user of this library to do something useful with +// such options. The `extra` package provides some of these extra features, like +// obfuscation support. +// +// Following the configuration format in the reference implementation, `minivpn` +// allows including files in the main configuration file, but only for the `ca`, +// `cert` and `key` options. +// +// Each inline file is started by the line . +// +// Here is an example of an inline file usage: +// +// ``` +// +// -----BEGIN CERTIFICATE----- +// [...] +// -----END CERTIFICATE----- +// +// ``` + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "strconv" + "strings" +) + +type ( + // Compression describes a Compression type (e.g., stub). + Compression string +) + +const ( + // CompressionStub adds the (empty) compression stub to the packets. + CompressionStub = Compression("stub") + + // CompressionEmpty is the empty compression. + CompressionEmpty = Compression("empty") + + // CompressionLZONo is lzo-no (another type of no-compression, older). + CompressionLZONo = Compression("lzo-no") +) + +// Proto is the main vpn mode (e.g., TCP or UDP). +type Proto string + +var _ fmt.Stringer = Proto("") + +// String implements fmt.Stringer +func (p Proto) String() string { + return string(p) +} + +// ProtoTCP is used for vpn in TCP mode. +const ProtoTCP = Proto("tcp") + +// ProtoUDP is used for vpn in UDP mode. +const ProtoUDP = Proto("udp") + +// ErrBadConfig is the generic error returned for invalid config files +var ErrBadConfig = errors.New("openvpn: bad config") + +// SupportCiphers defines the supported ciphers. +var SupportedCiphers = []string{ + "AES-128-CBC", + "AES-192-CBC", + "AES-256-CBC", + "AES-128-GCM", + "AES-192-GCM", + "AES-256-GCM", +} + +// SupportedAuth defines the supported authentication methods. +var SupportedAuth = []string{ + "SHA1", + "SHA256", + "SHA512", +} + +// Options make all the relevant configuration options accessible to the +// different modules that need it. +type Options struct { + // These options have the same name of OpenVPN options: + Remote string + Port string + Proto Proto + Username string + Password string + CAPath string + CertPath string + KeyPath string + CA []byte + Cert []byte + Key []byte + Cipher string + Auth string + TLSMaxVer string + + // Below are options that do not conform to the OpenVPN configuration format: + Compress Compression + ProxyOBFS4 string +} + +// ReadConfigFile expects a string with a path to a valid config file, +// and returns a pointer to a Options struct after parsing the file, and an +// error if the operation could not be completed. +func ReadConfigFile(filePath string) (*Options, error) { + lines, err := getLinesFromFile(filePath) + dir, _ := filepath.Split(filePath) + if err != nil { + return nil, err + } + return getOptionsFromLines(lines, dir) +} + +// ShouldLoadCertsFromPath returns true when the options object is configured to load +// certificates from paths; false when we have inline certificates. +func (o *Options) ShouldLoadCertsFromPath() bool { + return o.CertPath != "" && o.KeyPath != "" && o.CAPath != "" +} + +// HasAuthInfo returns true if: +// - we have paths for cert, key and ca; or +// - we have inline byte arrays for cert, key and ca; or +// - we have username + password info. +func (o *Options) HasAuthInfo() bool { + if o.CertPath != "" && o.KeyPath != "" && o.CAPath != "" { + return true + } + if len(o.Cert) != 0 && len(o.Key) != 0 && len(o.CA) != 0 { + return true + } + if o.Username != "" && o.Password != "" { + return true + } + return false +} + +// clientOptions is the options line we're passing to the OpenVPN server during the handshake. +const clientOptions = "V4,dev-type tun,link-mtu 1549,tun-mtu 1500,proto %sv4,cipher %s,auth %s,keysize %s,key-method 2,tls-client" + +// ServerOptionsString produces a comma-separated representation of the options, in the same +// order and format that the OpenVPN server expects from us. +func (o *Options) ServerOptionsString() string { + if o.Cipher == "" { + return "" + } + // TODO(ainghazal): this line of code crashes if the ciphers are not well formed + keysize := strings.Split(o.Cipher, "-")[1] + proto := strings.ToUpper(ProtoUDP.String()) + if o.Proto == ProtoTCP { + proto = strings.ToUpper(ProtoTCP.String()) + } + s := fmt.Sprintf(clientOptions, proto, o.Cipher, o.Auth, keysize) + if o.Compress == CompressionStub { + s = s + ",compress stub" + } else if o.Compress == "lzo-no" { + s = s + ",lzo-comp no" + } else if o.Compress == CompressionEmpty { + s = s + ",compress" + } + return s +} + +// TunnelInfo holds state about the VPN TunnelInfo that has longer duration than a +// given session. This information is gathered at different stages: +// - during the handshake (mtu). +// - after server pushes config options(ip, gw). +type TunnelInfo struct { + MTU int + IP string + GW string + PeerID int +} + +// NewTunnelInfoFromPushedOptions takes a map of string to array of strings, and returns +// a new tunnel struct with the relevant info. +func NewTunnelInfoFromPushedOptions(opts map[string][]string) *TunnelInfo { + t := &TunnelInfo{} + if r := opts["route"]; len(r) >= 1 { + t.GW = r[0] + } else if r := opts["route-gateway"]; len(r) >= 1 { + t.GW = r[0] + } + ip := opts["ifconfig"] + if len(ip) >= 1 { + t.IP = ip[0] + } + peerID := opts["peer-id"] + if len(peerID) == 1 { + peer, err := strconv.Atoi(peerID[0]) + if err != nil { + log.Println("Cannot parse peer-id:", err.Error()) + } else { + t.PeerID = peer + } + } + return t +} + +// parseIntFromOption parses an int from a null-terminated string +func parseIntFromOption(s string) (int, error) { + str := "" + for i := 0; i < len(s); i++ { + if byte(s[i]) == 0x00 { + return strconv.Atoi(str) + } + str = str + string(s[i]) + } + return 0, nil +} + +// PushedOptionsAsMap returns a map for the server-pushed options, +// where the options are the keys and each space-separated value is the value. +// This function always returns an initialized map, even if empty. +func PushedOptionsAsMap(pushedOptions []byte) map[string][]string { + optMap := make(map[string][]string) + if len(pushedOptions) == 0 { + return optMap + } + + optStr := string(pushedOptions[:len(pushedOptions)-1]) + + opts := strings.Split(optStr, ",") + for _, opt := range opts { + vals := strings.Split(opt, " ") + k, v := vals[0], vals[1:] + optMap[k] = v + } + return optMap +} + +func parseProto(p []string, o *Options) error { + if len(p) != 1 { + return fmt.Errorf("%w: %s", ErrBadConfig, "proto needs one arg") + } + m := p[0] + switch m { + case ProtoUDP.String(): + o.Proto = ProtoUDP + case ProtoTCP.String(): + o.Proto = ProtoTCP + default: + return fmt.Errorf("%w: bad proto: %s", ErrBadConfig, m) + + } + return nil +} + +// TODO(ainghazal): all these little functions can be better tested if we return the options object too + +func parseRemote(p []string, o *Options) error { + if len(p) != 2 { + return fmt.Errorf("%w: %s", ErrBadConfig, "remote needs two args") + } + o.Remote, o.Port = p[0], p[1] + return nil +} + +func parseCipher(p []string, o *Options) error { + if len(p) != 1 { + return fmt.Errorf("%w: %s", ErrBadConfig, "cipher expects one arg") + } + cipher := p[0] + if !hasElement(cipher, SupportedCiphers) { + return fmt.Errorf("%w: unsupported cipher: %s", ErrBadConfig, cipher) + } + o.Cipher = cipher + return nil +} + +func parseAuth(p []string, o *Options) error { + if len(p) != 1 { + return fmt.Errorf("%w: %s", ErrBadConfig, "invalid auth entry") + } + auth := p[0] + if !hasElement(auth, SupportedAuth) { + return fmt.Errorf("%w: unsupported auth: %s", ErrBadConfig, auth) + } + o.Auth = auth + return nil +} + +func parseCA(p []string, o *Options, basedir string) error { + e := fmt.Errorf("%w: %s", ErrBadConfig, "ca expects a valid file") + if len(p) != 1 { + return e + } + ca := toAbs(p[0], basedir) + if sub, _ := isSubdir(basedir, ca); !sub { + return fmt.Errorf("%w: %s", ErrBadConfig, "ca must be below config path") + } + if !existsFile(ca) { + return e + } + o.CAPath = ca + return nil +} + +func parseCert(p []string, o *Options, basedir string) error { + e := fmt.Errorf("%w: %s", ErrBadConfig, "cert expects a valid file") + if len(p) != 1 { + return e + } + cert := toAbs(p[0], basedir) + if sub, _ := isSubdir(basedir, cert); !sub { + return fmt.Errorf("%w: %s", ErrBadConfig, "cert must be below config path") + } + if !existsFile(cert) { + return e + } + o.CertPath = cert + return nil +} + +func parseKey(p []string, o *Options, basedir string) error { + e := fmt.Errorf("%w: %s", ErrBadConfig, "key expects a valid file") + if len(p) != 1 { + return e + } + key := toAbs(p[0], basedir) + if sub, _ := isSubdir(basedir, key); !sub { + return fmt.Errorf("%w: %s", ErrBadConfig, "key must be below config path") + } + if !existsFile(key) { + return e + } + o.KeyPath = key + return nil +} + +// parseAuthUser reads credentials from a given file, according to the openvpn +// format (user and pass on a line each). To avoid path traversal / LFI, the +// credentials file is expected to be in a subdirectory of the base dir. +func parseAuthUser(p []string, o *Options, basedir string) error { + e := fmt.Errorf("%w: %s", ErrBadConfig, "auth-user-pass expects a valid file") + if len(p) != 1 { + return e + } + auth := toAbs(p[0], basedir) + if sub, _ := isSubdir(basedir, auth); !sub { + return fmt.Errorf("%w: %s", ErrBadConfig, "auth must be below config path") + } + if !existsFile(auth) { + return e + } + creds, err := getCredentialsFromFile(auth) + if err != nil { + return err + } + o.Username, o.Password = creds[0], creds[1] + return nil +} + +func parseCompress(p []string, o *Options) error { + if len(p) > 1 { + return fmt.Errorf("%w: %s", ErrBadConfig, "compress: only empty/stub options supported") + } + if len(p) == 0 { + o.Compress = CompressionEmpty + return nil + } + if p[0] == "stub" { + o.Compress = CompressionStub + return nil + } + return fmt.Errorf("%w: %s", ErrBadConfig, "compress: only empty/stub options supported") +} + +func parseCompLZO(p []string, o *Options) error { + if p[0] != "no" { + return fmt.Errorf("%w: %s", ErrBadConfig, "comp-lzo: compression not supported") + } + o.Compress = "lzo-no" + return nil +} + +// parseTLSVerMax sets the maximum TLS version. This is currently ignored +// because we're using uTLS to parrot the Client Hello. +func parseTLSVerMax(p []string, o *Options) error { + if len(p) == 0 { + o.TLSMaxVer = "1.3" + return nil + } + if p[0] == "1.2" { + o.TLSMaxVer = "1.2" + } + return nil +} + +func parseProxyOBFS4(p []string, o *Options) error { + if len(p) != 1 { + return fmt.Errorf("%w: %s", ErrBadConfig, "proto-obfs4: need a properly configured proxy") + } + // TODO(ainghazal): can validate the obfs4://... scheme here + o.ProxyOBFS4 = p[0] + return nil +} + +var pMap = map[string]interface{}{ + "proto": parseProto, + "remote": parseRemote, + "cipher": parseCipher, + "auth": parseAuth, + "compress": parseCompress, + "comp-lzo": parseCompLZO, + "proxy-obfs4": parseProxyOBFS4, + "tls-version-max": parseTLSVerMax, // this is currently ignored because of uTLS +} + +var pMapDir = map[string]interface{}{ + "ca": parseCA, + "cert": parseCert, + "key": parseKey, + "auth-user-pass": parseAuthUser, +} + +func parseOption(o *Options, dir, key string, p []string, lineno int) error { + switch key { + case "proto", "remote", "cipher", "auth", "compress", "comp-lzo", "tls-version-max", "proxy-obfs4": + fn := pMap[key].(func([]string, *Options) error) + if e := fn(p, o); e != nil { + return e + } + case "ca", "cert", "key", "auth-user-pass": + fn := pMapDir[key].(func([]string, *Options, string) error) + if e := fn(p, o, dir); e != nil { + return e + } + default: + log.Printf("warn: unsupported key in line %d\n", lineno) + } + return nil +} + +// getOptionsFromLines tries to parse all the lines coming from a config file +// and raises validation errors if the values do not conform to the expected +// format. The config file supports inline file inclusion for , and . +func getOptionsFromLines(lines []string, dir string) (*Options, error) { + opt := &Options{} + + // tag and inlineBuf are used to parse inline files. + // these follow the format used by the reference openvpn implementation. + // each block (any of ca, key, cert) is marked by a line; lines in between are expected to contain + // the crypto block. + tag := "" + inlineBuf := new(bytes.Buffer) + + for lineno, l := range lines { + if strings.HasPrefix(l, "#") { + continue + } + l = strings.TrimSpace(l) + + // inline certs + if isClosingTag(l) { + // we expect an already existing inlineBuf + e := parseInlineTag(opt, tag, inlineBuf) + if e != nil { + return nil, e + } + tag = "" + inlineBuf = new(bytes.Buffer) + continue + } + if tag != "" { + inlineBuf.Write([]byte(l)) + inlineBuf.Write([]byte("\n")) + continue + } + if isOpeningTag(l) { + if len(inlineBuf.Bytes()) != 0 { + // something wrong: an opening tag should not be found + // when we still have bytes in the inline buffer. + return opt, fmt.Errorf("%w: %s", ErrBadConfig, "tag not closed") + } + tag = parseTag(l) + continue + } + + // parse parts in the same line + p := strings.Split(l, " ") + if len(p) == 0 { + continue + } + var ( + key string + parts []string + ) + if len(p) == 1 { + key = p[0] + } else { + key, parts = p[0], p[1:] + } + e := parseOption(opt, dir, key, parts, lineno) + if e != nil { + return nil, e + } + } + return opt, nil +} + +func isOpeningTag(key string) bool { + switch key { + case "", "", "": + return true + default: + return false + } +} + +func isClosingTag(key string) bool { + switch key { + case "", "", "": + return true + default: + return false + } +} + +func parseTag(tag string) string { + switch tag { + case "", "": + return "ca" + case "", "": + return "cert" + case "", "": + return "key" + default: + return "" + } +} + +// parseInlineTag +func parseInlineTag(o *Options, tag string, buf *bytes.Buffer) error { + b := buf.Bytes() + if len(b) == 0 { + return fmt.Errorf("%w: empty inline tag: %d", ErrBadConfig, len(b)) + } + switch tag { + case "ca": + o.CA = b + case "cert": + o.Cert = b + case "key": + o.Key = b + default: + return fmt.Errorf("%w: unknown tag: %s", ErrBadConfig, tag) + } + return nil +} + +// hasElement checks if a given string is present in a string array. returns +// true if that is the case, false otherwise. +func hasElement(el string, arr []string) bool { + for _, v := range arr { + if v == el { + return true + } + } + return false +} + +// existsFile returns true if the file to which the path refers to exists and +// is a regular file. +func existsFile(path string) bool { + statbuf, err := os.Stat(path) + return !errors.Is(err, os.ErrNotExist) && statbuf.Mode().IsRegular() +} + +// getLinesFromFile accepts a path parameter, and return a string array with +// its content and an error if the operation cannot be completed. +func getLinesFromFile(path string) ([]string, error) { + f, err := os.Open(path) + defer f.Close() + if err != nil { + return nil, err + } + + lines := make([]string, 0) + scanner := bufio.NewScanner(f) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + err = scanner.Err() + if err != nil { + return nil, err + } + return lines, nil +} + +// getCredentialsFromFile accepts a path string parameter, and return a string +// array containing the credentials in that file, and an error if the operation +// could not be completed. +func getCredentialsFromFile(path string) ([]string, error) { + lines, err := getLinesFromFile(path) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrBadConfig, err) + } + if len(lines) != 2 { + return nil, fmt.Errorf("%w: %s", ErrBadConfig, "malformed credentials file") + } + if len(lines[0]) == 0 { + return nil, fmt.Errorf("%w: %s", ErrBadConfig, "empty username in creds file") + } + if len(lines[1]) == 0 { + return nil, fmt.Errorf("%w: %s", ErrBadConfig, "empty password in creds file") + } + return lines, nil +} + +// toAbs return an absolute path if the given path is not already absolute; to +// do so, it will append the path to the given basedir. +func toAbs(path, basedir string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(basedir, path) +} + +// isSubdir checks if a given path is a subdirectory of another. It returns +// true if that's the case, and any error raise during the check. +func isSubdir(parent, sub string) (bool, error) { + p, err := filepath.Abs(parent) + if err != nil { + return false, err + } + s, err := filepath.Abs(sub) + if err != nil { + return false, err + } + return strings.HasPrefix(s, p), nil +} diff --git a/internal/model/packet.go b/internal/model/packet.go new file mode 100644 index 00000000..828faded --- /dev/null +++ b/internal/model/packet.go @@ -0,0 +1,308 @@ +package model + +// +// Packet +// +// Parsing and serializing OpenVPN packets. +// + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + + "github.com/ooni/minivpn/internal/bytesx" +) + +// Opcode is an OpenVPN packet opcode. +type Opcode byte + +// OpenVPN packets opcodes. +const ( + P_CONTROL_HARD_RESET_CLIENT_V1 = Opcode(iota + 1) // 1 + P_CONTROL_HARD_RESET_SERVER_V1 // 2 + P_CONTROL_SOFT_RESET_V1 // 3 + P_CONTROL_V1 // 4 + P_ACK_V1 // 5 + P_DATA_V1 // 6 + P_CONTROL_HARD_RESET_CLIENT_V2 // 7 + P_CONTROL_HARD_RESET_SERVER_V2 // 8 + P_DATA_V2 // 9 +) + +// String returns the opcode string representation +func (op Opcode) String() string { + switch op { + case P_CONTROL_HARD_RESET_CLIENT_V1: + return "P_CONTROL_HARD_RESET_CLIENT_V1" + + case P_CONTROL_HARD_RESET_SERVER_V1: + return "P_CONTROL_HARD_RESET_SERVER_V1" + + case P_CONTROL_SOFT_RESET_V1: + return "P_CONTROL_SOFT_RESET_V1" + + case P_CONTROL_V1: + return "P_CONTROL_V1" + + case P_ACK_V1: + return "P_ACK_V1" + + case P_DATA_V1: + return "P_DATA_V1" + + case P_CONTROL_HARD_RESET_CLIENT_V2: + return "P_CONTROL_HARD_RESET_CLIENT_V2" + + case P_CONTROL_HARD_RESET_SERVER_V2: + return "P_CONTROL_HARD_RESET_SERVER_V2" + + case P_DATA_V2: + return "P_DATA_V2" + + default: + return "P_UNKNOWN" + } +} + +// IsControl returns true when this opcode is a control opcode. +func (op Opcode) IsControl() bool { + switch op { + case P_CONTROL_HARD_RESET_CLIENT_V1, + P_CONTROL_HARD_RESET_SERVER_V1, + P_CONTROL_SOFT_RESET_V1, + P_CONTROL_V1, + P_CONTROL_HARD_RESET_CLIENT_V2, + P_CONTROL_HARD_RESET_SERVER_V2: + return true + default: + return false + } +} + +// IsData returns true when this opcode is a data opcode. +func (op Opcode) IsData() bool { + switch op { + case P_DATA_V1, P_DATA_V2: + return true + default: + return false + } +} + +// SessionID is the session identifier. +type SessionID [8]byte + +// PacketID is a packet identifier. +type PacketID uint32 + +// PeerID is the type of the P_DATA_V2 peer ID. +type PeerID [3]byte + +// Packet is an OpenVPN packet. +type Packet struct { + // Opcode is the packet message type (a P_* constant; high 5-bits of + // the first packet byte). + Opcode Opcode + + // The key_id refers to an already negotiated TLS session. + // This is the shortened version of the key-id (low 3-bits of the first + // packet byte). + KeyID byte + + // PeerID is the peer ID. + PeerID PeerID + + // LocalSessionID is the local session ID. + LocalSessionID SessionID + + // Acks contains the remote packets we're ACKing. + ACKs []PacketID + + // RemoteSessionID is the remote session ID. + RemoteSessionID SessionID + + // ID is the packet-id for replay protection. According to the spec: "4 or 8 bytes, + // includes sequence number and optional time_t timestamp". + // + // This library does not use the timestamp. + ID PacketID + + // Payload is the packet's payload. + Payload []byte +} + +// ErrPacketTooShort indicates that a packet is too short. +var ErrPacketTooShort = errors.New("openvpn: packet too short") + +// ParsePacket produces a packet after parsing the common header. We assume that +// the underlying connection has already stripped out the framing. +func ParsePacket(buf []byte) (*Packet, error) { + // parsing opcode and keyID + if len(buf) < 2 { + return nil, ErrPacketTooShort + } + opcode := Opcode(buf[0] >> 3) + keyID := buf[0] & 0x07 + + // extract the packet payload and possibly the peerID + var ( + payload []byte + peerID PeerID + ) + switch opcode { + case P_DATA_V2: + if len(buf) < 4 { + return nil, ErrPacketTooShort + } + copy(peerID[:], buf[1:4]) + payload = buf[4:] + default: + payload = buf[1:] + } + + // ACKs and control packets require more complex parsing + if opcode.IsControl() || opcode == P_ACK_V1 { + return parseControlOrACKPacket(opcode, keyID, payload) + } + + // otherwise just return the data packet. + p := &Packet{ + Opcode: opcode, + KeyID: keyID, + PeerID: peerID, + LocalSessionID: [8]byte{}, + ACKs: []PacketID{}, + RemoteSessionID: [8]byte{}, + ID: 0, + Payload: payload, + } + return p, nil +} + +// ErrEmptyPayload indicates tha the payload of an OpenVPN control packet is empty. +var ErrEmptyPayload = errors.New("openvpn: empty payload") + +// ErrParsePacket is a generic packet parse error which may be further qualified. +var ErrParsePacket = errors.New("openvpn: packet parse error") + +// parseControlOrACKPacket parses the contents of a control or ACK packet. +func parseControlOrACKPacket(opcode Opcode, keyID byte, payload []byte) (*Packet, error) { + // make sure we have payload to parse and we're parsing control or ACK + if len(payload) <= 0 { + return nil, ErrEmptyPayload + } + if !opcode.IsControl() && opcode != P_ACK_V1 { + return nil, fmt.Errorf("%w: %s", ErrParsePacket, "expected control/ack packet") + } + + // create a buffer for parsing the packet + buf := bytes.NewBuffer(payload) + + p := NewPacket(opcode, keyID, payload) + + // local session id + if _, err := io.ReadFull(buf, p.LocalSessionID[:]); err != nil { + return p, fmt.Errorf("%w: bad sessionID: %s", ErrParsePacket, err) + } + + // ack array length + ackArrayLenByte, err := buf.ReadByte() + if err != nil { + return p, fmt.Errorf("%w: bad ack: %s", ErrParsePacket, err) + } + ackArrayLen := int(ackArrayLenByte) + + // ack array + p.ACKs = make([]PacketID, ackArrayLen) + for i := 0; i < ackArrayLen; i++ { + val, err := bytesx.ReadUint32(buf) + if err != nil { + return p, fmt.Errorf("%w: cannot parse ack id: %s", ErrParsePacket, err) + } + p.ACKs[i] = PacketID(val) + } + + // remote session id + if ackArrayLen > 0 { + if _, err = io.ReadFull(buf, p.RemoteSessionID[:]); err != nil { + return p, fmt.Errorf("%w: bad remote sessionID: %s", ErrParsePacket, err) + } + } + + // packet id + if p.Opcode != P_ACK_V1 { + val, err := bytesx.ReadUint32(buf) + if err != nil { + return p, fmt.Errorf("%w: bad packetID: %s", ErrParsePacket, err) + } + p.ID = PacketID(val) + } + + // payload + p.Payload = buf.Bytes() + return p, nil +} + +// NewPacket returns a packet from the passed arguments: opcode, keyID and a raw payload. +func NewPacket(opcode Opcode, keyID uint8, payload []byte) *Packet { + return &Packet{ + Opcode: opcode, + KeyID: keyID, + PeerID: [3]byte{}, + LocalSessionID: [8]byte{}, + ACKs: []PacketID{}, + RemoteSessionID: [8]byte{}, + ID: 0, + Payload: payload, + } +} + +// ErrMarshalPacket is the error returned when we cannot marshal a packet. +var ErrMarshalPacket = errors.New("openvpn: cannot marshal packet") + +// Bytes returns a byte array that is ready to be sent on the wire. +func (p *Packet) Bytes() ([]byte, error) { + buf := &bytes.Buffer{} + + switch p.Opcode { + case P_DATA_V2: + // we assume this is an encrypted data packet, + // so we serialize just the encrypted payload + + default: + buf.WriteByte((byte(p.Opcode) << 3) | (p.KeyID & 0x07)) + buf.Write(p.LocalSessionID[:]) + // we write a byte with the number of acks, and then serialize each ack. + nAcks := len(p.ACKs) + if nAcks > math.MaxUint8 { + return nil, fmt.Errorf("%w: too many ACKs", ErrMarshalPacket) + } + buf.WriteByte(byte(nAcks)) + for i := 0; i < nAcks; i++ { + bytesx.WriteUint32(buf, uint32(p.ACKs[i])) + } + // remote session id + if len(p.ACKs) > 0 { + buf.Write(p.RemoteSessionID[:]) + } + if p.Opcode != P_ACK_V1 { + bytesx.WriteUint32(buf, uint32(p.ID)) + } + } + // payload + buf.Write(p.Payload) + return buf.Bytes(), nil +} + +// IsControl returns true if the packet is any of the control types. +func (p *Packet) IsControl() bool { + return p.Opcode.IsControl() +} + +// IsData returns true if the packet is of data type. +func (p *Packet) IsData() bool { + return p.Opcode.IsData() +} diff --git a/internal/networkio/closeonce.go b/internal/networkio/closeonce.go new file mode 100644 index 00000000..20c043c0 --- /dev/null +++ b/internal/networkio/closeonce.go @@ -0,0 +1,35 @@ +package networkio + +import ( + "net" + "sync" +) + +// closeOnceConn is a [net.Conn] where the Close method has once semantics. +// +// The zero value is invalid; use [newCloseOnceConn]. +type closeOnceConn struct { + // once ensures we close just once. + once sync.Once + + // Conn is the underlying conn. + net.Conn +} + +var _ net.Conn = &closeOnceConn{} + +// newCloseOnceConn creates a [closeOnceConn]. +func newCloseOnceConn(conn net.Conn) *closeOnceConn { + return &closeOnceConn{ + once: sync.Once{}, + Conn: conn, + } +} + +// Close implements net.Conn +func (c *closeOnceConn) Close() (err error) { + c.once.Do(func() { + err = c.Conn.Close() + }) + return +} diff --git a/internal/networkio/datagram.go b/internal/networkio/datagram.go new file mode 100644 index 00000000..efd3ffe0 --- /dev/null +++ b/internal/networkio/datagram.go @@ -0,0 +1,33 @@ +package networkio + +import ( + "math" + "net" +) + +// datagramConn wraps a datagram socket and implements OpenVPN framing. +type datagramConn struct { + net.Conn +} + +var _ FramingConn = &datagramConn{} + +// ReadRawPacket implements FramingConn +func (c *datagramConn) ReadRawPacket() ([]byte, error) { + buffer := make([]byte, math.MaxUint16) // maximum UDP datagram size + count, err := c.Read(buffer) + if err != nil { + return nil, err + } + pkt := buffer[:count] + return pkt, nil +} + +// WriteRawPacket implements FramingConn +func (c *datagramConn) WriteRawPacket(pkt []byte) error { + if len(pkt) > math.MaxUint16 { + return ErrPacketTooLarge + } + _, err := c.Conn.Write(pkt) + return err +} diff --git a/internal/networkio/dialer.go b/internal/networkio/dialer.go new file mode 100644 index 00000000..d68b5266 --- /dev/null +++ b/internal/networkio/dialer.go @@ -0,0 +1,49 @@ +package networkio + +import ( + "context" + + "github.com/ooni/minivpn/internal/model" +) + +// Dialer dials network connections. The zero value of this structure is +// invalid; please, use the [NewDialer] constructor. +type Dialer struct { + // dialer is the underlying [DialerContext] we use to dial. + dialer model.Dialer + + // logger is the [Logger] with which we log. + logger model.Logger +} + +// NewDialer creates a new [Dialer] instance. +func NewDialer(logger model.Logger, dialer model.Dialer) *Dialer { + return &Dialer{ + dialer: dialer, + logger: logger, + } +} + +// DialContext establishes a connection and, on success, automatically wraps the +// returned connection to implement OpenVPN framing when not using UDP. +func (d *Dialer) DialContext(ctx context.Context, network, address string) (FramingConn, error) { + // dial with the underlying dialer + conn, err := d.dialer.DialContext(ctx, network, address) + if err != nil { + d.logger.Warnf("networkio: dial failed: %s", err.Error()) + return nil, err + } + + d.logger.Debugf("networkio: connected to %s/%s", address, network) + + // make sure the conn has close once semantics + conn = newCloseOnceConn(conn) + + // wrap the conn and return + switch conn.LocalAddr().Network() { + case "udp", "udp4", "udp6": + return &datagramConn{conn}, nil + default: + return &streamConn{conn}, nil + } +} diff --git a/internal/networkio/doc.go b/internal/networkio/doc.go new file mode 100644 index 00000000..e09162a6 --- /dev/null +++ b/internal/networkio/doc.go @@ -0,0 +1,2 @@ +// Package networkio implements raw packets network I/O. +package networkio diff --git a/internal/networkio/framing.go b/internal/networkio/framing.go new file mode 100644 index 00000000..232327a4 --- /dev/null +++ b/internal/networkio/framing.go @@ -0,0 +1,31 @@ +package networkio + +import ( + "net" + "time" +) + +// FramingConn is an OpenVPN network connection that knows about +// the framing used by OpenVPN to read and write raw packets. +type FramingConn interface { + // ReadRawPacket reads and return a raw OpenVPN packet. + ReadRawPacket() ([]byte, error) + + // WriteRawPacket writes a raw OpenVPN packet. + WriteRawPacket(pkt []byte) error + + // SetReadDeadline is like net.Conn.SetReadDeadline. + SetReadDeadline(t time.Time) error + + // SetWriteDeadline is like net.Conn.SetWriteDeadline. + SetWriteDeadline(t time.Time) error + + // LocalAddr is like net.Conn.LocalAddr. + LocalAddr() net.Addr + + // RemoteAddr is like net.Conn.RemoteAddr. + RemoteAddr() net.Addr + + // Close is like net.Conn.Close. + Close() error +} diff --git a/internal/networkio/service.go b/internal/networkio/service.go new file mode 100644 index 00000000..77e25675 --- /dev/null +++ b/internal/networkio/service.go @@ -0,0 +1,115 @@ +package networkio + +import ( + "github.com/ooni/minivpn/internal/model" + "github.com/ooni/minivpn/internal/workers" +) + +// Service is the network I/O service. Make sure you initialize +// the channels before invoking [Service.StartWorkers]. +type Service struct { + // MuxerToNetwork moves bytes down from the muxer to the network IO layer + MuxerToNetwork chan []byte + + // NetworkToMuxer moves bytes up from the network IO layer to the muxer + NetworkToMuxer *chan []byte +} + +// StartWorkers starts the network I/O workers. See the [ARCHITECTURE] +// file for more information about the network I/O workers. +// +// [ARCHITECTURE]: https://github.com/ooni/minivpn/blob/main/ARCHITECTURE.md +func (svc *Service) StartWorkers( + logger model.Logger, + manager *workers.Manager, + conn FramingConn, +) { + ws := &workersState{ + conn: conn, + logger: logger, + manager: manager, + muxerToNetwork: svc.MuxerToNetwork, + networkToMuxer: *svc.NetworkToMuxer, + } + + manager.StartWorker(ws.moveUpWorker) + manager.StartWorker(ws.moveDownWorker) +} + +// workersState contains the service workers state +type workersState struct { + // conn is the connection to use + conn FramingConn + + // logger is the logger to use + logger model.Logger + + // manager controls the workers lifecycle + manager *workers.Manager + + // muxerToNetwork is the channel for reading outgoing packets + // that are coming down to us + muxerToNetwork <-chan []byte + + // networkToMuxer is the channel for writing incoming packets + // that are coming up to us from the net + networkToMuxer chan<- []byte +} + +// moveUpWorker moves packets up the stack. +func (ws *workersState) moveUpWorker() { + defer func() { + // make sure the manager knows we're done + ws.manager.OnWorkerDone() + + // tear down everything else because a workers exited + ws.manager.StartShutdown() + + // emit useful debug message + ws.logger.Debug("networkio: moveUpWorker: done") + }() + + ws.logger.Debug("networkio: moveUpWorker: started") + + for { + // POSSIBLY BLOCK on the connection to read a new packet + pkt, err := ws.conn.ReadRawPacket() + if err != nil { + ws.logger.Debugf("networkio: moveUpWorker: ReadRawPacket: %s", err.Error()) + return + } + + // POSSIBLY BLOCK on the channel to deliver the packet + select { + case ws.networkToMuxer <- pkt: + case <-ws.manager.ShouldShutdown(): + return + } + } +} + +// moveDownWorker moves packets down the stack +func (ws *workersState) moveDownWorker() { + defer func() { + ws.manager.StartShutdown() + ws.manager.OnWorkerDone() + ws.logger.Debug("networkio: moveDownWorker: done") + }() + + ws.logger.Debug("networkio: moveDownWorker: started") + + for { + // POSSIBLY BLOCK when receiving from channel. + select { + case pkt := <-ws.muxerToNetwork: + // POSSIBLY BLOCK on the connection to write the packet + if err := ws.conn.WriteRawPacket(pkt); err != nil { + ws.logger.Infof("networkio: moveDownWorker: WriteRawPacket: %s", err.Error()) + return + } + + case <-ws.manager.ShouldShutdown(): + return + } + } +} diff --git a/internal/networkio/stream.go b/internal/networkio/stream.go new file mode 100644 index 00000000..2fb22726 --- /dev/null +++ b/internal/networkio/stream.go @@ -0,0 +1,45 @@ +package networkio + +import ( + "encoding/binary" + "errors" + "io" + "math" + "net" +) + +// streamConn wraps a stream socket and implements OpenVPN framing. +type streamConn struct { + net.Conn +} + +var _ FramingConn = &streamConn{} + +// ReadRawPacket implements FramingConn +func (c *streamConn) ReadRawPacket() ([]byte, error) { + lenbuf := make([]byte, 2) + if _, err := io.ReadFull(c.Conn, lenbuf); err != nil { + return nil, err + } + length := binary.BigEndian.Uint16(lenbuf) + buf := make([]byte, length) + if _, err := io.ReadFull(c.Conn, buf); err != nil { + return nil, err + } + return buf, nil +} + +// ErrPacketTooLarge means that a packet is larger than [math.MaxUint16]. +var ErrPacketTooLarge = errors.New("openvpn: packet too large") + +// WriteRawPacket implements FramingConn +func (c *streamConn) WriteRawPacket(pkt []byte) error { + if len(pkt) > math.MaxUint16 { + return ErrPacketTooLarge + } + length := make([]byte, 2) + binary.BigEndian.PutUint16(length, uint16(len(pkt))) + pkt = append(length, pkt...) + _, err := c.Conn.Write(pkt) + return err +} diff --git a/internal/runtimex/runtimex.go b/internal/runtimex/runtimex.go new file mode 100644 index 00000000..5e135484 --- /dev/null +++ b/internal/runtimex/runtimex.go @@ -0,0 +1,19 @@ +// Package runtimex contains [runtime] extensions. +package runtimex + +// PanicIfFalse calls panic with the given message if the given statement is false. +func PanicIfFalse(stmt bool, message interface{}) { + if !stmt { + panic(message) + } +} + +// PanicIfTrue calls panic with the given message if the given statement is true. +func PanicIfTrue(stmt bool, message interface{}) { + if stmt { + panic(message) + } +} + +// Assert calls panic with the given message if the given statement is false. +var Assert = PanicIfFalse diff --git a/internal/workers/workers.go b/internal/workers/workers.go new file mode 100644 index 00000000..52fc4033 --- /dev/null +++ b/internal/workers/workers.go @@ -0,0 +1,63 @@ +// Package workers contains code to manage workers. +// +// A worker is a goroutine running in the background that performs some +// activity related to implementing the OpenVPN protocol. +package workers + +import ( + "errors" + "sync" +) + +// ErrShutdown is the error returned by a worker that is shutting down. +var ErrShutdown = errors.New("worker is shutting down") + +// Manager coordinates the lifeycles of the workers implementing the OpenVPN +// protocol. The zero value is invalid; use [NewManager]. +type Manager struct { + // shouldShutdown is closed to signal all workers to shut down. + shouldShutdown chan any + + // shutdownOnce ensures we close shutdownSignal once. + shutdownOnce sync.Once + + // wg tracks the running workers. + wg *sync.WaitGroup +} + +// NewManager creates a new [*Manager]. +func NewManager() *Manager { + return &Manager{ + shouldShutdown: make(chan any), + shutdownOnce: sync.Once{}, + wg: &sync.WaitGroup{}, + } +} + +// StartWorker starts a worker in a background goroutine. +func (m *Manager) StartWorker(fx func()) { + m.wg.Add(1) + go fx() +} + +// OnWorkerDone MUST be called when a worker goroutine terminates. +func (m *Manager) OnWorkerDone() { + m.wg.Done() +} + +// StartShutdown initiates the shutdown of all workers. +func (m *Manager) StartShutdown() { + m.shutdownOnce.Do(func() { + close(m.shouldShutdown) + }) +} + +// ShouldShutdown returns the channel closed when workers should shut down. +func (m *Manager) ShouldShutdown() <-chan any { + return m.shouldShutdown +} + +// WaitWorkersShutdown blocks until all workers have shut down. +func (m *Manager) WaitWorkersShutdown() { + m.wg.Wait() +}