refactor(netxlite): hide details without breaking the rest of the tree (#454)

## Description

This PR continues the refactoring of `netx` under the following principles:

1. do not break the rest of the tree and do not engage in extensive tree-wide refactoring yet
2. move under `netxlite` clearly related subpackages (e.g., `iox`, `netxmocks`)
3. move into `internal/netxlite/internal` stuff that is clearly private of `netxlite`
4. hide implementation details in `netxlite` pending new factories
5. refactor `tls` code in `netxlite` to clearly separate `crypto/tls` code from `utls` code

After each commit, I run `go test -short -race ./...` locally. Each individual commit explains what it does. I will squash, but this operation will preserve the original commit titles, so this will give further insight on each step.

## Commits

* refactor: rename netxmocks -> netxlite/mocks

Part of https://github.com/ooni/probe/issues/1591

* refactor: rename quicx -> netxlite/quicx

See https://github.com/ooni/probe/issues/1591

* refactor: rename iox -> netxlite/iox

Regenerate sources and make sure the tests pass.

See https://github.com/ooni/probe/issues/1591.

* refactor(iox): move MockableReader to netxlite/mocks

See https://github.com/ooni/probe/issues/1591

* refactor(netxlite): generator is an implementation detail

See https://github.com/ooni/probe/issues/1591

* refactor(netxlite): separate tls and utls code

See https://github.com/ooni/probe/issues/1591

* refactor(netxlite): hide most types but keep old names as legacy

With this change we avoid breaking the rest of the tree, but we start
hiding some implementation details a bit. Factories will follow.

See https://github.com/ooni/probe/issues/1591
This commit is contained in:
Simone Basso
2021-09-05 14:49:38 +02:00
committed by GitHub
parent ae799c4942
commit 2e0118d1a6
137 changed files with 1244 additions and 1173 deletions
+113 -77
View File
@@ -1,16 +1,16 @@
// Code generated by go generate; DO NOT EDIT.
// 2021-07-02 14:26:19.300018 +0200 CEST m=+0.978891751
// 2021-09-05 14:01:12.627844 +0200 CEST m=+0.441951210
// https://curl.haxx.se/ca/cacert.pem
package netxlite
//go:generate go run ./generator/ "https://curl.haxx.se/ca/cacert.pem"
//go:generate go run ./internal/generator/ "https://curl.haxx.se/ca/cacert.pem"
const pemcerts string = `
##
## Bundle of CA Root Certificates
##
## Certificate data from Mozilla as of: Tue May 25 03:12:05 2021 GMT
## Certificate data from Mozilla as of: Mon Jul 5 21:35:54 2021 GMT
##
## This is a bundle of X.509 certificates of public Certificate Authorities
## (CA). These were automatically extracted from Mozilla's root certificates
@@ -23,7 +23,7 @@ const pemcerts string = `
## Just configure this file as the SSLCACertificateFile.
##
## Conversion done with mk-ca-bundle.pl version 1.28.
## SHA256: e292bd4e2d500c86df45b830d89417be5c42ee670408f1d2c454c63d8a782865
## SHA256: c8f6733d1ff4e6a4769c182971a1234f95ae079247a9c439a13423fe8ba5c24f
##
@@ -165,38 +165,6 @@ Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z
12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
-----END CERTIFICATE-----
QuoVadis Root CA
================
-----BEGIN CERTIFICATE-----
MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE
ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz
MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp
cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD
EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk
J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL
F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL
YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen
AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w
PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y
ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7
MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj
YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs
ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh
Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW
Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu
BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw
FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0
aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6
tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo
fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul
LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x
gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi
5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi
5nrQNiOKSnQ2+Q==
-----END CERTIFICATE-----
QuoVadis Root CA 2
==================
-----BEGIN CERTIFICATE-----
@@ -284,26 +252,6 @@ s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ
FL39vmwLAw==
-----END CERTIFICATE-----
Sonera Class 2 Root CA
======================
-----BEGIN CERTIFICATE-----
MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG
U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw
NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh
IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3
/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT
dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG
f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P
tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH
nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT
XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt
0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI
cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph
Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx
EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH
llpwrN9M
-----END CERTIFICATE-----
XRamp Global CA Root
====================
-----BEGIN CERTIFICATE-----
@@ -1203,27 +1151,6 @@ OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9
vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
-----END CERTIFICATE-----
Trustis FPS Root CA
===================
-----BEGIN CERTIFICATE-----
MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG
EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290
IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV
BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ
RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk
H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa
cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt
o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA
AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd
BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c
GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC
yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P
8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV
l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl
iB6XzCGcKQENZetX2fNXlrtIzYE=
-----END CERTIFICATE-----
Buypass Class 2 Root CA
=======================
-----BEGIN CERTIFICATE-----
@@ -3146,4 +3073,113 @@ vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+
CAezNIm8BZ/3Hobui3A=
-----END CERTIFICATE-----
GLOBALTRUST 2020
================
-----BEGIN CERTIFICATE-----
MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQVQx
IzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVT
VCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAh
BgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAy
MDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWi
D59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9ZYybNpyrO
VPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3QWPKzv9pj2gOlTblzLmM
CcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPwyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCm
fecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKA
A1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9OR
JitHHmkHr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlG
DfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvU
clOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQ
mjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUw
AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1Ud
IwQYMBaAFNwuH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA
VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw
4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9
iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS
8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2
HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxS
vTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6CMUO+1918
oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn4rnvyOL2NSl6dPrFf4IF
YqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlxfv1k7/9nR4hYJS8+hge9+6jl
gqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg==
-----END CERTIFICATE-----
ANF Secure Server Root CA
=========================
-----BEGIN CERTIFICATE-----
MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2MzI4
NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lv
bjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNVBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3Qg
Q0EwHhcNMTkwOTA0MTAwMDM4WhcNMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEw
MQswCQYDVQQGEwJFUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQw
EgYDVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9vdCBDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCjcqQZAZ2cC4Ffc0m6p6zz
BE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9qyGFOtibBTI3/TO80sh9l2Ll49a2pcbnv
T1gdpd50IJeh7WhM3pIXS7yr/2WanvtH2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcv
B2VSAKduyK9o7PQUlrZXH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXse
zx76W0OLzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyRp1RM
VwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQzW7i1o0TJrH93PB0j
7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/SiOL9V8BY9KHcyi1Swr1+KuCLH5z
JTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJnLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe
8TZBAQIvfXOn3kLMTOmJDVb3n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVO
Hj1tyRRM4y5Bu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj
o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAOBgNVHQ8BAf8E
BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEATh65isagmD9uw2nAalxJ
UqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzx
j6ptBZNscsdW699QIyjlRRA96Gejrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDt
dD+4E5UGUcjohybKpFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM
5gf0vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjqOknkJjCb
5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ/zo1PqVUSlJZS2Db7v54
EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ92zg/LFis6ELhDtjTO0wugumDLmsx2d1H
hk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGy
g77FGr8H6lnco4g175x2MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3
r5+qPeoott7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw=
-----END CERTIFICATE-----
Certum EC-384 CA
================
-----BEGIN CERTIFICATE-----
MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQswCQYDVQQGEwJQ
TDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
dGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2
MDcyNDU0WhcNNDMwMzI2MDcyNDU0WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERh
dGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
GTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATEKI6rGFtq
vm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7TmFy8as10CW4kjPMIRBSqn
iBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68KjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD
VR0OBBYEFI0GZnQkdjrzife81r1HfS+8EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNo
ADBlAjADVS2m5hjEfO/JUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0
QoSZ/6vnnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k=
-----END CERTIFICATE-----
Certum Trusted Root CA
======================
-----BEGIN CERTIFICATE-----
MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQG
EwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0g
Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0Ew
HhcNMTgwMzE2MTIxMDEzWhcNNDMwMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMY
QXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBB
dXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZn0EGze2jusDbCSzBfN8p
fktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/qp1x4EaTByIVcJdPTsuclzxFUl6s1wB52
HO8AU5853BSlLCIls3Jy/I2z5T4IHhQqNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2
fJmItdUDmj0VDT06qKhF8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGt
g/BKEiJ3HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGamqi4
NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi7VdNIuJGmj8PkTQk
fVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSFytKAQd8FqKPVhJBPC/PgP5sZ0jeJ
P/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0PqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSY
njYJdmZm/Bo/6khUHL4wvYBQv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHK
HRzQ+8S1h9E6Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1
vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIBAEii1QAL
LtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4WxmB82M+w85bj/UvXgF2Ez8s
ALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvozMrnadyHncI013nR03e4qllY/p0m+jiGPp2K
h2RX5Rc64vmNueMzeMGQ2Ljdt4NR5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8
CYyqOhNf6DR5UMEQGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA
4kZf5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq0Uc9Nneo
WWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7DP78v3DSk+yshzWePS/Tj
6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTMqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmT
OPQD8rv7gmsHINFSH5pkAnuYZttcTVoP0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZck
bxJF0WddCajJFdr60qZfE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb
-----END CERTIFICATE-----
`
+12 -12
View File
@@ -12,17 +12,17 @@ type Dialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
// DefaultDialer is the Dialer we use by default.
var DefaultDialer = &net.Dialer{
// defaultDialer is the Dialer we use by default.
var defaultDialer = &net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 15 * time.Second,
}
var _ Dialer = DefaultDialer
var _ Dialer = defaultDialer
// DialerResolver is a dialer that uses the configured Resolver to resolver a
// dialerResolver is a dialer that uses the configured Resolver to resolver a
// domain name to IP addresses, and the configured Dialer to connect.
type DialerResolver struct {
type dialerResolver struct {
// Dialer is the underlying Dialer.
Dialer Dialer
@@ -30,10 +30,10 @@ type DialerResolver struct {
Resolver Resolver
}
var _ Dialer = &DialerResolver{}
var _ Dialer = &dialerResolver{}
// DialContext implements Dialer.DialContext.
func (d *DialerResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
func (d *dialerResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
onlyhost, onlyport, err := net.SplitHostPort(address)
if err != nil {
return nil, err
@@ -59,15 +59,15 @@ func (d *DialerResolver) DialContext(ctx context.Context, network, address strin
}
// lookupHost performs a domain name resolution.
func (d *DialerResolver) lookupHost(ctx context.Context, hostname string) ([]string, error) {
func (d *dialerResolver) lookupHost(ctx context.Context, hostname string) ([]string, error) {
if net.ParseIP(hostname) != nil {
return []string{hostname}, nil
}
return d.Resolver.LookupHost(ctx, hostname)
}
// DialerLogger is a Dialer with logging.
type DialerLogger struct {
// dialerLogger is a Dialer with logging.
type dialerLogger struct {
// Dialer is the underlying dialer.
Dialer Dialer
@@ -75,10 +75,10 @@ type DialerLogger struct {
Logger Logger
}
var _ Dialer = &DialerLogger{}
var _ Dialer = &dialerLogger{}
// DialContext implements Dialer.DialContext
func (d *DialerLogger) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
func (d *dialerLogger) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
d.Logger.Debugf("dial %s/%s...", address, network)
start := time.Now()
conn, err := d.Dialer.DialContext(ctx, network, address)
+17 -17
View File
@@ -10,11 +10,11 @@ import (
"time"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/netxmocks"
"github.com/ooni/probe-cli/v3/internal/netxlite/mocks"
)
func TestDialerResolverNoPort(t *testing.T) {
dialer := &DialerResolver{Dialer: &net.Dialer{}, Resolver: DefaultResolver}
dialer := &dialerResolver{Dialer: &net.Dialer{}, Resolver: DefaultResolver}
conn, err := dialer.DialContext(context.Background(), "tcp", "ooni.nu")
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
t.Fatal("not the error we expected", err)
@@ -25,7 +25,7 @@ func TestDialerResolverNoPort(t *testing.T) {
}
func TestDialerResolverLookupHostAddress(t *testing.T) {
dialer := &DialerResolver{Dialer: new(net.Dialer), Resolver: &netxmocks.Resolver{
dialer := &dialerResolver{Dialer: new(net.Dialer), Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, errors.New("we should not call this function")
},
@@ -41,7 +41,7 @@ func TestDialerResolverLookupHostAddress(t *testing.T) {
func TestDialerResolverLookupHostFailure(t *testing.T) {
expected := errors.New("mocked error")
dialer := &DialerResolver{Dialer: new(net.Dialer), Resolver: &netxmocks.Resolver{
dialer := &dialerResolver{Dialer: new(net.Dialer), Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, expected
},
@@ -57,7 +57,7 @@ func TestDialerResolverLookupHostFailure(t *testing.T) {
}
func TestDialerResolverDialForSingleIPFails(t *testing.T) {
dialer := &DialerResolver{Dialer: &netxmocks.Dialer{
dialer := &dialerResolver{Dialer: &mocks.Dialer{
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return nil, io.EOF
},
@@ -72,12 +72,12 @@ func TestDialerResolverDialForSingleIPFails(t *testing.T) {
}
func TestDialerResolverDialForManyIPFails(t *testing.T) {
dialer := &DialerResolver{
Dialer: &netxmocks.Dialer{
dialer := &dialerResolver{
Dialer: &mocks.Dialer{
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return nil, io.EOF
},
}, Resolver: &netxmocks.Resolver{
}, Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{"1.1.1.1", "8.8.8.8"}, nil
},
@@ -92,15 +92,15 @@ func TestDialerResolverDialForManyIPFails(t *testing.T) {
}
func TestDialerResolverDialForManyIPSuccess(t *testing.T) {
dialer := &DialerResolver{Dialer: &netxmocks.Dialer{
dialer := &dialerResolver{Dialer: &mocks.Dialer{
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return &netxmocks.Conn{
return &mocks.Conn{
MockClose: func() error {
return nil
},
}, nil
},
}, Resolver: &netxmocks.Resolver{
}, Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{"1.1.1.1", "8.8.8.8"}, nil
},
@@ -116,10 +116,10 @@ func TestDialerResolverDialForManyIPSuccess(t *testing.T) {
}
func TestDialerLoggerSuccess(t *testing.T) {
d := &DialerLogger{
Dialer: &netxmocks.Dialer{
d := &dialerLogger{
Dialer: &mocks.Dialer{
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return &netxmocks.Conn{
return &mocks.Conn{
MockClose: func() error {
return nil
},
@@ -139,8 +139,8 @@ func TestDialerLoggerSuccess(t *testing.T) {
}
func TestDialerLoggerFailure(t *testing.T) {
d := &DialerLogger{
Dialer: &netxmocks.Dialer{
d := &dialerLogger{
Dialer: &mocks.Dialer{
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return nil, io.EOF
},
@@ -158,7 +158,7 @@ func TestDialerLoggerFailure(t *testing.T) {
func TestDefaultDialerHasTimeout(t *testing.T) {
expected := 15 * time.Second
if DefaultDialer.Timeout != expected {
if defaultDialer.Timeout != expected {
t.Fatal("unexpected timeout value")
}
}
+6 -6
View File
@@ -17,8 +17,8 @@ type HTTPTransport interface {
CloseIdleConnections()
}
// HTTPTransportLogger is an HTTPTransport with logging.
type HTTPTransportLogger struct {
// httpTransportLogger is an HTTPTransport with logging.
type httpTransportLogger struct {
// HTTPTransport is the underlying HTTP transport.
HTTPTransport HTTPTransport
@@ -26,10 +26,10 @@ type HTTPTransportLogger struct {
Logger Logger
}
var _ HTTPTransport = &HTTPTransportLogger{}
var _ HTTPTransport = &httpTransportLogger{}
// RoundTrip implements HTTPTransport.RoundTrip.
func (txp *HTTPTransportLogger) RoundTrip(req *http.Request) (*http.Response, error) {
func (txp *httpTransportLogger) RoundTrip(req *http.Request) (*http.Response, error) {
host := req.Host
if host == "" {
host = req.URL.Host
@@ -39,7 +39,7 @@ func (txp *HTTPTransportLogger) RoundTrip(req *http.Request) (*http.Response, er
}
// logTrip is an HTTP round trip with logging.
func (txp *HTTPTransportLogger) logTrip(req *http.Request) (*http.Response, error) {
func (txp *httpTransportLogger) logTrip(req *http.Request) (*http.Response, error) {
txp.Logger.Debugf("> %s %s", req.Method, req.URL.String())
for key, values := range req.Header {
for _, value := range values {
@@ -63,7 +63,7 @@ func (txp *HTTPTransportLogger) logTrip(req *http.Request) (*http.Response, erro
}
// CloseIdleConnections implement HTTPTransport.CloseIdleConnections.
func (txp *HTTPTransportLogger) CloseIdleConnections() {
func (txp *httpTransportLogger) CloseIdleConnections() {
txp.HTTPTransport.CloseIdleConnections()
}
+3 -3
View File
@@ -8,9 +8,9 @@ import (
)
func TestHTTP3TransportWorks(t *testing.T) {
d := &QUICDialerResolver{
Dialer: &QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
d := &quicDialerResolver{
Dialer: &quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
},
Resolver: &net.Resolver{},
}
+16 -16
View File
@@ -13,14 +13,14 @@ import (
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/atomicx"
"github.com/ooni/probe-cli/v3/internal/iox"
"github.com/ooni/probe-cli/v3/internal/netxmocks"
"github.com/ooni/probe-cli/v3/internal/netxlite/iox"
"github.com/ooni/probe-cli/v3/internal/netxlite/mocks"
)
func TestHTTPTransportLoggerFailure(t *testing.T) {
txp := &HTTPTransportLogger{
txp := &httpTransportLogger{
Logger: log.Log,
HTTPTransport: &netxmocks.HTTPTransport{
HTTPTransport: &mocks.HTTPTransport{
MockRoundTrip: func(req *http.Request) (*http.Response, error) {
return nil, io.EOF
},
@@ -38,9 +38,9 @@ func TestHTTPTransportLoggerFailure(t *testing.T) {
func TestHTTPTransportLoggerFailureWithNoHostHeader(t *testing.T) {
foundHost := &atomicx.Int64{}
txp := &HTTPTransportLogger{
txp := &httpTransportLogger{
Logger: log.Log,
HTTPTransport: &netxmocks.HTTPTransport{
HTTPTransport: &mocks.HTTPTransport{
MockRoundTrip: func(req *http.Request) (*http.Response, error) {
if req.Header.Get("Host") == "www.google.com" {
foundHost.Add(1)
@@ -70,9 +70,9 @@ func TestHTTPTransportLoggerFailureWithNoHostHeader(t *testing.T) {
}
func TestHTTPTransportLoggerSuccess(t *testing.T) {
txp := &HTTPTransportLogger{
txp := &httpTransportLogger{
Logger: log.Log,
HTTPTransport: &netxmocks.HTTPTransport{
HTTPTransport: &mocks.HTTPTransport{
MockRoundTrip: func(req *http.Request) (*http.Response, error) {
return &http.Response{
Body: io.NopCloser(strings.NewReader("")),
@@ -95,8 +95,8 @@ func TestHTTPTransportLoggerSuccess(t *testing.T) {
func TestHTTPTransportLoggerCloseIdleConnections(t *testing.T) {
calls := &atomicx.Int64{}
txp := &HTTPTransportLogger{
HTTPTransport: &netxmocks.HTTPTransport{
txp := &httpTransportLogger{
HTTPTransport: &mocks.HTTPTransport{
MockCloseIdleConnections: func() {
calls.Add(1)
},
@@ -110,11 +110,11 @@ func TestHTTPTransportLoggerCloseIdleConnections(t *testing.T) {
}
func TestHTTPTransportWorks(t *testing.T) {
d := &DialerResolver{
Dialer: DefaultDialer,
d := &dialerResolver{
Dialer: defaultDialer,
Resolver: &net.Resolver{},
}
th := &TLSHandshakerConfigurable{}
th := &tlsHandshakerConfigurable{}
txp := NewHTTPTransport(d, &tls.Config{}, th)
client := &http.Client{Transport: txp}
resp, err := client.Get("https://www.google.com/robots.txt")
@@ -127,8 +127,8 @@ func TestHTTPTransportWorks(t *testing.T) {
func TestHTTPTransportWithFailingDialer(t *testing.T) {
expected := errors.New("mocked error")
d := &DialerResolver{
Dialer: &netxmocks.Dialer{
d := &dialerResolver{
Dialer: &mocks.Dialer{
MockDialContext: func(ctx context.Context,
network, address string) (net.Conn, error) {
return nil, expected
@@ -136,7 +136,7 @@ func TestHTTPTransportWithFailingDialer(t *testing.T) {
},
Resolver: &net.Resolver{},
}
th := &TLSHandshakerConfigurable{}
th := &tlsHandshakerConfigurable{}
txp := NewHTTPTransport(d, &tls.Config{}, th)
client := &http.Client{Transport: txp}
resp, err := client.Get("https://www.google.com/robots.txt")
@@ -19,7 +19,7 @@ import (
"text/template"
"time"
"github.com/ooni/probe-cli/v3/internal/iox"
"github.com/ooni/probe-cli/v3/internal/netxlite/iox"
)
var tmpl = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
@@ -28,7 +28,7 @@ var tmpl = template.Must(template.New("").Parse(`// Code generated by go generat
package netxlite
//{{ .GoGenerate }} go run ./generator/ "{{ .URL }}"
//{{ .GoGenerate }} go run ./internal/generator/ "{{ .URL }}"
const pemcerts string = ` + "`" + `
{{ .Bundle }}
+21
View File
@@ -0,0 +1,21 @@
package iox_test
import (
"context"
"fmt"
"log"
"strings"
"github.com/ooni/probe-cli/v3/internal/netxlite/iox"
)
func ExampleReadAllContext() {
r := strings.NewReader("deadbeef")
ctx := context.Background()
out, err := iox.ReadAllContext(ctx, r)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d\n", len(out))
// Output: 8
}
+58
View File
@@ -0,0 +1,58 @@
// Package iox contains io extensions.
package iox
import (
"context"
"io"
)
// ReadAllContext is like io.ReadAll but reads r in a
// background goroutine. This function will return
// earlier if the context is cancelled. In which case
// we will continue reading from r in the background
// goroutine, and we will discard the result. To stop
// the long-running goroutine, you need to close the
// connection bound to the r reader, if possible.
func ReadAllContext(ctx context.Context, r io.Reader) ([]byte, error) {
datach, errch := make(chan []byte, 1), make(chan error, 1) // buffers
go func() {
data, err := io.ReadAll(r)
if err != nil {
errch <- err
return
}
datach <- data
}()
select {
case data := <-datach:
return data, nil
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errch:
return nil, err
}
}
// CopyContext is like io.Copy but may terminate earlier
// when the context expires. This function has the same
// caveats of ReadAllContext regarding the temporary leaking
// of the background goroutine used to do I/O.
func CopyContext(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) {
countch, errch := make(chan int64, 1), make(chan error, 1) // buffers
go func() {
count, err := io.Copy(dst, src)
if err != nil {
errch <- err
return
}
countch <- count
}()
select {
case count := <-countch:
return count, nil
case <-ctx.Done():
return 0, ctx.Err()
case err := <-errch:
return 0, err
}
}
+134
View File
@@ -0,0 +1,134 @@
package iox
import (
"context"
"errors"
"io"
"strings"
"testing"
"time"
"github.com/ooni/probe-cli/v3/internal/netxlite/mocks"
)
func TestReadAllContextCommonCase(t *testing.T) {
r := strings.NewReader("deadbeef")
ctx := context.Background()
out, err := ReadAllContext(ctx, r)
if err != nil {
t.Fatal(err)
}
if len(out) != 8 {
t.Fatal("not the expected number of bytes")
}
}
func TestReadAllContextWithError(t *testing.T) {
expected := errors.New("mocked error")
r := &mocks.Reader{
MockRead: func(b []byte) (int, error) {
return 0, expected
},
}
ctx := context.Background()
out, err := ReadAllContext(ctx, r)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if len(out) != 0 {
t.Fatal("not the expected number of bytes")
}
}
func TestReadAllContextWithCancelledContext(t *testing.T) {
r := strings.NewReader("deadbeef")
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
out, err := ReadAllContext(ctx, r)
if !errors.Is(err, context.Canceled) {
t.Fatal("not the error we expected", err)
}
if len(out) != 0 {
t.Fatal("not the expected number of bytes")
}
}
func TestReadAllContextWithErrorAndCancelledContext(t *testing.T) {
expected := errors.New("mocked error")
r := &mocks.Reader{
MockRead: func(b []byte) (int, error) {
time.Sleep(time.Millisecond)
return 0, expected
},
}
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
out, err := ReadAllContext(ctx, r)
if !errors.Is(err, context.Canceled) {
t.Fatal("not the error we expected", err)
}
if len(out) != 0 {
t.Fatal("not the expected number of bytes")
}
}
func TestCopyContextCommonCase(t *testing.T) {
r := strings.NewReader("deadbeef")
ctx := context.Background()
out, err := CopyContext(ctx, io.Discard, r)
if err != nil {
t.Fatal(err)
}
if out != 8 {
t.Fatal("not the expected number of bytes")
}
}
func TestCopyContextWithError(t *testing.T) {
expected := errors.New("mocked error")
r := &mocks.Reader{
MockRead: func(b []byte) (int, error) {
return 0, expected
},
}
ctx := context.Background()
out, err := CopyContext(ctx, io.Discard, r)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if out != 0 {
t.Fatal("not the expected number of bytes")
}
}
func TestCopyContextWithCancelledContext(t *testing.T) {
r := strings.NewReader("deadbeef")
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
out, err := CopyContext(ctx, io.Discard, r)
if !errors.Is(err, context.Canceled) {
t.Fatal("not the error we expected", err)
}
if out != 0 {
t.Fatal("not the expected number of bytes")
}
}
func TestCopyContextWithErrorAndCancelledContext(t *testing.T) {
expected := errors.New("mocked error")
r := &mocks.Reader{
MockRead: func(b []byte) (int, error) {
time.Sleep(time.Millisecond)
return 0, expected
},
}
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
out, err := CopyContext(ctx, io.Discard, r)
if !errors.Is(err, context.Canceled) {
t.Fatal("not the error we expected", err)
}
if out != 0 {
t.Fatal("not the expected number of bytes")
}
}
+22
View File
@@ -37,3 +37,25 @@ func reduceErrors(errorslist []error) error {
// TODO(bassosimone): handle this case in a better way
return errorslist[0]
}
// These vars export internal names to legacy ooni/probe-cli code.
var (
DefaultDialer = defaultDialer
DefaultTLSHandshaker = defaultTLSHandshaker
)
// These types export internal names to legacy ooni/probe-cli code.
type (
DialerResolver = dialerResolver
DialerLogger = dialerLogger
HTTPTransportLogger = httpTransportLogger
QUICListenerStdlib = quicListenerStdlib
QUICDialerQUICGo = quicDialerQUICGo
QUICDialerResolver = quicDialerResolver
QUICDialerLogger = quicDialerLogger
ResolverSystem = resolverSystem
ResolverLogger = resolverLogger
ResolverIDNA = resolverIDNA
TLSHandshakerConfigurable = tlsHandshakerConfigurable
TLSHandshakerLogger = tlsHandshakerLogger
)
+60
View File
@@ -0,0 +1,60 @@
package mocks
import (
"net"
"time"
)
// Conn is a mockable net.Conn.
type Conn struct {
MockRead func(b []byte) (int, error)
MockWrite func(b []byte) (int, error)
MockClose func() error
MockLocalAddr func() net.Addr
MockRemoteAddr func() net.Addr
MockSetDeadline func(t time.Time) error
MockSetReadDeadline func(t time.Time) error
MockSetWriteDeadline func(t time.Time) error
}
// Read calls MockRead.
func (c *Conn) Read(b []byte) (int, error) {
return c.MockRead(b)
}
// Write calls MockWrite.
func (c *Conn) Write(b []byte) (int, error) {
return c.MockWrite(b)
}
// Close calls MockClose.
func (c *Conn) Close() error {
return c.MockClose()
}
// LocalAddr calls MockLocalAddr.
func (c *Conn) LocalAddr() net.Addr {
return c.MockLocalAddr()
}
// RemoteAddr calls MockRemoteAddr.
func (c *Conn) RemoteAddr() net.Addr {
return c.MockRemoteAddr()
}
// SetDeadline calls MockSetDeadline.
func (c *Conn) SetDeadline(t time.Time) error {
return c.MockSetDeadline(t)
}
// SetReadDeadline calls MockSetReadDeadline.
func (c *Conn) SetReadDeadline(t time.Time) error {
return c.MockSetReadDeadline(t)
}
// SetWriteDeadline calls MockSetWriteDeadline.
func (c *Conn) SetWriteDeadline(t time.Time) error {
return c.MockSetWriteDeadline(t)
}
var _ net.Conn = &Conn{}
+126
View File
@@ -0,0 +1,126 @@
package mocks
import (
"errors"
"net"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestConnReadWorks(t *testing.T) {
expected := errors.New("mocked error")
c := &Conn{
MockRead: func(b []byte) (int, error) {
return 0, expected
},
}
count, err := c.Read(make([]byte, 128))
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if count != 0 {
t.Fatal("expected 0 bytes")
}
}
func TestConnWriteWorks(t *testing.T) {
expected := errors.New("mocked error")
c := &Conn{
MockWrite: func(b []byte) (int, error) {
return 0, expected
},
}
count, err := c.Write(make([]byte, 128))
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if count != 0 {
t.Fatal("expected 0 bytes")
}
}
func TestConnCloseWorks(t *testing.T) {
expected := errors.New("mocked error")
c := &Conn{
MockClose: func() error {
return expected
},
}
err := c.Close()
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
}
func TestConnLocalAddrWorks(t *testing.T) {
expected := &net.TCPAddr{
IP: net.IPv6loopback,
Port: 1234,
}
c := &Conn{
MockLocalAddr: func() net.Addr {
return expected
},
}
out := c.LocalAddr()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatal(diff)
}
}
func TestConnRemoteAddrWorks(t *testing.T) {
expected := &net.TCPAddr{
IP: net.IPv6loopback,
Port: 1234,
}
c := &Conn{
MockRemoteAddr: func() net.Addr {
return expected
},
}
out := c.RemoteAddr()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatal(diff)
}
}
func TestConnSetDeadline(t *testing.T) {
expected := errors.New("mocked error")
c := &Conn{
MockSetDeadline: func(t time.Time) error {
return expected
},
}
err := c.SetDeadline(time.Time{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestConnSetReadDeadline(t *testing.T) {
expected := errors.New("mocked error")
c := &Conn{
MockSetReadDeadline: func(t time.Time) error {
return expected
},
}
err := c.SetReadDeadline(time.Time{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestConnSetWriteDeadline(t *testing.T) {
expected := errors.New("mocked error")
c := &Conn{
MockSetWriteDeadline: func(t time.Time) error {
return expected
},
}
err := c.SetWriteDeadline(time.Time{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
+16
View File
@@ -0,0 +1,16 @@
package mocks
import (
"context"
"net"
)
// Dialer is a mockable Dialer.
type Dialer struct {
MockDialContext func(ctx context.Context, network, address string) (net.Conn, error)
}
// DialContext calls MockDialContext.
func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return d.MockDialContext(ctx, network, address)
}
+25
View File
@@ -0,0 +1,25 @@
package mocks
import (
"context"
"errors"
"net"
"testing"
)
func TestDialerWorks(t *testing.T) {
expected := errors.New("mocked error")
d := Dialer{
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return nil, expected
},
}
ctx := context.Background()
conn, err := d.DialContext(ctx, "tcp", "8.8.8.8:53")
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if conn != nil {
t.Fatal("expected nil conn")
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package mocks contains mocks for netx types.
package mocks
+19
View File
@@ -0,0 +1,19 @@
package mocks
import "net/http"
// HTTPTransport mocks netxlite.HTTPTransport.
type HTTPTransport struct {
MockRoundTrip func(req *http.Request) (*http.Response, error)
MockCloseIdleConnections func()
}
// RoundTrip calls MockRoundTrip.
func (txp *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return txp.MockRoundTrip(req)
}
// CloseIdleConnections calls MockCloseIdleConnections.
func (txp *HTTPTransport) CloseIdleConnections() {
txp.MockCloseIdleConnections()
}
+38
View File
@@ -0,0 +1,38 @@
package mocks
import (
"errors"
"net/http"
"testing"
"github.com/ooni/probe-cli/v3/internal/atomicx"
)
func TestHTTPTransportRoundTrip(t *testing.T) {
expected := errors.New("mocked error")
txp := &HTTPTransport{
MockRoundTrip: func(req *http.Request) (*http.Response, error) {
return nil, expected
},
}
resp, err := txp.RoundTrip(&http.Request{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if resp != nil {
t.Fatal("expected nil response here")
}
}
func TestHTTPTransportCloseIdleConnections(t *testing.T) {
called := &atomicx.Int64{}
txp := &HTTPTransport{
MockCloseIdleConnections: func() {
called.Add(1)
},
}
txp.CloseIdleConnections()
if called.Load() != 1 {
t.Fatal("not called")
}
}
+197
View File
@@ -0,0 +1,197 @@
package mocks
import (
"context"
"crypto/tls"
"net"
"syscall"
"time"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/netxlite/quicx"
)
// QUICListener is a mockable netxlite.QUICListener.
type QUICListener struct {
MockListen func(addr *net.UDPAddr) (quicx.UDPLikeConn, error)
}
// Listen calls MockListen.
func (ql *QUICListener) Listen(addr *net.UDPAddr) (quicx.UDPLikeConn, error) {
return ql.MockListen(addr)
}
// QUICContextDialer is a mockable netxlite.QUICContextDialer.
type QUICContextDialer struct {
MockDialContext func(ctx context.Context, network, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error)
}
// DialContext calls MockDialContext.
func (qcd *QUICContextDialer) DialContext(ctx context.Context, network, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
return qcd.MockDialContext(ctx, network, address, tlsConfig, quicConfig)
}
// QUICEarlySession is a mockable quic.EarlySession.
type QUICEarlySession struct {
MockAcceptStream func(context.Context) (quic.Stream, error)
MockAcceptUniStream func(context.Context) (quic.ReceiveStream, error)
MockOpenStream func() (quic.Stream, error)
MockOpenStreamSync func(ctx context.Context) (quic.Stream, error)
MockOpenUniStream func() (quic.SendStream, error)
MockOpenUniStreamSync func(ctx context.Context) (quic.SendStream, error)
MockLocalAddr func() net.Addr
MockRemoteAddr func() net.Addr
MockCloseWithError func(code quic.ApplicationErrorCode, reason string) error
MockContext func() context.Context
MockConnectionState func() quic.ConnectionState
MockHandshakeComplete func() context.Context
MockNextSession func() quic.Session
MockSendMessage func(b []byte) error
MockReceiveMessage func() ([]byte, error)
}
var _ quic.EarlySession = &QUICEarlySession{}
// AcceptStream calls MockAcceptStream.
func (s *QUICEarlySession) AcceptStream(ctx context.Context) (quic.Stream, error) {
return s.MockAcceptStream(ctx)
}
// AcceptUniStream calls MockAcceptUniStream.
func (s *QUICEarlySession) AcceptUniStream(ctx context.Context) (quic.ReceiveStream, error) {
return s.MockAcceptUniStream(ctx)
}
// OpenStream calls MockOpenStream.
func (s *QUICEarlySession) OpenStream() (quic.Stream, error) {
return s.MockOpenStream()
}
// OpenStreamSync calls MockOpenStreamSync.
func (s *QUICEarlySession) OpenStreamSync(ctx context.Context) (quic.Stream, error) {
return s.MockOpenStreamSync(ctx)
}
// OpenUniStream calls MockOpenUniStream.
func (s *QUICEarlySession) OpenUniStream() (quic.SendStream, error) {
return s.MockOpenUniStream()
}
// OpenUniStreamSync calls MockOpenUniStreamSync.
func (s *QUICEarlySession) OpenUniStreamSync(ctx context.Context) (quic.SendStream, error) {
return s.MockOpenUniStreamSync(ctx)
}
// LocalAddr class MockLocalAddr.
func (c *QUICEarlySession) LocalAddr() net.Addr {
return c.MockLocalAddr()
}
// RemoteAddr calls MockRemoteAddr.
func (c *QUICEarlySession) RemoteAddr() net.Addr {
return c.MockRemoteAddr()
}
// CloseWithError calls MockCloseWithError.
func (c *QUICEarlySession) CloseWithError(
code quic.ApplicationErrorCode, reason string) error {
return c.MockCloseWithError(code, reason)
}
// Context calls MockContext.
func (s *QUICEarlySession) Context() context.Context {
return s.MockContext()
}
// ConnectionState calls MockConnectionState.
func (s *QUICEarlySession) ConnectionState() quic.ConnectionState {
return s.MockConnectionState()
}
// HandshakeComplete calls MockHandshakeComplete.
func (s *QUICEarlySession) HandshakeComplete() context.Context {
return s.MockHandshakeComplete()
}
// NextSession calls MockNextSession.
func (s *QUICEarlySession) NextSession() quic.Session {
return s.MockNextSession()
}
// SendMessage calls MockSendMessage.
func (s *QUICEarlySession) SendMessage(b []byte) error {
return s.MockSendMessage(b)
}
// ReceiveMessage calls MockReceiveMessage.
func (s *QUICEarlySession) ReceiveMessage() ([]byte, error) {
return s.MockReceiveMessage()
}
// QUICUDPConn is an UDP conn used by QUIC.
type QUICUDPConn struct {
MockWriteTo func(p []byte, addr net.Addr) (int, error)
MockClose func() error
MockLocalAddr func() net.Addr
MockRemoteAddr func() net.Addr
MockSetDeadline func(t time.Time) error
MockSetReadDeadline func(t time.Time) error
MockSetWriteDeadline func(t time.Time) error
MockReadFrom func(p []byte) (n int, addr net.Addr, err error)
MockSyscallConn func() (syscall.RawConn, error)
MockSetReadBuffer func(n int) error
}
var _ quicx.UDPLikeConn = &QUICUDPConn{}
// WriteTo calls MockWriteTo.
func (c *QUICUDPConn) WriteTo(p []byte, addr net.Addr) (int, error) {
return c.MockWriteTo(p, addr)
}
// Close calls MockClose.
func (c *QUICUDPConn) Close() error {
return c.MockClose()
}
// LocalAddr calls MockLocalAddr.
func (c *QUICUDPConn) LocalAddr() net.Addr {
return c.MockLocalAddr()
}
// RemoteAddr calls MockRemoteAddr.
func (c *QUICUDPConn) RemoteAddr() net.Addr {
return c.MockRemoteAddr()
}
// SetDeadline calls MockSetDeadline.
func (c *QUICUDPConn) SetDeadline(t time.Time) error {
return c.MockSetDeadline(t)
}
// SetReadDeadline calls MockSetReadDeadline.
func (c *QUICUDPConn) SetReadDeadline(t time.Time) error {
return c.MockSetReadDeadline(t)
}
// SetWriteDeadline calls MockSetWriteDeadline.
func (c *QUICUDPConn) SetWriteDeadline(t time.Time) error {
return c.MockSetWriteDeadline(t)
}
// ReadFrom calls MockReadFrom.
func (c *QUICUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
return c.MockReadFrom(b)
}
// SyscallConn calls MockSyscallConn.
func (c *QUICUDPConn) SyscallConn() (syscall.RawConn, error) {
return c.MockSyscallConn()
}
// SetReadBuffer calls MockSetReadBuffer.
func (c *QUICUDPConn) SetReadBuffer(n int) error {
return c.MockSetReadBuffer(n)
}
+422
View File
@@ -0,0 +1,422 @@
package mocks
import (
"context"
"crypto/tls"
"errors"
"net"
"reflect"
"syscall"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/netxlite/quicx"
)
func TestQUICListenerListen(t *testing.T) {
expected := errors.New("mocked error")
ql := &QUICListener{
MockListen: func(addr *net.UDPAddr) (quicx.UDPLikeConn, error) {
return nil, expected
},
}
pconn, err := ql.Listen(&net.UDPAddr{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", expected)
}
if pconn != nil {
t.Fatal("expected nil conn here")
}
}
func TestQUICContextDialerDialContext(t *testing.T) {
expected := errors.New("mocked error")
qcd := &QUICContextDialer{
MockDialContext: func(ctx context.Context, network string, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
return nil, expected
},
}
ctx := context.Background()
tlsConfig := &tls.Config{}
quicConfig := &quic.Config{}
sess, err := qcd.DialContext(ctx, "udp", "dns.google:443", tlsConfig, quicConfig)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if sess != nil {
t.Fatal("expected nil session")
}
}
func TestQUICEarlySessionAcceptStream(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockAcceptStream: func(ctx context.Context) (quic.Stream, error) {
return nil, expected
},
}
ctx := context.Background()
stream, err := sess.AcceptStream(ctx)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if stream != nil {
t.Fatal("expected nil stream here")
}
}
func TestQUICEarlySessionAcceptUniStream(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockAcceptUniStream: func(ctx context.Context) (quic.ReceiveStream, error) {
return nil, expected
},
}
ctx := context.Background()
stream, err := sess.AcceptUniStream(ctx)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if stream != nil {
t.Fatal("expected nil stream here")
}
}
func TestQUICEarlySessionOpenStream(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockOpenStream: func() (quic.Stream, error) {
return nil, expected
},
}
stream, err := sess.OpenStream()
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if stream != nil {
t.Fatal("expected nil stream here")
}
}
func TestQUICEarlySessionOpenStreamSync(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockOpenStreamSync: func(ctx context.Context) (quic.Stream, error) {
return nil, expected
},
}
ctx := context.Background()
stream, err := sess.OpenStreamSync(ctx)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if stream != nil {
t.Fatal("expected nil stream here")
}
}
func TestQUICEarlySessionOpenUniStream(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockOpenUniStream: func() (quic.SendStream, error) {
return nil, expected
},
}
stream, err := sess.OpenUniStream()
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if stream != nil {
t.Fatal("expected nil stream here")
}
}
func TestQUICEarlySessionOpenUniStreamSync(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockOpenUniStreamSync: func(ctx context.Context) (quic.SendStream, error) {
return nil, expected
},
}
ctx := context.Background()
stream, err := sess.OpenUniStreamSync(ctx)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if stream != nil {
t.Fatal("expected nil stream here")
}
}
func TestQUICEarlySessionLocalAddr(t *testing.T) {
sess := &QUICEarlySession{
MockLocalAddr: func() net.Addr {
return &net.UDPAddr{}
},
}
addr := sess.LocalAddr()
if !reflect.ValueOf(addr).Elem().IsZero() {
t.Fatal("expected a zero address here")
}
}
func TestQUICEarlySessionRemoteAddr(t *testing.T) {
sess := &QUICEarlySession{
MockRemoteAddr: func() net.Addr {
return &net.UDPAddr{}
},
}
addr := sess.RemoteAddr()
if !reflect.ValueOf(addr).Elem().IsZero() {
t.Fatal("expected a zero address here")
}
}
func TestQUICEarlySessionCloseWithError(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockCloseWithError: func(
code quic.ApplicationErrorCode, reason string) error {
return expected
},
}
err := sess.CloseWithError(0, "")
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestQUICEarlySessionContext(t *testing.T) {
ctx := context.Background()
sess := &QUICEarlySession{
MockContext: func() context.Context {
return ctx
},
}
out := sess.Context()
if !reflect.DeepEqual(ctx, out) {
t.Fatal("not the context we expected")
}
}
func TestQUICEarlySessionConnectionState(t *testing.T) {
state := quic.ConnectionState{SupportsDatagrams: true}
sess := &QUICEarlySession{
MockConnectionState: func() quic.ConnectionState {
return state
},
}
out := sess.ConnectionState()
if !reflect.DeepEqual(state, out) {
t.Fatal("not the context we expected")
}
}
func TestQUICEarlySessionHandshakeComplete(t *testing.T) {
ctx := context.Background()
sess := &QUICEarlySession{
MockHandshakeComplete: func() context.Context {
return ctx
},
}
out := sess.HandshakeComplete()
if !reflect.DeepEqual(ctx, out) {
t.Fatal("not the context we expected")
}
}
func TestQUICEarlySessionNextSession(t *testing.T) {
next := &QUICEarlySession{}
sess := &QUICEarlySession{
MockNextSession: func() quic.Session {
return next
},
}
out := sess.NextSession()
if !reflect.DeepEqual(next, out) {
t.Fatal("not the context we expected")
}
}
func TestQUICEarlySessionSendMessage(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockSendMessage: func(b []byte) error {
return expected
},
}
b := make([]byte, 17)
err := sess.SendMessage(b)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestQUICEarlySessionReceiveMessage(t *testing.T) {
expected := errors.New("mocked error")
sess := &QUICEarlySession{
MockReceiveMessage: func() ([]byte, error) {
return nil, expected
},
}
b, err := sess.ReceiveMessage()
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if b != nil {
t.Fatal("expected nil buffer here")
}
}
func TestQUICUDPConnWriteTo(t *testing.T) {
expected := errors.New("mocked error")
quc := &QUICUDPConn{
MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
return 0, expected
},
}
pkt := make([]byte, 128)
addr := &net.UDPAddr{}
cnt, err := quc.WriteTo(pkt, addr)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if cnt != 0 {
t.Fatal("expected zero here")
}
}
func TestQUICUDPConnClose(t *testing.T) {
expected := errors.New("mocked error")
quc := &QUICUDPConn{
MockClose: func() error {
return expected
},
}
err := quc.Close()
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestQUICUDPConnLocalAddrWorks(t *testing.T) {
expected := &net.TCPAddr{
IP: net.IPv6loopback,
Port: 1234,
}
c := &QUICUDPConn{
MockLocalAddr: func() net.Addr {
return expected
},
}
out := c.LocalAddr()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatal(diff)
}
}
func TestQUICUDPConnRemoteAddrWorks(t *testing.T) {
expected := &net.TCPAddr{
IP: net.IPv6loopback,
Port: 1234,
}
c := &QUICUDPConn{
MockRemoteAddr: func() net.Addr {
return expected
},
}
out := c.RemoteAddr()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatal(diff)
}
}
func TestQUICUDPConnSetDeadline(t *testing.T) {
expected := errors.New("mocked error")
c := &QUICUDPConn{
MockSetDeadline: func(t time.Time) error {
return expected
},
}
err := c.SetDeadline(time.Time{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestQUICUDPConnSetReadDeadline(t *testing.T) {
expected := errors.New("mocked error")
c := &QUICUDPConn{
MockSetReadDeadline: func(t time.Time) error {
return expected
},
}
err := c.SetReadDeadline(time.Time{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestQUICUDPConnSetWriteDeadline(t *testing.T) {
expected := errors.New("mocked error")
c := &QUICUDPConn{
MockSetWriteDeadline: func(t time.Time) error {
return expected
},
}
err := c.SetWriteDeadline(time.Time{})
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
func TestQUICUDPConnReadFrom(t *testing.T) {
expected := errors.New("mocked error")
quc := &QUICUDPConn{
MockReadFrom: func(b []byte) (int, net.Addr, error) {
return 0, nil, expected
},
}
b := make([]byte, 128)
n, addr, err := quc.ReadFrom(b)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if n != 0 {
t.Fatal("expected zero here")
}
if addr != nil {
t.Fatal("expected nil here")
}
}
func TestQUICUDPConnSyscallConn(t *testing.T) {
expected := errors.New("mocked error")
quc := &QUICUDPConn{
MockSyscallConn: func() (syscall.RawConn, error) {
return nil, expected
},
}
conn, err := quc.SyscallConn()
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("expected nil here")
}
}
func TestQUICUDPConnSetReadBuffer(t *testing.T) {
expected := errors.New("mocked error")
quc := &QUICUDPConn{
MockSetReadBuffer: func(n int) error {
return expected
},
}
err := quc.SetReadBuffer(1 << 10)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
+16
View File
@@ -0,0 +1,16 @@
package mocks
import "io"
// Reader allows to mock any io.Reader.
type Reader struct {
MockRead func(b []byte) (int, error)
}
// MockableReader implements an io.Reader.
var _ io.Reader = &Reader{}
// Read implements io.Reader.Read.
func (r *Reader) Read(b []byte) (int, error) {
return r.MockRead(b)
}
+23
View File
@@ -0,0 +1,23 @@
package mocks
import (
"errors"
"testing"
)
func TestReaderRead(t *testing.T) {
expected := errors.New("mocked error")
r := &Reader{
MockRead: func(b []byte) (int, error) {
return 0, expected
},
}
b := make([]byte, 128)
count, err := r.Read(b)
if !errors.Is(err, expected) {
t.Fatal("unexpected error", err)
}
if count != 0 {
t.Fatal("unexpected count", count)
}
}
+25
View File
@@ -0,0 +1,25 @@
package mocks
import "context"
// Resolver is a mockable Resolver.
type Resolver struct {
MockLookupHost func(ctx context.Context, domain string) ([]string, error)
MockNetwork func() string
MockAddress func() string
}
// LookupHost calls MockLookupHost.
func (r *Resolver) LookupHost(ctx context.Context, domain string) ([]string, error) {
return r.MockLookupHost(ctx, domain)
}
// Address calls MockAddress.
func (r *Resolver) Address() string {
return r.MockAddress()
}
// Network calls MockNetwork.
func (r *Resolver) Network() string {
return r.MockNetwork()
}
+46
View File
@@ -0,0 +1,46 @@
package mocks
import (
"context"
"errors"
"testing"
)
func TestResolverLookupHost(t *testing.T) {
expected := errors.New("mocked error")
r := &Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, expected
},
}
ctx := context.Background()
addrs, err := r.LookupHost(ctx, "dns.google")
if !errors.Is(err, expected) {
t.Fatal("unexpected error", err)
}
if addrs != nil {
t.Fatal("expected nil addr")
}
}
func TestResolverNetwork(t *testing.T) {
r := &Resolver{
MockNetwork: func() string {
return "antani"
},
}
if v := r.Network(); v != "antani" {
t.Fatal("unexpected network", v)
}
}
func TestResolverAddress(t *testing.T) {
r := &Resolver{
MockAddress: func() string {
return "1.1.1.1"
},
}
if v := r.Address(); v != "1.1.1.1" {
t.Fatal("unexpected address", v)
}
}
+25
View File
@@ -0,0 +1,25 @@
package mocks
import "crypto/tls"
// TLSConn allows to mock netxlite.TLSConn.
type TLSConn struct {
// Conn is the embedded mockable Conn.
Conn
// MockConnectionState allows to mock the ConnectionState method.
MockConnectionState func() tls.ConnectionState
// MockHandshake allows to mock the Handshake method.
MockHandshake func() error
}
// ConnectionState calls MockConnectionState.
func (c *TLSConn) ConnectionState() tls.ConnectionState {
return c.MockConnectionState()
}
// Handshake calls MockHandshake.
func (c *TLSConn) Handshake() error {
return c.MockHandshake()
}
+34
View File
@@ -0,0 +1,34 @@
package mocks
import (
"crypto/tls"
"errors"
"reflect"
"testing"
)
func TestTLSConnConnectionState(t *testing.T) {
state := tls.ConnectionState{Version: tls.VersionTLS12}
c := &TLSConn{
MockConnectionState: func() tls.ConnectionState {
return state
},
}
out := c.ConnectionState()
if !reflect.DeepEqual(out, state) {
t.Fatal("not the result we expected")
}
}
func TestTLSConnHandshake(t *testing.T) {
expected := errors.New("mocked error")
c := &TLSConn{
MockHandshake: func() error {
return expected
},
}
err := c.Handshake()
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
}
+19
View File
@@ -0,0 +1,19 @@
package mocks
import (
"context"
"crypto/tls"
"net"
)
// TLSHandshaker is a mockable TLS handshaker.
type TLSHandshaker struct {
MockHandshake func(ctx context.Context, conn net.Conn, config *tls.Config) (
net.Conn, tls.ConnectionState, error)
}
// Handshake calls MockHandshake.
func (th *TLSHandshaker) Handshake(ctx context.Context, conn net.Conn, config *tls.Config) (
net.Conn, tls.ConnectionState, error) {
return th.MockHandshake(ctx, conn, config)
}
@@ -0,0 +1,33 @@
package mocks
import (
"context"
"crypto/tls"
"errors"
"net"
"reflect"
"testing"
)
func TestTLSHandshakerHandshake(t *testing.T) {
expected := errors.New("mocked error")
conn := &Conn{}
ctx := context.Background()
config := &tls.Config{}
th := &TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn,
config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return nil, tls.ConnectionState{}, expected
},
}
tlsConn, connState, err := th.Handshake(ctx, conn, config)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if !reflect.ValueOf(connState).IsZero() {
t.Fatal("expected zero ConnectionState here")
}
if tlsConn != nil {
t.Fatal("expected nil conn here")
}
}
+21 -21
View File
@@ -8,7 +8,7 @@ import (
"strconv"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/quicx"
"github.com/ooni/probe-cli/v3/internal/netxlite/quicx"
)
// QUICContextDialer is a dialer for QUIC using Context.
@@ -26,18 +26,18 @@ type QUICListener interface {
Listen(addr *net.UDPAddr) (quicx.UDPLikeConn, error)
}
// QUICListenerStdlib is a QUICListener using the standard library.
type QUICListenerStdlib struct{}
// quicListenerStdlib is a QUICListener using the standard library.
type quicListenerStdlib struct{}
var _ QUICListener = &QUICListenerStdlib{}
var _ QUICListener = &quicListenerStdlib{}
// Listen implements QUICListener.Listen.
func (qls *QUICListenerStdlib) Listen(addr *net.UDPAddr) (quicx.UDPLikeConn, error) {
func (qls *quicListenerStdlib) Listen(addr *net.UDPAddr) (quicx.UDPLikeConn, error) {
return net.ListenUDP("udp", addr)
}
// QUICDialerQUICGo dials using the lucas-clemente/quic-go library.
type QUICDialerQUICGo struct {
// quicDialerQUICGo dials using the lucas-clemente/quic-go library.
type quicDialerQUICGo struct {
// QUICListener is the underlying QUICListener to use.
QUICListener QUICListener
@@ -47,7 +47,7 @@ type QUICDialerQUICGo struct {
quicConfig *quic.Config) (quic.EarlySession, error)
}
var _ QUICContextDialer = &QUICDialerQUICGo{}
var _ QUICContextDialer = &quicDialerQUICGo{}
// errInvalidIP indicates that a string is not a valid IP.
var errInvalidIP = errors.New("netxlite: invalid IP")
@@ -60,7 +60,7 @@ var errInvalidIP = errors.New("netxlite: invalid IP")
//
// 2. if tlsConfig.NextProtos is empty _and_ the port is 443 or 8853,
// then we configure, respectively, "h3" and "dq".
func (d *QUICDialerQUICGo) DialContext(ctx context.Context, network string,
func (d *quicDialerQUICGo) DialContext(ctx context.Context, network string,
address string, tlsConfig *tls.Config, quicConfig *quic.Config) (
quic.EarlySession, error) {
onlyhost, onlyport, err := net.SplitHostPort(address)
@@ -89,7 +89,7 @@ func (d *QUICDialerQUICGo) DialContext(ctx context.Context, network string,
return &quicSessionOwnsConn{EarlySession: sess, conn: pconn}, nil
}
func (d *QUICDialerQUICGo) dialEarlyContext(ctx context.Context,
func (d *quicDialerQUICGo) dialEarlyContext(ctx context.Context,
pconn net.PacketConn, remoteAddr net.Addr, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
if d.mockDialEarlyContext != nil {
@@ -102,7 +102,7 @@ func (d *QUICDialerQUICGo) dialEarlyContext(ctx context.Context,
// maybeApplyTLSDefaults ensures that we're using our certificate pool, if
// needed, and that we use a suitable ALPN, if needed, for h3 and dq.
func (d *QUICDialerQUICGo) maybeApplyTLSDefaults(config *tls.Config, port int) *tls.Config {
func (d *quicDialerQUICGo) maybeApplyTLSDefaults(config *tls.Config, port int) *tls.Config {
config = config.Clone()
if config.RootCAs == nil {
config.RootCAs = defaultCertPool
@@ -136,9 +136,9 @@ func (sess *quicSessionOwnsConn) CloseWithError(
return err
}
// QUICDialerResolver is a dialer that uses the configured Resolver
// quicDialerResolver is a dialer that uses the configured Resolver
// to resolve a domain name to IP addrs.
type QUICDialerResolver struct {
type quicDialerResolver struct {
// Dialer is the underlying QUIC dialer.
Dialer QUICContextDialer
@@ -146,14 +146,14 @@ type QUICDialerResolver struct {
Resolver Resolver
}
var _ QUICContextDialer = &QUICDialerResolver{}
var _ QUICContextDialer = &quicDialerResolver{}
// DialContext implements QUICContextDialer.DialContext. This function
// will apply the following TLS defaults:
//
// 1. if tlsConfig.ServerName is empty, we will use the hostname
// contained inside of the `address` endpoint.
func (d *QUICDialerResolver) DialContext(
func (d *quicDialerResolver) DialContext(
ctx context.Context, network, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
onlyhost, onlyport, err := net.SplitHostPort(address)
@@ -183,7 +183,7 @@ func (d *QUICDialerResolver) DialContext(
}
// maybeApplyTLSDefaults sets the SNI if it's not already configured.
func (d *QUICDialerResolver) maybeApplyTLSDefaults(config *tls.Config, host string) *tls.Config {
func (d *quicDialerResolver) maybeApplyTLSDefaults(config *tls.Config, host string) *tls.Config {
config = config.Clone()
if config.ServerName == "" {
config.ServerName = host
@@ -192,15 +192,15 @@ func (d *QUICDialerResolver) maybeApplyTLSDefaults(config *tls.Config, host stri
}
// lookupHost performs a domain name resolution.
func (d *QUICDialerResolver) lookupHost(ctx context.Context, hostname string) ([]string, error) {
func (d *quicDialerResolver) lookupHost(ctx context.Context, hostname string) ([]string, error) {
if net.ParseIP(hostname) != nil {
return []string{hostname}, nil
}
return d.Resolver.LookupHost(ctx, hostname)
}
// QUICDialerLogger is a dialer with logging.
type QUICDialerLogger struct {
// quicDialerLogger is a dialer with logging.
type quicDialerLogger struct {
// Dialer is the underlying QUIC dialer.
Dialer QUICContextDialer
@@ -208,10 +208,10 @@ type QUICDialerLogger struct {
Logger Logger
}
var _ QUICContextDialer = &QUICDialerLogger{}
var _ QUICContextDialer = &quicDialerLogger{}
// DialContext implements QUICContextDialer.DialContext.
func (d *QUICDialerLogger) DialContext(
func (d *quicDialerLogger) DialContext(
ctx context.Context, network, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
d.Logger.Debugf("quic %s/%s...", address, network)
+35 -35
View File
@@ -11,16 +11,16 @@ import (
"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/netxmocks"
"github.com/ooni/probe-cli/v3/internal/quicx"
"github.com/ooni/probe-cli/v3/internal/netxlite/mocks"
"github.com/ooni/probe-cli/v3/internal/netxlite/quicx"
)
func TestQUICDialerQUICGoCannotSplitHostPort(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "www.google.com",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
systemdialer := quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
}
ctx := context.Background()
sess, err := systemdialer.DialContext(
@@ -37,8 +37,8 @@ func TestQUICDialerQUICGoInvalidPort(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "www.google.com",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
systemdialer := quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
}
ctx := context.Background()
sess, err := systemdialer.DialContext(
@@ -55,8 +55,8 @@ func TestQUICDialerQUICGoInvalidIP(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "www.google.com",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
systemdialer := quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
}
ctx := context.Background()
sess, err := systemdialer.DialContext(
@@ -74,8 +74,8 @@ func TestQUICDialerQUICGoCannotListen(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "www.google.com",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &netxmocks.QUICListener{
systemdialer := quicDialerQUICGo{
QUICListener: &mocks.QUICListener{
MockListen: func(addr *net.UDPAddr) (quicx.UDPLikeConn, error) {
return nil, expected
},
@@ -96,8 +96,8 @@ func TestQUICDialerQUICGoCannotPerformHandshake(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "dns.google",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
systemdialer := quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
}
ctx, cancel := context.WithCancel(context.Background())
cancel() // fail immediately
@@ -115,8 +115,8 @@ func TestQUICDialerQUICGoWorksAsIntended(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "dns.google",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
systemdialer := quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
}
ctx := context.Background()
sess, err := systemdialer.DialContext(
@@ -136,8 +136,8 @@ func TestQUICDialerQUICGoTLSDefaultsForWeb(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "dns.google",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
systemdialer := quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn,
remoteAddr net.Addr, host string, tlsConfig *tls.Config,
quicConfig *quic.Config) (quic.EarlySession, error) {
@@ -177,8 +177,8 @@ func TestQUICDialerQUICGoTLSDefaultsForDoQ(t *testing.T) {
tlsConfig := &tls.Config{
ServerName: "dns.google",
}
systemdialer := QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
systemdialer := quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
mockDialEarlyContext: func(ctx context.Context, pconn net.PacketConn,
remoteAddr net.Addr, host string, tlsConfig *tls.Config,
quicConfig *quic.Config) (quic.EarlySession, error) {
@@ -214,9 +214,9 @@ func TestQUICDialerQUICGoTLSDefaultsForDoQ(t *testing.T) {
func TestQUICDialerResolverSuccess(t *testing.T) {
tlsConfig := &tls.Config{}
dialer := &QUICDialerResolver{
Resolver: &net.Resolver{}, Dialer: &QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
dialer := &quicDialerResolver{
Resolver: &net.Resolver{}, Dialer: &quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
}}
sess, err := dialer.DialContext(
context.Background(), "udp", "www.google.com:443",
@@ -232,8 +232,8 @@ func TestQUICDialerResolverSuccess(t *testing.T) {
func TestQUICDialerResolverNoPort(t *testing.T) {
tlsConfig := &tls.Config{}
dialer := &QUICDialerResolver{
Resolver: new(net.Resolver), Dialer: &QUICDialerQUICGo{}}
dialer := &quicDialerResolver{
Resolver: new(net.Resolver), Dialer: &quicDialerQUICGo{}}
sess, err := dialer.DialContext(
context.Background(), "udp", "www.google.com",
tlsConfig, &quic.Config{})
@@ -246,7 +246,7 @@ func TestQUICDialerResolverNoPort(t *testing.T) {
}
func TestQUICDialerResolverLookupHostAddress(t *testing.T) {
dialer := &QUICDialerResolver{Resolver: &netxmocks.Resolver{
dialer := &quicDialerResolver{Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
// We should not arrive here and call this function but if we do then
// there is going to be an error that fails this test.
@@ -265,7 +265,7 @@ func TestQUICDialerResolverLookupHostAddress(t *testing.T) {
func TestQUICDialerResolverLookupHostFailure(t *testing.T) {
tlsConfig := &tls.Config{}
expected := errors.New("mocked error")
dialer := &QUICDialerResolver{Resolver: &netxmocks.Resolver{
dialer := &quicDialerResolver{Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, expected
},
@@ -285,9 +285,9 @@ func TestQUICDialerResolverInvalidPort(t *testing.T) {
// This test allows us to check for the case where every attempt
// to establish a connection leads to a failure
tlsConf := &tls.Config{}
dialer := &QUICDialerResolver{
Resolver: new(net.Resolver), Dialer: &QUICDialerQUICGo{
QUICListener: &QUICListenerStdlib{},
dialer := &quicDialerResolver{
Resolver: new(net.Resolver), Dialer: &quicDialerQUICGo{
QUICListener: &quicListenerStdlib{},
}}
sess, err := dialer.DialContext(
context.Background(), "udp", "www.google.com:0",
@@ -308,8 +308,8 @@ func TestQUICDialerResolverApplyTLSDefaults(t *testing.T) {
expected := errors.New("mocked error")
var gotTLSConfig *tls.Config
tlsConfig := &tls.Config{}
dialer := &QUICDialerResolver{
Resolver: new(net.Resolver), Dialer: &netxmocks.QUICContextDialer{
dialer := &quicDialerResolver{
Resolver: new(net.Resolver), Dialer: &mocks.QUICContextDialer{
MockDialContext: func(ctx context.Context, network, address string,
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
gotTLSConfig = tlsConfig
@@ -334,12 +334,12 @@ func TestQUICDialerResolverApplyTLSDefaults(t *testing.T) {
}
func TestQUICDialerLoggerSuccess(t *testing.T) {
d := &QUICDialerLogger{
Dialer: &netxmocks.QUICContextDialer{
d := &quicDialerLogger{
Dialer: &mocks.QUICContextDialer{
MockDialContext: func(ctx context.Context, network string,
address string, tlsConfig *tls.Config,
quicConfig *quic.Config) (quic.EarlySession, error) {
return &netxmocks.QUICEarlySession{
return &mocks.QUICEarlySession{
MockCloseWithError: func(
code quic.ApplicationErrorCode, reason string) error {
return nil
@@ -363,8 +363,8 @@ func TestQUICDialerLoggerSuccess(t *testing.T) {
func TestQUICDialerLoggerFailure(t *testing.T) {
expected := errors.New("mocked error")
d := &QUICDialerLogger{
Dialer: &netxmocks.QUICContextDialer{
d := &quicDialerLogger{
Dialer: &mocks.QUICContextDialer{
MockDialContext: func(ctx context.Context, network string,
address string, tlsConfig *tls.Config,
quicConfig *quic.Config) (quic.EarlySession, error) {
+40
View File
@@ -0,0 +1,40 @@
// Package quicx contains lucas-clemente/quic-go extensions.
//
// This code introduces the UDPLikeConn, whose documentation explain
// why we need to introduce this new type. We could not put this
// code inside an existing package because it's used (as of 20 Aug 2021)
// by the netxlite package as well as by the mocks package.
package quicx
import (
"net"
"syscall"
)
// UDPLikeConn is a net.PacketConn with some extra functions
// required to convince the QUIC library (lucas-clemente/quic-go)
// to inflate the receive buffer of the connection.
//
// The QUIC library will treat this connection as a "dumb"
// net.PacketConn, calling its ReadFrom and WriteTo methods
// as opposed to more advanced methods that are available
// under Linux and FreeBSD and improve the performance.
//
// It seems fine to avoid performance optimizations, because
// they would complicate the implementation on our side and
// our use cases (blocking and heavy throttling) do not seem
// to require such optimizations.
//
// See https://github.com/ooni/probe/issues/1754 for a more
// comprehensive discussion of UDPLikeConn.
type UDPLikeConn interface {
// An UDPLikeConn is a net.PacketConn conn.
net.PacketConn
// SetReadBuffer allows setting the read buffer.
SetReadBuffer(bytes int) error
// SyscallConn returns a conn suitable for calling syscalls,
// which is also instrumental to setting the read buffer.
SyscallConn() (syscall.RawConn, error)
}
+18 -18
View File
@@ -14,39 +14,39 @@ type Resolver interface {
LookupHost(ctx context.Context, hostname string) (addrs []string, err error)
}
// ResolverSystem is the system resolver.
type ResolverSystem struct{}
// resolverSystem is the system resolver.
type resolverSystem struct{}
var _ Resolver = &ResolverSystem{}
var _ Resolver = &resolverSystem{}
// LookupHost implements Resolver.LookupHost.
func (r *ResolverSystem) LookupHost(ctx context.Context, hostname string) ([]string, error) {
func (r *resolverSystem) LookupHost(ctx context.Context, hostname string) ([]string, error) {
return net.DefaultResolver.LookupHost(ctx, hostname)
}
// Network implements Resolver.Network.
func (r *ResolverSystem) Network() string {
func (r *resolverSystem) Network() string {
return "system"
}
// Address implements Resolver.Address.
func (r *ResolverSystem) Address() string {
func (r *resolverSystem) Address() string {
return ""
}
// DefaultResolver is the resolver we use by default.
var DefaultResolver = &ResolverSystem{}
var DefaultResolver = &resolverSystem{}
// ResolverLogger is a resolver that emits events
type ResolverLogger struct {
// resolverLogger is a resolver that emits events
type resolverLogger struct {
Resolver
Logger Logger
}
var _ Resolver = &ResolverLogger{}
var _ Resolver = &resolverLogger{}
// LookupHost returns the IP addresses of a host
func (r *ResolverLogger) LookupHost(ctx context.Context, hostname string) ([]string, error) {
func (r *resolverLogger) LookupHost(ctx context.Context, hostname string) ([]string, error) {
r.Logger.Debugf("resolve %s...", hostname)
start := time.Now()
addrs, err := r.Resolver.LookupHost(ctx, hostname)
@@ -64,7 +64,7 @@ type resolverNetworker interface {
}
// Network implements Resolver.Network.
func (r *ResolverLogger) Network() string {
func (r *resolverLogger) Network() string {
if rn, ok := r.Resolver.(resolverNetworker); ok {
return rn.Network()
}
@@ -76,22 +76,22 @@ type resolverAddresser interface {
}
// Address implements Resolver.Address.
func (r *ResolverLogger) Address() string {
func (r *resolverLogger) Address() string {
if ra, ok := r.Resolver.(resolverAddresser); ok {
return ra.Address()
}
return ""
}
// ResolverIDNA supports resolving Internationalized Domain Names.
// resolverIDNA supports resolving Internationalized Domain Names.
//
// See RFC3492 for more information.
type ResolverIDNA struct {
type resolverIDNA struct {
Resolver
}
// LookupHost implements Resolver.LookupHost.
func (r *ResolverIDNA) LookupHost(ctx context.Context, hostname string) ([]string, error) {
func (r *resolverIDNA) LookupHost(ctx context.Context, hostname string) ([]string, error) {
host, err := idna.ToASCII(hostname)
if err != nil {
return nil, err
@@ -100,7 +100,7 @@ func (r *ResolverIDNA) LookupHost(ctx context.Context, hostname string) ([]strin
}
// Network implements Resolver.Network.
func (r *ResolverIDNA) Network() string {
func (r *resolverIDNA) Network() string {
if rn, ok := r.Resolver.(resolverNetworker); ok {
return rn.Network()
}
@@ -108,7 +108,7 @@ func (r *ResolverIDNA) Network() string {
}
// Address implements Resolver.Address.
func (r *ResolverIDNA) Address() string {
func (r *resolverIDNA) Address() string {
if ra, ok := r.Resolver.(resolverAddresser); ok {
return ra.Address()
}
+15 -15
View File
@@ -9,11 +9,11 @@ import (
"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/netxmocks"
"github.com/ooni/probe-cli/v3/internal/netxlite/mocks"
)
func TestResolverSystemNetworkAddress(t *testing.T) {
r := ResolverSystem{}
r := resolverSystem{}
if r.Network() != "system" {
t.Fatal("invalid Network")
}
@@ -23,7 +23,7 @@ func TestResolverSystemNetworkAddress(t *testing.T) {
}
func TestResolverSystemWorksAsIntended(t *testing.T) {
r := ResolverSystem{}
r := resolverSystem{}
addrs, err := r.LookupHost(context.Background(), "dns.google.com")
if err != nil {
t.Fatal(err)
@@ -35,9 +35,9 @@ func TestResolverSystemWorksAsIntended(t *testing.T) {
func TestResolverLoggerWithSuccess(t *testing.T) {
expected := []string{"1.1.1.1"}
r := ResolverLogger{
r := resolverLogger{
Logger: log.Log,
Resolver: &netxmocks.Resolver{
Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return expected, nil
},
@@ -54,9 +54,9 @@ func TestResolverLoggerWithSuccess(t *testing.T) {
func TestResolverLoggerWithFailure(t *testing.T) {
expected := errors.New("mocked error")
r := ResolverLogger{
r := resolverLogger{
Logger: log.Log,
Resolver: &netxmocks.Resolver{
Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, expected
},
@@ -72,7 +72,7 @@ func TestResolverLoggerWithFailure(t *testing.T) {
}
func TestResolverLoggerChildNetworkAddress(t *testing.T) {
r := &ResolverLogger{Logger: log.Log, Resolver: DefaultResolver}
r := &resolverLogger{Logger: log.Log, Resolver: DefaultResolver}
if r.Network() != "system" {
t.Fatal("invalid Network")
}
@@ -82,7 +82,7 @@ func TestResolverLoggerChildNetworkAddress(t *testing.T) {
}
func TestResolverLoggerNoChildNetworkAddress(t *testing.T) {
r := &ResolverLogger{Logger: log.Log, Resolver: &net.Resolver{}}
r := &resolverLogger{Logger: log.Log, Resolver: &net.Resolver{}}
if r.Network() != "logger" {
t.Fatal("invalid Network")
}
@@ -93,8 +93,8 @@ func TestResolverLoggerNoChildNetworkAddress(t *testing.T) {
func TestResolverIDNAWorksAsIntended(t *testing.T) {
expectedIPs := []string{"77.88.55.66"}
r := &ResolverIDNA{
Resolver: &netxmocks.Resolver{
r := &resolverIDNA{
Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
if domain != "xn--d1acpjx3f.xn--p1ai" {
return nil, errors.New("passed invalid domain")
@@ -113,8 +113,8 @@ func TestResolverIDNAWorksAsIntended(t *testing.T) {
}
}
func TestIDNAResolverWithInvalidPunycode(t *testing.T) {
r := &ResolverIDNA{Resolver: &netxmocks.Resolver{
func TestResolverIDNAWithInvalidPunycode(t *testing.T) {
r := &resolverIDNA{Resolver: &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, errors.New("should not happen")
},
@@ -131,7 +131,7 @@ func TestIDNAResolverWithInvalidPunycode(t *testing.T) {
}
func TestResolverIDNAChildNetworkAddress(t *testing.T) {
r := &ResolverIDNA{
r := &resolverIDNA{
Resolver: DefaultResolver,
}
if v := r.Network(); v != "system" {
@@ -143,7 +143,7 @@ func TestResolverIDNAChildNetworkAddress(t *testing.T) {
}
func TestResolverIDNANoChildNetworkAddress(t *testing.T) {
r := &ResolverIDNA{}
r := &resolverIDNA{}
if v := r.Network(); v != "idna" {
t.Fatal("invalid network", v)
}
+273
View File
@@ -0,0 +1,273 @@
package netxlite
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"time"
)
var (
tlsVersionString = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
tls.VersionTLS13: "TLSv1.3",
0: "", // guarantee correct behaviour
}
tlsCipherSuiteString = map[uint16]string{
tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA",
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_CBC_SHA256: "TLS_RSA_WITH_AES_128_CBC_SHA256",
tls.TLS_RSA_WITH_AES_128_GCM_SHA256: "TLS_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
tls.TLS_AES_128_GCM_SHA256: "TLS_AES_128_GCM_SHA256",
tls.TLS_AES_256_GCM_SHA384: "TLS_AES_256_GCM_SHA384",
tls.TLS_CHACHA20_POLY1305_SHA256: "TLS_CHACHA20_POLY1305_SHA256",
0: "", // guarantee correct behaviour
}
)
// TLSVersionString returns a TLS version string.
func TLSVersionString(value uint16) string {
if str, found := tlsVersionString[value]; found {
return str
}
return fmt.Sprintf("TLS_VERSION_UNKNOWN_%d", value)
}
// TLSCipherSuiteString returns the TLS cipher suite as a string.
func TLSCipherSuiteString(value uint16) string {
if str, found := tlsCipherSuiteString[value]; found {
return str
}
return fmt.Sprintf("TLS_CIPHER_SUITE_UNKNOWN_%d", value)
}
// NewDefaultCertPool returns a copy of the default x509
// certificate pool that we bundle from Mozilla.
func NewDefaultCertPool() *x509.CertPool {
pool := x509.NewCertPool()
// Assumption: AppendCertsFromPEM cannot fail because we
// run this function already in the generate.go file
pool.AppendCertsFromPEM([]byte(pemcerts))
return pool
}
// ErrInvalidTLSVersion indicates that you passed us a string
// that does not represent a valid TLS version.
var ErrInvalidTLSVersion = errors.New("invalid TLS version")
// ConfigureTLSVersion configures the correct TLS version into
// the specified *tls.Config or returns an error.
func ConfigureTLSVersion(config *tls.Config, version string) error {
switch version {
case "TLSv1.3":
config.MinVersion = tls.VersionTLS13
config.MaxVersion = tls.VersionTLS13
case "TLSv1.2":
config.MinVersion = tls.VersionTLS12
config.MaxVersion = tls.VersionTLS12
case "TLSv1.1":
config.MinVersion = tls.VersionTLS11
config.MaxVersion = tls.VersionTLS11
case "TLSv1.0", "TLSv1":
config.MinVersion = tls.VersionTLS10
config.MaxVersion = tls.VersionTLS10
case "":
// nothing
default:
return ErrInvalidTLSVersion
}
return nil
}
// TLSConn is any tls.Conn-like structure.
type TLSConn interface {
// net.Conn is the embedded conn.
net.Conn
// ConnectionState returns the TLS connection state.
ConnectionState() tls.ConnectionState
// Handshake performs the handshake.
Handshake() error
}
// TLSHandshaker is the generic TLS handshaker.
type TLSHandshaker interface {
// Handshake creates a new TLS connection from the given connection and
// the given config. This function DOES NOT take ownership of the connection
// and it's your responsibility to close it on failure.
Handshake(ctx context.Context, conn net.Conn, config *tls.Config) (
net.Conn, tls.ConnectionState, error)
}
// tlsHandshakerConfigurable is a configurable TLS handshaker that
// uses by default the standard library's TLS implementation.
type tlsHandshakerConfigurable struct {
// NewConn is the OPTIONAL factory for creating a new connection. If
// this factory is not set, we'll use the stdlib.
NewConn func(conn net.Conn, config *tls.Config) TLSConn
// Timeout is the OPTIONAL timeout imposed on the TLS handshake. If zero
// or negative, we will use default timeout of 10 seconds.
Timeout time.Duration
}
var _ TLSHandshaker = &tlsHandshakerConfigurable{}
// defaultCertPool is the cert pool we use by default. We store this
// value into a private variable to enable for unit testing.
var defaultCertPool = NewDefaultCertPool()
// Handshake implements Handshaker.Handshake. This function will
// configure the code to use the built-in Mozilla CA if the config
// field contains a nil RootCAs field.
//
// Bug
//
// Until Go 1.17 is released, this function will not honour
// the context. We'll however always enforce an overall timeout.
func (h *tlsHandshakerConfigurable) Handshake(
ctx context.Context, conn net.Conn, config *tls.Config,
) (net.Conn, tls.ConnectionState, error) {
timeout := h.Timeout
if timeout <= 0 {
timeout = 10 * time.Second
}
defer conn.SetDeadline(time.Time{})
conn.SetDeadline(time.Now().Add(timeout))
if config.RootCAs == nil {
config = config.Clone()
config.RootCAs = defaultCertPool
}
tlsconn := h.newConn(conn, config)
if err := tlsconn.Handshake(); err != nil {
return nil, tls.ConnectionState{}, err
}
return tlsconn, tlsconn.ConnectionState(), nil
}
// newConn creates a new TLSConn.
func (h *tlsHandshakerConfigurable) newConn(conn net.Conn, config *tls.Config) TLSConn {
if h.NewConn != nil {
return h.NewConn(conn, config)
}
return tls.Client(conn, config)
}
// defaultTLSHandshaker is the default TLS handshaker.
var defaultTLSHandshaker = &tlsHandshakerConfigurable{}
// tlsHandshakerLogger is a TLSHandshaker with logging.
type tlsHandshakerLogger struct {
// TLSHandshaker is the underlying handshaker.
TLSHandshaker TLSHandshaker
// Logger is the underlying logger.
Logger Logger
}
var _ TLSHandshaker = &tlsHandshakerLogger{}
// Handshake implements Handshaker.Handshake
func (h *tlsHandshakerLogger) Handshake(
ctx context.Context, conn net.Conn, config *tls.Config,
) (net.Conn, tls.ConnectionState, error) {
h.Logger.Debugf(
"tls {sni=%s next=%+v}...", config.ServerName, config.NextProtos)
start := time.Now()
tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config)
elapsed := time.Since(start)
if err != nil {
h.Logger.Debugf(
"tls {sni=%s next=%+v}... %s in %s", config.ServerName,
config.NextProtos, err, elapsed)
return nil, tls.ConnectionState{}, err
}
h.Logger.Debugf(
"tls {sni=%s next=%+v}... ok in %s {next=%s cipher=%s v=%s}",
config.ServerName, config.NextProtos, elapsed, state.NegotiatedProtocol,
TLSCipherSuiteString(state.CipherSuite),
TLSVersionString(state.Version))
return tlsconn, state, nil
}
// TLSDialer is the TLS dialer
type TLSDialer struct {
// Config is the OPTIONAL tls config.
Config *tls.Config
// Dialer is the MANDATORY dialer.
Dialer Dialer
// TLSHandshaker is the MANDATORY TLS handshaker.
TLSHandshaker TLSHandshaker
}
// DialTLSContext dials a TLS connection.
func (d *TLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
conn, err := d.Dialer.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
config := d.config(host, port)
tlsconn, _, err := d.TLSHandshaker.Handshake(ctx, conn, config)
if err != nil {
conn.Close()
return nil, err
}
return tlsconn, nil
}
// config creates a new config. If d.Config is nil, then we start
// from an empty config. Otherwise, we clone d.Config.
//
// We set the ServerName field if not already set.
//
// We set the ALPN if the port is 443 or 853, if not already set.
func (d *TLSDialer) config(host, port string) *tls.Config {
config := d.Config
if config == nil {
config = &tls.Config{}
}
config = config.Clone() // operate on a clone
if config.ServerName == "" {
config.ServerName = host
}
if len(config.NextProtos) <= 0 {
switch port {
case "443":
config.NextProtos = []string{"h2", "http/1.1"}
case "853":
config.NextProtos = []string{"dot"}
}
}
return config
}
+409
View File
@@ -0,0 +1,409 @@
package netxlite
import (
"context"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/netxlite/mocks"
)
func TestVersionString(t *testing.T) {
if TLSVersionString(tls.VersionTLS13) != "TLSv1.3" {
t.Fatal("not working for existing version")
}
if TLSVersionString(1) != "TLS_VERSION_UNKNOWN_1" {
t.Fatal("not working for nonexisting version")
}
if TLSVersionString(0) != "" {
t.Fatal("not working for zero version")
}
}
func TestCipherSuite(t *testing.T) {
if TLSCipherSuiteString(tls.TLS_AES_128_GCM_SHA256) != "TLS_AES_128_GCM_SHA256" {
t.Fatal("not working for existing cipher suite")
}
if TLSCipherSuiteString(1) != "TLS_CIPHER_SUITE_UNKNOWN_1" {
t.Fatal("not working for nonexisting cipher suite")
}
if TLSCipherSuiteString(0) != "" {
t.Fatal("not working for zero cipher suite")
}
}
func TestNewDefaultCertPoolWorks(t *testing.T) {
pool := NewDefaultCertPool()
if pool == nil {
t.Fatal("expected non-nil value here")
}
}
func TestConfigureTLSVersion(t *testing.T) {
tests := []struct {
name string
version string
wantErr error
versionMin int
versionMax int
}{{
name: "with TLSv1.3",
version: "TLSv1.3",
wantErr: nil,
versionMin: tls.VersionTLS13,
versionMax: tls.VersionTLS13,
}, {
name: "with TLSv1.2",
version: "TLSv1.2",
wantErr: nil,
versionMin: tls.VersionTLS12,
versionMax: tls.VersionTLS12,
}, {
name: "with TLSv1.1",
version: "TLSv1.1",
wantErr: nil,
versionMin: tls.VersionTLS11,
versionMax: tls.VersionTLS11,
}, {
name: "with TLSv1.0",
version: "TLSv1.0",
wantErr: nil,
versionMin: tls.VersionTLS10,
versionMax: tls.VersionTLS10,
}, {
name: "with TLSv1",
version: "TLSv1",
wantErr: nil,
versionMin: tls.VersionTLS10,
versionMax: tls.VersionTLS10,
}, {
name: "with default",
version: "",
wantErr: nil,
versionMin: 0,
versionMax: 0,
}, {
name: "with invalid version",
version: "TLSv999",
wantErr: ErrInvalidTLSVersion,
versionMin: 0,
versionMax: 0,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := new(tls.Config)
err := ConfigureTLSVersion(conf, tt.version)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("not the error we expected: %+v", err)
}
if conf.MinVersion != uint16(tt.versionMin) {
t.Fatalf("not the min version we expected: %+v", conf.MinVersion)
}
if conf.MaxVersion != uint16(tt.versionMax) {
t.Fatalf("not the max version we expected: %+v", conf.MaxVersion)
}
})
}
}
func TestTLSHandshakerConfigurableWithError(t *testing.T) {
var times []time.Time
h := &tlsHandshakerConfigurable{}
tcpConn := &mocks.Conn{
MockWrite: func(b []byte) (int, error) {
return 0, io.EOF
},
MockSetDeadline: func(t time.Time) error {
times = append(times, t)
return nil
},
}
ctx := context.Background()
conn, _, err := h.Handshake(ctx, tcpConn, &tls.Config{
ServerName: "x.org",
})
if err != io.EOF {
t.Fatal("not the error that we expected")
}
if conn != nil {
t.Fatal("expected nil con here")
}
if len(times) != 2 {
t.Fatal("expected two time entries")
}
if !times[0].After(time.Now()) {
t.Fatal("timeout not in the future")
}
if !times[1].IsZero() {
t.Fatal("did not clear timeout on exit")
}
}
func TestTLSHandshakerConfigurableSuccess(t *testing.T) {
handler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(200)
})
srvr := httptest.NewTLSServer(handler)
defer srvr.Close()
URL, err := url.Parse(srvr.URL)
if err != nil {
t.Fatal(err)
}
conn, err := net.Dial("tcp", URL.Host)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
handshaker := &tlsHandshakerConfigurable{}
ctx := context.Background()
config := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
ServerName: URL.Hostname(),
}
tlsConn, connState, err := handshaker.Handshake(ctx, conn, config)
if err != nil {
t.Fatal(err)
}
defer tlsConn.Close()
if connState.Version != tls.VersionTLS13 {
t.Fatal("unexpected TLS version")
}
}
func TestTLSHandshakerConfigurableSetsDefaultRootCAs(t *testing.T) {
expected := errors.New("mocked error")
var gotTLSConfig *tls.Config
handshaker := &tlsHandshakerConfigurable{
NewConn: func(conn net.Conn, config *tls.Config) TLSConn {
gotTLSConfig = config
return &mocks.TLSConn{
MockHandshake: func() error {
return expected
},
}
},
}
ctx := context.Background()
config := &tls.Config{}
conn := &mocks.Conn{
MockSetDeadline: func(t time.Time) error {
return nil
},
}
tlsConn, connState, err := handshaker.Handshake(ctx, conn, config)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if !reflect.ValueOf(connState).IsZero() {
t.Fatal("expected zero connState here")
}
if tlsConn != nil {
t.Fatal("expected nil tlsConn here")
}
if config.RootCAs != nil {
t.Fatal("config.RootCAs should still be nil")
}
if gotTLSConfig.RootCAs != defaultCertPool {
t.Fatal("gotTLSConfig.RootCAs has not been correctly set")
}
}
func TestTLSHandshakerLoggerSuccess(t *testing.T) {
th := &tlsHandshakerLogger{
TLSHandshaker: &mocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return tls.Client(conn, config), tls.ConnectionState{}, nil
},
},
Logger: log.Log,
}
conn := &mocks.Conn{
MockClose: func() error {
return nil
},
}
config := &tls.Config{}
ctx := context.Background()
tlsConn, connState, err := th.Handshake(ctx, conn, config)
if err != nil {
t.Fatal(err)
}
if err := tlsConn.Close(); err != nil {
t.Fatal(err)
}
if !reflect.ValueOf(connState).IsZero() {
t.Fatal("expected zero ConnectionState here")
}
}
func TestTLSHandshakerLoggerFailure(t *testing.T) {
expected := errors.New("mocked error")
th := &tlsHandshakerLogger{
TLSHandshaker: &mocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return nil, tls.ConnectionState{}, expected
},
},
Logger: log.Log,
}
conn := &mocks.Conn{
MockClose: func() error {
return nil
},
}
config := &tls.Config{}
ctx := context.Background()
tlsConn, connState, err := th.Handshake(ctx, conn, config)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if tlsConn != nil {
t.Fatal("expected nil conn here")
}
if !reflect.ValueOf(connState).IsZero() {
t.Fatal("expected zero ConnectionState here")
}
}
func TestTLSDialerFailureSplitHostPort(t *testing.T) {
dialer := &TLSDialer{}
ctx := context.Background()
const address = "www.google.com" // missing port
conn, err := dialer.DialTLSContext(ctx, "tcp", address)
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("connection is not nil")
}
}
func TestTLSDialerFailureDialing(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately fail
dialer := TLSDialer{Dialer: &net.Dialer{}}
conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443")
if err == nil || !strings.HasSuffix(err.Error(), "operation was canceled") {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("connection is not nil")
}
}
func TestTLSDialerFailureHandshaking(t *testing.T) {
ctx := context.Background()
dialer := TLSDialer{
Config: &tls.Config{},
Dialer: &mocks.Dialer{MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return &mocks.Conn{MockWrite: func(b []byte) (int, error) {
return 0, io.EOF
}, MockClose: func() error {
return nil
}, MockSetDeadline: func(t time.Time) error {
return nil
}}, nil
}},
TLSHandshaker: &tlsHandshakerConfigurable{},
}
conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443")
if !errors.Is(err, io.EOF) {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("connection is not nil")
}
}
func TestTLSDialerSuccessHandshaking(t *testing.T) {
ctx := context.Background()
dialer := TLSDialer{
Dialer: &mocks.Dialer{MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return &mocks.Conn{MockWrite: func(b []byte) (int, error) {
return 0, io.EOF
}, MockClose: func() error {
return nil
}, MockSetDeadline: func(t time.Time) error {
return nil
}}, nil
}},
TLSHandshaker: &mocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return tls.Client(conn, config), tls.ConnectionState{}, nil
},
},
}
conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443")
if err != nil {
t.Fatal(err)
}
if conn == nil {
t.Fatal("connection is nil")
}
conn.Close()
}
func TestTLSDialerConfigFromEmptyConfigForWeb(t *testing.T) {
d := &TLSDialer{}
config := d.config("www.google.com", "443")
if config.ServerName != "www.google.com" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"h2", "http/1.1"}); diff != "" {
t.Fatal(diff)
}
}
func TestTLSDialerConfigFromEmptyConfigForDoT(t *testing.T) {
d := &TLSDialer{}
config := d.config("dns.google", "853")
if config.ServerName != "dns.google" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"dot"}); diff != "" {
t.Fatal(diff)
}
}
func TestTLSDialerConfigWithServerName(t *testing.T) {
d := &TLSDialer{
Config: &tls.Config{
ServerName: "example.com",
},
}
config := d.config("dns.google", "853")
if config.ServerName != "example.com" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"dot"}); diff != "" {
t.Fatal(diff)
}
}
func TestTLSDialerConfigWithALPN(t *testing.T) {
d := &TLSDialer{
Config: &tls.Config{
NextProtos: []string{"h2"},
},
}
config := d.config("dns.google", "853")
if config.ServerName != "dns.google" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"h2"}); diff != "" {
t.Fatal(diff)
}
}
-18
View File
@@ -1,18 +0,0 @@
package netxlite
import (
"crypto/tls"
"net"
)
// TLSConn is any tls.Conn-like structure.
type TLSConn interface {
// net.Conn is the embedded conn.
net.Conn
// ConnectionState returns the TLS connection state.
ConnectionState() tls.ConnectionState
// Handshake performs the handshake.
Handshake() error
}
-64
View File
@@ -1,64 +0,0 @@
package netxlite
import (
"context"
"crypto/tls"
"net"
)
// TLSDialer is the TLS dialer
type TLSDialer struct {
// Config is the OPTIONAL tls config.
Config *tls.Config
// Dialer is the MANDATORY dialer.
Dialer Dialer
// TLSHandshaker is the MANDATORY TLS handshaker.
TLSHandshaker TLSHandshaker
}
// DialTLSContext dials a TLS connection.
func (d *TLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
conn, err := d.Dialer.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
config := d.config(host, port)
tlsconn, _, err := d.TLSHandshaker.Handshake(ctx, conn, config)
if err != nil {
conn.Close()
return nil, err
}
return tlsconn, nil
}
// config creates a new config. If d.Config is nil, then we start
// from an empty config. Otherwise, we clone d.Config.
//
// We set the ServerName field if not already set.
//
// We set the ALPN if the port is 443 or 853, if not already set.
func (d *TLSDialer) config(host, port string) *tls.Config {
config := d.Config
if config == nil {
config = &tls.Config{}
}
config = config.Clone() // operate on a clone
if config.ServerName == "" {
config.ServerName = host
}
if len(config.NextProtos) <= 0 {
switch port {
case "443":
config.NextProtos = []string{"h2", "http/1.1"}
case "853":
config.NextProtos = []string{"dot"}
}
}
return config
}
-145
View File
@@ -1,145 +0,0 @@
package netxlite
import (
"context"
"crypto/tls"
"errors"
"io"
"net"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/netxmocks"
)
func TestTLSDialerFailureSplitHostPort(t *testing.T) {
dialer := &TLSDialer{}
ctx := context.Background()
const address = "www.google.com" // missing port
conn, err := dialer.DialTLSContext(ctx, "tcp", address)
if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("connection is not nil")
}
}
func TestTLSDialerFailureDialing(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // immediately fail
dialer := TLSDialer{Dialer: &net.Dialer{}}
conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443")
if err == nil || !strings.HasSuffix(err.Error(), "operation was canceled") {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("connection is not nil")
}
}
func TestTLSDialerFailureHandshaking(t *testing.T) {
ctx := context.Background()
dialer := TLSDialer{
Config: &tls.Config{},
Dialer: &netxmocks.Dialer{MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return &netxmocks.Conn{MockWrite: func(b []byte) (int, error) {
return 0, io.EOF
}, MockClose: func() error {
return nil
}, MockSetDeadline: func(t time.Time) error {
return nil
}}, nil
}},
TLSHandshaker: &TLSHandshakerConfigurable{},
}
conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443")
if !errors.Is(err, io.EOF) {
t.Fatal("not the error we expected", err)
}
if conn != nil {
t.Fatal("connection is not nil")
}
}
func TestTLSDialerSuccessHandshaking(t *testing.T) {
ctx := context.Background()
dialer := TLSDialer{
Dialer: &netxmocks.Dialer{MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return &netxmocks.Conn{MockWrite: func(b []byte) (int, error) {
return 0, io.EOF
}, MockClose: func() error {
return nil
}, MockSetDeadline: func(t time.Time) error {
return nil
}}, nil
}},
TLSHandshaker: &netxmocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return tls.Client(conn, config), tls.ConnectionState{}, nil
},
},
}
conn, err := dialer.DialTLSContext(ctx, "tcp", "www.google.com:443")
if err != nil {
t.Fatal(err)
}
if conn == nil {
t.Fatal("connection is nil")
}
conn.Close()
}
func TestTLSDialerConfigFromEmptyConfigForWeb(t *testing.T) {
d := &TLSDialer{}
config := d.config("www.google.com", "443")
if config.ServerName != "www.google.com" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"h2", "http/1.1"}); diff != "" {
t.Fatal(diff)
}
}
func TestTLSDialerConfigFromEmptyConfigForDoT(t *testing.T) {
d := &TLSDialer{}
config := d.config("dns.google", "853")
if config.ServerName != "dns.google" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"dot"}); diff != "" {
t.Fatal(diff)
}
}
func TestTLSDialerConfigWithServerName(t *testing.T) {
d := &TLSDialer{
Config: &tls.Config{
ServerName: "example.com",
},
}
config := d.config("dns.google", "853")
if config.ServerName != "example.com" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"dot"}); diff != "" {
t.Fatal(diff)
}
}
func TestTLSDialerConfigWithALPN(t *testing.T) {
d := &TLSDialer{
Config: &tls.Config{
NextProtos: []string{"h2"},
},
}
config := d.config("dns.google", "853")
if config.ServerName != "dns.google" {
t.Fatal("invalid server name")
}
if diff := cmp.Diff(config.NextProtos, []string{"h2"}); diff != "" {
t.Fatal(diff)
}
}
-148
View File
@@ -1,148 +0,0 @@
package netxlite
import (
"context"
"crypto/tls"
"net"
"time"
utls "gitlab.com/yawning/utls.git"
)
// TLSHandshaker is the generic TLS handshaker.
type TLSHandshaker interface {
// Handshake creates a new TLS connection from the given connection and
// the given config. This function DOES NOT take ownership of the connection
// and it's your responsibility to close it on failure.
Handshake(ctx context.Context, conn net.Conn, config *tls.Config) (
net.Conn, tls.ConnectionState, error)
}
// TLSHandshakerConfigurable is a configurable TLS handshaker that
// uses by default the standard library's TLS implementation.
type TLSHandshakerConfigurable struct {
// NewConn is the OPTIONAL factory for creating a new connection. If
// this factory is not set, we'll use the stdlib.
NewConn func(conn net.Conn, config *tls.Config) TLSConn
// Timeout is the OPTIONAL timeout imposed on the TLS handshake. If zero
// or negative, we will use default timeout of 10 seconds.
Timeout time.Duration
}
var _ TLSHandshaker = &TLSHandshakerConfigurable{}
// defaultCertPool is the cert pool we use by default. We store this
// value into a private variable to enable for unit testing.
var defaultCertPool = NewDefaultCertPool()
// Handshake implements Handshaker.Handshake. This function will
// configure the code to use the built-in Mozilla CA if the config
// field contains a nil RootCAs field.
//
// Bug
//
// Until Go 1.17 is released, this function will not honour
// the context. We'll however always enforce an overall timeout.
func (h *TLSHandshakerConfigurable) Handshake(
ctx context.Context, conn net.Conn, config *tls.Config,
) (net.Conn, tls.ConnectionState, error) {
timeout := h.Timeout
if timeout <= 0 {
timeout = 10 * time.Second
}
defer conn.SetDeadline(time.Time{})
conn.SetDeadline(time.Now().Add(timeout))
if config.RootCAs == nil {
config = config.Clone()
config.RootCAs = defaultCertPool
}
tlsconn := h.newConn(conn, config)
if err := tlsconn.Handshake(); err != nil {
return nil, tls.ConnectionState{}, err
}
return tlsconn, tlsconn.ConnectionState(), nil
}
// newConn creates a new TLSConn.
func (h *TLSHandshakerConfigurable) newConn(conn net.Conn, config *tls.Config) TLSConn {
if h.NewConn != nil {
return h.NewConn(conn, config)
}
return tls.Client(conn, config)
}
// DefaultTLSHandshaker is the default TLS handshaker.
var DefaultTLSHandshaker = &TLSHandshakerConfigurable{}
// TLSHandshakerLogger is a TLSHandshaker with logging.
type TLSHandshakerLogger struct {
// TLSHandshaker is the underlying handshaker.
TLSHandshaker TLSHandshaker
// Logger is the underlying logger.
Logger Logger
}
// Handshake implements Handshaker.Handshake
func (h *TLSHandshakerLogger) Handshake(
ctx context.Context, conn net.Conn, config *tls.Config,
) (net.Conn, tls.ConnectionState, error) {
h.Logger.Debugf(
"tls {sni=%s next=%+v}...", config.ServerName, config.NextProtos)
start := time.Now()
tlsconn, state, err := h.TLSHandshaker.Handshake(ctx, conn, config)
elapsed := time.Since(start)
if err != nil {
h.Logger.Debugf(
"tls {sni=%s next=%+v}... %s in %s", config.ServerName,
config.NextProtos, err, elapsed)
return nil, tls.ConnectionState{}, err
}
h.Logger.Debugf(
"tls {sni=%s next=%+v}... ok in %s {next=%s cipher=%s v=%s}",
config.ServerName, config.NextProtos, elapsed, state.NegotiatedProtocol,
TLSCipherSuiteString(state.CipherSuite),
TLSVersionString(state.Version))
return tlsconn, state, nil
}
// UTLSConn implements TLSConn and uses a utls UConn as its underlying connection
type UTLSConn struct {
*utls.UConn
}
// NewConnUTLS creates a NewConn function creating a utls connection with a specified ClientHelloID
func NewConnUTLS(clientHello *utls.ClientHelloID) func(conn net.Conn, config *tls.Config) TLSConn {
return func(conn net.Conn, config *tls.Config) TLSConn {
uConfig := &utls.Config{
RootCAs: config.RootCAs,
NextProtos: config.NextProtos,
ServerName: config.ServerName,
InsecureSkipVerify: config.InsecureSkipVerify,
DynamicRecordSizingDisabled: config.DynamicRecordSizingDisabled,
}
tlsConn := utls.UClient(conn, uConfig, *clientHello)
return &UTLSConn{tlsConn}
}
}
func (c *UTLSConn) ConnectionState() tls.ConnectionState {
uState := c.Conn.ConnectionState()
return tls.ConnectionState{
Version: uState.Version,
HandshakeComplete: uState.HandshakeComplete,
DidResume: uState.DidResume,
CipherSuite: uState.CipherSuite,
NegotiatedProtocol: uState.NegotiatedProtocol,
NegotiatedProtocolIsMutual: true,
ServerName: uState.ServerName,
PeerCertificates: uState.PeerCertificates,
VerifiedChains: uState.VerifiedChains,
SignedCertificateTimestamps: uState.SignedCertificateTimestamps,
OCSPResponse: uState.OCSPResponse,
TLSUnique: uState.TLSUnique,
}
}
var _ TLSHandshaker = &TLSHandshakerLogger{}
-198
View File
@@ -1,198 +0,0 @@
package netxlite
import (
"context"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"time"
"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/netxmocks"
utls "gitlab.com/yawning/utls.git"
)
func TestTLSHandshakerConfigurableWithError(t *testing.T) {
var times []time.Time
h := &TLSHandshakerConfigurable{}
tcpConn := &netxmocks.Conn{
MockWrite: func(b []byte) (int, error) {
return 0, io.EOF
},
MockSetDeadline: func(t time.Time) error {
times = append(times, t)
return nil
},
}
ctx := context.Background()
conn, _, err := h.Handshake(ctx, tcpConn, &tls.Config{
ServerName: "x.org",
})
if err != io.EOF {
t.Fatal("not the error that we expected")
}
if conn != nil {
t.Fatal("expected nil con here")
}
if len(times) != 2 {
t.Fatal("expected two time entries")
}
if !times[0].After(time.Now()) {
t.Fatal("timeout not in the future")
}
if !times[1].IsZero() {
t.Fatal("did not clear timeout on exit")
}
}
func TestTLSHandshakerConfigurableSuccess(t *testing.T) {
handler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(200)
})
srvr := httptest.NewTLSServer(handler)
defer srvr.Close()
URL, err := url.Parse(srvr.URL)
if err != nil {
t.Fatal(err)
}
conn, err := net.Dial("tcp", URL.Host)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
handshaker := &TLSHandshakerConfigurable{}
ctx := context.Background()
config := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
ServerName: URL.Hostname(),
}
tlsConn, connState, err := handshaker.Handshake(ctx, conn, config)
if err != nil {
t.Fatal(err)
}
defer tlsConn.Close()
if connState.Version != tls.VersionTLS13 {
t.Fatal("unexpected TLS version")
}
}
func TestTLSHandshakerConfigurableSetsDefaultRootCAs(t *testing.T) {
expected := errors.New("mocked error")
var gotTLSConfig *tls.Config
handshaker := &TLSHandshakerConfigurable{
NewConn: func(conn net.Conn, config *tls.Config) TLSConn {
gotTLSConfig = config
return &netxmocks.TLSConn{
MockHandshake: func() error {
return expected
},
}
},
}
ctx := context.Background()
config := &tls.Config{}
conn := &netxmocks.Conn{
MockSetDeadline: func(t time.Time) error {
return nil
},
}
tlsConn, connState, err := handshaker.Handshake(ctx, conn, config)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if !reflect.ValueOf(connState).IsZero() {
t.Fatal("expected zero connState here")
}
if tlsConn != nil {
t.Fatal("expected nil tlsConn here")
}
if config.RootCAs != nil {
t.Fatal("config.RootCAs should still be nil")
}
if gotTLSConfig.RootCAs != defaultCertPool {
t.Fatal("gotTLSConfig.RootCAs has not been correctly set")
}
}
func TestTLSHandshakerLoggerSuccess(t *testing.T) {
th := &TLSHandshakerLogger{
TLSHandshaker: &netxmocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return tls.Client(conn, config), tls.ConnectionState{}, nil
},
},
Logger: log.Log,
}
conn := &netxmocks.Conn{
MockClose: func() error {
return nil
},
}
config := &tls.Config{}
ctx := context.Background()
tlsConn, connState, err := th.Handshake(ctx, conn, config)
if err != nil {
t.Fatal(err)
}
if err := tlsConn.Close(); err != nil {
t.Fatal(err)
}
if !reflect.ValueOf(connState).IsZero() {
t.Fatal("expected zero ConnectionState here")
}
}
func TestTLSHandshakerLoggerFailure(t *testing.T) {
expected := errors.New("mocked error")
th := &TLSHandshakerLogger{
TLSHandshaker: &netxmocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return nil, tls.ConnectionState{}, expected
},
},
Logger: log.Log,
}
conn := &netxmocks.Conn{
MockClose: func() error {
return nil
},
}
config := &tls.Config{}
ctx := context.Background()
tlsConn, connState, err := th.Handshake(ctx, conn, config)
if !errors.Is(err, expected) {
t.Fatal("not the error we expected", err)
}
if tlsConn != nil {
t.Fatal("expected nil conn here")
}
if !reflect.ValueOf(connState).IsZero() {
t.Fatal("expected zero ConnectionState here")
}
}
func TestUTLSHandshakerChrome(t *testing.T) {
h := &TLSHandshakerConfigurable{
NewConn: NewConnUTLS(&utls.HelloChrome_Auto),
}
cfg := &tls.Config{ServerName: "google.com"}
conn, err := net.Dial("tcp", "google.com:443")
if err != nil {
t.Fatal("unexpected error", err)
}
conn, _, err = h.Handshake(context.Background(), conn, cfg)
if err != nil {
t.Fatal("unexpected error", err)
}
if conn == nil {
t.Fatal("nil connection")
}
}
-101
View File
@@ -1,101 +0,0 @@
package netxlite
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
)
var (
tlsVersionString = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
tls.VersionTLS13: "TLSv1.3",
0: "", // guarantee correct behaviour
}
tlsCipherSuiteString = map[uint16]string{
tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA",
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_CBC_SHA256: "TLS_RSA_WITH_AES_128_CBC_SHA256",
tls.TLS_RSA_WITH_AES_128_GCM_SHA256: "TLS_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
tls.TLS_AES_128_GCM_SHA256: "TLS_AES_128_GCM_SHA256",
tls.TLS_AES_256_GCM_SHA384: "TLS_AES_256_GCM_SHA384",
tls.TLS_CHACHA20_POLY1305_SHA256: "TLS_CHACHA20_POLY1305_SHA256",
0: "", // guarantee correct behaviour
}
)
// TLSVersionString returns a TLS version string.
func TLSVersionString(value uint16) string {
if str, found := tlsVersionString[value]; found {
return str
}
return fmt.Sprintf("TLS_VERSION_UNKNOWN_%d", value)
}
// TLSCipherSuiteString returns the TLS cipher suite as a string.
func TLSCipherSuiteString(value uint16) string {
if str, found := tlsCipherSuiteString[value]; found {
return str
}
return fmt.Sprintf("TLS_CIPHER_SUITE_UNKNOWN_%d", value)
}
// NewDefaultCertPool returns a copy of the default x509
// certificate pool that we bundle from Mozilla.
func NewDefaultCertPool() *x509.CertPool {
pool := x509.NewCertPool()
// Assumption: AppendCertsFromPEM cannot fail because we
// run this function already in the generate.go file
pool.AppendCertsFromPEM([]byte(pemcerts))
return pool
}
// ErrInvalidTLSVersion indicates that you passed us a string
// that does not represent a valid TLS version.
var ErrInvalidTLSVersion = errors.New("invalid TLS version")
// ConfigureTLSVersion configures the correct TLS version into
// the specified *tls.Config or returns an error.
func ConfigureTLSVersion(config *tls.Config, version string) error {
switch version {
case "TLSv1.3":
config.MinVersion = tls.VersionTLS13
config.MaxVersion = tls.VersionTLS13
case "TLSv1.2":
config.MinVersion = tls.VersionTLS12
config.MaxVersion = tls.VersionTLS12
case "TLSv1.1":
config.MinVersion = tls.VersionTLS11
config.MaxVersion = tls.VersionTLS11
case "TLSv1.0", "TLSv1":
config.MinVersion = tls.VersionTLS10
config.MaxVersion = tls.VersionTLS10
case "":
// nothing
default:
return ErrInvalidTLSVersion
}
return nil
}
-105
View File
@@ -1,105 +0,0 @@
package netxlite
import (
"crypto/tls"
"errors"
"testing"
)
func TestVersionString(t *testing.T) {
if TLSVersionString(tls.VersionTLS13) != "TLSv1.3" {
t.Fatal("not working for existing version")
}
if TLSVersionString(1) != "TLS_VERSION_UNKNOWN_1" {
t.Fatal("not working for nonexisting version")
}
if TLSVersionString(0) != "" {
t.Fatal("not working for zero version")
}
}
func TestCipherSuite(t *testing.T) {
if TLSCipherSuiteString(tls.TLS_AES_128_GCM_SHA256) != "TLS_AES_128_GCM_SHA256" {
t.Fatal("not working for existing cipher suite")
}
if TLSCipherSuiteString(1) != "TLS_CIPHER_SUITE_UNKNOWN_1" {
t.Fatal("not working for nonexisting cipher suite")
}
if TLSCipherSuiteString(0) != "" {
t.Fatal("not working for zero cipher suite")
}
}
func TestNewDefaultCertPoolWorks(t *testing.T) {
pool := NewDefaultCertPool()
if pool == nil {
t.Fatal("expected non-nil value here")
}
}
func TestConfigureTLSVersion(t *testing.T) {
tests := []struct {
name string
version string
wantErr error
versionMin int
versionMax int
}{{
name: "with TLSv1.3",
version: "TLSv1.3",
wantErr: nil,
versionMin: tls.VersionTLS13,
versionMax: tls.VersionTLS13,
}, {
name: "with TLSv1.2",
version: "TLSv1.2",
wantErr: nil,
versionMin: tls.VersionTLS12,
versionMax: tls.VersionTLS12,
}, {
name: "with TLSv1.1",
version: "TLSv1.1",
wantErr: nil,
versionMin: tls.VersionTLS11,
versionMax: tls.VersionTLS11,
}, {
name: "with TLSv1.0",
version: "TLSv1.0",
wantErr: nil,
versionMin: tls.VersionTLS10,
versionMax: tls.VersionTLS10,
}, {
name: "with TLSv1",
version: "TLSv1",
wantErr: nil,
versionMin: tls.VersionTLS10,
versionMax: tls.VersionTLS10,
}, {
name: "with default",
version: "",
wantErr: nil,
versionMin: 0,
versionMax: 0,
}, {
name: "with invalid version",
version: "TLSv999",
wantErr: ErrInvalidTLSVersion,
versionMin: 0,
versionMax: 0,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := new(tls.Config)
err := ConfigureTLSVersion(conf, tt.version)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("not the error we expected: %+v", err)
}
if conf.MinVersion != uint16(tt.versionMin) {
t.Fatalf("not the min version we expected: %+v", conf.MinVersion)
}
if conf.MaxVersion != uint16(tt.versionMax) {
t.Fatalf("not the max version we expected: %+v", conf.MaxVersion)
}
})
}
}
+46
View File
@@ -0,0 +1,46 @@
package netxlite
import (
"crypto/tls"
"net"
utls "gitlab.com/yawning/utls.git"
)
// utlsConn implements TLSConn and uses a utls UConn as its underlying connection
type utlsConn struct {
*utls.UConn
}
// NewConnUTLS creates a NewConn function creating a utls connection with a specified ClientHelloID
func NewConnUTLS(clientHello *utls.ClientHelloID) func(conn net.Conn, config *tls.Config) TLSConn {
return func(conn net.Conn, config *tls.Config) TLSConn {
uConfig := &utls.Config{
RootCAs: config.RootCAs,
NextProtos: config.NextProtos,
ServerName: config.ServerName,
InsecureSkipVerify: config.InsecureSkipVerify,
DynamicRecordSizingDisabled: config.DynamicRecordSizingDisabled,
}
tlsConn := utls.UClient(conn, uConfig, *clientHello)
return &utlsConn{tlsConn}
}
}
func (c *utlsConn) ConnectionState() tls.ConnectionState {
uState := c.Conn.ConnectionState()
return tls.ConnectionState{
Version: uState.Version,
HandshakeComplete: uState.HandshakeComplete,
DidResume: uState.DidResume,
CipherSuite: uState.CipherSuite,
NegotiatedProtocol: uState.NegotiatedProtocol,
NegotiatedProtocolIsMutual: uState.NegotiatedProtocolIsMutual,
ServerName: uState.ServerName,
PeerCertificates: uState.PeerCertificates,
VerifiedChains: uState.VerifiedChains,
SignedCertificateTimestamps: uState.SignedCertificateTimestamps,
OCSPResponse: uState.OCSPResponse,
TLSUnique: uState.TLSUnique,
}
}
+28
View File
@@ -0,0 +1,28 @@
package netxlite
import (
"context"
"crypto/tls"
"net"
"testing"
utls "gitlab.com/yawning/utls.git"
)
func TestUTLSHandshakerChrome(t *testing.T) {
h := &tlsHandshakerConfigurable{
NewConn: NewConnUTLS(&utls.HelloChrome_Auto),
}
cfg := &tls.Config{ServerName: "google.com"}
conn, err := net.Dial("tcp", "google.com:443")
if err != nil {
t.Fatal("unexpected error", err)
}
conn, _, err = h.Handshake(context.Background(), conn, cfg)
if err != nil {
t.Fatal("unexpected error", err)
}
if conn == nil {
t.Fatal("nil connection")
}
}