feat: add an experimental quicping experiment (#677)
This experiment pings a QUIC-able host. It can be used to measure QUIC availability independently from TLS. This is the reference issue: https://github.com/ooni/probe/issues/1994 ### A QUIC PING is: - a QUIC Initial packet with a size of 1200 bytes (minimum datagram size defined in the [RFC 9000](https://www.rfc-editor.org/rfc/rfc9000.html#initial-size)), - with a random payload (i.e. no TLS ClientHello), - with the version string 0xbabababa which forces Version Negotiation at the server. QUIC-able hosts respond to the QUIC PING with a Version Negotiation packet. The input is a domain name or an IP address. The default port used by quicping is 443, as this is the port used by HTTP/3. The port can be modified with the `-O Port=` option. The default number of repetitions is 10, it can be changed with `-O Repetitions=`. ### Usage: ``` ./miniooni -i google.com quicping ./miniooni -i 142.250.181.206 quicping ./miniooni -i 142.250.181.206 -OPort=443 quicping ./miniooni -i 142.250.181.206 -ORepetitions=2 quicping ```
This commit is contained in:
parent
0735e2018f
commit
88236a4352
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/httphostheader"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/httphostheader"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/ndt7"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/ndt7"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/quicping"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/run"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/run"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/signal"
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/signal"
|
||||||
|
@ -142,6 +143,18 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"quicping": func(session *Session) *ExperimentBuilder {
|
||||||
|
return &ExperimentBuilder{
|
||||||
|
build: func(config interface{}) *Experiment {
|
||||||
|
return NewExperiment(session, quicping.NewExperimentMeasurer(
|
||||||
|
*config.(*quicping.Config),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
config: &quicping.Config{},
|
||||||
|
inputPolicy: InputStrictlyRequired,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"riseupvpn": func(session *Session) *ExperimentBuilder {
|
"riseupvpn": func(session *Session) *ExperimentBuilder {
|
||||||
return &ExperimentBuilder{
|
return &ExperimentBuilder{
|
||||||
build: func(config interface{}) *Experiment {
|
build: func(config interface{}) *Experiment {
|
||||||
|
|
180
internal/engine/experiment/quicping/crypto.go
Normal file
180
internal/engine/experiment/quicping/crypto.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
package quicping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
// This code is borrowed from https://github.com/marten-seemann/qtls-go1-15
|
||||||
|
// https://github.com/marten-seemann/qtls-go1-15/blob/0d137e9e3594d8e9c864519eff97b323321e5e74/cipher_suites.go#L281
|
||||||
|
type aead interface {
|
||||||
|
cipher.AEAD
|
||||||
|
|
||||||
|
// explicitNonceLen returns the number of bytes of explicit nonce
|
||||||
|
// included in each record. This is eight for older AEADs and
|
||||||
|
// zero for modern ones.
|
||||||
|
explicitNonceLen() int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
aeadNonceLength = 12
|
||||||
|
noncePrefixLength = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
// This code is borrowed from https://github.com/marten-seemann/qtls-go1-15
|
||||||
|
// https://github.com/marten-seemann/qtls-go1-15/blob/0d137e9e3594d8e9c864519eff97b323321e5e74/cipher_suites.go#L375
|
||||||
|
func aeadAESGCMTLS13(key, nonceMask []byte) aead {
|
||||||
|
if len(nonceMask) != aeadNonceLength {
|
||||||
|
panic("tls: internal error: wrong nonce length")
|
||||||
|
}
|
||||||
|
aes, err := aes.NewCipher(key)
|
||||||
|
runtimex.PanicOnError(err, fmt.Sprintf("aes.NewCipher failed: %s", err))
|
||||||
|
aead, err := cipher.NewGCM(aes)
|
||||||
|
runtimex.PanicOnError(err, fmt.Sprintf("cipher.NewGCM failed: %s", err))
|
||||||
|
ret := &xorNonceAEAD{aead: aead}
|
||||||
|
copy(ret.nonceMask[:], nonceMask)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// This code is borrowed from https://github.com/lucas-clemente/quic-go/
|
||||||
|
// https://github.com/lucas-clemente/quic-go/blob/f3b098775e40f96486c0065204145ddc8675eb7c/internal/handshake/initial_aead.go#L60
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#protection-keys
|
||||||
|
//
|
||||||
|
// computeInitialKeyAndIV derives the packet protection key and Initialization Vector (IV) from the initial secret.
|
||||||
|
func computeInitialKeyAndIV(secret []byte) (key, iv []byte) {
|
||||||
|
key = hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
|
||||||
|
iv = hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#protection-keys
|
||||||
|
//
|
||||||
|
// computeHP derives the header protection key from the initial secret.
|
||||||
|
func computeHP(secret []byte) (hp []byte) {
|
||||||
|
hp = hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// This code is borrowed from https://github.com/lucas-clemente/quic-go/
|
||||||
|
// https://github.com/lucas-clemente/quic-go/blob/f3b098775e40f96486c0065204145ddc8675eb7c/internal/handshake/initial_aead.go#L53
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets
|
||||||
|
//
|
||||||
|
// computeSecrets computes the initial secrets based on the destination connection ID.
|
||||||
|
func computeSecrets(destConnID []byte) (clientSecret, serverSecret []byte) {
|
||||||
|
initialSalt := []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
|
||||||
|
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, initialSalt)
|
||||||
|
clientSecret = hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
|
||||||
|
serverSecret = hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "server in", crypto.SHA256.Size())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-client-initial
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection
|
||||||
|
//
|
||||||
|
// encryptHeader applies header protection to the packet bytes (raw).
|
||||||
|
func encryptHeader(raw, hdr, clientSecret []byte) []byte {
|
||||||
|
hp := computeHP(clientSecret)
|
||||||
|
block, err := aes.NewCipher(hp)
|
||||||
|
runtimex.PanicOnError(err, fmt.Sprintf("error creating new AES cipher: %s", err))
|
||||||
|
hdroffset := 0
|
||||||
|
payloadOffset := len(hdr)
|
||||||
|
sample := raw[payloadOffset : payloadOffset+16]
|
||||||
|
|
||||||
|
mask := make([]byte, block.BlockSize())
|
||||||
|
if len(sample) != len(mask) {
|
||||||
|
panic("invalid sample size")
|
||||||
|
}
|
||||||
|
block.Encrypt(mask, sample)
|
||||||
|
|
||||||
|
pnOffset := len(hdr) - 4
|
||||||
|
pnBytes := raw[pnOffset:payloadOffset]
|
||||||
|
raw[hdroffset] ^= mask[0] & 0xf
|
||||||
|
for i := range pnBytes {
|
||||||
|
pnBytes[i] ^= mask[i+1]
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection
|
||||||
|
//
|
||||||
|
// encryptPayload encrypts the payload of the packet.
|
||||||
|
func encryptPayload(payload, destConnID connectionID, clientSecret []byte) []byte {
|
||||||
|
myKey, myIV := computeInitialKeyAndIV(clientSecret)
|
||||||
|
encrypter := aeadAESGCMTLS13(myKey, myIV)
|
||||||
|
|
||||||
|
nonceBuf := make([]byte, encrypter.NonceSize())
|
||||||
|
var pn int64 = 2
|
||||||
|
binary.BigEndian.PutUint64(nonceBuf[len(nonceBuf)-8:], uint64(pn))
|
||||||
|
|
||||||
|
encrypted := encrypter.Seal(nil, nonceBuf, payload, nil)
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// This code is borrowed from https://github.com/lucas-clemente/quic-go/
|
||||||
|
// https://github.com/lucas-clemente/quic-go/blob/master/internal/handshake/hkdf.go
|
||||||
|
//
|
||||||
|
// hkdfExpandLabel HKDF expands a label.
|
||||||
|
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
|
||||||
|
b := make([]byte, 3, 3+6+len(label)+1+len(context))
|
||||||
|
binary.BigEndian.PutUint16(b, uint16(length))
|
||||||
|
b[2] = uint8(6 + len(label))
|
||||||
|
b = append(b, []byte("tls13 ")...)
|
||||||
|
b = append(b, []byte(label)...)
|
||||||
|
b = b[:3+6+len(label)+1]
|
||||||
|
b[3+6+len(label)] = uint8(len(context))
|
||||||
|
b = append(b, context...)
|
||||||
|
|
||||||
|
out := make([]byte, length)
|
||||||
|
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
|
||||||
|
if err != nil || n != length {
|
||||||
|
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
// This code is borrowed from https://github.com/marten-seemann/qtls-go1-15
|
||||||
|
// https://github.com/marten-seemann/qtls-go1-15/blob/0d137e9e3594d8e9c864519eff97b323321e5e74/cipher_suites.go#L319
|
||||||
|
//
|
||||||
|
// xoredNonceAEAD wraps an AEAD by XORing in a fixed pattern to the nonce before each call.
|
||||||
|
type xorNonceAEAD struct {
|
||||||
|
nonceMask [aeadNonceLength]byte
|
||||||
|
aead cipher.AEAD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *xorNonceAEAD) NonceSize() int { return 8 } // 64-bit sequence number
|
||||||
|
func (f *xorNonceAEAD) Overhead() int { return f.aead.Overhead() }
|
||||||
|
func (f *xorNonceAEAD) explicitNonceLen() int { return 0 }
|
||||||
|
|
||||||
|
func (f *xorNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte {
|
||||||
|
for i, b := range nonce {
|
||||||
|
f.nonceMask[4+i] ^= b
|
||||||
|
}
|
||||||
|
result := f.aead.Seal(out, f.nonceMask[:], plaintext, additionalData)
|
||||||
|
for i, b := range nonce {
|
||||||
|
f.nonceMask[4+i] ^= b
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *xorNonceAEAD) Open(out, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||||
|
for i, b := range nonce {
|
||||||
|
f.nonceMask[4+i] ^= b
|
||||||
|
}
|
||||||
|
result, err := f.aead.Open(out, f.nonceMask[:], ciphertext, additionalData)
|
||||||
|
for i, b := range nonce {
|
||||||
|
f.nonceMask[4+i] ^= b
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
132
internal/engine/experiment/quicping/quic.go
Normal file
132
internal/engine/experiment/quicping/quic.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package quicping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildHeader creates the unprotected QUIC header.
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9000.html#name-initial-packet
|
||||||
|
func buildHeader(destConnID, srcConnID connectionID, payloadLen int) []byte {
|
||||||
|
hdr := []byte{0xc3} // long header type, fixed
|
||||||
|
|
||||||
|
version := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(version, uint32(0xbabababa))
|
||||||
|
hdr = append(hdr, version...) // version
|
||||||
|
|
||||||
|
lendID := uint8(len(destConnID))
|
||||||
|
hdr = append(hdr, lendID) // destination connection ID length
|
||||||
|
hdr = append(hdr, destConnID...) // destination connection ID
|
||||||
|
|
||||||
|
lensID := uint8(len(srcConnID))
|
||||||
|
hdr = append(hdr, lensID) // source connection ID length
|
||||||
|
hdr = append(hdr, srcConnID...) // source connection ID
|
||||||
|
|
||||||
|
hdr = append(hdr, 0x0) // token length
|
||||||
|
|
||||||
|
remainder := 4 + payloadLen
|
||||||
|
remainder_mask := 0b100000000000000
|
||||||
|
remainder_mask |= remainder
|
||||||
|
remainder_b := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(remainder_b, uint16(remainder_mask))
|
||||||
|
hdr = append(hdr, remainder_b...) // remainder length: packet number + encrypted payload
|
||||||
|
|
||||||
|
pn := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(pn, uint32(2))
|
||||||
|
hdr = append(hdr, pn...) // packet number
|
||||||
|
|
||||||
|
return hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPacket constructs an Initial QUIC packet
|
||||||
|
// and applies Initial protection.
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9001.html#name-client-initial
|
||||||
|
func buildPacket() ([]byte, connectionID, connectionID) {
|
||||||
|
destConnID, srcConnID := generateConnectionIDs()
|
||||||
|
// generate random payload
|
||||||
|
minPayloadSize := 1200 - 14 - (len(destConnID) + len(srcConnID))
|
||||||
|
randomPayload := make([]byte, minPayloadSize)
|
||||||
|
rand.Read(randomPayload)
|
||||||
|
|
||||||
|
clientSecret, _ := computeSecrets(destConnID)
|
||||||
|
encrypted := encryptPayload(randomPayload, destConnID, clientSecret)
|
||||||
|
hdr := buildHeader(destConnID, srcConnID, len(encrypted))
|
||||||
|
raw := append(hdr, encrypted...)
|
||||||
|
|
||||||
|
raw = encryptHeader(raw, hdr, clientSecret)
|
||||||
|
return raw, destConnID, srcConnID
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateConnectionID generates a connection ID using cryptographic random
|
||||||
|
func generateConnectionID(len int) connectionID {
|
||||||
|
b := make([]byte, len)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
runtimex.PanicOnError(err, "rand.Read failed")
|
||||||
|
return connectionID(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateConnectionIDForInitial generates a connection ID for the Initial packet.
|
||||||
|
// It uses a length randomly chosen between 8 and 18 bytes.
|
||||||
|
func generateConnectionIDForInitial() connectionID {
|
||||||
|
r := make([]byte, 1)
|
||||||
|
_, err := rand.Read(r)
|
||||||
|
runtimex.PanicOnError(err, "rand.Read failed")
|
||||||
|
len := minConnectionIDLenInitial + int(r[0])%(maxConnectionIDLen-minConnectionIDLenInitial+1)
|
||||||
|
return generateConnectionID(len)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateConnectionIDs generates a destination and source connection ID.
|
||||||
|
func generateConnectionIDs() ([]byte, []byte) {
|
||||||
|
destConnID := generateConnectionIDForInitial()
|
||||||
|
srcConnID := generateConnectionID(defaultConnectionIDLength)
|
||||||
|
return destConnID, srcConnID
|
||||||
|
}
|
||||||
|
|
||||||
|
// dissectVersionNegotiation dissects the Version Negotiation response.
|
||||||
|
// It returns the supported versions and the destination connection ID of the response,
|
||||||
|
// The destination connection ID of the response has to coincide with the source connection ID of the request.
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9000.html#name-version-negotiation-packet
|
||||||
|
func (m *Measurer) dissectVersionNegotiation(i []byte) ([]uint32, connectionID, error) {
|
||||||
|
firstByte := uint8(i[0])
|
||||||
|
mask := 0b10000000
|
||||||
|
mask &= int(firstByte)
|
||||||
|
if mask == 0 {
|
||||||
|
return nil, nil, &errUnexpectedResponse{msg: "not a long header packet"}
|
||||||
|
}
|
||||||
|
|
||||||
|
versionBytes := i[1:5]
|
||||||
|
v := binary.BigEndian.Uint32(versionBytes)
|
||||||
|
if v != 0 {
|
||||||
|
return nil, nil, &errUnexpectedResponse{msg: "unexpected Version Negotiation format"}
|
||||||
|
}
|
||||||
|
|
||||||
|
dstLength := i[5]
|
||||||
|
offset := 6 + uint8(dstLength)
|
||||||
|
dst := i[6:offset]
|
||||||
|
|
||||||
|
srcLength := i[offset]
|
||||||
|
offset = offset + 1 + srcLength
|
||||||
|
|
||||||
|
n := uint8(len(i))
|
||||||
|
var supportedVersions []uint32
|
||||||
|
for offset < n {
|
||||||
|
supportedVersions = append(supportedVersions, binary.BigEndian.Uint32(i[offset:offset+4]))
|
||||||
|
offset += 4
|
||||||
|
}
|
||||||
|
return supportedVersions, dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errUnexpectedResponse is thrown when the response from the server
|
||||||
|
// is not a valid Version Negotiation packet
|
||||||
|
type errUnexpectedResponse struct {
|
||||||
|
error
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements error.Error()
|
||||||
|
func (e *errUnexpectedResponse) Error() string {
|
||||||
|
return fmt.Sprintf("unexptected response: %s", e.msg)
|
||||||
|
}
|
357
internal/engine/experiment/quicping/quicping.go
Normal file
357
internal/engine/experiment/quicping/quicping.go
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
// Package quicping implements the quicping network experiment. This
|
||||||
|
// implements, in particular, v0.1.0 of the spec.
|
||||||
|
//
|
||||||
|
// See https://github.com/ooni/spec/blob/master/nettests/ts-031-quicping.md.
|
||||||
|
package quicping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "crypto/sha256"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/netx/archival"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A connectionID in QUIC
|
||||||
|
type connectionID []byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxConnectionIDLen = 18
|
||||||
|
minConnectionIDLenInitial = 8
|
||||||
|
defaultConnectionIDLength = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testName = "quicping"
|
||||||
|
testVersion = "0.1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains the experiment configuration.
|
||||||
|
type Config struct {
|
||||||
|
// Repetitions is the number of repetitions for each ping.
|
||||||
|
Repetitions int64 `ooni:"number of times to repeat the measurement"`
|
||||||
|
|
||||||
|
// Port is the port to test.
|
||||||
|
Port int64 `ooni:"port is the port to test"`
|
||||||
|
|
||||||
|
// networkLibrary is the underlying network library. Can be used for testing.
|
||||||
|
networkLib model.UnderlyingNetworkLibrary
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) repetitions() int64 {
|
||||||
|
if c.Repetitions > 0 {
|
||||||
|
return c.Repetitions
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) port() string {
|
||||||
|
if c.Port != 0 {
|
||||||
|
return strconv.FormatInt(c.Port, 10)
|
||||||
|
}
|
||||||
|
return "443"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) networkLibrary() model.UnderlyingNetworkLibrary {
|
||||||
|
if c.networkLib != nil {
|
||||||
|
return c.networkLib
|
||||||
|
}
|
||||||
|
return &netxlite.TProxyStdlib{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestKeys contains the experiment results.
|
||||||
|
type TestKeys struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Pings []*SinglePing `json:"pings"`
|
||||||
|
UnexpectedResponses []*SinglePingResponse `json:"unexpected_responses"`
|
||||||
|
Repetitions int64 `json:"repetitions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SinglePing is a result of a single ping operation.
|
||||||
|
type SinglePing struct {
|
||||||
|
ConnIdDst string `json:"conn_id_dst"`
|
||||||
|
ConnIdSrc string `json:"conn_id_src"`
|
||||||
|
Failure *string `json:"failure"`
|
||||||
|
Request *model.ArchivalMaybeBinaryData `json:"request"`
|
||||||
|
T float64 `json:"t"`
|
||||||
|
Responses []*SinglePingResponse `json:"responses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SinglePingResponse struct {
|
||||||
|
Data *model.ArchivalMaybeBinaryData `json:"response_data"`
|
||||||
|
Failure *string `json:"failure"`
|
||||||
|
T float64 `json:"t"`
|
||||||
|
SupportedVersions []uint32 `json:"supported_versions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeResponse is a utility function to create a SinglePingResponse
|
||||||
|
func makeResponse(resp *responseInfo) *SinglePingResponse {
|
||||||
|
var data *model.ArchivalMaybeBinaryData
|
||||||
|
if resp.raw != nil {
|
||||||
|
data = &model.ArchivalMaybeBinaryData{Value: string(resp.raw)}
|
||||||
|
}
|
||||||
|
return &SinglePingResponse{
|
||||||
|
Data: data,
|
||||||
|
Failure: archival.NewFailure(resp.err),
|
||||||
|
T: resp.t,
|
||||||
|
SupportedVersions: resp.versions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measurer performs the measurement.
|
||||||
|
type Measurer struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExperimentName implements ExperimentMeasurer.ExperimentName.
|
||||||
|
func (m *Measurer) ExperimentName() string {
|
||||||
|
return testName
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||||
|
func (m *Measurer) ExperimentVersion() string {
|
||||||
|
return testVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// pingInfo contains information about a ping request
|
||||||
|
// and the corresponding ping responses
|
||||||
|
type pingInfo struct {
|
||||||
|
request *requestInfo
|
||||||
|
responses []*responseInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestInfo contains the information of a sent ping request.
|
||||||
|
type requestInfo struct {
|
||||||
|
t float64
|
||||||
|
raw []byte
|
||||||
|
dstID string
|
||||||
|
srcID string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// responseInfo contains the information of a received ping reponse.
|
||||||
|
type responseInfo struct {
|
||||||
|
t float64
|
||||||
|
raw []byte
|
||||||
|
dstID string
|
||||||
|
versions []uint32
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// sender sends a ping requests to the target hosts every second
|
||||||
|
func (m *Measurer) sender(
|
||||||
|
ctx context.Context,
|
||||||
|
pconn model.UDPLikeConn,
|
||||||
|
destAddr *net.UDPAddr,
|
||||||
|
out chan<- requestInfo,
|
||||||
|
sess model.ExperimentSession,
|
||||||
|
measurement *model.Measurement,
|
||||||
|
) {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for i := int64(0); i < m.config.repetitions(); i++ {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return // user aborted or timeout expired
|
||||||
|
|
||||||
|
case stime := <-ticker.C:
|
||||||
|
sendTime := stime.Sub(measurement.MeasurementStartTimeSaved).Seconds()
|
||||||
|
packet, dstID, srcID := buildPacket() // build QUIC Initial packet
|
||||||
|
_, err := pconn.WriteTo(packet, destAddr) // send Initial packet
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Logger().Infof("PING %s", destAddr)
|
||||||
|
|
||||||
|
// propagate send information, including errors
|
||||||
|
out <- requestInfo{raw: packet, t: sendTime, dstID: hex.EncodeToString(dstID), srcID: hex.EncodeToString(srcID), err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiver receives incoming server responses and
|
||||||
|
// dissects the payload of the version negotiation response
|
||||||
|
func (m *Measurer) receiver(
|
||||||
|
ctx context.Context,
|
||||||
|
pconn model.UDPLikeConn,
|
||||||
|
out chan<- responseInfo,
|
||||||
|
sess model.ExperimentSession,
|
||||||
|
measurement *model.Measurement,
|
||||||
|
) {
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
// read (timeout was set in Run)
|
||||||
|
buffer := make([]byte, 1024)
|
||||||
|
n, addr, err := pconn.ReadFrom(buffer)
|
||||||
|
respTime := time.Since(measurement.MeasurementStartTimeSaved).Seconds()
|
||||||
|
if err != nil {
|
||||||
|
// stop if the connection is already closed
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// store read failures and continue receiving
|
||||||
|
out <- responseInfo{t: respTime, err: err}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp := buffer[:n]
|
||||||
|
|
||||||
|
// dissect server response
|
||||||
|
supportedVersions, dst, err := m.dissectVersionNegotiation(resp)
|
||||||
|
if err != nil {
|
||||||
|
// the response was likely not the expected version negotiation response
|
||||||
|
sess.Logger().Infof(fmt.Sprintf("response dissection failed: %s", err))
|
||||||
|
out <- responseInfo{raw: resp, t: respTime, err: err}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// propagate receive information
|
||||||
|
out <- responseInfo{raw: resp, t: respTime, dstID: hex.EncodeToString(dst), versions: supportedVersions}
|
||||||
|
|
||||||
|
sess.Logger().Infof("PING got response from %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run implements ExperimentMeasurer.Run.
|
||||||
|
func (m *Measurer) Run(
|
||||||
|
ctx context.Context,
|
||||||
|
sess model.ExperimentSession,
|
||||||
|
measurement *model.Measurement,
|
||||||
|
callbacks model.ExperimentCallbacks,
|
||||||
|
) error {
|
||||||
|
host := string(measurement.Input)
|
||||||
|
// allow URL input
|
||||||
|
if u, err := url.ParseRequestURI(host); err == nil {
|
||||||
|
host = u.Host
|
||||||
|
}
|
||||||
|
service := net.JoinHostPort(host, m.config.port())
|
||||||
|
udpAddr, err := net.ResolveUDPAddr("udp4", service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rep := m.config.repetitions()
|
||||||
|
tk := &TestKeys{
|
||||||
|
Domain: host,
|
||||||
|
Repetitions: rep,
|
||||||
|
}
|
||||||
|
measurement.TestKeys = tk
|
||||||
|
|
||||||
|
// create UDP socket
|
||||||
|
pconn, err := m.config.networkLibrary().ListenUDP("udp", &net.UDPAddr{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer pconn.Close()
|
||||||
|
|
||||||
|
// set context and read timeouts
|
||||||
|
deadline := time.Duration(rep*2) * time.Second
|
||||||
|
pconn.SetDeadline(time.Now().Add(deadline))
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, deadline)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sendInfoChan := make(chan requestInfo)
|
||||||
|
recvInfoChan := make(chan responseInfo)
|
||||||
|
pingMap := make(map[string]*pingInfo)
|
||||||
|
|
||||||
|
// start sender and receiver goroutines
|
||||||
|
go m.sender(ctx, pconn, udpAddr, sendInfoChan, sess, measurement)
|
||||||
|
go m.receiver(ctx, pconn, recvInfoChan, sess, measurement)
|
||||||
|
L:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case req := <-sendInfoChan: // a new ping was sent
|
||||||
|
if req.err != nil {
|
||||||
|
tk.Pings = append(tk.Pings, &SinglePing{
|
||||||
|
ConnIdDst: req.dstID,
|
||||||
|
ConnIdSrc: req.srcID,
|
||||||
|
Failure: archival.NewFailure(req.err),
|
||||||
|
Request: &model.ArchivalMaybeBinaryData{Value: string(req.raw)},
|
||||||
|
T: req.t,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pingMap[req.srcID] = &pingInfo{request: &req}
|
||||||
|
|
||||||
|
case resp := <-recvInfoChan: // a new response has been received
|
||||||
|
if resp.err != nil {
|
||||||
|
// resp failure means we cannot assign the response to a request
|
||||||
|
tk.UnexpectedResponses = append(tk.UnexpectedResponses, makeResponse(&resp))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
ping *pingInfo
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
// match response to request
|
||||||
|
if ping, ok = pingMap[resp.dstID]; !ok {
|
||||||
|
// version negotiation response with an unknown destination ID
|
||||||
|
tk.UnexpectedResponses = append(tk.UnexpectedResponses, makeResponse(&resp))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ping.responses = append(ping.responses, &resp)
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
break L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// transform ping requests into TestKeys.Pings
|
||||||
|
timeoutErr := errors.New("i/o timeout")
|
||||||
|
for _, ping := range pingMap {
|
||||||
|
if ping.request == nil { // this should not happen
|
||||||
|
return errors.New("internal error: ping.request is nil")
|
||||||
|
}
|
||||||
|
if len(ping.responses) <= 0 {
|
||||||
|
tk.Pings = append(tk.Pings, &SinglePing{
|
||||||
|
ConnIdDst: ping.request.dstID,
|
||||||
|
ConnIdSrc: ping.request.srcID,
|
||||||
|
Failure: archival.NewFailure(timeoutErr),
|
||||||
|
Request: &model.ArchivalMaybeBinaryData{Value: string(ping.request.raw)},
|
||||||
|
T: ping.request.t,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var responses []*SinglePingResponse
|
||||||
|
for _, resp := range ping.responses {
|
||||||
|
responses = append(responses, makeResponse(resp))
|
||||||
|
}
|
||||||
|
tk.Pings = append(tk.Pings, &SinglePing{
|
||||||
|
ConnIdDst: ping.request.dstID,
|
||||||
|
ConnIdSrc: ping.request.srcID,
|
||||||
|
Failure: nil,
|
||||||
|
Request: &model.ArchivalMaybeBinaryData{Value: string(ping.request.raw)},
|
||||||
|
T: ping.request.t,
|
||||||
|
Responses: responses,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(tk.Pings, func(i, j int) bool {
|
||||||
|
return tk.Pings[i].T < tk.Pings[j].T
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExperimentMeasurer creates a new ExperimentMeasurer.
|
||||||
|
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||||
|
return &Measurer{config: config}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SummaryKeys contains summary keys for this experiment.
|
||||||
|
//
|
||||||
|
// Note that this structure is part of the ABI contract with probe-cli
|
||||||
|
// therefore we should be careful when changing it.
|
||||||
|
type SummaryKeys struct {
|
||||||
|
IsAnomaly bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||||
|
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||||
|
return SummaryKeys{IsAnomaly: false}, nil
|
||||||
|
}
|
310
internal/engine/experiment/quicping/quicping_test.go
Normal file
310
internal/engine/experiment/quicping/quicping_test.go
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
package quicping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/apex/log"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FailStdLib is a failing model.UnderlyingNetworkLibrary.
|
||||||
|
type FailStdLib struct {
|
||||||
|
conn model.UDPLikeConn
|
||||||
|
err error
|
||||||
|
writeErr error
|
||||||
|
readErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenUDP implements UnderlyingNetworkLibrary.ListenUDP.
|
||||||
|
func (f *FailStdLib) ListenUDP(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) {
|
||||||
|
conn, _ := net.ListenUDP(network, laddr)
|
||||||
|
f.conn = model.UDPLikeConn(conn)
|
||||||
|
if f.err != nil {
|
||||||
|
return nil, f.err
|
||||||
|
}
|
||||||
|
if f.writeErr != nil {
|
||||||
|
return &mocks.UDPLikeConn{
|
||||||
|
MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
|
||||||
|
return 0, f.writeErr
|
||||||
|
},
|
||||||
|
MockReadFrom: func(p []byte) (int, net.Addr, error) {
|
||||||
|
return f.conn.ReadFrom(p)
|
||||||
|
},
|
||||||
|
MockSetDeadline: func(t time.Time) error {
|
||||||
|
return f.conn.SetDeadline(t)
|
||||||
|
},
|
||||||
|
MockClose: func() error {
|
||||||
|
return f.conn.Close()
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if f.readErr != nil {
|
||||||
|
return &mocks.UDPLikeConn{
|
||||||
|
MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
|
||||||
|
return f.conn.WriteTo(p, addr)
|
||||||
|
},
|
||||||
|
MockReadFrom: func(p []byte) (int, net.Addr, error) {
|
||||||
|
return 0, nil, f.readErr
|
||||||
|
},
|
||||||
|
MockSetDeadline: func(t time.Time) error {
|
||||||
|
return f.conn.SetDeadline(t)
|
||||||
|
},
|
||||||
|
MockClose: func() error {
|
||||||
|
return f.conn.Close()
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return &mocks.UDPLikeConn{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupHost implements UnderlyingNetworkLibrary.LookupHost.
|
||||||
|
func (f *FailStdLib) LookupHost(ctx context.Context, domain string) ([]string, error) {
|
||||||
|
return nil, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSimpleDialer implements UnderlyingNetworkLibrary.NewSimpleDialer.
|
||||||
|
func (f *FailStdLib) NewSimpleDialer(timeout time.Duration) model.SimpleDialer {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewExperimentMeasurer(t *testing.T) {
|
||||||
|
measurer := NewExperimentMeasurer(Config{})
|
||||||
|
if measurer.ExperimentName() != "quicping" {
|
||||||
|
t.Fatal("unexpected name")
|
||||||
|
}
|
||||||
|
if measurer.ExperimentVersion() != "0.1.0" {
|
||||||
|
t.Fatal("unexpected version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidHost(t *testing.T) {
|
||||||
|
measurer := NewExperimentMeasurer(Config{
|
||||||
|
Port: 443,
|
||||||
|
Repetitions: 1,
|
||||||
|
})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("a.a.a.a")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
err := measurer.Run(context.Background(), sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error here")
|
||||||
|
}
|
||||||
|
if _, ok := err.(*net.DNSError); !ok {
|
||||||
|
t.Fatal("unexpected error type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLInput(t *testing.T) {
|
||||||
|
measurer := NewExperimentMeasurer(Config{
|
||||||
|
Repetitions: 1,
|
||||||
|
})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("https://google.com/")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
err := measurer.Run(context.Background(), sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error")
|
||||||
|
}
|
||||||
|
tk := measurement.TestKeys.(*TestKeys)
|
||||||
|
if tk.Domain != "google.com" {
|
||||||
|
t.Fatal("unexpected domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuccess(t *testing.T) {
|
||||||
|
measurer := NewExperimentMeasurer(Config{})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("google.com")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
err := measurer.Run(context.Background(), sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error here")
|
||||||
|
}
|
||||||
|
tk := measurement.TestKeys.(*TestKeys)
|
||||||
|
if tk.Domain != "google.com" {
|
||||||
|
t.Fatal("unexpected domain")
|
||||||
|
}
|
||||||
|
if tk.Repetitions != 10 {
|
||||||
|
t.Fatal("unexpected number of repetitions, default is 10")
|
||||||
|
}
|
||||||
|
if tk.Pings == nil || len(tk.Pings) != 10 {
|
||||||
|
t.Fatal("unexpected number of pings", len(tk.Pings))
|
||||||
|
}
|
||||||
|
for i, ping := range tk.Pings {
|
||||||
|
if ping.Failure != nil {
|
||||||
|
t.Fatal("ping failed unexpectedly", i, *ping.Failure)
|
||||||
|
}
|
||||||
|
for _, resp := range ping.Responses {
|
||||||
|
if resp.Failure != nil {
|
||||||
|
t.Fatal("unexepcted response failure")
|
||||||
|
}
|
||||||
|
if resp.SupportedVersions == nil || len(resp.SupportedVersions) == 0 {
|
||||||
|
t.Fatal("server did not respond with supported versions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sk, err := measurer.GetSummaryKeys(measurement)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := sk.(SummaryKeys); !ok {
|
||||||
|
t.Fatal("invalid type for summary keys")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithCancelledContext(t *testing.T) {
|
||||||
|
measurer := NewExperimentMeasurer(Config{})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("google.com")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
err := measurer.Run(ctx, sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error here")
|
||||||
|
}
|
||||||
|
tk := measurement.TestKeys.(*TestKeys)
|
||||||
|
if len(tk.Pings) > 0 {
|
||||||
|
t.Fatal("there should not be any measurements")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenFails(t *testing.T) {
|
||||||
|
expected := errors.New("expected")
|
||||||
|
measurer := NewExperimentMeasurer(Config{
|
||||||
|
networkLib: &FailStdLib{err: expected, readErr: nil, writeErr: nil},
|
||||||
|
})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("google.com")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
err := measurer.Run(context.Background(), sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error here")
|
||||||
|
}
|
||||||
|
if err != expected {
|
||||||
|
t.Fatal("unexpected error type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFails(t *testing.T) {
|
||||||
|
expected := errors.New("expected")
|
||||||
|
measurer := NewExperimentMeasurer(Config{
|
||||||
|
networkLib: &FailStdLib{err: nil, readErr: nil, writeErr: expected},
|
||||||
|
Repetitions: 1,
|
||||||
|
})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("google.com")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
err := measurer.Run(context.Background(), sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error")
|
||||||
|
}
|
||||||
|
tk := measurement.TestKeys.(*TestKeys)
|
||||||
|
if tk.Pings == nil || len(tk.Pings) != 1 {
|
||||||
|
t.Fatal("unexpected number of pings", len(tk.Pings))
|
||||||
|
}
|
||||||
|
for i, ping := range tk.Pings {
|
||||||
|
if ping.Failure == nil {
|
||||||
|
t.Fatal("expected an error here, ping", i)
|
||||||
|
}
|
||||||
|
if !strings.Contains(*ping.Failure, "expected") {
|
||||||
|
t.Fatal("ping: unexpected error type", i, *ping.Failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadFails(t *testing.T) {
|
||||||
|
expected := errors.New("expected")
|
||||||
|
measurer := NewExperimentMeasurer(Config{
|
||||||
|
networkLib: &FailStdLib{err: nil, readErr: expected, writeErr: nil},
|
||||||
|
Repetitions: 1,
|
||||||
|
})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("google.com")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
err := measurer.Run(context.Background(), sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error")
|
||||||
|
}
|
||||||
|
tk := measurement.TestKeys.(*TestKeys)
|
||||||
|
if tk.Pings == nil || len(tk.Pings) != 1 {
|
||||||
|
t.Fatal("unexpected number of pings", len(tk.Pings))
|
||||||
|
}
|
||||||
|
for i, ping := range tk.Pings {
|
||||||
|
if ping.Failure == nil {
|
||||||
|
t.Fatal("expected an error here, ping", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoResponse(t *testing.T) {
|
||||||
|
measurer := NewExperimentMeasurer(Config{
|
||||||
|
Repetitions: 1,
|
||||||
|
})
|
||||||
|
measurement := new(model.Measurement)
|
||||||
|
measurement.Input = model.MeasurementTarget("ooni.org")
|
||||||
|
sess := &mockable.Session{MockableLogger: log.Log}
|
||||||
|
err := measurer.Run(context.Background(), sess, measurement,
|
||||||
|
model.NewPrinterCallbacks(log.Log))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error here")
|
||||||
|
}
|
||||||
|
tk := measurement.TestKeys.(*TestKeys)
|
||||||
|
if tk.Pings == nil || len(tk.Pings) != 1 {
|
||||||
|
t.Fatal("unexpected number of pings", len(tk.Pings))
|
||||||
|
}
|
||||||
|
if tk.Pings[0].Failure == nil {
|
||||||
|
t.Fatal("expected an error here")
|
||||||
|
}
|
||||||
|
if *tk.Pings[0].Failure != "generic_timeout_error" {
|
||||||
|
t.Fatal("unexpected error type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDissect(t *testing.T) {
|
||||||
|
// destID--srcID: 040b9649d3fd4c038ab6c073966f3921--44d064031288e97646451f
|
||||||
|
versionNegotiationResponse, _ := hex.DecodeString("eb0000000010040b9649d3fd4c038ab6c073966f39210b44d064031288e97646451f00000001ff00001dff00001cff00001b")
|
||||||
|
measurer := NewExperimentMeasurer(Config{})
|
||||||
|
destID := "040b9649d3fd4c038ab6c073966f3921"
|
||||||
|
_, dst, err := measurer.(*Measurer).dissectVersionNegotiation(versionNegotiationResponse)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("unexpected error", err)
|
||||||
|
}
|
||||||
|
if hex.EncodeToString(dst) != destID {
|
||||||
|
t.Fatal("unexpected destination connection ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
versionNegotiationResponse[1] = byte(0xff)
|
||||||
|
_, _, err = measurer.(*Measurer).dissectVersionNegotiation(versionNegotiationResponse)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error here", err)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(err.Error(), "unexpected Version Negotiation format") {
|
||||||
|
t.Fatal("unexpected error type", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionNegotiationResponse[0] = byte(0x01)
|
||||||
|
_, _, err = measurer.(*Measurer).dissectVersionNegotiation(versionNegotiationResponse)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error here", err)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(err.Error(), "not a long header packet") {
|
||||||
|
t.Fatal("unexpected error type", err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user