88236a4352
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 ```
181 lines
6.4 KiB
Go
181 lines
6.4 KiB
Go
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
|
|
}
|