├── .gitignore ├── README.md ├── crypto.go └── quicping.go /.gitignore: -------------------------------------------------------------------------------- 1 | packet.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### QUIC-PING 2 | A UDP client for sending "QUIC PING"s. 3 | 4 | ### What is a QUIC PING? 5 | A QUIC Initial packet with random payload and the version ```0xbabababa``` to force Version Negotiation. 6 | 7 | The QUIC PING packet satifies the minimum Initial datagram size (as specified in [RFC 9000](https://www.rfc-editor.org/rfc/rfc9000.html#initial-size)), i.e. the unencrypted QUIC packet (header + payload) has a size of exactly 1200 bytes. 8 | 9 | ### Usage 10 | ``` 11 | go mod init quicping 12 | go get golang.org/x/crypto/hkdf 13 | go build . 14 | ./quicping google.com:443 [--hexdump] 15 | 16 | ``` 17 | The option ```--hexdump``` saves the generated QUIC packet as hexdump to ```packet.txt```. 18 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "encoding/binary" 8 | 9 | "golang.org/x/crypto/hkdf" 10 | ) 11 | 12 | // https://github.com/marten-seemann/qtls-go1-15/blob/0d137e9e3594d8e9c864519eff97b323321e5e74/cipher_suites.go#L281 13 | type aead interface { 14 | cipher.AEAD 15 | 16 | // explicitNonceLen returns the number of bytes of explicit nonce 17 | // included in each record. This is eight for older AEADs and 18 | // zero for modern ones. 19 | explicitNonceLen() int 20 | } 21 | 22 | const ( 23 | aeadNonceLength = 12 24 | noncePrefixLength = 4 25 | ) 26 | 27 | // https://github.com/marten-seemann/qtls-go1-15/blob/0d137e9e3594d8e9c864519eff97b323321e5e74/cipher_suites.go#L375 28 | func aeadAESGCMTLS13(key, nonceMask []byte) aead { 29 | if len(nonceMask) != aeadNonceLength { 30 | panic("tls: internal error: wrong nonce length") 31 | } 32 | aes, err := aes.NewCipher(key) 33 | if err != nil { 34 | panic(err) 35 | } 36 | aead, err := cipher.NewGCM(aes) 37 | if err != nil { 38 | panic(err) 39 | } 40 | ret := &xorNonceAEAD{aead: aead} 41 | copy(ret.nonceMask[:], nonceMask) 42 | return ret 43 | } 44 | 45 | // xoredNonceAEAD wraps an AEAD by XORing in a fixed pattern to the nonce before each call. 46 | // https://github.com/marten-seemann/qtls-go1-15/blob/0d137e9e3594d8e9c864519eff97b323321e5e74/cipher_suites.go#L319 47 | type xorNonceAEAD struct { 48 | nonceMask [aeadNonceLength]byte 49 | aead cipher.AEAD 50 | } 51 | 52 | func (f *xorNonceAEAD) NonceSize() int { return 8 } // 64-bit sequence number 53 | func (f *xorNonceAEAD) Overhead() int { return f.aead.Overhead() } 54 | func (f *xorNonceAEAD) explicitNonceLen() int { return 0 } 55 | 56 | func (f *xorNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte { 57 | for i, b := range nonce { 58 | f.nonceMask[4+i] ^= b 59 | } 60 | result := f.aead.Seal(out, f.nonceMask[:], plaintext, additionalData) 61 | for i, b := range nonce { 62 | f.nonceMask[4+i] ^= b 63 | } 64 | return result 65 | } 66 | 67 | func (f *xorNonceAEAD) Open(out, nonce, ciphertext, additionalData []byte) ([]byte, error) { 68 | for i, b := range nonce { 69 | f.nonceMask[4+i] ^= b 70 | } 71 | result, err := f.aead.Open(out, f.nonceMask[:], ciphertext, additionalData) 72 | for i, b := range nonce { 73 | f.nonceMask[4+i] ^= b 74 | } 75 | return result, err 76 | } 77 | 78 | // hkdfExpandLabel HKDF expands a label. 79 | // https://github.com/lucas-clemente/quic-go/blob/master/internal/handshake/hkdf.go 80 | func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { 81 | b := make([]byte, 3, 3+6+len(label)+1+len(context)) 82 | binary.BigEndian.PutUint16(b, uint16(length)) 83 | b[2] = uint8(6 + len(label)) 84 | b = append(b, []byte("tls13 ")...) 85 | b = append(b, []byte(label)...) 86 | b = b[:3+6+len(label)+1] 87 | b[3+6+len(label)] = uint8(len(context)) 88 | b = append(b, context...) 89 | 90 | out := make([]byte, length) 91 | n, err := hkdf.Expand(hash.New, secret, b).Read(out) 92 | if err != nil || n != length { 93 | panic("quic: HKDF-Expand-Label invocation failed unexpectedly") 94 | } 95 | return out 96 | } 97 | 98 | // computeSecrets computes the initial secrets based on the destination connection ID. 99 | // https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets 100 | func computeSecrets(destConnID []byte) (clientSecret, serverSecret []byte) { 101 | initialSalt := []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} 102 | // initial_secret = HKDF-Extract(initial_salt,client_dst_connection_id) 103 | initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, initialSalt) 104 | // client_initial_secret = HKDF-Expand-Label(initial_secret, "client in", "", 32) = c00cf151ca5be075ed0ebfb5c80323c42d6b7db67881289af4008f1f6c357aea 105 | clientSecret = hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size()) 106 | serverSecret = hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "server in", crypto.SHA256.Size()) 107 | return 108 | } 109 | 110 | // computeInitialKeyAndIV derives the packet protection key and Initialization Vector (IV) 111 | // from the initial secret. 112 | // https://www.rfc-editor.org/rfc/rfc9001.html#protection-keys 113 | func computeInitialKeyAndIV(secret []byte) (key, iv []byte) { 114 | key = hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) 115 | iv = hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) 116 | return 117 | } 118 | 119 | // computeHP derives the header protection key from the initial secret. 120 | // https://www.rfc-editor.org/rfc/rfc9001.html#protection-keys 121 | func computeHP(secret []byte) (hp []byte) { 122 | hp = hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16) 123 | return 124 | } 125 | -------------------------------------------------------------------------------- /quicping.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "fmt" 9 | random "math/rand" 10 | "net" 11 | "os" 12 | "time" 13 | 14 | _ "crypto/sha256" 15 | ) 16 | 17 | // A ConnectionID in QUIC 18 | type ConnectionID []byte 19 | 20 | const maxConnectionIDLen = 18 21 | const MinConnectionIDLenInitial = 8 22 | const DefaultConnectionIDLength = 16 23 | 24 | // buildHeader creates the unprotected QUIC header. 25 | // https://www.rfc-editor.org/rfc/rfc9000.html#name-initial-packet 26 | func buildHeader(destConnID, srcConnID ConnectionID, payloadLen int) []byte { 27 | hdr := []byte{0xc3} // long header type, fixed 28 | 29 | version := make([]byte, 4) 30 | binary.BigEndian.PutUint32(version, uint32(0xbabababa)) 31 | hdr = append(hdr, version...) // version 32 | 33 | lendID := uint8(len(destConnID)) 34 | hdr = append(hdr, lendID) // destination connection ID length 35 | hdr = append(hdr, destConnID...) // destination connection ID 36 | 37 | lensID := uint8(len(srcConnID)) 38 | hdr = append(hdr, lensID) // source connection ID length 39 | hdr = append(hdr, srcConnID...) // source connection ID 40 | 41 | hdr = append(hdr, 0x0) // token length 42 | 43 | remainder := 4 + payloadLen 44 | remainder_mask := 0b100000000000000 45 | remainder_mask |= remainder 46 | remainder_b := make([]byte, 2) 47 | binary.BigEndian.PutUint16(remainder_b, uint16(remainder_mask)) 48 | hdr = append(hdr, remainder_b...) // remainder length: packet number + encrypted payload 49 | 50 | pn := make([]byte, 4) 51 | binary.BigEndian.PutUint32(pn, uint32(2)) 52 | hdr = append(hdr, pn...) // packet number 53 | 54 | return hdr 55 | } 56 | 57 | // buildPacket constructs an Initial QUIC packet 58 | // and applies Initial protection. 59 | // https://www.rfc-editor.org/rfc/rfc9001.html#name-client-initial 60 | func buildPacket() ([]byte, ConnectionID, ConnectionID) { 61 | destConnID, srcConnID := generateConnectionIDs() 62 | // generate random payload 63 | minPayloadSize := 1200 - 14 - (len(destConnID) + len(srcConnID)) 64 | randomPayload := make([]byte, minPayloadSize) 65 | random.Seed(time.Now().UnixNano()) 66 | random.Read(randomPayload) 67 | 68 | clientSecret, _ := computeSecrets(destConnID) 69 | encrypted := encryptPayload(randomPayload, destConnID, clientSecret) 70 | hdr := buildHeader(destConnID, srcConnID, len(encrypted)) 71 | raw := append(hdr, encrypted...) 72 | 73 | raw = encryptHeader(raw, hdr, clientSecret) 74 | return raw, destConnID, srcConnID 75 | } 76 | 77 | // encryptHeader applies header protection to the packet bytes (raw). 78 | // https://www.rfc-editor.org/rfc/rfc9001.html#name-client-initial 79 | // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection 80 | func encryptHeader(raw, hdr, clientSecret []byte) []byte { 81 | hp := computeHP(clientSecret) 82 | block, err := aes.NewCipher(hp) 83 | if err != nil { 84 | panic(fmt.Sprintf("error creating new AES cipher: %s", err)) 85 | } 86 | hdroffset := 0 87 | payloadOffset := len(hdr) 88 | sample := raw[payloadOffset : payloadOffset+16] 89 | 90 | mask := make([]byte, block.BlockSize()) 91 | if len(sample) != len(mask) { 92 | panic("invalid sample size") 93 | } 94 | block.Encrypt(mask, sample) 95 | 96 | pnOffset := len(hdr) - 4 97 | pnBytes := raw[pnOffset:payloadOffset] 98 | raw[hdroffset] ^= mask[0] & 0xf 99 | for i := range pnBytes { 100 | pnBytes[i] ^= mask[i+1] 101 | } 102 | return raw 103 | } 104 | 105 | // encryptPayload encrypts the payload of the packet. 106 | // https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection 107 | func encryptPayload(payload, destConnID ConnectionID, clientSecret []byte) []byte { 108 | myKey, myIV := computeInitialKeyAndIV(clientSecret) 109 | encrypter := aeadAESGCMTLS13(myKey, myIV) 110 | 111 | nonceBuf := make([]byte, encrypter.NonceSize()) 112 | var pn int64 = 2 113 | binary.BigEndian.PutUint64(nonceBuf[len(nonceBuf)-8:], uint64(pn)) 114 | 115 | encrypted := encrypter.Seal(nil, nonceBuf, payload, nil) 116 | return encrypted 117 | } 118 | 119 | // generateConnectionID generates a connection ID using cryptographic random 120 | func generateConnectionID(len int) (ConnectionID, error) { 121 | b := make([]byte, len) 122 | if _, err := rand.Read(b); err != nil { 123 | return nil, err 124 | } 125 | return ConnectionID(b), nil 126 | } 127 | 128 | // generateConnectionIDForInitial generates a connection ID for the Initial packet. 129 | // It uses a length randomly chosen between 8 and 18 bytes. 130 | func generateConnectionIDForInitial() (ConnectionID, error) { 131 | r := make([]byte, 1) 132 | if _, err := rand.Read(r); err != nil { 133 | return nil, err 134 | } 135 | len := MinConnectionIDLenInitial + int(r[0])%(maxConnectionIDLen-MinConnectionIDLenInitial+1) 136 | return generateConnectionID(len) 137 | } 138 | 139 | // generateConnectionIDs generates a destination and source connection ID. 140 | func generateConnectionIDs() ([]byte, []byte) { 141 | destConnID, err := generateConnectionIDForInitial() 142 | checkError(err) 143 | srcConnID, err := generateConnectionID(DefaultConnectionIDLength) 144 | checkError(err) 145 | return destConnID, srcConnID 146 | } 147 | 148 | func main() { 149 | if len(os.Args) < 2 || len(os.Args) > 3 { 150 | fmt.Fprintf(os.Stderr, "Usage: %s host:port [--hexdump]\n --hexdump: save the generated QUIC packet to packet.txt\n", os.Args[0]) 151 | os.Exit(1) 152 | } 153 | service := os.Args[1] 154 | hexdumpFile := "" 155 | if len(os.Args) == 3 { 156 | hexdumpFile = "packet.txt" 157 | } 158 | 159 | udpAddr, err := net.ResolveUDPAddr("udp4", service) 160 | checkError(err) 161 | 162 | conn, err := net.DialUDP("udp", nil, udpAddr) 163 | checkError(err) 164 | 165 | defer conn.Close() 166 | 167 | send, dstID, srcID := buildPacket() 168 | conn.Write(send) 169 | 170 | if hexdumpFile != "" { 171 | f, err := os.Create(hexdumpFile) 172 | checkError(err) 173 | defer f.Close() 174 | hexdump := hex.EncodeToString(send) 175 | _, err = f.WriteString(hexdump) 176 | } 177 | 178 | buffer := make([]byte, 1024) 179 | n, _, err := conn.ReadFromUDP(buffer) 180 | if err != nil { 181 | fmt.Println(err) 182 | return 183 | } 184 | dissectVersionNegotiation(buffer[0:n], dstID, srcID) 185 | } 186 | 187 | // dissectVersionNegotiation dissects the Version Negotiation response 188 | // and prints it to the command line. 189 | // https://www.rfc-editor.org/rfc/rfc9000.html#name-version-negotiation-packet 190 | func dissectVersionNegotiation(i []byte, dstID, srcID ConnectionID) { 191 | firstByte := uint8(i[0]) 192 | mask := 0b10000000 193 | mask &= int(firstByte) 194 | if mask == 0 { 195 | fmt.Println("not a long header packet") 196 | return 197 | } 198 | 199 | versionBytes := i[1:5] 200 | v := binary.BigEndian.Uint32(versionBytes) 201 | if v != 0 { 202 | fmt.Println("unexpected version in Version Negotiation packet") 203 | return 204 | } 205 | 206 | dstLength := i[5] 207 | offset := 6 + uint8(dstLength) 208 | dst := i[6:offset] 209 | if hex.EncodeToString(dst) != hex.EncodeToString(srcID) { 210 | fmt.Println("unexpected destination connection ID in response", dst, dstID) 211 | return 212 | } 213 | srcLength := i[offset] 214 | src := i[offset+1 : offset+1+srcLength] 215 | offset = offset + 1 + srcLength 216 | if hex.EncodeToString(src) != hex.EncodeToString(dstID) { 217 | fmt.Println("unexpected source connection ID in response", dst, dstID) 218 | return 219 | } 220 | 221 | n := uint8(len(i)) 222 | fmt.Println("Supported Versions:") 223 | for offset < n { 224 | supportedVersion := binary.BigEndian.Uint32(i[offset : offset+4]) 225 | fmt.Printf("%08x ", supportedVersion) 226 | offset += 4 227 | } 228 | fmt.Println("") 229 | } 230 | 231 | // checkError does error handling 232 | func checkError(err error) { 233 | if err != nil { 234 | fmt.Fprintf(os.Stderr, "Fatal error ", err.Error()) 235 | os.Exit(1) 236 | } 237 | } 238 | --------------------------------------------------------------------------------