├── .deepsource.toml ├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── clienthello.go ├── fingerprint_hash.go ├── go.mod ├── go.sum ├── internal ├── testdata │ ├── QUIC_ClientHello_Chrome_124.bin │ ├── QUIC_Frame_Chrome_124_CRYPTO_0.bin │ ├── QUIC_Frame_Chrome_124_CRYPTO_1191.bin │ ├── QUIC_Frame_Chrome_124_CRYPTO_1287.bin │ ├── QUIC_Frame_Chrome_124_CRYPTO_1561.bin │ ├── QUIC_Frame_Chrome_124_CRYPTO_1663.bin │ ├── QUIC_IETF_Chrome_125_PKN1.bin │ ├── QUIC_IETF_Chrome_125_PKN2.bin │ ├── QUIC_IETF_Firefox_126.bin │ ├── QUIC_IETF_Firefox_126_0-RTT.bin │ └── TLS_ClientHello_Firefox_126.bin └── utils │ ├── arr_dedup.go │ ├── rewindconn.go │ ├── rewindreader.go │ ├── typeconv.go │ ├── udppacket.go │ ├── uint8_not_string.go │ ├── uint8_not_string_test.go │ └── utls.go ├── modcaddy ├── Caddyfile ├── README.md ├── app │ ├── caddyfile.go │ └── reservoir.go ├── handler │ └── handler.go ├── listener │ └── listener.go └── mod_caddy.go ├── quic_client_initial.go ├── quic_client_initial_test.go ├── quic_clienthello.go ├── quic_clienthello_reconstructor.go ├── quic_clienthello_reconstructor_test.go ├── quic_clienthello_test.go ├── quic_common.go ├── quic_common_test.go ├── quic_crypto.go ├── quic_crypto_test.go ├── quic_fingerprint.go ├── quic_frame.go ├── quic_frame_test.go ├── quic_header.go ├── quic_header_test.go ├── quic_transport_parameters.go ├── quic_transport_parameters_test.go └── tls_fingerprint.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["**/*_test.go"] 4 | 5 | [[analyzers]] 6 | name = "go" 7 | 8 | [analyzers.meta] 9 | import_root = "github.com/gaukas/clienthellod" 10 | 11 | [[transformers]] 12 | name = "gofumpt" -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: "1.22.x" 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -timeout 10s -v ./... 29 | 30 | - name: Race Detection 31 | run: go test -timeout 10s -race ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Caddy binary 132 | */caddy 133 | caddy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `clienthellod`: TLS ClientHello/QUIC Initial Packet reflection service 2 | ![Go Build Status](https://github.com/gaukas/clienthellod/actions/workflows/go.yml/badge.svg) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/gaukas/clienthellod)](https://goreportcard.com/report/github.com/gaukas/clienthellod) 4 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod?ref=badge_shield&issueType=license) 5 | [![Go Doc](https://pkg.go.dev/badge/github.com/refraction-networking/water.svg)](https://pkg.go.dev/github.com/refraction-networking/water) 6 | 7 | `clienthellod`, read as "client-hello-D", is a TLS ClientHello/QUIC Initial Packet reflection service. It can be used to parses TLS ClientHello messages and QUIC Initial Packets into human-readable and highly programmable formats such as JSON. 8 | 9 | Is is a part of the TLS fingerprintability research project which spans [tlsfingerprint.io](https://tlsfingerprint.io) and [quic.tlsfingerprint.io](https://quic.tlsfingerprint.io). It parses the ClientHello messages sent by TLS clients and QUIC Client Initial Packets sent by QUIC clients and display the parsed information in a human-readable format with high programmability. 10 | 11 | See [tlsfingerprint.io](https://tlsfingerprint.io) and [quic.tlsfingerprint.io](https://quic.tlsfingerprint.io) for more details about the project. 12 | 13 | ## Quick Start 14 | 15 | `clienthellod` comes as a Go library, which can be used to parse both TLS and QUIC protocols. 16 | 17 | ### TLS/QUIC Fingerprinter 18 | 19 | ```go 20 | tlsFingerprinter := clienthellod.NewTLSFingerprinter() 21 | ``` 22 | 23 | ```go 24 | quicFingerprinter := clienthellod.NewQUICFingerprinter() 25 | ``` 26 | 27 | ### TLS ClientHello 28 | 29 | #### From a `net.Conn` 30 | 31 | ```go 32 | tcpLis, err := net.Listen("tcp", ":443") 33 | defer tcpLis.Close() 34 | 35 | conn, err := tcpLis.Accept() 36 | if err != nil { 37 | panic(err) 38 | } 39 | defer conn.Close() 40 | 41 | ch, err := clienthellod.ReadClientHello(conn) // reads ClientHello from the connection 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | err := ch.ParseClientHello() // parses ClientHello's fields 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | jsonB, err = json.MarshalIndent(ch, "", " ") 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | fmt.Println(string(jsonB)) 57 | fmt.Println("ClientHello ID: " + ch.HexID) // prints ClientHello's original fingerprint ID calculated using observed TLS extension order 58 | fmt.Println("ClientHello NormID: " + ch.NormHexID) // prints ClientHello's normalized fingerprint ID calculated using sorted TLS extension list 59 | ``` 60 | 61 | #### From raw `[]byte` 62 | 63 | ```go 64 | ch, err := clienthellod.UnmarshalClientHello(raw) 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | // err := ch.ParseClientHello() // no need to call again, UnmarshalClientHello automatically calls ParseClientHello 70 | ``` 71 | 72 | ### QUIC Initial Packets (Client-sourced) 73 | 74 | #### Single packet 75 | 76 | ```go 77 | udpConn, err := net.ListenUDP("udp", ":443") 78 | defer udpConn.Close() 79 | 80 | buf := make([]byte, 65535) 81 | n, addr, err := udpConn.ReadFromUDP(buf) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | ci, err := clienthellod.UnmarshalQUICClientInitialPacket(buf[:n]) // decodes QUIC Client Initial Packet 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | jsonB, err = json.MarshalIndent(cip, "", " ") 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | fmt.Println(string(jsonB)) // including fingerprint IDs of: ClientInitialPacket, QUIC Header, QUIC ClientHello, QUIC Transport Parameters' combination 97 | ``` 98 | 99 | #### Multiple packets 100 | 101 | Implementations including Chrome/Chromium sends oversized Client Hello which does not fit into one single QUIC packet, in which case multiple QUIC Initial Packets are sent. 102 | 103 | ```go 104 | gci := GatherClientInitials() // Each GatherClientInitials reassembles one QUIC Client Initial Packets stream. Use a QUIC Fingerprinter for multiple potential senders, which automatically demultiplexes the packets based on the source address. 105 | 106 | udpConn, err := net.ListenUDP("udp", ":443") 107 | defer udpConn.Close() 108 | 109 | for { 110 | buf := make([]byte, 65535) 111 | n, addr, err := udpConn.ReadFromUDP(buf) 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | if addr != knownSenderAddr { 117 | continue 118 | } 119 | 120 | ci, err := clienthellod.UnmarshalQUICClientInitialPacket(buf[:n]) // decodes QUIC Client Initial Packet 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | err = gci.AddPacket(ci) 126 | if err != nil { 127 | panic(err) 128 | } 129 | } 130 | ``` 131 | 132 | ### Use with Caddy 133 | 134 | We also provide clienthellod as a Caddy Module in `modcaddy`, which you can use with Caddy to capture ClientHello messages and QUIC Client Initial Packets. See [modcaddy](https://github.com/gaukas/clienthellod/tree/master/modcaddy) for more details. 135 | 136 | ## License 137 | 138 | This project is developed and distributed under Apache-2.0 license. 139 | 140 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod?ref=badge_large&issueType=license) 141 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Use the form [here](https://github.com/gaukas/clienthellod/security/advisories/new) to report a vulnerability privately to the developers of this project. 6 | 7 | For low-severity or other impractical vulnerabilities, you may feel free to open an issue to discuss it. 8 | -------------------------------------------------------------------------------- /clienthello.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "runtime" 10 | "sort" 11 | 12 | "github.com/gaukas/clienthellod/internal/utils" 13 | tls "github.com/refraction-networking/utls" 14 | "github.com/refraction-networking/utls/dicttls" 15 | "golang.org/x/crypto/cryptobyte" 16 | ) 17 | 18 | // ClientHello represents a captured ClientHello message with all fingerprintable fields. 19 | type ClientHello struct { 20 | raw []byte 21 | 22 | TLSRecordVersion uint16 `json:"tls_record_version"` // TLS record version (major, minor) 23 | TLSHandshakeVersion uint16 `json:"tls_handshake_version"` // TLS handshake version (major, minor) 24 | 25 | CipherSuites []uint16 `json:"cipher_suites"` 26 | CompressionMethods utils.Uint8Arr `json:"compression_methods"` 27 | Extensions []uint16 `json:"extensions"` // extension IDs in original order 28 | ExtensionsNormalized []uint16 `json:"extensions_normalized"` // sorted extension IDs 29 | 30 | ServerName string `json:"server_name"` // server_name(0) 31 | NamedGroupList []uint16 `json:"supported_groups"` // supported_groups(10) a.k.a elliptic_curves 32 | ECPointFormatList utils.Uint8Arr `json:"ec_point_formats"` // ec_point_formats(11) 33 | SignatureSchemeList []uint16 `json:"signature_algorithms"` // signature_algorithms(13) 34 | ALPN []string `json:"alpn"` // alpn(16) 35 | CertCompressAlgo []uint16 `json:"compress_certificate"` // compress_certificate(27) 36 | RecordSizeLimit utils.Uint8Arr `json:"record_size_limit"` // record_size_limit(28) 37 | SupportedVersions []uint16 `json:"supported_versions"` // supported_versions(43) 38 | PSKKeyExchangeModes utils.Uint8Arr `json:"psk_key_exchange_modes"` // psk_key_exchange_modes(45) 39 | KeyShare []uint16 `json:"key_share"` // key_share(51) 40 | ApplicationSettings []string `json:"application_settings"` // application_settings(17513) a.k.a ALPS 41 | 42 | UserAgent string `json:"user_agent,omitempty"` // User-Agent header, set by the caller 43 | 44 | NumID int64 `json:"num_id,omitempty"` // NID of the fingerprint 45 | NormNumID int64 `json:"norm_num_id,omitempty"` // Normalized NID of the fingerprint 46 | HexID string `json:"hex_id,omitempty"` // ID of the fingerprint (hex string) 47 | NormHexID string `json:"norm_hex_id,omitempty"` // Normalized ID of the fingerprint (hex string) 48 | 49 | // below are ONLY used for calculating the fingerprint (hash) 50 | lengthPrefixedSupportedGroups []uint16 51 | lengthPrefixedEcPointFormats []uint8 52 | lengthPrefixedSignatureAlgos []uint16 53 | alpnWithLengths []uint8 54 | lengthPrefixedCertCompressAlgos []uint8 55 | keyshareGroupsWithLengths []uint16 56 | 57 | // QUIC-only, nil if not QUIC 58 | qtp *QUICTransportParameters 59 | } 60 | 61 | // ReadClientHello reads a ClientHello from a connection (io.Reader) 62 | // and returns a ClientHello struct. 63 | // 64 | // It will return an error if the reader does not give a stream of bytes 65 | // representing a valid ClientHello. But all bytes read from the reader 66 | // will be stored in the ClientHello struct to be rewinded by the caller 67 | // if ever needed. 68 | // 69 | // This function does not automatically call [ClientHello.ParseClientHello]. 70 | func ReadClientHello(r io.Reader) (ch *ClientHello, err error) { 71 | ch = new(ClientHello) 72 | // Read a TLS record 73 | // Read exactly 5 bytes from the reader 74 | ch.raw = make([]byte, 5) 75 | if _, err = io.ReadFull(r, ch.raw); err != nil { 76 | return 77 | } 78 | 79 | // Check if the first byte is 0x16 (TLS Handshake) 80 | if ch.raw[0] != 0x16 { 81 | err = errors.New("not a TLS handshake record") 82 | return 83 | } 84 | 85 | // Read exactly length bytes from the reader 86 | ch.raw = append(ch.raw, make([]byte, binary.BigEndian.Uint16(ch.raw[3:5]))...) 87 | _, err = io.ReadFull(r, ch.raw[5:]) 88 | return 89 | } 90 | 91 | // UnmarshalClientHello unmarshals a ClientHello from a byte slice 92 | // and returns a ClientHello struct. Any extra bytes after the ClientHello 93 | // message will be ignored. 94 | // 95 | // This function automatically calls [ClientHello.ParseClientHello]. 96 | func UnmarshalClientHello(p []byte) (ch *ClientHello, err error) { 97 | r := bytes.NewReader(p) 98 | ch, err = ReadClientHello(r) 99 | if err != nil { 100 | return 101 | } 102 | 103 | err = ch.ParseClientHello() 104 | return 105 | } 106 | 107 | func (ch *ClientHello) Raw() []byte { 108 | return ch.raw 109 | } 110 | 111 | // ParseClientHello parses the raw bytes of a ClientHello into a ClientHello struct. 112 | func (ch *ClientHello) ParseClientHello() error { 113 | // Call uTLS to parse the raw bytes into ClientHelloSpec 114 | fingerprinter := tls.Fingerprinter{ 115 | AllowBluntMimicry: true, // we will need all the extensions even when not recognized 116 | } 117 | chs, err := fingerprinter.RawClientHello(ch.raw) 118 | if err != nil { 119 | return fmt.Errorf("failed to parse ClientHello, (*tls.Fingerprinter).RawClientHello(): %w", err) 120 | } 121 | 122 | // ch.TLSRecordVersion = chs.TLSVersMin // won't work for TLS 1.3 123 | // ch.TLSHandshakeVersion = chs.TLSVersMax // won't work for TLS 1.3 124 | ch.CipherSuites = chs.CipherSuites 125 | ch.CompressionMethods = chs.CompressionMethods 126 | 127 | // parse extensions 128 | ch.parseExtensions(chs) 129 | 130 | // Call uTLS to parse the raw bytes into ClientHelloMsg 131 | chm := tls.UnmarshalClientHello(ch.raw[5:]) 132 | if chm == nil { 133 | return errors.New("failed to parse ClientHello, (*tls.ClientHelloInfo).Unmarshal(): nil") 134 | } 135 | ch.ServerName = chm.ServerName 136 | 137 | runtime.SetFinalizer(ch, func(c *ClientHello) { 138 | c.qtp = nil // other trivial types are easy to GC 139 | }) 140 | 141 | // In the end parse extra information from raw 142 | return ch.parseExtra() 143 | } 144 | 145 | func (ch *ClientHello) parseExtensions(chs *tls.ClientHelloSpec) { // skipcq: GO-R1005 146 | for _, ext := range chs.Extensions { 147 | switch ext := ext.(type) { 148 | case *tls.SupportedCurvesExtension: 149 | for _, curve := range ext.Curves { 150 | ch.NamedGroupList = append(ch.NamedGroupList, uint16(curve)) 151 | } 152 | ch.lengthPrefixedSupportedGroups = append(ch.lengthPrefixedSupportedGroups, 2*uint16(len(ch.NamedGroupList))) 153 | ch.lengthPrefixedSupportedGroups = append(ch.lengthPrefixedSupportedGroups, ch.NamedGroupList...) 154 | case *tls.SupportedPointsExtension: 155 | ch.ECPointFormatList = ext.SupportedPoints 156 | ch.lengthPrefixedEcPointFormats = append(ch.lengthPrefixedEcPointFormats, uint8(len(ext.SupportedPoints))) 157 | ch.lengthPrefixedEcPointFormats = append(ch.lengthPrefixedEcPointFormats, ext.SupportedPoints...) 158 | case *tls.SignatureAlgorithmsExtension: 159 | for _, sig := range ext.SupportedSignatureAlgorithms { 160 | ch.SignatureSchemeList = append(ch.SignatureSchemeList, uint16(sig)) 161 | } 162 | ch.lengthPrefixedSignatureAlgos = append(ch.lengthPrefixedSignatureAlgos, 2*uint16(len(ch.SignatureSchemeList))) 163 | ch.lengthPrefixedSignatureAlgos = append(ch.lengthPrefixedSignatureAlgos, ch.SignatureSchemeList...) 164 | case *tls.ALPNExtension: 165 | ch.ALPN = ext.AlpnProtocols 166 | // we will get alpnWithLengths from raw 167 | case *tls.UtlsCompressCertExtension: 168 | for _, algo := range ext.Algorithms { 169 | ch.CertCompressAlgo = append(ch.CertCompressAlgo, uint16(algo)) 170 | } 171 | ch.lengthPrefixedCertCompressAlgos = append(ch.lengthPrefixedCertCompressAlgos, 2*uint8(len(ch.CertCompressAlgo))) 172 | ch.lengthPrefixedCertCompressAlgos = append( 173 | ch.lengthPrefixedCertCompressAlgos, 174 | utils.Uint16ToUint8(ch.CertCompressAlgo)..., 175 | ) 176 | case *tls.FakeRecordSizeLimitExtension: 177 | ch.RecordSizeLimit = append(ch.RecordSizeLimit, uint8(ext.Limit>>8), uint8(ext.Limit)) 178 | case *tls.SupportedVersionsExtension: 179 | for _, ver := range ext.Versions { 180 | ch.SupportedVersions = append(ch.SupportedVersions, uint16(ver)) 181 | } 182 | case *tls.PSKKeyExchangeModesExtension: 183 | ch.PSKKeyExchangeModes = ext.Modes 184 | case *tls.KeyShareExtension: 185 | for _, ks := range ext.KeyShares { 186 | ch.KeyShare = append(ch.KeyShare, uint16(ks.Group)) 187 | // get below from raw instead 188 | // ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, uint16(ks.Group)) 189 | // ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, uint16(len(ks.Data))) 190 | } 191 | case *tls.ApplicationSettingsExtension: 192 | ch.ApplicationSettings = ext.SupportedProtocols 193 | case *tls.GenericExtension: 194 | if ext.Id == dicttls.ExtType_quic_transport_parameters { 195 | ch.qtp = ParseQUICTransportParameters(ext.Data) 196 | } 197 | } 198 | } 199 | } 200 | 201 | // parseExtra parses extra information from raw bytes which couldn't be parsed by uTLS. 202 | func (ch *ClientHello) parseExtra() error { 203 | // parse alpnWithLengths and Extensions 204 | s := cryptobyte.String(ch.raw) 205 | var recordVersion uint16 206 | if !s.Skip(1) || !s.ReadUint16(&recordVersion) || !s.Skip(2) { // skip TLS record header 207 | return errors.New("failed to parse TLS header, cryptobyte.String().Skip(): false") 208 | } 209 | ch.TLSRecordVersion = recordVersion 210 | var handshakeVersion uint16 211 | if !s.Skip(1) || // skip Handshake type 212 | !s.Skip(3) || // skip Handshake length 213 | !s.ReadUint16(&handshakeVersion) || // parse ClientHello version 214 | !s.Skip(32) { // skip ClientHello random 215 | return errors.New("failed to parse ClientHello, cryptobyte.String().Skip(): false") 216 | } 217 | ch.TLSHandshakeVersion = handshakeVersion 218 | 219 | var ignoredSessionID cryptobyte.String 220 | if !s.ReadUint8LengthPrefixed(&ignoredSessionID) { 221 | return errors.New("unable to read session id") 222 | } 223 | 224 | var ignoredCipherSuites cryptobyte.String 225 | if !s.ReadUint16LengthPrefixed(&ignoredCipherSuites) { 226 | return errors.New("unable to read ciphersuites") 227 | } 228 | 229 | var ignoredCompressionMethods cryptobyte.String 230 | if !s.ReadUint8LengthPrefixed(&ignoredCompressionMethods) { 231 | return errors.New("unable to read compression methods") 232 | } 233 | 234 | if s.Empty() { 235 | return nil // no extensions 236 | } 237 | 238 | var extensions cryptobyte.String 239 | if !s.ReadUint16LengthPrefixed(&extensions) { 240 | return errors.New("unable to read extensions data") 241 | } 242 | 243 | err := ch.parseExtensionsExtra(extensions) 244 | if err != nil { 245 | return fmt.Errorf("failed to parse extensions, parseExtensionsExtra(): %w", err) 246 | } 247 | 248 | // sort ch.Extensions and put result to ch.ExtensionsNormalized 249 | ch.ExtensionsNormalized = make([]uint16, len(ch.Extensions)) 250 | copy(ch.ExtensionsNormalized, ch.Extensions) 251 | sort.Slice(ch.ExtensionsNormalized, func(i, j int) bool { 252 | return ch.ExtensionsNormalized[i] < ch.ExtensionsNormalized[j] 253 | }) 254 | 255 | // calculate fingerprint 256 | ch.NumID, ch.NormNumID = ch.calcNumericID() 257 | ch.HexID = FingerprintID(ch.NumID).AsHex() 258 | ch.NormHexID = FingerprintID(ch.NormNumID).AsHex() 259 | 260 | return nil 261 | } 262 | 263 | func (ch *ClientHello) parseExtensionsExtra(extensions cryptobyte.String) error { 264 | var extensionIDs []uint16 265 | for !extensions.Empty() { 266 | var extensionID uint16 267 | var extensionData cryptobyte.String 268 | if !extensions.ReadUint16(&extensionID) { 269 | return errors.New("unable to read extension ID") 270 | } 271 | if !extensions.ReadUint16LengthPrefixed(&extensionData) { 272 | return errors.New("unable to read extension data") 273 | } 274 | 275 | extensionID, err := ch.parseExtensionExtra(extensionID, extensionData) 276 | if err != nil { 277 | return fmt.Errorf("failed to parse extension, parseExtensionExtra(): %w", err) 278 | } 279 | extensionIDs = append(extensionIDs, extensionID) // extension ID might need to be overridden by parseExtensionExtra() in case of GREASE 280 | } 281 | ch.Extensions = extensionIDs 282 | 283 | return nil 284 | } 285 | 286 | func (ch *ClientHello) parseExtensionExtra(extensionID uint16, extensionData cryptobyte.String) (uint16, error) { 287 | switch extensionID { 288 | case 16: // ALPN 289 | ch.alpnWithLengths = extensionData 290 | case 51: // keyshare 291 | if !extensionData.Skip(2) { 292 | return 0, errors.New("unable to skip keyshare total length") 293 | } 294 | for !extensionData.Empty() { 295 | var group uint16 296 | var length uint16 297 | if !extensionData.ReadUint16(&group) || !extensionData.ReadUint16(&length) { 298 | return 0, errors.New("unable to read keyshare group") 299 | } 300 | if utils.IsGREASEUint16(group) { 301 | group = tls.GREASE_PLACEHOLDER 302 | } 303 | ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, group, length) 304 | 305 | if !extensionData.Skip(int(length)) { 306 | return 0, errors.New("unable to skip keyshare data") 307 | } 308 | } 309 | default: 310 | if utils.IsGREASEUint16(extensionID) { 311 | return tls.GREASE_PLACEHOLDER, nil 312 | } 313 | } 314 | 315 | return extensionID, nil 316 | } 317 | -------------------------------------------------------------------------------- /fingerprint_hash.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "crypto/sha1" // skipcq: GSC-G505 5 | "encoding/binary" 6 | "encoding/hex" 7 | "hash" 8 | 9 | "github.com/gaukas/clienthellod/internal/utils" 10 | ) 11 | 12 | func updateArr(h hash.Hash, arr []byte) { 13 | binary.Write(h, binary.BigEndian, uint32(len(arr))) 14 | h.Write(arr) 15 | } 16 | 17 | func updateU32(h hash.Hash, i uint32) { 18 | binary.Write(h, binary.BigEndian, i) 19 | } 20 | 21 | func updateU64(h hash.Hash, i uint64) { 22 | binary.Write(h, binary.BigEndian, i) 23 | } 24 | 25 | // FingerprintID is the type of fingerprint ID. 26 | type FingerprintID int64 27 | 28 | // AsHex returns the hex representation of this fingerprint ID. 29 | func (id FingerprintID) AsHex() string { 30 | hid := make([]byte, 8) 31 | binary.BigEndian.PutUint64(hid, uint64(id)) 32 | return hex.EncodeToString(hid) 33 | } 34 | 35 | // calcNumericID returns the numeric ID of this client hello. 36 | func (ch *ClientHello) calcNumericID() (orig, norm int64) { 37 | for _, normalized := range []bool{false, true} { 38 | h := sha1.New() // skipcq: GO-S1025, GSC-G401, 39 | binary.Write(h, binary.BigEndian, uint16(ch.TLSRecordVersion)) 40 | binary.Write(h, binary.BigEndian, uint16(ch.TLSHandshakeVersion)) 41 | 42 | updateArr(h, utils.Uint16ToUint8(ch.CipherSuites)) 43 | updateArr(h, ch.CompressionMethods) 44 | if normalized { 45 | updateArr(h, utils.Uint16ToUint8(ch.ExtensionsNormalized)) 46 | } else { 47 | updateArr(h, utils.Uint16ToUint8(ch.Extensions)) 48 | } 49 | updateArr(h, utils.Uint16ToUint8(ch.lengthPrefixedSupportedGroups)) 50 | updateArr(h, ch.lengthPrefixedEcPointFormats) 51 | updateArr(h, utils.Uint16ToUint8(ch.lengthPrefixedSignatureAlgos)) 52 | updateArr(h, ch.alpnWithLengths) 53 | updateArr(h, utils.Uint16ToUint8(ch.keyshareGroupsWithLengths)) 54 | updateArr(h, ch.PSKKeyExchangeModes) 55 | updateArr(h, utils.Uint16ToUint8(ch.SupportedVersions)) 56 | updateArr(h, ch.lengthPrefixedCertCompressAlgos) 57 | updateArr(h, ch.RecordSizeLimit) 58 | 59 | if normalized { 60 | norm = int64(binary.BigEndian.Uint64(h.Sum(nil)[:8])) 61 | } else { 62 | orig = int64(binary.BigEndian.Uint64(h.Sum(nil)[:8])) 63 | } 64 | } 65 | 66 | return 67 | } 68 | 69 | // calcNumericID returns the numeric ID of this gathered client initial. 70 | func (gci *GatheredClientInitials) calcNumericID() uint64 { 71 | h := sha1.New() // skipcq: GO-S1025, GSC-G401 72 | updateArr(h, gci.Packets[0].Header.Version) 73 | updateU32(h, gci.Packets[0].Header.DCIDLength) 74 | updateU32(h, gci.Packets[0].Header.SCIDLength) 75 | updateArr(h, gci.Packets[0].Header.PacketNumber) 76 | 77 | // merge, deduplicate, and sort all frames from all packets 78 | var allFrameIDs []uint8 79 | for _, p := range gci.Packets { 80 | allFrameIDs = append(allFrameIDs, p.frames.FrameTypesUint8()...) 81 | } 82 | dedupAllFrameIDs := utils.DedupIntArr(allFrameIDs) 83 | updateArr(h, dedupAllFrameIDs) 84 | 85 | if gci.Packets[0].Header.HasToken { 86 | updateU32(h, TOKEN_PRESENT) 87 | } else { 88 | updateU32(h, TOKEN_ABSENT) 89 | } 90 | 91 | return binary.BigEndian.Uint64(h.Sum(nil)[0:8]) 92 | } 93 | 94 | // calcNumericID returns the numeric ID of this transport parameters combination. 95 | func (qtp *QUICTransportParameters) calcNumericID() uint64 { 96 | h := sha1.New() // skipcq: GO-S1025, GSC-G401 97 | updateArr(h, qtp.MaxIdleTimeout) 98 | updateArr(h, qtp.MaxUDPPayloadSize) 99 | updateArr(h, qtp.InitialMaxData) 100 | updateArr(h, qtp.InitialMaxStreamDataBidiLocal) 101 | updateArr(h, qtp.InitialMaxStreamDataBidiRemote) 102 | updateArr(h, qtp.InitialMaxStreamDataUni) 103 | updateArr(h, qtp.InitialMaxStreamsBidi) 104 | updateArr(h, qtp.InitialMaxStreamsUni) 105 | updateArr(h, qtp.AckDelayExponent) 106 | updateArr(h, qtp.MaxAckDelay) 107 | updateArr(h, qtp.ActiveConnectionIDLimit) 108 | 109 | updateU32(h, uint32(len(qtp.QTPIDs))) 110 | for _, id := range qtp.QTPIDs { 111 | updateU64(h, id) 112 | } 113 | 114 | return binary.BigEndian.Uint64(h.Sum(nil)) 115 | } 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gaukas/clienthellod 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.22.3 6 | 7 | require ( 8 | github.com/caddyserver/caddy/v2 v2.8.4 9 | github.com/google/gopacket v1.1.19 10 | github.com/refraction-networking/utls v1.6.6 11 | go.uber.org/zap v1.27.0 12 | golang.org/x/crypto v0.23.0 13 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 14 | ) 15 | 16 | require ( 17 | filippo.io/edwards25519 v1.1.0 // indirect 18 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 19 | github.com/Masterminds/goutils v1.1.1 // indirect 20 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 21 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 22 | github.com/Microsoft/go-winio v0.6.0 // indirect 23 | github.com/andybalholm/brotli v1.0.6 // indirect 24 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 25 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/caddyserver/certmagic v0.21.3 // indirect 28 | github.com/caddyserver/zerossl v0.1.3 // indirect 29 | github.com/cespare/xxhash v1.1.0 // indirect 30 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 31 | github.com/chzyer/readline v1.5.1 // indirect 32 | github.com/cloudflare/circl v1.3.7 // indirect 33 | github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 34 | github.com/dgraph-io/badger v1.6.2 // indirect 35 | github.com/dgraph-io/badger/v2 v2.2007.4 // indirect 36 | github.com/dgraph-io/ristretto v0.1.0 // indirect 37 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 38 | github.com/dustin/go-humanize v1.0.1 // indirect 39 | github.com/go-jose/go-jose/v3 v3.0.3 // indirect 40 | github.com/go-kit/kit v0.13.0 // indirect 41 | github.com/go-kit/log v0.2.1 // indirect 42 | github.com/go-logfmt/logfmt v0.6.0 // indirect 43 | github.com/go-sql-driver/mysql v1.7.1 // indirect 44 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 45 | github.com/golang/glog v1.2.0 // indirect 46 | github.com/golang/protobuf v1.5.4 // indirect 47 | github.com/golang/snappy v0.0.4 // indirect 48 | github.com/google/cel-go v0.20.1 // indirect 49 | github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect 50 | github.com/google/uuid v1.6.0 // indirect 51 | github.com/huandu/xstrings v1.3.3 // indirect 52 | github.com/imdario/mergo v0.3.12 // indirect 53 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 54 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 55 | github.com/jackc/pgconn v1.14.3 // indirect 56 | github.com/jackc/pgio v1.0.0 // indirect 57 | github.com/jackc/pgpassfile v1.0.0 // indirect 58 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 59 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 60 | github.com/jackc/pgtype v1.14.0 // indirect 61 | github.com/jackc/pgx/v4 v4.18.3 // indirect 62 | github.com/klauspost/compress v1.17.8 // indirect 63 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 64 | github.com/libdns/libdns v0.2.2 // indirect 65 | github.com/manifoldco/promptui v0.9.0 // indirect 66 | github.com/mattn/go-colorable v0.1.13 // indirect 67 | github.com/mattn/go-isatty v0.0.20 // indirect 68 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 69 | github.com/mholt/acmez/v2 v2.0.1 // indirect 70 | github.com/miekg/dns v1.1.59 // indirect 71 | github.com/mitchellh/copystructure v1.2.0 // indirect 72 | github.com/mitchellh/go-ps v1.0.0 // indirect 73 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 74 | github.com/onsi/ginkgo/v2 v2.13.2 // indirect 75 | github.com/pkg/errors v0.9.1 // indirect 76 | github.com/prometheus/client_golang v1.19.1 // indirect 77 | github.com/prometheus/client_model v0.5.0 // indirect 78 | github.com/prometheus/common v0.48.0 // indirect 79 | github.com/prometheus/procfs v0.12.0 // indirect 80 | github.com/quic-go/qpack v0.4.0 // indirect 81 | github.com/quic-go/quic-go v0.44.0 // indirect 82 | github.com/rs/xid v1.5.0 // indirect 83 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 84 | github.com/shopspring/decimal v1.2.0 // indirect 85 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 86 | github.com/slackhq/nebula v1.6.1 // indirect 87 | github.com/smallstep/certificates v0.26.1 // indirect 88 | github.com/smallstep/nosql v0.6.1 // indirect 89 | github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect 90 | github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect 91 | github.com/smallstep/truststore v0.13.0 // indirect 92 | github.com/spf13/cast v1.4.1 // indirect 93 | github.com/spf13/cobra v1.8.0 // indirect 94 | github.com/spf13/pflag v1.0.5 // indirect 95 | github.com/stoewer/go-strcase v1.2.0 // indirect 96 | github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect 97 | github.com/urfave/cli v1.22.14 // indirect 98 | github.com/zeebo/blake3 v0.2.3 // indirect 99 | go.etcd.io/bbolt v1.3.9 // indirect 100 | go.step.sm/cli-utils v0.9.0 // indirect 101 | go.step.sm/crypto v0.45.0 // indirect 102 | go.step.sm/linkedca v0.20.1 // indirect 103 | go.uber.org/automaxprocs v1.5.3 // indirect 104 | go.uber.org/mock v0.4.0 // indirect 105 | go.uber.org/multierr v1.11.0 // indirect 106 | go.uber.org/zap/exp v0.2.0 // indirect 107 | golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect 108 | golang.org/x/mod v0.17.0 // indirect 109 | golang.org/x/net v0.25.0 // indirect 110 | golang.org/x/sync v0.7.0 // indirect 111 | golang.org/x/sys v0.20.0 // indirect 112 | golang.org/x/term v0.20.0 // indirect 113 | golang.org/x/text v0.15.0 // indirect 114 | golang.org/x/time v0.5.0 // indirect 115 | golang.org/x/tools v0.21.0 // indirect 116 | google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect 117 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect 118 | google.golang.org/grpc v1.63.2 // indirect 119 | google.golang.org/protobuf v1.34.1 // indirect 120 | gopkg.in/yaml.v3 v3.0.1 // indirect 121 | howett.net/plist v1.0.0 // indirect 122 | ) 123 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= 2 | cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= 3 | cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= 4 | cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= 5 | cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= 6 | cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= 7 | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 8 | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 9 | cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= 10 | cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= 11 | cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY= 12 | cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc= 13 | cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= 14 | cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= 15 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 16 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 17 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= 18 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 19 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 20 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 21 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 22 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 23 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 24 | github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= 25 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 26 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= 27 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= 28 | github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= 29 | github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= 30 | github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= 31 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 32 | github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= 33 | github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 34 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 35 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 36 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 37 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= 38 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 39 | github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= 40 | github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= 41 | github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A= 42 | github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs= 43 | github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE= 44 | github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI= 45 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= 46 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= 47 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= 48 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= 49 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= 50 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= 51 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 52 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 53 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= 54 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= 55 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= 56 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= 57 | github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0= 58 | github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ= 59 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs= 60 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= 61 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw= 62 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= 63 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns= 64 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= 65 | github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= 66 | github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 67 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 68 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 69 | github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= 70 | github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw= 71 | github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0= 72 | github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI= 73 | github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= 74 | github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= 75 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 76 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 77 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 78 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 79 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 80 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 81 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 82 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 83 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 84 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 85 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 86 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 87 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 88 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 89 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 90 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 91 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 92 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 93 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 94 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 95 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 96 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 97 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 98 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 99 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 100 | github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= 101 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 102 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 103 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 104 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 105 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 106 | github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= 107 | github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= 108 | github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= 109 | github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= 110 | github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= 111 | github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= 112 | github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= 113 | github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= 114 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 115 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= 116 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 117 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 118 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 119 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 120 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 121 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 122 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 123 | github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= 124 | github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 125 | github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 126 | github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= 127 | github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= 128 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 129 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 130 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 131 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 132 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 133 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 134 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 135 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 136 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 137 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 138 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 139 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 140 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 141 | github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 142 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 143 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 144 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 145 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 146 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 147 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 148 | github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= 149 | github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 150 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 151 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 152 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 153 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 154 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 155 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 156 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 157 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 158 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 159 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 160 | github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= 161 | github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= 162 | github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= 163 | github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= 164 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 165 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 166 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 167 | github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= 168 | github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= 169 | github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= 170 | github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= 171 | github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= 172 | github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= 173 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 174 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 175 | github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU= 176 | github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= 177 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 178 | github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= 179 | github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= 180 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 181 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 182 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 183 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= 184 | github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= 185 | github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= 186 | github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= 187 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 188 | github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= 189 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 190 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 191 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 192 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 193 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 194 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 195 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 196 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 197 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 198 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 199 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 200 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 201 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 202 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 203 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 204 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 205 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 206 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 207 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 208 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 209 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 210 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 211 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 212 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 213 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 214 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 215 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 216 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 217 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 218 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 219 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 220 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 221 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 222 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 223 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 224 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 225 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 226 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 227 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 228 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 229 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 230 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 231 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 232 | github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= 233 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 234 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 235 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 236 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 237 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 238 | github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= 239 | github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 240 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 241 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 242 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 243 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 244 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 245 | github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 246 | github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= 247 | github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 248 | github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 249 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 250 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 251 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 252 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 253 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 254 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 255 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 256 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 257 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 258 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 259 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 260 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 261 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 262 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 263 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 264 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 265 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 266 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 267 | github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= 268 | github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 269 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 270 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 271 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 272 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 273 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 274 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 275 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 276 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 277 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 278 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 279 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 280 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 281 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 282 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 283 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 284 | github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k= 285 | github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= 286 | github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= 287 | github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= 288 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 289 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 290 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 291 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 292 | github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= 293 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 294 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 295 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 296 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 297 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 298 | github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= 299 | github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= 300 | github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= 301 | github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 302 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 303 | github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= 304 | github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= 305 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 306 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 307 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 308 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 309 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 310 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 311 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 312 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 313 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 314 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 315 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 316 | github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= 317 | github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= 318 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 319 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 320 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= 321 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= 322 | github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0= 323 | github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek= 324 | github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= 325 | github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= 326 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 327 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 328 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 329 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 330 | github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= 331 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 332 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 333 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 334 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 335 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 336 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 337 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 338 | github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= 339 | github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= 340 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 341 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 342 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 343 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 344 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 345 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 346 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 347 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 348 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 349 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 350 | github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM= 351 | github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI= 352 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= 353 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= 354 | github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o= 355 | github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis= 356 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= 357 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= 358 | github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y= 359 | github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y= 360 | github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg= 361 | github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y= 362 | github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw= 363 | github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU= 364 | github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= 365 | github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= 366 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 367 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 368 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 369 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 370 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 371 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 372 | github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= 373 | github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 374 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 375 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 376 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 377 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 378 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 379 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 380 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 381 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 382 | github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= 383 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 384 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 385 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 386 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 387 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 388 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 389 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 390 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 391 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 392 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 393 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 394 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 395 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 396 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 397 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 398 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 399 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 400 | github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU= 401 | github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= 402 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 403 | github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= 404 | github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= 405 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 406 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 407 | github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= 408 | github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 409 | github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= 410 | github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= 411 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 412 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 413 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 414 | go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= 415 | go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= 416 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 417 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 418 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= 419 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= 420 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 421 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 422 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 423 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 424 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 425 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 426 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 427 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 428 | go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ= 429 | go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8= 430 | go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc= 431 | go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY= 432 | go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU= 433 | go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw= 434 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 435 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 436 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 437 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 438 | go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= 439 | go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= 440 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 441 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 442 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 443 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 444 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 445 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 446 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 447 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 448 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 449 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 450 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 451 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 452 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 453 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 454 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 455 | go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= 456 | go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= 457 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 458 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 459 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 460 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 461 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 462 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 463 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 464 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 465 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 466 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 467 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 468 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 469 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 470 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 471 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 472 | golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= 473 | golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= 474 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 475 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 476 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 477 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 478 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 479 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 480 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 481 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 482 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 483 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 484 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 485 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 486 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 487 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 488 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 489 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 490 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 491 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 492 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 493 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 494 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 495 | golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= 496 | golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 497 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 498 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 499 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 500 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 501 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 502 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 503 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 504 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 505 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 506 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 507 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 508 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 509 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 510 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 511 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 512 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 513 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 514 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 515 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 516 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 517 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 518 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 519 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 520 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 521 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 522 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 523 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 524 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 525 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 526 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 527 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 528 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 529 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 530 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 531 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 532 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 533 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 534 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 535 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 536 | golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= 537 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 538 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 539 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 540 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 541 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 542 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 543 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 544 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 545 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 546 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 547 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 548 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 549 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 550 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 551 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 552 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 553 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 554 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 555 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 556 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 557 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 558 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 559 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 560 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 561 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 562 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 563 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 564 | golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= 565 | golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 566 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 567 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 568 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 569 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 570 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 571 | google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= 572 | google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= 573 | google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= 574 | google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= 575 | google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= 576 | google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= 577 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= 578 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= 579 | google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= 580 | google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= 581 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 582 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 583 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 584 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 585 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 586 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 587 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 588 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 589 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 590 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 591 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 592 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 593 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 594 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 595 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 596 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 597 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 598 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 599 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 600 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 601 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= 602 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 603 | -------------------------------------------------------------------------------- /internal/testdata/QUIC_ClientHello_Chrome_124.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_ClientHello_Chrome_124.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_0.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1191.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1191.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1287.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1287.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1561.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1561.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1663.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1663.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_IETF_Chrome_125_PKN1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_IETF_Chrome_125_PKN1.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_IETF_Chrome_125_PKN2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_IETF_Chrome_125_PKN2.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_IETF_Firefox_126.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_IETF_Firefox_126.bin -------------------------------------------------------------------------------- /internal/testdata/QUIC_IETF_Firefox_126_0-RTT.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/QUIC_IETF_Firefox_126_0-RTT.bin -------------------------------------------------------------------------------- /internal/testdata/TLS_ClientHello_Firefox_126.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaukas/clienthellod/560e27ce8432c6eae9a23f559ccdc5027d11b35e/internal/testdata/TLS_ClientHello_Firefox_126.bin -------------------------------------------------------------------------------- /internal/utils/arr_dedup.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "sort" 4 | 5 | type SliceIntType interface { 6 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 7 | } 8 | 9 | // DedupIntArr eliminates the duplicates in an integer array. 10 | func DedupIntArr[T SliceIntType](arr []T) []T { 11 | // Sort the array 12 | sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] }) 13 | 14 | // Dedup 15 | j := 0 16 | for i := 1; i < len(arr); i++ { 17 | if arr[j] != arr[i] { 18 | j++ 19 | arr[j] = arr[i] 20 | } 21 | } 22 | return arr[:j+1] 23 | } 24 | -------------------------------------------------------------------------------- /internal/utils/rewindconn.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net" 8 | ) 9 | 10 | // Interface guards 11 | var ( 12 | _ net.Conn = (*rewindConn)(nil) 13 | ) 14 | 15 | type rewindConn struct { 16 | net.Conn 17 | reader bytes.Reader 18 | } 19 | 20 | func RewindConn(c net.Conn, buf []byte) (net.Conn, error) { 21 | if c == nil { 22 | return nil, errors.New("cannot rewind nil connection") 23 | } 24 | 25 | if len(buf) == 0 { 26 | return c, nil 27 | } 28 | 29 | return &rewindConn{ 30 | Conn: c, 31 | reader: *bytes.NewReader(buf), 32 | }, nil 33 | } 34 | 35 | // Read is ... 36 | func (c *rewindConn) Read(b []byte) (int, error) { 37 | if c.reader.Size() == 0 { 38 | return c.Conn.Read(b) 39 | } 40 | n, err := c.reader.Read(b) 41 | if errors.Is(err, io.EOF) { 42 | c.reader.Reset([]byte{}) 43 | return n, nil 44 | } 45 | return n, err 46 | } 47 | 48 | // CloseWrite is ... 49 | func (c *rewindConn) CloseWrite() error { 50 | if cc, ok := c.Conn.(*net.TCPConn); ok { 51 | return cc.CloseWrite() 52 | } 53 | if cw, ok := c.Conn.(interface { 54 | CloseWrite() error 55 | }); ok { 56 | return cw.CloseWrite() 57 | } 58 | return errors.New("not supported") 59 | } 60 | -------------------------------------------------------------------------------- /internal/utils/rewindreader.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | // Interface guards 10 | var ( 11 | _ io.Reader = (*rewindReader)(nil) 12 | ) 13 | 14 | type rewindReader struct { 15 | io.Reader 16 | rr bytes.Reader 17 | } 18 | 19 | func RewindReader(r io.Reader, buf []byte) io.Reader { 20 | if len(buf) == 0 { 21 | return r 22 | } 23 | 24 | return &rewindReader{ 25 | Reader: r, 26 | rr: *bytes.NewReader(buf), 27 | } 28 | } 29 | 30 | // Read implements io.Reader 31 | // Read is ... 32 | func (c *rewindReader) Read(b []byte) (int, error) { 33 | if c.rr.Size() == 0 { 34 | return c.Reader.Read(b) 35 | } 36 | n, err := c.rr.Read(b) 37 | if errors.Is(err, io.EOF) || c.rr.Len() == 0 { 38 | c.rr.Reset([]byte{}) 39 | n2, err := c.Reader.Read(b[n:]) // read the rest if possible 40 | return n + n2, err 41 | } 42 | return n, err 43 | } 44 | -------------------------------------------------------------------------------- /internal/utils/typeconv.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func Uint16ToUint8(a []uint16) []uint8 { 4 | b := make([]uint8, 0) 5 | for _, v := range a { 6 | b = append(b, uint8(v>>8), uint8(v)) 7 | } 8 | return b 9 | } 10 | -------------------------------------------------------------------------------- /internal/utils/udppacket.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/google/gopacket" 5 | "github.com/google/gopacket/layers" 6 | ) 7 | 8 | // ParseUDPPacket parses the IP packet 9 | func ParseUDPPacket(buf []byte) (*layers.UDP, error) { 10 | var udp *layers.UDP = &layers.UDP{} 11 | err := udp.DecodeFromBytes(buf, gopacket.NilDecodeFeedback) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return udp, nil 16 | } 17 | -------------------------------------------------------------------------------- /internal/utils/uint8_not_string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Uint8Arr redefines how []uint8 is marshalled to JSON 9 | // in order to display it as a list of numbers instead of a string 10 | type Uint8Arr []uint8 11 | 12 | func (u Uint8Arr) MarshalJSON() ([]byte, error) { 13 | var result string 14 | if u == nil { 15 | result = "[]" 16 | } else { 17 | result = strings.Join(strings.Fields(fmt.Sprintf("%d", u)), ",") 18 | } 19 | return []byte(result), nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/utils/uint8_not_string_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | type TestStruct struct { 9 | Name string `json:"name"` 10 | Age int `json:"age"` 11 | Topics Uint8Arr `json:"topics"` 12 | } 13 | 14 | func TestUint8Arr(t *testing.T) { 15 | testStruct := TestStruct{ 16 | Name: "gaukas", 17 | Age: 18, 18 | Topics: Uint8Arr{'H', 'e', 'l', 'l', 'o'}, 19 | } 20 | 21 | // testStruct: {Name:gaukas Age:18 Topics:[72 101 108 108 111]} 22 | _, err := json.Marshal(testStruct) 23 | if err != nil { 24 | t.Fatalf("json.Marshal error: %v", err) 25 | } 26 | // t.Logf("json.Marshal: %s", m) 27 | } 28 | -------------------------------------------------------------------------------- /internal/utils/utls.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func IsGREASEUint16(v uint16) bool { 4 | // First byte is same as second byte 5 | // and lowest nibble is 0xa 6 | return ((v >> 8) == v&0xff) && v&0xf == 0xa 7 | } 8 | -------------------------------------------------------------------------------- /modcaddy/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | debug # for debugging purpose 3 | # https_port 443 # currently, QUIC listener works only on port 443, otherwise you need to make changes to the code 4 | order clienthellod before file_server # make sure it hits handler before file_server 5 | clienthellod { # app 6 | tls_ttl 5s # ttl can be shorter to reduce memory consumption 7 | quic_ttl 30s # slightly longer than tls_ttl to display QUIC fingerprints for H3 requests reusing QUIC connection 8 | } 9 | servers { 10 | listener_wrappers { # listener 11 | clienthellod { # make sure packets hit clienthellod before caddy's TLS server 12 | tcp # listens for TCP and fingerprints TLS Client Hello messages 13 | udp # listens for UDP and fingerprints QUIC Initial packets 14 | } 15 | tls 16 | } 17 | } 18 | } 19 | 20 | tls.gauk.as, *.tls.gauk.as { 21 | tls { 22 | dns cloudflare YOUR_API_TOKEN # for wildcard cert, see https://github.com/libdns/cloudflare 23 | resolvers 1.1.1.1 24 | alpn http/1.1 # to use Connection: close header and close the connection immediately 25 | } 26 | clienthellod { # handler 27 | # global.servers.listener_wrappers.clienthellod.tcp must present 28 | tls # mutually exclusive with quic 29 | } 30 | file_server { 31 | root /var/www/html 32 | } 33 | } 34 | 35 | quic.gauk.as, *.quic.gauk.as { 36 | tls { 37 | dns cloudflare YOUR_API_TOKEN # for wildcard cert, see https://github.com/libdns/cloudflare 38 | resolvers 1.1.1.1 39 | } 40 | clienthellod { # handler 41 | # global.servers.listener_wrappers.clienthellod.udp must present 42 | quic # mutually exclusive with tls 43 | } 44 | file_server { 45 | root /var/www/html 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /modcaddy/README.md: -------------------------------------------------------------------------------- 1 | # `clienthellod/modcaddy`: clienthellod as a Caddy module 2 | 3 | 4 | `clienthellod` is also provided as a Caddy plugin, `modcaddy`, which can be used to capture ClientHello messages and QUIC Client Initial Packets. See Section [modcaddy](#modcaddy) for more details. 5 | 6 | `modcaddy` contains a Caddy plugin that provides: 7 | - An caddy `app` that can be used to temporarily store captured ClientHello messages and QUIC Client Initial Packets. 8 | - A caddy `handler` that can be used to serve the ClientHello messages and QUIC Client Initial Packets to the client sending the request. 9 | - A caddy `listener` that can be used to capture ClientHello messages and QUIC Client Initial Packets. 10 | 11 | You will need to use [xcaddy](https://github.com/caddyserver/xcaddy) to rebuild Caddy with `modcaddy` included. 12 | 13 | It is worth noting that some web browsers may not choose to switch to QUIC protocol in localhost environment, which may result in the QUIC Client Initial Packet not being sent and therefore not being captured/analyzed. 14 | 15 | ## Build 16 | 17 | ```bash 18 | xcaddy build --with github.com/gaukas/clienthellod/modcaddy 19 | ``` 20 | 21 | ### When build locally with changes 22 | 23 | ```bash 24 | xcaddy build --with github.com/gaukas/clienthellod/modcaddy --with github.com/gaukas/clienthellod/=./ 25 | ``` 26 | 27 | ## sample Caddyfile 28 | 29 | A sample Caddyfile is provided in this directory. 30 | 31 | ## Known issues 32 | 33 | ### QUIC can't be fingerprinted when web browser chooses H2 not H3 34 | 35 | Under certain network condition or configurations, a web browser may decide not to switch to QUIC protocol even when the server advertises the support for QUIC. This issue is more likely to happen under the scenarios with low-latency such as in localhost/intranet. 36 | 37 | There is no trivial solution to this issue, as there seems to be no way to force the web browser to use QUIC. 38 | 39 | ### QUIC fingerprint missing for the first request 40 | 41 | It is possible that a client sends both H2-over-TCP (TLS) and H3-over-UDP (QUIC) for the first time requesting a web page and decide to render the response from H2-over-TCP (TLS). In this case, the QUIC Client Initial Packet might be not yet recorded. 42 | 43 | Reloading the page might help by fetching the cached QUIC fingerprint if it is captured and not yet expired. 44 | 45 | ### Fingerprint gone after reloading/refreshing the web page 46 | 47 | Some web browsers may decide to reuse the existing unclosed connection for new HTTP requests instead of establishing a new one by sending a new TLS Client Hello or QUIC Initial Packet(s). In which case, no new fingerprint will be captured and if the old fingerprint is expired or otherwise removed, the fingerprint will be gone and nothing will be displayed. 48 | 49 | Forcing the web browser to establish a new connection by closing the existing connection, opening a new tab, or use different domain names every time might help. -------------------------------------------------------------------------------- /modcaddy/app/caddyfile.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/caddyserver/caddy/v2" 5 | "github.com/caddyserver/caddy/v2/caddyconfig" 6 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 7 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 8 | ) 9 | 10 | func init() { 11 | httpcaddyfile.RegisterGlobalOption(CaddyAppID, parseCaddyfile) 12 | } 13 | 14 | /* 15 | Caddyfile syntax: 16 | 17 | trojan { 18 | validfor 5s [2s] 19 | } 20 | 21 | The second argument is an optional cleaning interval, if left out, it will be the same 22 | as the first argument (validfor). 23 | */ 24 | func parseCaddyfile(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { 25 | app := &Reservoir{ 26 | TlsTTL: caddy.Duration(DEFAULT_TLS_FP_TTL), 27 | QuicTTL: caddy.Duration(DEFAULT_QUIC_FP_TTL), 28 | } 29 | 30 | for d.Next() { 31 | for d.NextBlock(0) { 32 | switch d.Val() { // skipcq: CRT-A0014 33 | case "tls_ttl": // Time-to-Live for each entry 34 | if app.TlsTTL != caddy.Duration(DEFAULT_TLS_FP_TTL) { 35 | return nil, d.Err("only one tls_ttl is allowed") 36 | } 37 | args := d.RemainingArgs() 38 | if len(args) == 0 { 39 | return nil, d.ArgErr() 40 | } 41 | duration, err := caddy.ParseDuration(args[0]) 42 | if err != nil { 43 | return nil, d.Errf("invalid duration: %v", err) 44 | } 45 | app.TlsTTL = caddy.Duration(duration) 46 | 47 | if len(args) > 1 { 48 | return nil, d.Err("too many arguments") 49 | } 50 | case "quic_ttl": // Time-to-Live for each entry 51 | if app.QuicTTL != caddy.Duration(DEFAULT_QUIC_FP_TTL) { 52 | return nil, d.Err("only one quic_ttl is allowed") 53 | } 54 | args := d.RemainingArgs() 55 | if len(args) == 0 { 56 | return nil, d.ArgErr() 57 | } 58 | duration, err := caddy.ParseDuration(args[0]) 59 | if err != nil { 60 | return nil, d.Errf("invalid duration: %v", err) 61 | } 62 | app.QuicTTL = caddy.Duration(duration) 63 | 64 | if len(args) > 1 { 65 | return nil, d.Err("too many arguments") 66 | } 67 | } 68 | } 69 | } 70 | 71 | return httpcaddyfile.App{ 72 | Name: CaddyAppID, 73 | Value: caddyconfig.JSON(app, nil), 74 | }, nil 75 | } 76 | -------------------------------------------------------------------------------- /modcaddy/app/reservoir.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "github.com/caddyserver/caddy/v2" 9 | "github.com/gaukas/clienthellod" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | const ( 14 | CaddyAppID = "clienthellod" 15 | 16 | DEFAULT_TLS_FP_TTL = clienthellod.DEFAULT_TLSFINGERPRINT_EXPIRY // TODO: select a reasonable value 17 | DEFAULT_QUIC_FP_TTL = clienthellod.DEFAULT_QUICFINGERPRINT_EXPIRY // TODO: select a reasonable value 18 | ) 19 | 20 | func init() { 21 | caddy.RegisterModule(Reservoir{}) 22 | } 23 | 24 | // Reservoir implements [caddy.App] and [caddy.Provisioner]. 25 | // It is used to store the ClientHello extracted from the incoming TLS 26 | // by ListenerWrapper for later use by the Handler when ServeHTTP is called. 27 | type Reservoir struct { 28 | // TlsTTL (Time-to-Live) is the duration for which each TLS fingerprint 29 | // is valid. The entry will remain in the reservoir for at most this 30 | // duration. 31 | // 32 | // There are scenarios an entry gets removed sooner than this duration, including 33 | // when a TLS ClientHello is successfully served by the handler. 34 | TlsTTL caddy.Duration `json:"tls_ttl,omitempty"` 35 | 36 | // QuicTTL (Time-to-Live) is the duration for which each QUIC fingerprint 37 | // is valid. The entry will remain in the reservoir for at most this 38 | // duration. 39 | // 40 | // Given the fact that some implementations would prefer reusing the previously established 41 | // QUIC connection instead of establishing a new one everytime, it is recommended to set 42 | // a longer TTL for QUIC. 43 | QuicTTL caddy.Duration `json:"quic_ttl,omitempty"` 44 | 45 | tlsFingerprinter *clienthellod.TLSFingerprinter 46 | quicFingerprinter *clienthellod.QUICFingerprinter 47 | mapLastQUICVisitorPerIP *sync.Map // sometimes even when a complete QUIC handshake is done, client decide to connect using HTTP/2 48 | 49 | logger *zap.Logger 50 | } 51 | 52 | // CaddyModule implements CaddyModule() of caddy.Module. 53 | // It returns the Caddy module information. 54 | func (Reservoir) CaddyModule() caddy.ModuleInfo { // skipcq: GO-W1029 55 | return caddy.ModuleInfo{ 56 | ID: CaddyAppID, 57 | New: func() caddy.Module { 58 | reservoir := &Reservoir{ 59 | TlsTTL: caddy.Duration(DEFAULT_TLS_FP_TTL), 60 | QuicTTL: caddy.Duration(DEFAULT_QUIC_FP_TTL), 61 | } 62 | 63 | return reservoir 64 | }, 65 | } 66 | } 67 | 68 | // TLSFingerprinter returns the TLSFingerprinter instance. 69 | func (r *Reservoir) TLSFingerprinter() *clienthellod.TLSFingerprinter { // skipcq: GO-W1029 70 | return r.tlsFingerprinter 71 | } 72 | 73 | // QUICFingerprinter returns the QUICFingerprinter instance. 74 | func (r *Reservoir) QUICFingerprinter() *clienthellod.QUICFingerprinter { // skipcq: GO-W1029 75 | return r.quicFingerprinter 76 | } 77 | 78 | // NewQUICVisitor updates the map entry for the given IP address. 79 | func (r *Reservoir) NewQUICVisitor(ip, fullKey string) { // skipcq: GO-W1029 80 | r.mapLastQUICVisitorPerIP.Store(ip, fullKey) 81 | 82 | // delete it after TTL if not updated 83 | go func() { 84 | <-time.After(time.Duration(r.QuicTTL)) 85 | r.mapLastQUICVisitorPerIP.CompareAndDelete(ip, fullKey) 86 | }() 87 | } 88 | 89 | // GetLastQUICVisitor returns the last QUIC visitor for the given IP address. 90 | func (r *Reservoir) GetLastQUICVisitor(ip string) (string, bool) { // skipcq: GO-W1029 91 | if v, ok := r.mapLastQUICVisitorPerIP.Load(ip); ok { 92 | if fullKey, ok := v.(string); ok { 93 | return fullKey, true 94 | } 95 | } 96 | return "", false 97 | } 98 | 99 | // Start implements Start() of caddy.App. 100 | func (r *Reservoir) Start() error { // skipcq: GO-W1029 101 | if r.QuicTTL <= 0 || r.TlsTTL <= 0 { 102 | return errors.New("ttl must be a positive duration") 103 | } 104 | 105 | r.logger.Info("clienthellod reservoir is started") 106 | 107 | return nil 108 | } 109 | 110 | // Stop implements Stop() of caddy.App. 111 | func (r *Reservoir) Stop() error { // skipcq: GO-W1029 112 | r.quicFingerprinter.Close() 113 | r.tlsFingerprinter.Close() 114 | return nil 115 | } 116 | 117 | // Provision implements Provision() of caddy.Provisioner. 118 | func (r *Reservoir) Provision(ctx caddy.Context) error { // skipcq: GO-W1029 119 | r.tlsFingerprinter = clienthellod.NewTLSFingerprinterWithTimeout(time.Duration(r.TlsTTL)) 120 | r.quicFingerprinter = clienthellod.NewQUICFingerprinterWithTimeout(time.Duration(r.QuicTTL)) 121 | r.mapLastQUICVisitorPerIP = new(sync.Map) 122 | 123 | r.logger = ctx.Logger(r) 124 | 125 | r.logger.Info("clienthellod reservoir is provisioned") 126 | return nil 127 | } 128 | 129 | var ( 130 | _ caddy.App = (*Reservoir)(nil) 131 | _ caddy.Provisioner = (*Reservoir)(nil) 132 | ) 133 | -------------------------------------------------------------------------------- /modcaddy/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | 10 | "github.com/caddyserver/caddy/v2" 11 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 12 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 13 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 14 | "github.com/gaukas/clienthellod/modcaddy/app" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | func init() { 19 | caddy.RegisterModule(Handler{}) 20 | httpcaddyfile.RegisterHandlerDirective("clienthellod", func(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { 21 | m := &Handler{} 22 | if err := m.UnmarshalCaddyfile(h.Dispenser); err != nil { 23 | return nil, err 24 | } 25 | return m, nil 26 | }) 27 | } 28 | 29 | type Handler struct { 30 | // TLS enables handler to look up TLS ClientHello from reservoir. 31 | // 32 | // Mutually exclusive with QUIC. One and only one of TLS or QUIC must be true. 33 | TLS bool `json:"tls,omitempty"` 34 | 35 | // QUIC enables handler to look up QUIC ClientHello from reservoir. 36 | // 37 | // Mutually exclusive with TLS. One and only one of TLS or QUIC must be true. 38 | QUIC bool `json:"quic,omitempty"` 39 | 40 | logger *zap.Logger 41 | reservoir *app.Reservoir 42 | } 43 | 44 | // CaddyModule returns the Caddy module information. 45 | func (Handler) CaddyModule() caddy.ModuleInfo { // skipcq: GO-W1029 46 | return caddy.ModuleInfo{ 47 | ID: "http.handlers.clienthellod", 48 | New: func() caddy.Module { return new(Handler) }, 49 | } 50 | } 51 | 52 | // Provision implements caddy.Provisioner. 53 | func (h *Handler) Provision(ctx caddy.Context) error { // skipcq: GO-W1029 54 | h.logger = ctx.Logger(h) 55 | h.logger.Info("clienthellod handler logger loaded.") 56 | 57 | if a, err := ctx.AppIfConfigured(app.CaddyAppID); err != nil { 58 | return err 59 | } else { 60 | h.reservoir = a.(*app.Reservoir) 61 | h.logger.Info("clienthellod handler reservoir loaded.") 62 | } 63 | 64 | if h.TLS && h.QUIC { 65 | return errors.New("clienthellod handler: mutually exclusive TLS and QUIC are both enabled") 66 | } else if !(h.TLS || h.QUIC) { 67 | return errors.New("clienthellod handler: one and only one of TLS or QUIC must be enabled") 68 | } 69 | 70 | h.logger.Info("clienthellod handler provisioned.") 71 | 72 | return nil 73 | } 74 | 75 | // ServeHTTP 76 | func (h *Handler) ServeHTTP(wr http.ResponseWriter, req *http.Request, next caddyhttp.Handler) error { // skipcq: GO-W1029 77 | h.logger.Debug("Serving HTTP to " + req.RemoteAddr + " on Protocol " + req.Proto) 78 | 79 | if h.TLS && req.ProtoMajor <= 2 { // When TLS is enabled and for HTTP/1.0 or HTTP/1.1 or H2 served over TLS 80 | return h.serveTLS(wr, req, next) 81 | } else if h.QUIC { // When QUIC is enabled 82 | // if req.ProtoMajor == 3 { // QUIC 83 | // return h.serveQUIC(wr, req, next) 84 | // } else { 85 | // h.logger.Debug("Serving QUIC Fingerprint over TLS") 86 | // return h.serveQUICFingerprintOverTLS(wr, req, next) 87 | // } 88 | return h.serveQUIC(wr, req, next) 89 | } 90 | return next.ServeHTTP(wr, req) 91 | } 92 | 93 | // serveTLS handles HTTP/1.0, HTTP/1.1, H2 requests by looking up the 94 | // ClientHello from the reservoir and writing it to the response. 95 | func (h *Handler) serveTLS(wr http.ResponseWriter, req *http.Request, next caddyhttp.Handler) error { // skipcq: GO-W1029 96 | // Moved to the top to prevent web broswers caching QUIC after an tls fetching error 97 | wr.Header().Set("Alt-Svc", "clear") // to prevent web broswers switching to QUIC 98 | 99 | // get the client hello from the reservoir 100 | ch := h.reservoir.TLSFingerprinter().Pop(req.RemoteAddr) 101 | if ch == nil { 102 | h.logger.Debug(fmt.Sprintf("Unable to fetch TLS ClientHello sent by %s, maybe not TLS connection?", req.RemoteAddr)) 103 | return next.ServeHTTP(wr, req) 104 | } 105 | // h.logger.Debug(fmt.Sprintf("Fetched TLS ClientHello for %s", req.RemoteAddr)) 106 | 107 | ch.UserAgent = req.UserAgent() 108 | 109 | // dump JSON 110 | var b []byte 111 | var err error 112 | if req.URL.Query().Get("beautify") == "true" { 113 | b, err = json.MarshalIndent(ch, "", " ") 114 | } else { 115 | b, err = json.Marshal(ch) 116 | } 117 | if err != nil { 118 | h.logger.Error("failed to marshal TLS ClientHello into JSON", zap.Error(err)) 119 | return next.ServeHTTP(wr, req) 120 | } 121 | 122 | // Properly set the Content-Type header 123 | wr.Header().Set("Content-Type", "application/json") 124 | 125 | // Close the HTTP connection after sending the response 126 | // 127 | // HTTP/1.X only. Forbidden in HTTP/2 (RFC 9113 Section 8.2.2) 128 | // and HTTP/3 (RFC 9114 Section 4.2) 129 | if req.ProtoMajor == 1 { 130 | wr.Header().Set("Connection", "close") 131 | } 132 | 133 | _, err = wr.Write(b) 134 | if err != nil { 135 | h.logger.Error("failed to write response", zap.Error(err)) 136 | return next.ServeHTTP(wr, req) 137 | } 138 | return nil 139 | } 140 | 141 | // serveQUIC handles QUIC requests by looking up the ClientHello from the 142 | // reservoir and writing it to the response. 143 | func (h *Handler) serveQUIC(wr http.ResponseWriter, req *http.Request, next caddyhttp.Handler) error { // skipcq: GO-W1029 144 | var from string 145 | if req.ProtoMajor == 3 { 146 | from = req.RemoteAddr 147 | h.logger.Debug(fmt.Sprintf("Fetching QUIC Fingerprint directly sent by QUIC client at %s", from)) 148 | } else { 149 | // Get IP part of the RemoteAddr 150 | ip, _, err := net.SplitHostPort(req.RemoteAddr) 151 | if err != nil { 152 | h.logger.Error(fmt.Sprintf("Can't split IP from %s: %v", req.RemoteAddr, err)) 153 | return next.ServeHTTP(wr, req) 154 | } 155 | 156 | // Get the last QUIC visitor 157 | var ok bool 158 | from, ok = h.reservoir.GetLastQUICVisitor(ip) 159 | if !ok { 160 | h.logger.Debug(fmt.Sprintf("Can't find last QUIC visitor for %s", ip)) 161 | return next.ServeHTTP(wr, req) 162 | } 163 | 164 | h.logger.Debug(fmt.Sprintf("Fetching most recent QUIC Fingerprint sent by %s for TLS client at %s", from, req.RemoteAddr)) 165 | } 166 | 167 | // get the client hello from the reservoir 168 | qfp, err := h.reservoir.QUICFingerprinter().PeekAwait(from) 169 | if err != nil { 170 | h.logger.Debug(fmt.Sprintf("Unable to fetch QUIC fingerprint sent by %s: %v", req.RemoteAddr, err)) 171 | return next.ServeHTTP(wr, req) 172 | } 173 | 174 | // h.logger.Debug(fmt.Sprintf("Fetched QUIC fingerprint for %s", req.RemoteAddr)) 175 | 176 | // If this is a QUIC request, we record the IP address as a QUIC visitor 177 | // so this QUIC fingerprint is associated with the IP address and can be 178 | // fetched for even HTTP-over-TLS (TCP-based) requests. 179 | if req.ProtoMajor == 3 { 180 | // Get IP part of the RemoteAddr 181 | ip, _, err := net.SplitHostPort(req.RemoteAddr) 182 | if err == nil { 183 | h.reservoir.NewQUICVisitor(ip, req.RemoteAddr) 184 | } else { 185 | h.logger.Error(fmt.Sprintf("Can't extract IP from %s: %v", req.RemoteAddr, err)) 186 | } 187 | } 188 | 189 | qfp.UserAgent = req.UserAgent() 190 | 191 | // dump JSON 192 | var b []byte 193 | if req.URL.Query().Get("beautify") == "true" { 194 | b, err = json.MarshalIndent(qfp, "", " ") 195 | } else { 196 | b, err = json.Marshal(qfp) 197 | } 198 | if err != nil { 199 | h.logger.Error("failed to marshal QUIC fingerprint into JSON", zap.Error(err)) 200 | return next.ServeHTTP(wr, req) 201 | } 202 | 203 | // Properly set the Content-Type header 204 | wr.Header().Set("Content-Type", "application/json") 205 | 206 | // Close the HTTP connection after sending the response 207 | // 208 | // HTTP/1.X only. Forbidden in HTTP/2 (RFC 9113 Section 8.2.2) 209 | // and HTTP/3 (RFC 9114 Section 4.2) 210 | if req.ProtoMajor == 1 { 211 | wr.Header().Set("Connection", "close") 212 | } 213 | 214 | _, err = wr.Write(b) 215 | if err != nil { 216 | h.logger.Error("failed to write response", zap.Error(err)) 217 | return next.ServeHTTP(wr, req) 218 | } 219 | return nil 220 | } 221 | 222 | // UnmarshalCaddyfile unmarshals Caddyfile tokens into h. 223 | func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // skipcq: GO-W1029 224 | for d.Next() { 225 | for d.NextBlock(0) { 226 | switch d.Val() { 227 | case "tls": 228 | if h.TLS { 229 | return d.Err("clienthellod: repeated tls in block") 230 | } else if h.QUIC { 231 | return d.Err("clienthellod: tls and quic are mutually exclusive in one block") 232 | } 233 | h.TLS = true 234 | case "quic": 235 | if h.QUIC { 236 | return d.Err("clienthellod: repeated quic in block") 237 | } else if h.TLS { 238 | return d.Err("clienthellod: tls and quic are mutually exclusive in one block") 239 | } 240 | h.QUIC = true 241 | } 242 | } 243 | } 244 | return nil 245 | } 246 | 247 | // Interface guards 248 | var ( 249 | _ caddy.Provisioner = (*Handler)(nil) 250 | _ caddyhttp.MiddlewareHandler = (*Handler)(nil) 251 | _ caddyfile.Unmarshaler = (*Handler)(nil) 252 | ) 253 | -------------------------------------------------------------------------------- /modcaddy/listener/listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/caddyserver/caddy/v2" 7 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 8 | "github.com/gaukas/clienthellod/modcaddy/app" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func init() { 13 | caddy.RegisterModule(ListenerWrapper{}) 14 | } 15 | 16 | // ListenerWrapper implements caddy.ListenerWrapper. 17 | // It is used to extract the ClientHello from the incoming TLS 18 | // connection before it reaches the "tls" in caddy.listeners 19 | // 20 | // For clienthellod to work, it must be placed before the "tls" in 21 | // the Caddyfile's listener_wrappers directive. For example: 22 | // 23 | // listener_wrappers { 24 | // clienthellod 25 | // tls 26 | // } 27 | type ListenerWrapper struct { 28 | TCP bool `json:"tcp,omitempty"` 29 | UDP bool `json:"udp,omitempty"` 30 | 31 | logger *zap.Logger 32 | reservoir *app.Reservoir 33 | udpListener *net.IPConn 34 | udp6Listener *net.IPConn 35 | } 36 | 37 | // CaddyModule returns the Caddy module information. 38 | func (ListenerWrapper) CaddyModule() caddy.ModuleInfo { // skipcq: GO-W1029 39 | return caddy.ModuleInfo{ 40 | ID: "caddy.listeners.clienthellod", 41 | New: func() caddy.Module { return new(ListenerWrapper) }, 42 | } 43 | } 44 | 45 | func (lw *ListenerWrapper) Cleanup() error { // skipcq: GO-W1029 46 | if lw.UDP && lw.udpListener != nil { 47 | return lw.udpListener.Close() 48 | } 49 | if lw.UDP && lw.udp6Listener != nil { 50 | return lw.udp6Listener.Close() 51 | } 52 | return nil 53 | } 54 | 55 | func (lw *ListenerWrapper) Provision(ctx caddy.Context) error { // skipcq: GO-W1029 56 | // logger 57 | lw.logger = ctx.Logger(lw) 58 | lw.logger.Info("clienthellod listener logger loaded.") 59 | 60 | // reservoir 61 | if a, err := ctx.AppIfConfigured(app.CaddyAppID); err != nil { 62 | return err 63 | } else { 64 | lw.reservoir = a.(*app.Reservoir) 65 | lw.logger.Info("clienthellod listener reservoir loaded.") 66 | } 67 | 68 | var err error 69 | // UDP listener if enabled and not already provisioned 70 | if lw.UDP && lw.udpListener == nil { 71 | lw.udpListener, err = net.ListenIP("ip4:udp", &net.IPAddr{}) 72 | if err != nil { 73 | return err 74 | } 75 | go lw.reservoir.QUICFingerprinter().HandleIPConn(lw.udpListener) 76 | 77 | lw.udp6Listener, err = net.ListenIP("ip6:udp", &net.IPAddr{}) 78 | if err != nil { 79 | return err 80 | } 81 | go lw.reservoir.QUICFingerprinter().HandleIPConn(lw.udp6Listener) 82 | 83 | lw.logger.Info("clienthellod listener UDP listener loaded.") 84 | } 85 | 86 | lw.logger.Info("clienthellod listener provisioned.") 87 | return nil 88 | } 89 | 90 | func (lw *ListenerWrapper) WrapListener(l net.Listener) net.Listener { // skipcq: GO-W1029 91 | lw.logger.Info("Wrapping listener " + l.Addr().String() + "on network " + l.Addr().Network() + "...") 92 | 93 | if l.Addr().Network() == "tcp" || l.Addr().Network() == "tcp4" || l.Addr().Network() == "tcp6" { 94 | if lw.TCP { 95 | return wrapTlsListener(l, lw.reservoir, lw.logger) 96 | } else { 97 | lw.logger.Debug("TCP not enabled. Skipping...") 98 | } 99 | } else { 100 | lw.logger.Debug("Not TCP. Skipping...") 101 | } 102 | 103 | return l 104 | } 105 | 106 | type tlsListener struct { 107 | net.Listener 108 | reservoir *app.Reservoir 109 | logger *zap.Logger 110 | } 111 | 112 | func wrapTlsListener(in net.Listener, r *app.Reservoir, logger *zap.Logger) net.Listener { 113 | return &tlsListener{ 114 | Listener: in, 115 | reservoir: r, 116 | logger: logger, 117 | } 118 | } 119 | 120 | func (l *tlsListener) Accept() (net.Conn, error) { 121 | conn, err := l.Listener.Accept() 122 | if err != nil { 123 | return conn, err 124 | } 125 | 126 | // ch, err := clienthellod.ReadClientHello(conn) 127 | // if err == nil { 128 | // l.reservoir.DepositClientHello(conn.RemoteAddr().String(), ch) 129 | // l.logger.Debug("Deposited ClientHello from " + conn.RemoteAddr().String()) 130 | // } else { 131 | // l.logger.Error("Failed to read ClientHello from "+conn.RemoteAddr().String(), zap.Error(err)) 132 | // } 133 | 134 | rewindConn, err := l.reservoir.TLSFingerprinter().HandleTCPConn(conn) 135 | if err != nil { 136 | l.logger.Error("internal error: TLSFingerprinter failed to handle TCP connection", zap.Error(err)) 137 | return conn, err 138 | } 139 | 140 | // No matter what happens, rewind the connection 141 | return rewindConn, nil 142 | } 143 | 144 | func (lw *ListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // skipcq: GO-W1029 145 | for d.Next() { 146 | for d.NextBlock(0) { 147 | switch d.Val() { 148 | case "tcp": 149 | if lw.TCP { 150 | return d.Err("clienthellod: tcp already specified") 151 | } 152 | lw.TCP = true 153 | case "udp": 154 | if lw.UDP { 155 | return d.Err("clienthellod: udp already specified") 156 | } 157 | lw.UDP = true 158 | } 159 | } 160 | } 161 | return nil 162 | } 163 | 164 | // Interface guards 165 | var ( 166 | _ caddy.CleanerUpper = (*ListenerWrapper)(nil) 167 | _ caddy.Provisioner = (*ListenerWrapper)(nil) 168 | _ caddy.ListenerWrapper = (*ListenerWrapper)(nil) 169 | _ caddyfile.Unmarshaler = (*ListenerWrapper)(nil) 170 | ) 171 | -------------------------------------------------------------------------------- /modcaddy/mod_caddy.go: -------------------------------------------------------------------------------- 1 | package modcaddy 2 | 3 | import ( 4 | _ "github.com/gaukas/clienthellod/modcaddy/app" 5 | _ "github.com/gaukas/clienthellod/modcaddy/handler" 6 | _ "github.com/gaukas/clienthellod/modcaddy/listener" 7 | ) 8 | -------------------------------------------------------------------------------- /quic_client_initial.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime" 7 | "sort" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | // ClientInitial represents a QUIC Initial Packet sent by the Client. 14 | type ClientInitial struct { 15 | Header *QUICHeader `json:"header,omitempty"` // QUIC header 16 | FrameTypes []uint64 `json:"frames,omitempty"` // frames ID in order 17 | frames QUICFrames // frames in order 18 | raw []byte 19 | } 20 | 21 | // UnmarshalQUICClientInitialPacket is similar to ParseQUICCIP, but on error 22 | // such as ClientHello cannot be parsed, it returns a partially completed 23 | // ClientInitialPacket instead of nil. 24 | func UnmarshalQUICClientInitialPacket(p []byte) (ci *ClientInitial, err error) { 25 | ci = &ClientInitial{ 26 | raw: p, 27 | } 28 | 29 | ci.Header, ci.frames, err = DecodeQUICHeaderAndFrames(p) 30 | if err != nil { 31 | return 32 | } 33 | 34 | ci.FrameTypes = ci.frames.FrameTypes() 35 | 36 | // Make sure first GC completely releases all resources as possible 37 | runtime.SetFinalizer(ci, func(c *ClientInitial) { 38 | c.Header = nil 39 | c.FrameTypes = nil 40 | c.frames = nil 41 | c.raw = nil 42 | }) 43 | 44 | return ci, nil 45 | } 46 | 47 | // GatheredClientInitials represents a series of Initial Packets sent by the Client to initiate 48 | // the QUIC handshake. 49 | type GatheredClientInitials struct { 50 | Packets []*ClientInitial `json:"packets,omitempty"` // sorted by ClientInitial.PacketNumber 51 | maxPacketNumber uint64 // if incomingPacketNumber > maxPacketNumber, will reject the packet 52 | maxPacketCount uint64 // if len(Packets) >= maxPacketCount, will reject any new packets 53 | pktsMutex *sync.Mutex 54 | 55 | clientHelloReconstructor *QUICClientHelloReconstructor 56 | ClientHello *QUICClientHello `json:"client_hello,omitempty"` // TLS ClientHello 57 | TransportParameters *QUICTransportParameters `json:"transport_parameters,omitempty"` // QUIC Transport Parameters extracted from the extension in ClientHello 58 | 59 | HexID string `json:"hex_id,omitempty"` 60 | NumID uint64 `json:"num_id,omitempty"` 61 | 62 | deadline time.Time 63 | completed atomic.Bool 64 | completeChan chan struct{} 65 | completeChanCloseOnce sync.Once 66 | } 67 | 68 | const ( 69 | DEFAULT_MAX_INITIAL_PACKET_NUMBER uint64 = 32 70 | DEFAULT_MAX_INITIAL_PACKET_COUNT uint64 = 4 71 | ) 72 | 73 | var ( 74 | ErrGatheringExpired = errors.New("ClientInitials gathering has expired") 75 | ErrPacketRejected = errors.New("packet rejected based upon rules") 76 | ErrGatheredClientInitialsChannelClosedBeforeCompletion = errors.New("completion notification channel closed before setting completion flag") 77 | ) 78 | 79 | // GatherClientInitialPackets reads a series of Client Initial Packets from the input channel 80 | // and returns the result of the gathered packets. 81 | func GatherClientInitials() *GatheredClientInitials { 82 | gci := &GatheredClientInitials{ 83 | Packets: make([]*ClientInitial, 0, 4), // expecting 4 packets at max 84 | maxPacketNumber: DEFAULT_MAX_INITIAL_PACKET_NUMBER, 85 | maxPacketCount: DEFAULT_MAX_INITIAL_PACKET_COUNT, 86 | pktsMutex: &sync.Mutex{}, 87 | clientHelloReconstructor: NewQUICClientHelloReconstructor(), 88 | completed: atomic.Bool{}, 89 | completeChan: make(chan struct{}), 90 | completeChanCloseOnce: sync.Once{}, 91 | } 92 | 93 | // Make sure first GC completely releases all resources as possible 94 | runtime.SetFinalizer(gci, func(g *GatheredClientInitials) { 95 | g.Packets = nil 96 | 97 | g.clientHelloReconstructor = nil 98 | g.ClientHello = nil 99 | g.TransportParameters = nil 100 | 101 | g.completeChanCloseOnce.Do(func() { 102 | close(g.completeChan) 103 | }) 104 | g.completeChan = nil 105 | }) 106 | 107 | return gci 108 | } 109 | 110 | // GatherClientInitialsWithDeadline is a helper function to create a GatheredClientInitials with a deadline. 111 | func GatherClientInitialsWithDeadline(deadline time.Time) *GatheredClientInitials { 112 | gci := GatherClientInitials() 113 | gci.SetDeadline(deadline) 114 | return gci 115 | } 116 | 117 | func (gci *GatheredClientInitials) AddPacket(cip *ClientInitial) error { 118 | gci.pktsMutex.Lock() 119 | defer gci.pktsMutex.Unlock() 120 | 121 | if gci.Expired() { // not allowing new packets after expiry 122 | return ErrGatheringExpired 123 | } 124 | 125 | if gci.ClientHello != nil { // parse complete, new packet likely to be an ACK-only frame, ignore 126 | return nil 127 | } 128 | 129 | // check if packet needs to be rejected based upon set maxPacketNumber and maxPacketCount 130 | if cip.Header.initialPacketNumber > atomic.LoadUint64(&gci.maxPacketNumber) || 131 | uint64(len(gci.Packets)) >= atomic.LoadUint64(&gci.maxPacketCount) { 132 | return ErrPacketRejected 133 | } 134 | 135 | // check if duplicate packet number was received, if so, discard 136 | for _, p := range gci.Packets { 137 | if p.Header.initialPacketNumber == cip.Header.initialPacketNumber { 138 | return nil 139 | } 140 | } 141 | 142 | gci.Packets = append(gci.Packets, cip) 143 | 144 | // sort by initialPacketNumber 145 | sort.Slice(gci.Packets, func(i, j int) bool { 146 | return gci.Packets[i].Header.initialPacketNumber < gci.Packets[j].Header.initialPacketNumber 147 | }) 148 | 149 | if err := gci.clientHelloReconstructor.FromFrames(cip.frames); err != nil { 150 | if errors.Is(err, ErrNeedMoreFrames) { 151 | return nil // abort early, need more frames before ClientHello can be reconstructed 152 | } else { 153 | return fmt.Errorf("failed to reassemble ClientHello: %w", err) 154 | } 155 | } 156 | 157 | return gci.lockedGatherComplete() 158 | } 159 | 160 | // Completed returns true if the GatheredClientInitials is complete. 161 | func (gci *GatheredClientInitials) Completed() bool { 162 | return gci.completed.Load() 163 | } 164 | 165 | // Expired returns true if the GatheredClientInitials has expired. 166 | func (gci *GatheredClientInitials) Expired() bool { 167 | return time.Now().After(gci.deadline) 168 | } 169 | 170 | func (gci *GatheredClientInitials) lockedGatherComplete() error { 171 | var err error 172 | // First, reconstruct the ClientHello 173 | gci.ClientHello, err = gci.clientHelloReconstructor.Reconstruct() 174 | if err != nil { 175 | return fmt.Errorf("failed to reconstruct ClientHello: %w", err) 176 | } 177 | 178 | // Next, point the TransportParameters to the ClientHello's qtp 179 | gci.TransportParameters = gci.ClientHello.qtp 180 | 181 | // Then calculate the NumericID 182 | numericID := gci.calcNumericID() 183 | atomic.StoreUint64(&gci.NumID, numericID) 184 | gci.HexID = FingerprintID(numericID).AsHex() 185 | 186 | // Finally, mark the completion 187 | gci.completed.Store(true) 188 | gci.completeChanCloseOnce.Do(func() { 189 | close(gci.completeChan) 190 | }) 191 | 192 | return nil 193 | } 194 | 195 | // SetDeadline sets the deadline for the GatheredClientInitials to complete. 196 | func (gci *GatheredClientInitials) SetDeadline(deadline time.Time) { 197 | gci.deadline = deadline 198 | } 199 | 200 | // SetMaxPacketNumber sets the maximum packet number to be gathered. 201 | // If a Client Initial packet with a higher packet number is received, it will be rejected. 202 | // 203 | // This function can be used as a precaution against memory exhaustion attacks. 204 | func (gci *GatheredClientInitials) SetMaxPacketNumber(maxPacketNumber uint64) { 205 | atomic.StoreUint64(&gci.maxPacketNumber, maxPacketNumber) 206 | } 207 | 208 | // SetMaxPacketCount sets the maximum number of packets to be gathered. 209 | // If more Client Initial packets are received, they will be rejected. 210 | // 211 | // This function can be used as a precaution against memory exhaustion attacks. 212 | func (gci *GatheredClientInitials) SetMaxPacketCount(maxPacketCount uint64) { 213 | atomic.StoreUint64(&gci.maxPacketCount, maxPacketCount) 214 | } 215 | 216 | // Wait blocks until the GatheredClientInitials is complete or expired. 217 | func (gci *GatheredClientInitials) Wait() error { 218 | if gci.completed.Load() { 219 | return nil 220 | } 221 | 222 | select { 223 | case <-time.After(time.Until(gci.deadline)): 224 | return ErrGatheringExpired 225 | case <-gci.completeChan: 226 | if gci.completed.Load() { 227 | return nil 228 | } 229 | return ErrGatheredClientInitialsChannelClosedBeforeCompletion // divergent state, only possible reason is GC 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /quic_client_initial_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/gaukas/clienthellod" 9 | ) 10 | 11 | var mapGatheredClientInitials = map[string][][]byte{ 12 | "Chrome125": { 13 | quicIETFData_Chrome125_PKN1, 14 | quicIETFData_Chrome125_PKN2, 15 | }, 16 | "Firefox126": { 17 | quicIETFData_Firefox126, 18 | }, 19 | "Firefox126_0-RTT": { 20 | quicIETFData_Firefox126_0_RTT, 21 | }, 22 | } 23 | 24 | func TestGatherClientInitials(t *testing.T) { 25 | for name, test := range mapGatheredClientInitials { 26 | t.Run(name, func(t *testing.T) { 27 | testGatherClientInitialsWithRawPayload(t, test) 28 | }) 29 | } 30 | } 31 | 32 | func testGatherClientInitialsWithRawPayload(t *testing.T, data [][]byte) { 33 | until := time.Now().Add(1 * time.Second) // must be gathered within 1 second 34 | 35 | ci := GatherClientInitialsWithDeadline(until) 36 | for _, d := range data { 37 | cip, err := UnmarshalQUICClientInitialPacket(d) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | err = ci.AddPacket(cip) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | } 47 | 48 | if !ci.Completed() { 49 | t.Fatalf("GatheredClientInitials is not completed") 50 | } 51 | } 52 | 53 | func TestGatheredClientInitialsGC(t *testing.T) { 54 | gcOk := make(chan bool, 1) 55 | gci := GatherClientInitials() 56 | 57 | // Use a dummy ClientHello to detect if the GatheredClientInitials is GCed 58 | dummyClientHello := &QUICClientHello{} 59 | gci.ClientHello = dummyClientHello 60 | runtime.SetFinalizer(dummyClientHello, func(c *QUICClientHello) { 61 | close(gcOk) 62 | }) 63 | 64 | gcCnt := 0 65 | for gcCnt < 5 { 66 | select { 67 | case <-gcOk: 68 | return 69 | default: 70 | runtime.GC() 71 | gcCnt++ 72 | } 73 | } 74 | 75 | t.Fatalf("GatheredClientInitials is not GCed within 5 cycles") 76 | } 77 | -------------------------------------------------------------------------------- /quic_clienthello.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // QUICClientHello represents a QUIC ClientHello. 8 | type QUICClientHello struct { 9 | ClientHello 10 | } 11 | 12 | // ParseQUICClientHello parses a QUIC ClientHello from a QUIC Initial Packet. 13 | func ParseQUICClientHello(p []byte) (*QUICClientHello, error) { 14 | // patch TLS record header to make it a valid TLS record 15 | record := make([]byte, 5+len(p)) 16 | record[0] = 0x16 // TLS handshake 17 | record[1] = 0x00 // Dummy TLS version MSB - 00 18 | record[2] = 0x00 // Dummy TLS version LSB - 00 19 | record[3] = byte(len(p) >> 8) 20 | record[4] = byte(len(p)) 21 | copy(record[5:], p) 22 | 23 | // parse TLS record 24 | r := bytes.NewReader(record) 25 | ch, err := ReadClientHello(r) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | if err = ch.ParseClientHello(); err != nil { 31 | return nil, err 32 | } 33 | 34 | if ch.qtp == nil { 35 | return nil, ErrNotQUICInitialPacket 36 | } 37 | 38 | if ch.qtp.ParseError() != nil { 39 | return nil, ch.qtp.ParseError() 40 | } 41 | 42 | return &QUICClientHello{ClientHello: *ch}, nil 43 | } 44 | 45 | // Raw returns the raw bytes of the QUIC ClientHello. 46 | func (qch *QUICClientHello) Raw() []byte { 47 | return qch.ClientHello.Raw()[5:] // strip TLS record header which is added by ParseQUICClientHello 48 | } 49 | -------------------------------------------------------------------------------- /quic_clienthello_reconstructor.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "runtime" 9 | ) 10 | 11 | // QUICClientHello can be used to parse fragments of a QUIC ClientHello. 12 | type QUICClientHelloReconstructor struct { 13 | fullLen uint32 // parse from first fragment 14 | buf []byte 15 | 16 | frags map[uint64][]byte // offset: fragment, pending to be parsed 17 | } 18 | 19 | // NewQUICClientHelloReconstructor creates a new QUICClientHelloReconstructor. 20 | func NewQUICClientHelloReconstructor() *QUICClientHelloReconstructor { 21 | qchr := &QUICClientHelloReconstructor{ 22 | frags: make(map[uint64][]byte), 23 | } 24 | 25 | runtime.SetFinalizer(qchr, func(q *QUICClientHelloReconstructor) { 26 | q.buf = nil 27 | q.frags = nil 28 | }) 29 | 30 | return qchr 31 | } 32 | 33 | var ( 34 | ErrDuplicateFragment = errors.New("duplicate CRYPTO frame detected") 35 | ErrOverlapFragment = errors.New("overlap CRYPTO frame detected") 36 | ErrTooManyFragments = errors.New("too many CRYPTO fragments") 37 | ErrOffsetTooHigh = errors.New("offset too high") 38 | ErrNeedMoreFrames = errors.New("need more CRYPTO frames") 39 | ) 40 | 41 | const ( 42 | maxCRYPTOFragments = 32 43 | maxCRYPTOLength = 0x10000 // 10KiB 44 | ) 45 | 46 | // AddCRYPTOFragment adds a CRYPTO frame fragment to the reconstructor. 47 | // By default, all fragments are saved into an internal map as a pending 48 | // fragment, UNLESS all fragments before it have been reassembled. 49 | // If the fragment is the last one, it will return io.EOF. 50 | func (qchr *QUICClientHelloReconstructor) AddCRYPTOFragment(offset uint64, frag []byte) error { // skipcq: GO-R1005 51 | // Check for duplicate. The new fragment should not be a duplicate 52 | // of any pending-reassemble fragments. 53 | if _, ok := qchr.frags[offset]; ok { 54 | return ErrDuplicateFragment 55 | } 56 | 57 | // Check for overlap. For all pending-reassemble fragments, none of them 58 | // should overlap with the new fragment. 59 | for off, f := range qchr.frags { 60 | if (off < offset && off+uint64(len(f)) > offset) || (offset < off && offset+uint64(len(frag)) > off) { 61 | return ErrOverlapFragment 62 | } 63 | } 64 | 65 | // The newly added fragment should not overlap with the already-reassembled 66 | // buffer. 67 | if offset < uint64(len(qchr.buf)) { 68 | return ErrOverlapFragment 69 | } 70 | 71 | // Check for pending fragments count 72 | if len(qchr.frags) > maxCRYPTOFragments { 73 | return ErrTooManyFragments 74 | } 75 | 76 | // Check for offset and length: must not be exceeding 77 | // the maximum length of a CRYPTO frame. 78 | if offset+uint64(len(frag)) > maxCRYPTOLength { 79 | return ErrOffsetTooHigh 80 | } 81 | 82 | // Save fragment 83 | qchr.frags[offset] = frag 84 | 85 | for { 86 | // assemble next available fragment until no more 87 | if f, ok := qchr.frags[uint64(len(qchr.buf))]; ok { 88 | copyF := make([]byte, len(f)) 89 | copy(copyF, f) 90 | delete(qchr.frags, uint64(len(qchr.buf))) 91 | qchr.buf = append(qchr.buf, copyF...) 92 | } else { 93 | break 94 | } 95 | } 96 | 97 | // If fullLeh is yet to be determined and we expect to have 98 | // enough bytes to parse the full length, then parse it. 99 | if qchr.fullLen == 0 { 100 | if len(qchr.buf) > 4 { 101 | qchr.fullLen = binary.BigEndian.Uint32([]byte{ 102 | 0x0, qchr.buf[1], qchr.buf[2], qchr.buf[3], 103 | }) + 4 // Handshake Type (1) + uint24 Length (3) + ClientHello body 104 | 105 | if qchr.fullLen > maxCRYPTOLength { 106 | return ErrOffsetTooHigh 107 | } 108 | } 109 | } 110 | 111 | if qchr.fullLen > 0 && uint32(len(qchr.buf)) >= qchr.fullLen { // if we have at least the full length bytes of data, we conclude the CRYPTO frame is complete 112 | return io.EOF // io.EOF means no more fragments expected 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // ReconstructAsBytes reassembles the ClientHello as bytes. 119 | func (qchr *QUICClientHelloReconstructor) ReconstructAsBytes() []byte { 120 | if qchr.fullLen == 0 { 121 | return nil 122 | } else if uint32(len(qchr.buf)) < qchr.fullLen { 123 | return nil 124 | } else { 125 | return qchr.buf 126 | } 127 | } 128 | 129 | // Reconstruct reassembles the ClientHello as a QUICClientHello struct. 130 | func (qchr *QUICClientHelloReconstructor) Reconstruct() (*QUICClientHello, error) { 131 | if b := qchr.ReconstructAsBytes(); len(b) > 0 { 132 | return ParseQUICClientHello(b) 133 | } 134 | 135 | return nil, ErrNeedMoreFrames 136 | } 137 | 138 | // FromFrames reassembles the ClientHello from the CRYPTO frames 139 | func (qr *QUICClientHelloReconstructor) FromFrames(frames []Frame) error { 140 | // Collect all CRYPTO frames 141 | for _, frame := range frames { 142 | if frame.FrameType() == QUICFrame_CRYPTO { 143 | switch c := frame.(type) { 144 | case *CRYPTO: 145 | if err := qr.AddCRYPTOFragment(c.Offset, c.data); err != nil { 146 | if errors.Is(err, io.EOF) { 147 | return nil 148 | } else { 149 | return err 150 | } 151 | } 152 | default: 153 | return fmt.Errorf("unknown CRYPTO frame type %T", c) 154 | } 155 | } 156 | } 157 | 158 | return ErrNeedMoreFrames 159 | } 160 | -------------------------------------------------------------------------------- /quic_clienthello_reconstructor_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "io" 8 | "math" 9 | "math/big" 10 | "math/rand" 11 | "testing" 12 | 13 | crand "crypto/rand" 14 | 15 | . "github.com/gaukas/clienthellod" 16 | ) 17 | 18 | var ( 19 | //go:embed internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_0.bin 20 | quicFrames_Chrome124_CRYPTO_0 []byte 21 | //go:embed internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1191.bin 22 | quicFrames_Chrome124_CRYPTO_1191 []byte 23 | //go:embed internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1287.bin 24 | quicFrames_Chrome124_CRYPTO_1287 []byte 25 | //go:embed internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1561.bin 26 | quicFrames_Chrome124_CRYPTO_1561 []byte 27 | //go:embed internal/testdata/QUIC_Frame_Chrome_124_CRYPTO_1663.bin 28 | quicFrames_Chrome124_CRYPTO_1663 []byte 29 | 30 | //go:embed internal/testdata/QUIC_ClientHello_Chrome_124.bin 31 | quicClientHelloTruth_Chrome124 []byte 32 | ) 33 | 34 | var Chrome124_CRYPTO []struct { 35 | offset uint64 36 | pl []byte 37 | } = []struct { 38 | offset uint64 39 | pl []byte 40 | }{ 41 | {0, quicFrames_Chrome124_CRYPTO_0}, 42 | {1191, quicFrames_Chrome124_CRYPTO_1191}, 43 | {1287, quicFrames_Chrome124_CRYPTO_1287}, 44 | {1561, quicFrames_Chrome124_CRYPTO_1561}, 45 | {1663, quicFrames_Chrome124_CRYPTO_1663}, 46 | } 47 | 48 | func TestQUICClientHelloReconstructor(t *testing.T) { 49 | r := NewQUICClientHelloReconstructor() 50 | 51 | // shuffle the fragments 52 | randInt64, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64)) 53 | if err != nil { 54 | t.Fatal(err) 55 | } else { 56 | rand.New(rand.NewSource(randInt64.Int64())).Shuffle(len(Chrome124_CRYPTO), func(i, j int) { // skipcq: GSC-G404 57 | Chrome124_CRYPTO[i], Chrome124_CRYPTO[j] = Chrome124_CRYPTO[j], Chrome124_CRYPTO[i] 58 | }) 59 | } 60 | 61 | for i, frag := range Chrome124_CRYPTO { 62 | if err := r.AddCRYPTOFragment(frag.offset, frag.pl); err != nil { 63 | if i == len(Chrome124_CRYPTO)-1 && errors.Is(err, io.EOF) { 64 | break 65 | } 66 | t.Fatal(err) 67 | } 68 | } 69 | 70 | if !bytes.Equal(r.ReconstructAsBytes(), quicClientHelloTruth_Chrome124) { 71 | t.Fatalf("Reassembled ClientHello mismatch") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /quic_clienthello_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "golang.org/x/exp/slices" 8 | 9 | . "github.com/gaukas/clienthellod" 10 | ) 11 | 12 | func TestParseQUICClientHello(t *testing.T) { 13 | t.Run("Google Chrome", testParseQUICClientHelloGoogleChrome) 14 | } 15 | 16 | func testParseQUICClientHelloGoogleChrome(t *testing.T) { // skipcq: GO-R1005 17 | var rawQCH []byte = []byte{ 18 | 0x01, 0x00, 0x01, 0x22, 0x03, 0x03, 0xe3, 0x1b, 19 | 0x6b, 0x88, 0xce, 0x0e, 0xff, 0x48, 0x08, 0x52, 20 | 0xa6, 0x21, 0x03, 0x90, 0x84, 0x92, 0x5d, 0xf6, 21 | 0x8a, 0xcb, 0xad, 0x66, 0xdb, 0x9f, 0x3c, 0x94, 22 | 0x3f, 0x0e, 0xba, 0xf2, 0x4a, 0x3c, 0x00, 0x00, 23 | 0x06, 0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0x01, 24 | 0x00, 0x00, 0xf3, 0x44, 0x69, 0x00, 0x05, 0x00, 25 | 0x03, 0x02, 0x68, 0x33, 0x00, 0x39, 0x00, 0x5d, 26 | 0x09, 0x02, 0x40, 0x67, 0x0f, 0x00, 0x01, 0x04, 27 | 0x80, 0x00, 0x75, 0x30, 0x05, 0x04, 0x80, 0x60, 28 | 0x00, 0x00, 0xe2, 0xd0, 0x11, 0x38, 0x87, 0x0c, 29 | 0x6f, 0x9f, 0x01, 0x96, 0x07, 0x04, 0x80, 0x60, 30 | 0x00, 0x00, 0x71, 0x28, 0x04, 0x52, 0x56, 0x43, 31 | 0x4d, 0x03, 0x02, 0x45, 0xc0, 0x20, 0x04, 0x80, 32 | 0x01, 0x00, 0x00, 0x08, 0x02, 0x40, 0x64, 0x80, 33 | 0xff, 0x73, 0xdb, 0x0c, 0x00, 0x00, 0x00, 0x01, 34 | 0xba, 0xca, 0x5a, 0x5a, 0x00, 0x00, 0x00, 0x01, 35 | 0x80, 0x00, 0x47, 0x52, 0x04, 0x00, 0x00, 0x00, 36 | 0x01, 0x06, 0x04, 0x80, 0x60, 0x00, 0x00, 0x04, 37 | 0x04, 0x80, 0xf0, 0x00, 0x00, 0x00, 0x33, 0x00, 38 | 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0xf8, 39 | 0x82, 0xf6, 0x48, 0x2b, 0x20, 0x0c, 0xa0, 0x60, 40 | 0x79, 0x1c, 0x45, 0xa5, 0xb8, 0x43, 0x58, 0x11, 41 | 0x26, 0x64, 0xec, 0x4f, 0xf7, 0xd6, 0xea, 0x10, 42 | 0x30, 0xf6, 0x9f, 0x36, 0x80, 0x49, 0x43, 0x00, 43 | 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 44 | 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 45 | 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 46 | 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x33, 47 | 0x00, 0x0a, 0x00, 0x08, 0x00, 0x06, 0x00, 0x1d, 48 | 0x00, 0x17, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1a, 49 | 0x00, 0x18, 0x00, 0x00, 0x15, 0x71, 0x2e, 0x63, 50 | 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x68, 0x65, 0x6c, 51 | 0x6c, 0x6f, 0x2e, 0x67, 0x61, 0x75, 0x6b, 0x2e, 52 | 0x61, 0x73, 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 53 | 0x04, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 54 | 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, 55 | } 56 | 57 | qch, err := ParseQUICClientHello(rawQCH) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | if !bytes.Equal(qch.Raw(), rawQCH) { 63 | t.Fatal("marshaled QUIC Client Hello does not match original") 64 | } 65 | 66 | if qch.TLSHandshakeVersion != 0x0303 { 67 | t.Fatalf("TLS handshake version does not match, expecting 0x0303, got %.4x", qch.TLSHandshakeVersion) 68 | } 69 | 70 | if !slices.Equal( 71 | qch.CipherSuites, 72 | []uint16{0x1301, 0x1302, 0x1303}) { 73 | t.Fatalf("cipher suites do not match, expecting [0x1301, 0x1302, 0x1303], got %v", qch.CipherSuites) 74 | } 75 | 76 | if qch.ServerName != "q.clienthello.gauk.as" { 77 | t.Fatal("server name does not match") 78 | } 79 | 80 | if !slices.Equal( 81 | qch.NamedGroupList, 82 | []uint16{0x001d, 0x0017, 0x0018}) { 83 | t.Fatalf("named group list does not match, expecting [0x001d, 0x0017, 0x0018], got %v", qch.NamedGroupList) 84 | } 85 | 86 | if len(qch.ECPointFormatList) != 0 { 87 | t.Fatalf("EC point format list does not match, expecting [], got %v", qch.ECPointFormatList) 88 | } 89 | 90 | if !slices.Equal( 91 | qch.SignatureSchemeList, 92 | []uint16{ 93 | 0x0403, 94 | 0x0804, 95 | 0x0401, 96 | 0x0503, 97 | 0x0805, 98 | 0x0501, 99 | 0x0806, 100 | 0x0601, 101 | 0x0201, 102 | }) { 103 | t.Fatalf("signature scheme list does not match, expecting [0x0403, 0x0804, 0x0401, 0x0503, 0x0805, 0x0501, 0x0806, 0x0601, 0x0201], got %v", qch.SignatureSchemeList) 104 | } 105 | 106 | if !slices.Equal( 107 | qch.ALPN, 108 | []string{"h3"}) { 109 | t.Fatalf("ALPN does not match, expecting [\"h3\"], got %v", qch.ALPN) 110 | } 111 | 112 | if !slices.Equal( 113 | qch.CertCompressAlgo, 114 | []uint16{0x0002}) { 115 | t.Fatalf("cert compression algorithm does not match, expecting [0x0002], got %v", qch.CertCompressAlgo) 116 | } 117 | 118 | if len(qch.RecordSizeLimit) != 0 { 119 | t.Fatalf("record size limit does not match, expecting [], got %v", qch.RecordSizeLimit) 120 | } 121 | 122 | if !slices.Equal( 123 | qch.SupportedVersions, 124 | []uint16{ 125 | 0x0304, 126 | }) { 127 | t.Fatalf("supported versions does not match, expecting [0x0304], got %v", qch.SupportedVersions) 128 | } 129 | 130 | if !slices.Equal( 131 | qch.PSKKeyExchangeModes, 132 | []uint8{ 133 | 0x01, 134 | }) { 135 | t.Fatalf("PSK key exchange modes does not match, expecting [0x01], got %v", qch.PSKKeyExchangeModes) 136 | } 137 | 138 | if !slices.Equal( 139 | qch.KeyShare, 140 | []uint16{0x001d}) { 141 | t.Fatalf("key share does not match, expecting [0x001d], got %v", qch.KeyShare) 142 | } 143 | 144 | if !slices.Equal( 145 | qch.ApplicationSettings, 146 | []string{"h3"}) { 147 | t.Fatalf("application settings does not match, expecting [\"h3\"], got %v", qch.ApplicationSettings) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /quic_common.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | 8 | "github.com/gaukas/clienthellod/internal/utils" 9 | "golang.org/x/crypto/cryptobyte" 10 | ) 11 | 12 | // ReadNextVLI unpacks the next variable-length integer from the given 13 | // io.Reader. It returns the decoded value and the number of bytes read. 14 | // For example: 15 | // 16 | // 0x0a -> 0xa, 1 17 | // 0x80 0x10 0x00 0x00 -> 0x100000, 4 18 | func ReadNextVLI(r io.Reader) (val uint64, n int, err error) { 19 | // read the first byte 20 | var encodedBytes []byte = make([]byte, 1) 21 | _, err = r.Read(encodedBytes) 22 | if err != nil { 23 | return 0, 0, err 24 | } 25 | 26 | // check MSBs of the first byte 27 | switch encodedBytes[0] & 0xc0 { // 0xc0 = 0b11000000, when the first 2 bits in a byte is set 28 | case 0x00: 29 | n = 1 30 | case 0x40: 31 | n = 2 32 | case 0x80: 33 | n = 4 34 | case 0xc0: 35 | n = 8 36 | default: 37 | return 0, 0, errors.New("invalid first byte") 38 | } 39 | 40 | // read the rest bytes 41 | if n > 1 { 42 | encodedBytes = append(encodedBytes, make([]byte, n-1)...) 43 | _, err = r.Read(encodedBytes[1:]) 44 | if err != nil { 45 | return 0, 0, err 46 | } 47 | } 48 | 49 | // decode 50 | encodedBytes[0] &= 0x3f // 0x3f = 0b00111111, clear MSBs 51 | for i := 0; i < n; i++ { 52 | val <<= 8 53 | val |= uint64(encodedBytes[i]) 54 | } 55 | 56 | return 57 | } 58 | 59 | // DecodeVLI decodes a variable-length integer from the given byte slice. 60 | func DecodeVLI(vli []byte) (val uint64, err error) { 61 | var n int 62 | val, n, err = ReadNextVLI(bytes.NewReader(vli)) 63 | if err != nil { 64 | return 0, err 65 | } 66 | if n != len(vli) { 67 | return 0, errors.New("invalid VLI length") 68 | } 69 | return 70 | } 71 | 72 | func unsetVLIBits(vli []byte) { 73 | if UNSET_VLI_BITS { 74 | vli[0] &= 0x3f // 0x3f = 0b00111111, clear MSBs 75 | } 76 | } 77 | 78 | // IsGREASETransportParameter checks if the given transport parameter type is a GREASE value. 79 | func IsGREASETransportParameter(paramType uint64) bool { 80 | return paramType >= 27 && (paramType-27)%31 == 0 // reserved values are 27, 58, 89, ... 81 | } 82 | 83 | var ( 84 | ErrNotQUICLongHeaderFormat = errors.New("not a QUIC Long Header Format Packet") 85 | ErrNotQUICInitialPacket = errors.New("not a QUIC Initial Packet") 86 | ) 87 | 88 | // DecodeQUICHeaderAndFrames decodes a QUIC initial packet and returns a QUICHeader. 89 | func DecodeQUICHeaderAndFrames(p []byte) (hdr *QUICHeader, frames QUICFrames, err error) { // skipcq: GO-R1005 90 | if len(p) < 7 { // at least 7 bytes before TokenLength 91 | return nil, nil, errors.New("packet too short") 92 | } 93 | 94 | // make a copy of the packet, so we can use it for crypto later 95 | recdata := make([]byte, len(p)) 96 | copy(recdata, p) 97 | 98 | hdr = &QUICHeader{} 99 | 100 | packetHeaderByteProtected := p[0] 101 | 102 | // check if it's in QUIC long header format: 103 | // - MSB highest bit is 1 (long header format) 104 | // - MSB 2nd highest bit is 1 (always set for QUIC) 105 | if packetHeaderByteProtected&0xc0 != 0xc0 { 106 | return nil, nil, ErrNotQUICLongHeaderFormat 107 | } 108 | 109 | // check if it's a QUIC Initial Packet: MSB lower 2 bits are 0 110 | if packetHeaderByteProtected&0x30 != 0 { 111 | return nil, nil, ErrNotQUICInitialPacket 112 | } 113 | 114 | // LSB of the first byte is protected, we will resolve it later 115 | 116 | hdr.Version = make(utils.Uint8Arr, 4) 117 | copy(hdr.Version, p[1:5]) 118 | s := cryptobyte.String(p[5:]) 119 | initialRandom := new(cryptobyte.String) 120 | if !s.ReadUint8LengthPrefixed(initialRandom) { 121 | return nil, nil, errors.New("failed to read DCID (initial random)") 122 | } 123 | hdr.DCIDLength = uint32(len(*initialRandom)) 124 | 125 | var scidLenUint8 uint8 126 | if !s.ReadUint8(&scidLenUint8) || 127 | !s.Skip(int(scidLenUint8)) { 128 | return nil, nil, errors.New("failed to read SCID") 129 | } 130 | hdr.SCIDLength = uint32(scidLenUint8) 131 | 132 | // token length is a VLI 133 | r := bytes.NewReader(s) 134 | tokenLen, _, err := ReadNextVLI(r) 135 | if err != nil { 136 | return nil, nil, err 137 | } 138 | // read token bytes 139 | token := make([]byte, tokenLen) 140 | n, err := r.Read(token) 141 | if err != nil { 142 | return nil, nil, err 143 | } 144 | if n != int(tokenLen) { 145 | return nil, nil, errors.New("failed to read all token bytes, short read") 146 | } 147 | if tokenLen > 0 { 148 | hdr.HasToken = true 149 | } 150 | 151 | // packet length is a VLI 152 | packetLen, _, err := ReadNextVLI(r) 153 | if err != nil { 154 | return nil, nil, err 155 | } 156 | if packetLen < 20 { 157 | return nil, nil, errors.New("packet length too short, ignore") 158 | } 159 | 160 | // read all remaining bytes as payload 161 | payload := make([]byte, packetLen) 162 | n, err = r.Read(payload) 163 | if err != nil { 164 | return nil, nil, err 165 | } 166 | if n != int(packetLen) { 167 | return nil, nil, errors.New("failed to read all payload bytes, short read") 168 | } 169 | 170 | // do key calculation 171 | clientKey, clientIV, clientHpKey, err := ClientInitialKeysCalc(*initialRandom) 172 | if err != nil { 173 | return nil, nil, err 174 | } 175 | 176 | // compute header protection 177 | hp, err := ComputeHeaderProtection(clientHpKey, payload[4:20]) 178 | if err != nil { 179 | return nil, nil, err 180 | } 181 | 182 | // prepare recdata 183 | // truncate recdata to remove following (possibly) padding bytes 184 | recdata = recdata[:len(recdata)-r.Len()] 185 | // remove payload bytes 186 | recdata = recdata[:len(recdata)-len(payload)] // recdata: [...headers...] [packet number] 187 | 188 | // decipher packet header byte 189 | headerByte := packetHeaderByteProtected ^ (hp[0] & 0x0f) // only lower 4 bits are protected and thus need to be XORed 190 | recdata[0] = headerByte 191 | hdr.initialPacketNumberLength = uint32(headerByte&0x03) + 1 // LSB lower 2 bits are packet number length (-1) 192 | packetNumberBytes := payload[:hdr.initialPacketNumberLength] 193 | for i, b := range packetNumberBytes { 194 | unprotectedByte := b ^ hp[i+1] 195 | recdata = append(recdata, unprotectedByte) 196 | hdr.initialPacketNumber = hdr.initialPacketNumber<<8 + uint64(unprotectedByte) 197 | hdr.PacketNumber = append(hdr.PacketNumber, unprotectedByte) 198 | } 199 | 200 | cipherPayload := payload[hdr.initialPacketNumberLength : len(payload)-16] // payload: [packet number (i-byte)] [encrypted data] [auth tag (16-byte)] 201 | authTag := payload[len(payload)-16:] 202 | 203 | // decipher payload 204 | plainPayload, err := DecryptAES128GCM(clientIV, hdr.initialPacketNumber, clientKey, cipherPayload, recdata, authTag) 205 | if err != nil { 206 | return nil, nil, err 207 | } 208 | 209 | // parse frames 210 | frames, err = ReadAllFrames(bytes.NewBuffer(plainPayload)) 211 | if err != nil { 212 | return nil, nil, err 213 | } 214 | 215 | // // deduplicate frame IDs 216 | // qHdr.FrameIDs = utils.DedupIntArr(qHdr.FrameIDs) 217 | 218 | return 219 | } 220 | -------------------------------------------------------------------------------- /quic_common_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | _ "embed" 8 | 9 | . "github.com/gaukas/clienthellod" 10 | ) 11 | 12 | var mapValueToVLI = map[uint64][]byte{ 13 | 0: {0x00}, 14 | 26: {0x1a}, 15 | 110: {0x40, 0x6e}, 16 | 158: {0x40, 0x9e}, 17 | 184: {0x40, 0xb8}, 18 | 1212: {0x44, 0xbc}, 19 | 30000: {0x80, 0x00, 0x75, 0x30}, 20 | 6291456: {0x80, 0x60, 0x00, 0x00}, 21 | 0x22d01138870c6f9f: {0xe2, 0xd0, 0x11, 0x38, 0x87, 0x0c, 0x6f, 0x9f}, 22 | } 23 | 24 | func TestReadNextVLI(t *testing.T) { 25 | for v, vli := range mapValueToVLI { 26 | val, n, err := ReadNextVLI(bytes.NewReader(vli)) 27 | if err != nil { 28 | t.Errorf("ReadNextVLI(%v) error: %v", vli, err) 29 | } 30 | if val != v { 31 | t.Errorf("ReadNextVLI(%v) = %v, want %v", vli, val, v) 32 | } 33 | if n != len(vli) { 34 | t.Errorf("ReadNextVLI(%v) = %v, want %v", vli, n, len(vli)) 35 | } 36 | } 37 | } 38 | 39 | func TestDecodeVLI(t *testing.T) { 40 | for v, vli := range mapValueToVLI { 41 | val, err := DecodeVLI(vli) 42 | if err != nil { 43 | t.Errorf("DecodeVLI(%v) error: %v", vli, err) 44 | } 45 | if val != v { 46 | t.Errorf("DecodeVLI(%v) = %v, want %v", vli, val, v) 47 | } 48 | } 49 | } 50 | 51 | var mapQUICGREASEValues = map[uint64]bool{ 52 | 0x01: false, 53 | 0x02: false, 54 | 0x03: false, 55 | 0x04: false, 56 | 0x05: false, 57 | 0x06: false, 58 | 0x07: false, 59 | 0x08: false, 60 | 0x09: false, 61 | 0x0a: false, 62 | 0x0b: false, 63 | 0x0c: false, 64 | 0x0d: false, 65 | 0x0e: false, 66 | 0x0f: false, 67 | 0x10: false, 68 | 0x11: false, 69 | 0x12: false, 70 | 0x13: false, 71 | 0x14: false, 72 | 0x15: false, 73 | 0x16: false, 74 | 0x17: false, 75 | 0x18: false, 76 | 0x19: false, 77 | 0x1a: false, 78 | 27: true, 79 | 31: false, 80 | 58: true, 81 | 89: true, 82 | 2508523926926946207: true, 83 | } 84 | 85 | func TestIsGREASETransportParameter(t *testing.T) { 86 | for v, grease := range mapQUICGREASEValues { 87 | if IsGREASETransportParameter(v) != grease { 88 | t.Errorf("IsGREASETransportParameter(%v) = %v, want %v", v, !grease, grease) 89 | } 90 | } 91 | } 92 | 93 | var ( 94 | //go:embed internal/testdata/QUIC_IETF_Chrome_125_PKN1.bin 95 | quicIETFData_Chrome125_PKN1 []byte 96 | //go:embed internal/testdata/QUIC_IETF_Chrome_125_PKN2.bin 97 | quicIETFData_Chrome125_PKN2 []byte 98 | 99 | //go:embed internal/testdata/QUIC_IETF_Firefox_126.bin 100 | quicIETFData_Firefox126 []byte 101 | //go:embed internal/testdata/QUIC_IETF_Firefox_126_0-RTT.bin 102 | quicIETFData_Firefox126_0_RTT []byte 103 | ) 104 | 105 | var mapTestDecodeQUICHeaderAndFrames = map[string]struct { 106 | data []byte 107 | headerTruth *QUICHeader 108 | framesTruth QUICFrames 109 | }{ 110 | "Chrome125_PKN1": { 111 | data: quicIETFData_Chrome125_PKN1, 112 | headerTruth: quicHeaderTruth_Chrome125_PKN1, 113 | framesTruth: quicFramesTruth_Chrome125_PKN1, 114 | }, 115 | "Chrome125_PKN2": { 116 | data: quicIETFData_Chrome125_PKN2, 117 | headerTruth: quicHeaderTruth_Chrome125_PKN2, 118 | framesTruth: quicFramesTruth_Chrome125_PKN2, 119 | }, 120 | 121 | "Firefox126": { 122 | data: quicIETFData_Firefox126, 123 | headerTruth: quicHeaderTruth_Firefox126, 124 | framesTruth: quicFramesTruth_Firefox126, 125 | }, 126 | "Firefox126_with_0-RTT": { 127 | data: quicIETFData_Firefox126_0_RTT, 128 | headerTruth: quicHeaderTruth_Firefox126_0_RTT, 129 | framesTruth: quicFramesTruth_Firefox126_0_RTT, 130 | }, 131 | } 132 | 133 | func TestDecodeQUICHeaderAndFrames(t *testing.T) { 134 | for name, test := range mapTestDecodeQUICHeaderAndFrames { 135 | t.Run(name, func(t *testing.T) { 136 | testDecodeQUICHeaderAndFramesWithTruth(t, test.data, test.headerTruth, test.framesTruth) 137 | }) 138 | } 139 | } 140 | 141 | func testDecodeQUICHeaderAndFramesWithTruth(t *testing.T, data []byte, headerTruth *QUICHeader, framesTruth QUICFrames) { 142 | decodedHeader, decodedFrames, err := DecodeQUICHeaderAndFrames(data) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | t.Run("header", func(t *testing.T) { 148 | testQUICHeaderEqualsTruth(t, decodedHeader, headerTruth) 149 | }) 150 | 151 | t.Run("frames", func(t *testing.T) { 152 | testQUICFramesEqualsTruth(t, decodedFrames, framesTruth) 153 | }) 154 | 155 | t.Skip("skipping testing decoded frames") 156 | } 157 | -------------------------------------------------------------------------------- /quic_crypto.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/sha256" 7 | "errors" 8 | 9 | "golang.org/x/crypto/cryptobyte" 10 | "golang.org/x/crypto/hkdf" 11 | ) 12 | 13 | // ClientInitialKeysCalc calculates the client key, IV and header protection key from the initial random. 14 | func ClientInitialKeysCalc(initialRandom []byte) (clientKey, clientIV, clientHpKey []byte, err error) { 15 | initialSalt := []byte{ 16 | 0x38, 0x76, 0x2c, 0xf7, 17 | 0xf5, 0x59, 0x34, 0xb3, 18 | 0x4d, 0x17, 0x9a, 0xe6, 19 | 0xa4, 0xc8, 0x0c, 0xad, 20 | 0xcc, 0xbb, 0x7f, 0x0a, 21 | } // magic value, the first SHA-1 collision 22 | 23 | initialSecret := hkdf.Extract(sha256.New, initialRandom, initialSalt) 24 | 25 | clientSecret, err := hkdfExpandLabel(initialSecret, "client in", nil, 32) 26 | if err != nil { 27 | return nil, nil, nil, err 28 | } 29 | clientKey, err = hkdfExpandLabel(clientSecret, "quic key", nil, 16) 30 | if err != nil { 31 | return nil, nil, nil, err 32 | } 33 | clientIV, err = hkdfExpandLabel(clientSecret, "quic iv", nil, 12) 34 | if err != nil { 35 | return nil, nil, nil, err 36 | } 37 | clientHpKey, err = hkdfExpandLabel(clientSecret, "quic hp", nil, 16) 38 | if err != nil { 39 | return nil, nil, nil, err 40 | } 41 | 42 | return 43 | } 44 | 45 | func hkdfExpandLabel(key []byte, label string, context []byte, length uint16) ([]byte, error) { 46 | // see https://tools.ietf.org/html/rfc8446#section-7.1 47 | // code from crypto/tls 48 | var hkdfLabel cryptobyte.Builder 49 | hkdfLabel.AddUint16(uint16(length)) 50 | hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { 51 | b.AddBytes([]byte("tls13 ")) 52 | b.AddBytes([]byte(label)) 53 | }) 54 | hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { 55 | b.AddBytes(context) 56 | }) 57 | hkdfLabelBytes, err := hkdfLabel.Bytes() 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | r := hkdf.Expand(sha256.New, key, hkdfLabelBytes) 63 | out := make([]byte, length) 64 | n, err := r.Read(out) 65 | if err != nil { 66 | return nil, err 67 | } 68 | if n != int(length) { 69 | return nil, errors.New("failed to read all bytes, short read") 70 | } 71 | return out, nil 72 | } 73 | 74 | // ComputeHeaderProtection computes the header protection for the client. 75 | func ComputeHeaderProtection(clientHpKey, sample []byte) ([]byte, error) { 76 | if len(clientHpKey) != 16 || len(sample) != 16 { 77 | panic("invalid input") 78 | } 79 | 80 | // AES-128-ECB 81 | cipher, err := aes.NewCipher([]byte(clientHpKey)) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | var headerProtection []byte = make([]byte, 16) 87 | cipher.Encrypt(headerProtection, sample) 88 | 89 | return headerProtection[:5], nil 90 | } 91 | 92 | // DecryptAES128GCM decrypts the AES-128-GCM encrypted data. 93 | func DecryptAES128GCM(iv []byte, recordNum uint64, key, ciphertext, recdata, authtag []byte) (plaintext []byte, err error) { 94 | buildIV(iv, recordNum) 95 | 96 | if len(iv) != 12 || len(key) != 16 || len(authtag) != 16 { 97 | return nil, errors.New("invalid input") 98 | } 99 | 100 | block, err := aes.NewCipher(key) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | aesgcm, err := cipher.NewGCM(block) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | return aesgcm.Open(nil, iv, append(ciphertext, authtag...), recdata) 111 | } 112 | 113 | // https://quic.xargs.org/files/aes_128_gcm_decrypt.c 114 | // 115 | // static void build_iv(uchar *iv, uint64_t seq) 116 | // 117 | // { 118 | // size_t i; 119 | // for (i = 0; i < 8; i++) { 120 | // iv[gcm_ivlen-1-i] ^= ((seq>>(i*8))&0xFF); 121 | // } 122 | // } 123 | func buildIV(iv []byte, seq uint64) { 124 | for i := 0; i < 8; i++ { 125 | iv[11-i] ^= byte((seq >> (i * 8)) & 0xFF) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /quic_crypto_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "testing" 7 | 8 | . "github.com/gaukas/clienthellod" 9 | ) 10 | 11 | func TestClientInitialKeysCalc(t *testing.T) { 12 | initialRandom := []byte{ 13 | 0x00, 0x01, 0x02, 0x03, 14 | 0x04, 0x05, 0x06, 0x07, 15 | } 16 | 17 | clientKey, clientIV, clientHpKey, err := ClientInitialKeysCalc(initialRandom) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | if !bytes.Equal(clientKey, []byte{ 23 | 0xb1, 0x4b, 0x91, 0x81, 0x24, 0xfd, 0xa5, 0xc8, 24 | 0xd7, 0x98, 0x47, 0x60, 0x2f, 0xa3, 0x52, 0x0b, 25 | }) { 26 | t.Fatalf("clientKey mismatch, got %x", clientKey) 27 | } 28 | 29 | if !bytes.Equal(clientIV, []byte{ 30 | 0xdd, 0xbc, 0x15, 0xde, 0xa8, 0x09, 0x25, 0xa5, 0x56, 0x86, 0xa7, 0xdf, 31 | }) { 32 | t.Fatalf("clientIV mismatch, got %x", clientIV) 33 | } 34 | 35 | if !bytes.Equal(clientHpKey, []byte{ 36 | 0x6d, 0xf4, 0xe9, 0xd7, 0x37, 0xcd, 0xf7, 0x14, 37 | 0x71, 0x1d, 0x7c, 0x61, 0x7e, 0xe8, 0x29, 0x81, 38 | }) { 39 | t.Fatalf("clientHpKey mismatch, got %x", clientHpKey) 40 | } 41 | } 42 | 43 | func TestComputeHeaderProtection(t *testing.T) { 44 | hp, err := ComputeHeaderProtection( 45 | []byte{ 46 | 0x6d, 0xf4, 0xe9, 0xd7, 0x37, 0xcd, 0xf7, 0x14, 47 | 0x71, 0x1d, 0x7c, 0x61, 0x7e, 0xe8, 0x29, 0x81, 48 | }, 49 | []byte{ 50 | 0xed, 0x78, 0x71, 0x6b, 0xe9, 0x71, 0x1b, 0xa4, 51 | 0x98, 0xb7, 0xed, 0x86, 0x84, 0x43, 0xbb, 0x2e, 52 | }, 53 | ) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | if !bytes.Equal(hp, []byte{0xed, 0x98, 0x95, 0xbb, 0x15}) { 59 | t.Fatalf("unexpected header protection: %x", hp) 60 | } 61 | } 62 | 63 | func TestDecryptAES128GCM(t *testing.T) { 64 | var recordNum uint64 = 0x00 65 | iv, _ := hex.DecodeString("ddbc15dea80925a55686a7df") 66 | key, _ := hex.DecodeString("b14b918124fda5c8d79847602fa3520b") 67 | cipherText, _ := hex.DecodeString("1c36a7ed78716be9711ba498b7ed868443bb2e0c514d4d848eadcc7a00d25ce9f9afa483978088de836be68c0b32a24595d7813ea5414a9199329a6d9f7f760dd8bb249bf3f53d9a77fbb7b395b8d66d7879a51fe59ef9601f79998eb3568e1fdc789f640acab3858a82ef2930fa5ce14b5b9ea0bdb29f4572da85aa3def39b7efafffa074b9267070d50b5d07842e49bba3bc787ff295d6ae3b514305f102afe5a047b3fb4c99eb92a274d244d60492c0e2e6e212cef0f9e3f62efd0955e71c768aa6bb3cd80bbb3755c8b7ebee32712f40f2245119487021b4b84e1565e3ca31967ac8604d4032170dec280aeefa095d08") 68 | recdata, _ := hex.DecodeString("c00000000108000102030405060705635f63696400410300") 69 | authtag, _ := hex.DecodeString("b3b7241ef6646a6c86e5c62ce08be099") 70 | 71 | plaintext, err := DecryptAES128GCM(iv, recordNum, key, cipherText, recdata, authtag) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | expectedPlaintext, _ := hex.DecodeString("060040ee010000ea0303000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f000006130113021303010000bb0000001800160000136578616d706c652e756c666865696d2e6e6574000a00080006001d001700180010000b00090870696e672f312e30000d00140012040308040401050308050501080606010201003300260024001d0020358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254002d00020101002b00030203040039003103048000fff7040480a0000005048010000006048010000007048010000008010a09010a0a01030b01190f05635f636964") 77 | 78 | if !bytes.Equal(plaintext, expectedPlaintext) { 79 | t.Fatalf("unexpected plaintext: %x", plaintext) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /quic_fingerprint.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "crypto/sha1" // skipcq: GSC-G505 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "net" 9 | "runtime" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/gaukas/clienthellod/internal/utils" 15 | ) 16 | 17 | // QUICFingerprint can be used to generate a fingerprint of a QUIC connection. 18 | type QUICFingerprint struct { 19 | ClientInitials *GatheredClientInitials 20 | 21 | HexID string `json:"hex_id,omitempty"` 22 | NumID uint64 `json:"num_id,omitempty"` 23 | 24 | UserAgent string `json:"user_agent,omitempty"` // User-Agent header, set by the caller 25 | } 26 | 27 | // GenerateQUICFingerprint generates a QUICFingerprint from the gathered ClientInitials. 28 | func GenerateQUICFingerprint(gci *GatheredClientInitials) (*QUICFingerprint, error) { 29 | if err := gci.Wait(); err != nil { 30 | return nil, err // GatheringClientInitials failed (expired before complete) 31 | } 32 | 33 | qfp := &QUICFingerprint{ 34 | ClientInitials: gci, 35 | // UserAgent: userAgent, 36 | } 37 | 38 | // TODO: calculate hash 39 | h := sha1.New() // skipcq: GO-S1025, GSC-G401 40 | updateU64(h, gci.NumID) 41 | updateU64(h, uint64(gci.ClientHello.NormNumID)) 42 | updateU64(h, gci.TransportParameters.NumID) 43 | 44 | qfp.NumID = binary.BigEndian.Uint64(h.Sum(nil)) 45 | qfp.HexID = FingerprintID(qfp.NumID).AsHex() 46 | 47 | runtime.SetFinalizer(qfp, func(q *QUICFingerprint) { 48 | q.ClientInitials = nil 49 | }) 50 | 51 | return qfp, nil 52 | } 53 | 54 | const DEFAULT_QUICFINGERPRINT_EXPIRY = 60 * time.Second 55 | 56 | // QUICFingerprinter can be used to fingerprint QUIC connections. 57 | type QUICFingerprinter struct { 58 | mapGatheringClientInitials *sync.Map 59 | 60 | timeout time.Duration 61 | closed atomic.Bool 62 | } 63 | 64 | // NewQUICFingerprinter creates a new QUICFingerprinter. 65 | func NewQUICFingerprinter() *QUICFingerprinter { 66 | return &QUICFingerprinter{ 67 | mapGatheringClientInitials: new(sync.Map), 68 | closed: atomic.Bool{}, 69 | } 70 | } 71 | 72 | // NewQUICFingerprinterWithTimeout creates a new QUICFingerprinter with a timeout. 73 | func NewQUICFingerprinterWithTimeout(timeout time.Duration) *QUICFingerprinter { 74 | return &QUICFingerprinter{ 75 | mapGatheringClientInitials: new(sync.Map), 76 | timeout: timeout, 77 | closed: atomic.Bool{}, 78 | } 79 | } 80 | 81 | // SetTimeout sets the timeout for gathering ClientInitials. 82 | func (qfp *QUICFingerprinter) SetTimeout(timeout time.Duration) { 83 | qfp.timeout = timeout 84 | } 85 | 86 | // HandlePacket handles a QUIC packet. 87 | func (qfp *QUICFingerprinter) HandlePacket(from string, p []byte) error { 88 | if qfp.closed.Load() { 89 | return errors.New("QUICFingerprinter closed") 90 | } 91 | 92 | ci, err := UnmarshalQUICClientInitialPacket(p) 93 | if err != nil { 94 | if errors.Is(err, ErrNotQUICLongHeaderFormat) || errors.Is(err, ErrNotQUICInitialPacket) { 95 | return nil // totally fine, we don't care about non QUIC initials 96 | } 97 | return err 98 | } 99 | 100 | var testGci *GatheredClientInitials 101 | if qfp.timeout == time.Duration(0) { 102 | testGci = GatherClientInitials() 103 | } else { 104 | testGci = GatherClientInitialsWithDeadline(time.Now().Add(qfp.timeout)) 105 | } 106 | 107 | chosenGci, existing := qfp.mapGatheringClientInitials.LoadOrStore(from, testGci) 108 | if !existing { 109 | // if we stored the testGci, we need to delete it after the timeout 110 | funcExpiringAfter := func(d time.Duration) { 111 | <-time.After(d) 112 | qfp.mapGatheringClientInitials.Delete(from) 113 | } 114 | 115 | if qfp.timeout == time.Duration(0) { 116 | go funcExpiringAfter(DEFAULT_QUICFINGERPRINT_EXPIRY) 117 | } else { 118 | go funcExpiringAfter(qfp.timeout) 119 | } 120 | } 121 | 122 | gci, ok := chosenGci.(*GatheredClientInitials) 123 | if !ok { 124 | return errors.New("GatheredClientInitials loaded from sync.Map failed type assertion") 125 | } 126 | 127 | return gci.AddPacket(ci) 128 | } 129 | 130 | // HandleUDPConn handles a QUIC connection over UDP. 131 | func (qfp *QUICFingerprinter) HandleUDPConn(pc net.PacketConn) error { 132 | var buf [2048]byte 133 | for { 134 | if qfp.closed.Load() { 135 | return errors.New("QUICFingerprinter closed") 136 | } 137 | 138 | n, addr, err := pc.ReadFrom(buf[:]) 139 | if err != nil { 140 | if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed) { 141 | return err 142 | } 143 | continue // ignore errors unless connection is closed 144 | } 145 | 146 | qfp.HandlePacket(addr.String(), buf[:n]) 147 | } 148 | } 149 | 150 | // HandleIPConn handles a QUIC connection over IP. 151 | func (qfp *QUICFingerprinter) HandleIPConn(ipc *net.IPConn) error { 152 | var buf [2048]byte 153 | for { 154 | if qfp.closed.Load() { 155 | return errors.New("QUICFingerprinter closed") 156 | } 157 | 158 | n, ipAddr, err := ipc.ReadFromIP(buf[:]) 159 | if err != nil { 160 | if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed) { 161 | return err 162 | } 163 | continue // ignore errors unless connection is closed 164 | } 165 | 166 | udpPkt, err := utils.ParseUDPPacket(buf[:n]) 167 | if err != nil { 168 | continue 169 | } 170 | if udpPkt.DstPort != 443 { 171 | continue 172 | } 173 | udpAddr := &net.UDPAddr{IP: ipAddr.IP, Port: int(udpPkt.SrcPort)} 174 | 175 | qfp.HandlePacket(udpAddr.String(), udpPkt.Payload) 176 | } 177 | } 178 | 179 | // Peek looks up a QUICFingerprint for a given key. 180 | func (qfp *QUICFingerprinter) Peek(from string) *QUICFingerprint { 181 | gci, ok := qfp.mapGatheringClientInitials.Load(from) 182 | if !ok { 183 | return nil 184 | } 185 | 186 | gatheredCI, ok := gci.(*GatheredClientInitials) 187 | if !ok { 188 | return nil 189 | } 190 | 191 | if !gatheredCI.Completed() { 192 | return nil // gathering incomplete 193 | } 194 | 195 | qf, err := GenerateQUICFingerprint(gatheredCI) 196 | if err != nil { 197 | return nil 198 | } 199 | 200 | return qf 201 | } 202 | 203 | // PeekAwait looks up a QUICFingerprint for a given key. 204 | // It will wait for the gathering to complete if the key exists but the 205 | // gathering is not yet complete, e.g., when CRYPTO frames spread across 206 | // multiple initial packets and some but not all of them are received. 207 | func (qfp *QUICFingerprinter) PeekAwait(from string) (*QUICFingerprint, error) { 208 | gci, ok := qfp.mapGatheringClientInitials.Load(from) 209 | if !ok { 210 | return nil, errors.New("GatheredClientInitials not found for the given key") 211 | } 212 | 213 | gatheredCI, ok := gci.(*GatheredClientInitials) 214 | if !ok { 215 | return nil, errors.New("GatheredClientInitials loaded from sync.Map failed type assertion") 216 | } 217 | 218 | qf, err := GenerateQUICFingerprint(gatheredCI) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | return qf, nil 224 | } 225 | 226 | // Pop looks up a QUICFingerprint for a given key and deletes it from 227 | // the fingerprinter if found. 228 | func (qfp *QUICFingerprinter) Pop(from string) *QUICFingerprint { 229 | gci, ok := qfp.mapGatheringClientInitials.LoadAndDelete(from) 230 | if !ok { 231 | return nil 232 | } 233 | 234 | gatheredCI, ok := gci.(*GatheredClientInitials) 235 | if !ok { 236 | return nil 237 | } 238 | 239 | if !gatheredCI.Completed() { 240 | return nil // gathering incomplete 241 | } 242 | 243 | qf, err := GenerateQUICFingerprint(gatheredCI) 244 | if err != nil { 245 | return nil 246 | } 247 | 248 | return qf 249 | } 250 | 251 | // PopAwait looks up a QUICFingerprint for a given key and deletes it from 252 | // the fingerprinter if found. 253 | // It will wait for the gathering to complete if the key exists but the 254 | // gathering is not yet complete, e.g., when CRYPTO frames spread across 255 | // multiple initial packets and some but not all of them are received. 256 | func (qfp *QUICFingerprinter) PopAwait(from string) (*QUICFingerprint, error) { 257 | gci, ok := qfp.mapGatheringClientInitials.LoadAndDelete(from) 258 | if !ok { 259 | return nil, errors.New("GatheredClientInitials not found for the given key") 260 | } 261 | 262 | gatheredCI, ok := gci.(*GatheredClientInitials) 263 | if !ok { 264 | return nil, errors.New("GatheredClientInitials loaded from sync.Map failed type assertion") 265 | } 266 | 267 | qf, err := GenerateQUICFingerprint(gatheredCI) 268 | if err != nil { 269 | return nil, err 270 | } 271 | 272 | return qf, nil 273 | } 274 | 275 | // Close closes the QUICFingerprinter. 276 | func (qfp *QUICFingerprinter) Close() { 277 | qfp.closed.Store(true) 278 | } 279 | -------------------------------------------------------------------------------- /quic_frame.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | 8 | "github.com/gaukas/clienthellod/internal/utils" 9 | ) 10 | 11 | const ( 12 | QUICFrame_PADDING uint64 = 0 // 0 13 | QUICFrame_PING uint64 = 1 // 1 14 | QUICFrame_CRYPTO uint64 = 6 // 6 15 | ) 16 | 17 | // QUICFrame is the interface that wraps the basic methods of a QUIC frame. 18 | type QUICFrame interface { 19 | // FrameType returns the type of the frame. 20 | FrameType() uint64 21 | 22 | // ReadReader takes a Reader and reads the rest of the frame from it, 23 | // starting from the first byte after the frame type. 24 | // 25 | // The returned io.Reader contains the rest of the frame, it could be 26 | // the input Reader itself (if no extra bytes are read) or a rewinded 27 | // Reader (if extra bytes are read and rewinding is needed). 28 | ReadReader(io.Reader) (io.Reader, error) 29 | } 30 | 31 | // ReadAllFrames reads all QUIC frames from the input reader. 32 | func ReadAllFrames(r io.Reader) ([]QUICFrame, error) { 33 | var frames []QUICFrame = make([]QUICFrame, 0) 34 | 35 | for { 36 | // QUICFrame Type 37 | frameType, _, err := ReadNextVLI(r) 38 | if err != nil { 39 | if err == io.EOF { 40 | return frames, nil 41 | } 42 | return nil, err 43 | } 44 | 45 | // QUICFrame 46 | var frame QUICFrame 47 | switch frameType { 48 | case QUICFrame_PADDING: 49 | frame = &PADDING{} 50 | case QUICFrame_PING: 51 | frame = &PING{} 52 | case QUICFrame_CRYPTO: 53 | frame = &CRYPTO{} 54 | default: 55 | return nil, fmt.Errorf("unknown frame type: 0x%.2x", frameType) 56 | } 57 | 58 | // Read the rest of the frame 59 | r, err = frame.ReadReader(r) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | // Append the frame 65 | frames = append(frames, frame) 66 | } 67 | } 68 | 69 | // ReassembleCRYPTOFrames reassembles CRYPTO frames into a single byte slice that 70 | // consists of the entire CRYPTO data. 71 | func ReassembleCRYPTOFrames(frames []QUICFrame) ([]byte, error) { 72 | var cryptoFrames []QUICFrame = make([]QUICFrame, 0) 73 | 74 | // Collect all CRYPTO frames 75 | for _, frame := range frames { 76 | if frame.FrameType() == QUICFrame_CRYPTO { 77 | cryptoFrames = append(cryptoFrames, frame) 78 | } 79 | } 80 | 81 | if len(cryptoFrames) == 0 { 82 | return nil, nil // no CRYPTO frames is not an error 83 | } 84 | 85 | // Sort CRYPTO frames by offset 86 | sort.Slice(cryptoFrames, func(i, j int) bool { 87 | return cryptoFrames[i].(*CRYPTO).Offset < cryptoFrames[j].(*CRYPTO).Offset 88 | }) 89 | 90 | // Reassemble CRYPTO frames 91 | var reassembled []byte = make([]byte, 0) 92 | for _, frame := range cryptoFrames { 93 | if uint64(len(reassembled)) == frame.(*CRYPTO).Offset { 94 | reassembled = append(reassembled, frame.(*CRYPTO).data...) 95 | } else { 96 | return nil, fmt.Errorf("failed to reassemble CRYPTO frames") 97 | } 98 | } 99 | 100 | return reassembled, nil 101 | } 102 | 103 | // QUICFrames is a slice of QUICFrame. 104 | type QUICFrames []QUICFrame 105 | 106 | // FrameTypes returns the frame types of all QUIC frames. 107 | func (qfs QUICFrames) FrameTypes() []uint64 { 108 | var frameTypes []uint64 = make([]uint64, 0) 109 | 110 | for _, f := range qfs { 111 | frameTypes = append(frameTypes, f.FrameType()) 112 | } 113 | 114 | return frameTypes 115 | } 116 | 117 | // FrameTypesUint8 returns the frame types of all QUIC frames as uint8. 118 | func (qfs QUICFrames) FrameTypesUint8() []uint8 { 119 | var frameTypesUint8 []uint8 = make([]uint8, 0) 120 | 121 | for _, f := range qfs { 122 | frameTypesUint8 = append(frameTypesUint8, uint8(f.FrameType()&0xFF)) 123 | } 124 | 125 | return frameTypesUint8 126 | } 127 | 128 | // PADDING frame 129 | type PADDING struct { 130 | Length uint64 `json:"length,omitempty"` // count 0x00 bytes until not 0x00 131 | } 132 | 133 | // FrameType implements QUICFrame interface. 134 | func (*PADDING) FrameType() uint64 { 135 | return QUICFrame_PADDING 136 | } 137 | 138 | // ReadFrom implements QUICFrame interface. It keeps reading until it finds a 139 | // non-zero byte, then the non-zero byte is rewinded back to the reader and 140 | // the reader is returned. 141 | func (f *PADDING) ReadReader(r io.Reader) (rr io.Reader, err error) { 142 | f.Length = 1 // starting from 1, since type is already read 143 | 144 | var b []byte = make([]byte, 1) 145 | for { 146 | _, err = r.Read(b) 147 | if err != nil { 148 | if err == io.EOF { 149 | return r, nil // EOF is not an error, it just means all frames are read 150 | } 151 | return r, err 152 | } 153 | if b[0] != 0x00 { 154 | // rewind the reader 155 | rr = utils.RewindReader(r, b) 156 | return 157 | } 158 | f.Length++ 159 | } 160 | } 161 | 162 | // PING frame 163 | type PING struct{} 164 | 165 | // FrameType implements QUICFrame interface. 166 | func (*PING) FrameType() uint64 { 167 | return QUICFrame_PING 168 | } 169 | 170 | // ReadFrom implements QUICFrame interface. It does nothing and returns the 171 | // input reader. 172 | func (*PING) ReadReader(r io.Reader) (rr io.Reader, err error) { 173 | return r, nil 174 | } 175 | 176 | // CRYPTO frame 177 | type CRYPTO struct { 178 | Offset uint64 `json:"offset,omitempty"` // offset of crypto data, from VLI 179 | Length uint64 `json:"length,omitempty"` // length of crypto data, from VLI 180 | // DataIn []byte `json:"data,omitempty"` // TODO: input crypto data, used for unmarshal only 181 | data []byte 182 | } 183 | 184 | // FrameType implements QUICFrame interface. 185 | func (*CRYPTO) FrameType() uint64 { 186 | return QUICFrame_CRYPTO 187 | } 188 | 189 | // ReadFrom implements QUICFrame interface. It reads the offset, length and 190 | // crypto data from the input reader. 191 | func (f *CRYPTO) ReadReader(r io.Reader) (rr io.Reader, err error) { 192 | // Offset 193 | f.Offset, _, err = ReadNextVLI(r) 194 | if err != nil { 195 | return r, err 196 | } 197 | 198 | // Length 199 | f.Length, _, err = ReadNextVLI(r) 200 | if err != nil { 201 | return r, err 202 | } 203 | 204 | // Crypto Data 205 | f.data = make([]byte, f.Length) 206 | _, err = r.Read(f.data) 207 | return r, err 208 | } 209 | 210 | // Data returns a copy of the crypto data. 211 | func (f *CRYPTO) Data() []byte { 212 | return append([]byte{}, f.data...) 213 | } 214 | 215 | // This is an old name reserved for compatibility purpose, it is 216 | // equivalent to [QUICFrame]. 217 | // 218 | // Deprecated: use the new name [QUICFrame] instead. 219 | type Frame = QUICFrame 220 | 221 | // type guards: 222 | var ( 223 | _ QUICFrame = (*PADDING)(nil) 224 | _ QUICFrame = (*PING)(nil) 225 | _ QUICFrame = (*CRYPTO)(nil) 226 | ) 227 | -------------------------------------------------------------------------------- /quic_frame_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "bytes" 5 | "math/rand" 6 | "testing" 7 | 8 | . "github.com/gaukas/clienthellod" 9 | ) 10 | 11 | func TestPADDING(t *testing.T) { 12 | var randLen int = rand.Int() % 512 // skipcq: GSC-G404 13 | var rdBuf []byte = make([]byte, randLen+5) 14 | copy(rdBuf[randLen:], "hello") 15 | 16 | var padding PADDING 17 | if padding.FrameType() != QUICFrame_PADDING { 18 | t.Errorf("padding.FrameType() = %d, want %d", padding.FrameType(), QUICFrame_PADDING) 19 | } 20 | 21 | r, err := padding.ReadReader(bytes.NewReader(rdBuf)) 22 | if err != nil { 23 | t.Errorf("padding.ReadReader() error = %v", err) 24 | } 25 | 26 | if padding.Length != uint64(randLen)+1 { 27 | t.Errorf("padding.Length = %d, want %d", padding.Length, randLen+1) 28 | } 29 | 30 | // check what's left in the Reader 31 | buf := make([]byte, 10) 32 | n, err := r.Read(buf) 33 | if err != nil { 34 | t.Errorf("padding.ReadReader() error = %v", err) 35 | } 36 | 37 | if n != 5 || string(buf[:n]) != "hello" { 38 | t.Errorf("padding.ReadReader() = %d, %s, want 5, hello", n, string(buf[:n])) 39 | } 40 | } 41 | 42 | func TestCRYPTO(t *testing.T) { 43 | var cryptoRaw []byte = []byte{ 44 | /* 0x06, */ // Frame Type, to be read by ReadAllFrames() 45 | 0x40, 0x9e, 0x1a, 0x33, 0x00, 0x26, 0x00, 46 | 0x24, 0x00, 0x1d, 0x00, 0x20, 0xf8, 0x82, 0xf6, 47 | 0x48, 0x2b, 0x20, 0x0c, 0xa0, 0x60, 0x79, 0x1c, 48 | 0x45, 0xa5, 0xb8, 0x43, 0x58, 0x11, 49 | 'h', 'e', 'l', 'l', 'o', // extra bytes shouldn't be read 50 | } 51 | 52 | var crypto CRYPTO 53 | if crypto.FrameType() != QUICFrame_CRYPTO { 54 | t.Errorf("crypto.FrameType() = %d, want %d", crypto.FrameType(), QUICFrame_CRYPTO) 55 | } 56 | 57 | r, err := crypto.ReadReader(bytes.NewReader(cryptoRaw)) 58 | if err != nil { 59 | t.Errorf("crypto.ReadReader() error = %v", err) 60 | } 61 | 62 | if crypto.Offset != 158 { 63 | t.Errorf("crypto.Offset = %d, want %d", crypto.Offset, 158) 64 | } 65 | 66 | if crypto.Length != 26 { 67 | t.Errorf("crypto.Length = %d, want %d", crypto.Length, 26) 68 | } 69 | 70 | var cryptoDataTruth []byte = []byte{ 71 | 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 72 | 0x20, 0xf8, 0x82, 0xf6, 0x48, 0x2b, 0x20, 0x0c, 73 | 0xa0, 0x60, 0x79, 0x1c, 0x45, 0xa5, 0xb8, 0x43, 74 | 0x58, 0x11, 75 | } 76 | if !bytes.Equal(crypto.Data(), cryptoDataTruth) { 77 | t.Errorf("crypto.Data = %v, want %v", crypto.Data(), cryptoDataTruth) 78 | } 79 | 80 | // check what's left in the Reader 81 | buf := make([]byte, 10) 82 | n, err := r.Read(buf) 83 | if err != nil { 84 | t.Errorf("crypto.ReadReader() error = %v", err) 85 | } 86 | 87 | if n != 5 || string(buf[:n]) != "hello" { 88 | t.Errorf("crypto.ReadReader() = %d, %s, want 5, hello", n, string(buf[:n])) 89 | } 90 | } 91 | 92 | func TestReadAllFramesAndReassemble(t *testing.T) { 93 | frames, err := ReadAllFrames(bytes.NewReader(allFramesRaw)) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | if len(frames) != 13 { 98 | t.Fatalf("len(frames) = %d, want 13", len(frames)) 99 | } 100 | 101 | var frameTypesTruth []uint64 = []uint64{ 102 | 0x01, 0x00, 0x06, 0x00, 0x01, 0x06, 0x00, 0x06, 103 | 0x01, 0x01, 0x01, 0x01, 0x01, 104 | } 105 | 106 | var paddingLengthTruth []uint64 = []uint64{ 107 | 627, 135, 119, 108 | } 109 | 110 | var cryptoOffsetTruth []uint64 = []uint64{ 111 | 184, 158, 0, 112 | } 113 | var cryptoLengthTruth []uint64 = []uint64{ 114 | 110, 26, 158, 115 | } 116 | 117 | var idxPadding int 118 | var idxCrypto int 119 | for i, frame := range frames { 120 | if frame.FrameType() != frameTypesTruth[i] { 121 | t.Fatalf("frame#%d type mismatch: %d != %d", i, frame.FrameType(), frameTypesTruth[i]) 122 | } 123 | 124 | if frame.FrameType() == QUICFrame_PADDING { 125 | if frame.(*PADDING).Length != paddingLengthTruth[idxPadding] { 126 | t.Fatalf("frame#%d padding length mismatch: %d != %d", i, frame.(*PADDING).Length, paddingLengthTruth[idxPadding]) 127 | } 128 | idxPadding++ 129 | } 130 | 131 | if frame.FrameType() == QUICFrame_CRYPTO { 132 | if frame.(*CRYPTO).Offset != cryptoOffsetTruth[idxCrypto] { 133 | t.Fatalf("frame#%d crypto offset mismatch: %d != %d", i, frame.(*CRYPTO).Offset, cryptoOffsetTruth[idxCrypto]) 134 | } 135 | if frame.(*CRYPTO).Length != cryptoLengthTruth[idxCrypto] { 136 | t.Fatalf("frame#%d crypto length mismatch: %d != %d", i, frame.(*CRYPTO).Length, cryptoLengthTruth[idxCrypto]) 137 | } 138 | idxCrypto++ 139 | } 140 | } 141 | 142 | crypto, err := ReassembleCRYPTOFrames(frames) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | if !bytes.Equal(crypto, reassembledCryptoTruth) { 148 | t.Fatalf("crypto mismatch: expected %v, got %v", reassembledCryptoTruth, crypto) 149 | } 150 | } 151 | 152 | var ( 153 | reassembledCryptoTruth = []byte{ 154 | 0x01, 0x00, 0x01, 0x22, 0x03, 0x03, 0xe3, 0x1b, 155 | 0x6b, 0x88, 0xce, 0x0e, 0xff, 0x48, 0x08, 0x52, 156 | 0xa6, 0x21, 0x03, 0x90, 0x84, 0x92, 0x5d, 0xf6, 157 | 0x8a, 0xcb, 0xad, 0x66, 0xdb, 0x9f, 0x3c, 0x94, 158 | 0x3f, 0x0e, 0xba, 0xf2, 0x4a, 0x3c, 0x00, 0x00, 159 | 0x06, 0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0x01, 160 | 0x00, 0x00, 0xf3, 0x44, 0x69, 0x00, 0x05, 0x00, 161 | 0x03, 0x02, 0x68, 0x33, 0x00, 0x39, 0x00, 0x5d, 162 | 0x09, 0x02, 0x40, 0x67, 0x0f, 0x00, 0x01, 0x04, 163 | 0x80, 0x00, 0x75, 0x30, 0x05, 0x04, 0x80, 0x60, 164 | 0x00, 0x00, 0xe2, 0xd0, 0x11, 0x38, 0x87, 0x0c, 165 | 0x6f, 0x9f, 0x01, 0x96, 0x07, 0x04, 0x80, 0x60, 166 | 0x00, 0x00, 0x71, 0x28, 0x04, 0x52, 0x56, 0x43, 167 | 0x4d, 0x03, 0x02, 0x45, 0xc0, 0x20, 0x04, 0x80, 168 | 0x01, 0x00, 0x00, 0x08, 0x02, 0x40, 0x64, 0x80, 169 | 0xff, 0x73, 0xdb, 0x0c, 0x00, 0x00, 0x00, 0x01, 170 | 0xba, 0xca, 0x5a, 0x5a, 0x00, 0x00, 0x00, 0x01, 171 | 0x80, 0x00, 0x47, 0x52, 0x04, 0x00, 0x00, 0x00, 172 | 0x01, 0x06, 0x04, 0x80, 0x60, 0x00, 0x00, 0x04, 173 | 0x04, 0x80, 0xf0, 0x00, 0x00, 0x00, 0x33, 0x00, 174 | 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0xf8, 175 | 0x82, 0xf6, 0x48, 0x2b, 0x20, 0x0c, 0xa0, 0x60, 176 | 0x79, 0x1c, 0x45, 0xa5, 0xb8, 0x43, 0x58, 0x11, 177 | 0x26, 0x64, 0xec, 0x4f, 0xf7, 0xd6, 0xea, 0x10, 178 | 0x30, 0xf6, 0x9f, 0x36, 0x80, 0x49, 0x43, 0x00, 179 | 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 180 | 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 181 | 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 182 | 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x33, 183 | 0x00, 0x0a, 0x00, 0x08, 0x00, 0x06, 0x00, 0x1d, 184 | 0x00, 0x17, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1a, 185 | 0x00, 0x18, 0x00, 0x00, 0x15, 0x71, 0x2e, 0x63, 186 | 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x68, 0x65, 0x6c, 187 | 0x6c, 0x6f, 0x2e, 0x67, 0x61, 0x75, 0x6b, 0x2e, 188 | 0x61, 0x73, 0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 189 | 0x04, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 190 | 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, 191 | } 192 | 193 | allFramesRaw = []byte{ 194 | // PING 195 | 0x01, 196 | // PADDING 197 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 198 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 199 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 200 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 201 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 202 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 203 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 204 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 205 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 206 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 207 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 208 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 209 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 210 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 211 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 212 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 213 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 214 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 215 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 216 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 217 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 218 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 219 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 220 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 221 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 222 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 223 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 224 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 225 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 226 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 227 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 228 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 229 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 230 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 231 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 232 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 233 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 234 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 235 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 236 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 237 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 238 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 239 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 240 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 241 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 242 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 243 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 244 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 245 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 246 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 247 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 248 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 249 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 250 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 251 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 252 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 253 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 254 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 255 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 256 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 257 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 258 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 259 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 260 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 261 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 262 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 263 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 264 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 265 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 266 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 267 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 268 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 269 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 270 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 271 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 272 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 273 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 274 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 275 | 0x00, 0x00, 0x00, 276 | // CRYPTO 277 | 0x06, 0x40, 0xb8, 0x40, 0x6e, 0x26, 0x64, 0xec, 278 | 0x4f, 0xf7, 0xd6, 0xea, 0x10, 0x30, 0xf6, 0x9f, 279 | 0x36, 0x80, 0x49, 0x43, 0x00, 0x0d, 0x00, 0x14, 280 | 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 281 | 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 282 | 0x06, 0x01, 0x02, 0x01, 0x00, 0x10, 0x00, 0x05, 283 | 0x00, 0x03, 0x02, 0x68, 0x33, 0x00, 0x0a, 0x00, 284 | 0x08, 0x00, 0x06, 0x00, 0x1d, 0x00, 0x17, 0x00, 285 | 0x18, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x18, 0x00, 286 | 0x00, 0x15, 0x71, 0x2e, 0x63, 0x6c, 0x69, 0x65, 287 | 0x6e, 0x74, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 288 | 0x67, 0x61, 0x75, 0x6b, 0x2e, 0x61, 0x73, 0x00, 289 | 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04, 0x00, 0x2d, 290 | 0x00, 0x02, 0x01, 0x01, 0x00, 0x1b, 0x00, 0x03, 291 | 0x02, 0x00, 0x02, 292 | // PADDING 293 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 294 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 295 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 296 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 297 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 298 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 299 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 300 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 301 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 302 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 303 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 304 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 305 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 306 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 307 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 308 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 309 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 310 | // PING 311 | 0x01, 312 | // CRYPTO 313 | 0x06, 0x40, 0x9e, 0x1a, 0x33, 0x00, 0x26, 0x00, 314 | 0x24, 0x00, 0x1d, 0x00, 0x20, 0xf8, 0x82, 0xf6, 315 | 0x48, 0x2b, 0x20, 0x0c, 0xa0, 0x60, 0x79, 0x1c, 316 | 0x45, 0xa5, 0xb8, 0x43, 0x58, 0x11, 317 | // PADDING 318 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 319 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 320 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 321 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 322 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 323 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 324 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 325 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 326 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 327 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 328 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 329 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 330 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 331 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 332 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 333 | // CRYPTO 334 | 0x06, 0x00, 0x40, 0x9e, 0x01, 0x00, 0x01, 0x22, 335 | 0x03, 0x03, 0xe3, 0x1b, 0x6b, 0x88, 0xce, 0x0e, 336 | 0xff, 0x48, 0x08, 0x52, 0xa6, 0x21, 0x03, 0x90, 337 | 0x84, 0x92, 0x5d, 0xf6, 0x8a, 0xcb, 0xad, 0x66, 338 | 0xdb, 0x9f, 0x3c, 0x94, 0x3f, 0x0e, 0xba, 0xf2, 339 | 0x4a, 0x3c, 0x00, 0x00, 0x06, 0x13, 0x01, 0x13, 340 | 0x02, 0x13, 0x03, 0x01, 0x00, 0x00, 0xf3, 0x44, 341 | 0x69, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x33, 342 | 0x00, 0x39, 0x00, 0x5d, 0x09, 0x02, 0x40, 0x67, 343 | 0x0f, 0x00, 0x01, 0x04, 0x80, 0x00, 0x75, 0x30, 344 | 0x05, 0x04, 0x80, 0x60, 0x00, 0x00, 0xe2, 0xd0, 345 | 0x11, 0x38, 0x87, 0x0c, 0x6f, 0x9f, 0x01, 0x96, 346 | 0x07, 0x04, 0x80, 0x60, 0x00, 0x00, 0x71, 0x28, 347 | 0x04, 0x52, 0x56, 0x43, 0x4d, 0x03, 0x02, 0x45, 348 | 0xc0, 0x20, 0x04, 0x80, 0x01, 0x00, 0x00, 0x08, 349 | 0x02, 0x40, 0x64, 0x80, 0xff, 0x73, 0xdb, 0x0c, 350 | 0x00, 0x00, 0x00, 0x01, 0xba, 0xca, 0x5a, 0x5a, 351 | 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x47, 0x52, 352 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x06, 0x04, 0x80, 353 | 0x60, 0x00, 0x00, 0x04, 0x04, 0x80, 0xf0, 0x00, 354 | 0x00, 0x00, 355 | // PING * 5 356 | 0x01, 0x01, 0x01, 0x01, 0x01, 357 | } 358 | ) 359 | 360 | var ( 361 | quicFramesTruth_Chrome125_PKN1 = QUICFrames{ 362 | &CRYPTO{Offset: 0, Length: 1211}, 363 | } 364 | quicFramesTruth_Chrome125_PKN2 = QUICFrames{ 365 | &CRYPTO{Offset: 1211, Length: 8}, 366 | &PADDING{Length: 80}, 367 | &CRYPTO{Offset: 1720, Length: 35}, 368 | &CRYPTO{Offset: 1677, Length: 43}, 369 | &PADDING{Length: 2}, 370 | &PING{}, 371 | &PADDING{Length: 235}, 372 | &CRYPTO{Offset: 1755, Length: 21}, 373 | &CRYPTO{Offset: 1219, Length: 238}, 374 | &PADDING{Length: 305}, 375 | &CRYPTO{Offset: 1457, Length: 220}, 376 | &PING{}, 377 | } 378 | quicFramesTruth_Firefox126 = QUICFrames{ 379 | &CRYPTO{Offset: 0, Length: 633}, 380 | } 381 | quicFramesTruth_Firefox126_0_RTT = QUICFrames{ 382 | &CRYPTO{Offset: 0, Length: 594}, 383 | } 384 | ) 385 | 386 | func testQUICFramesEqualsTruth(t *testing.T, frames, truths QUICFrames) { 387 | if len(frames) != len(truths) { 388 | t.Fatalf("Expected %d frames, got %d", len(truths), len(frames)) 389 | } 390 | 391 | for i, truth := range truths { 392 | switch truth := truth.(type) { 393 | case *CRYPTO: 394 | if frame, ok := frames[i].(*CRYPTO); ok { 395 | if frame.Offset != truth.Offset || frame.Length != truth.Length { 396 | t.Errorf("Frame %d: expected %+v, got %+v", i, truth, frame) 397 | } 398 | } else { 399 | t.Errorf("Frame %d: expected CRYPTO, got %T", i, frames[i]) 400 | } 401 | case *PADDING: 402 | if frame, ok := frames[i].(*PADDING); ok { 403 | if frame.Length != truth.Length { 404 | t.Errorf("Frame %d: expected %+v, got %+v", i, truth, frame) 405 | } 406 | } else { 407 | t.Errorf("Frame %d: expected PADDING, got %T", i, frames[i]) 408 | } 409 | case *PING: 410 | if _, ok := frames[i].(*PING); !ok { 411 | t.Errorf("Frame %d: expected PING, got %T", i, frames[i]) 412 | } 413 | default: 414 | t.Fatalf("Unknown frame type: %T", truth) 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /quic_header.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "github.com/gaukas/clienthellod/internal/utils" 5 | ) 6 | 7 | const ( 8 | TOKEN_ABSENT uint32 = 0x00000000 9 | TOKEN_PRESENT uint32 = 0x00000001 10 | ) 11 | 12 | // QUICHeader includes header fields of a QUIC packet and the following 13 | // frames. It is used to calculate the fingerprint of a QUIC Header. 14 | type QUICHeader struct { 15 | Version utils.Uint8Arr `json:"version,omitempty"` // 4-byte version 16 | DCIDLength uint32 `json:"dest_conn_id_len,omitempty"` 17 | SCIDLength uint32 `json:"source_conn_id_len,omitempty"` 18 | PacketNumber utils.Uint8Arr `json:"packet_number,omitempty"` // VLI 19 | initialPacketNumberLength uint32 20 | initialPacketNumber uint64 21 | 22 | HasToken bool `json:"token,omitempty"` 23 | } 24 | -------------------------------------------------------------------------------- /quic_header_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | . "github.com/gaukas/clienthellod" 8 | ) 9 | 10 | // TODO: update test data to the latest and move test data to separate files (in binary format) 11 | 12 | var ( 13 | quicHeaderTruth_Chrome125_PKN1 = &QUICHeader{ 14 | Version: []byte{0x00, 0x00, 0x00, 0x01}, 15 | DCIDLength: 8, 16 | SCIDLength: 0, 17 | PacketNumber: []byte{0x01}, 18 | 19 | HasToken: false, 20 | } 21 | quicHeaderTruth_Chrome125_PKN2 = &QUICHeader{ 22 | Version: []byte{0x00, 0x00, 0x00, 0x01}, 23 | DCIDLength: 8, 24 | SCIDLength: 0, 25 | PacketNumber: []byte{0x02}, 26 | 27 | HasToken: false, 28 | } 29 | 30 | quicHeaderTruth_Firefox126 = &QUICHeader{ 31 | Version: []byte{0x00, 0x00, 0x00, 0x01}, 32 | DCIDLength: 8, 33 | SCIDLength: 3, 34 | PacketNumber: []byte{0x00}, 35 | 36 | HasToken: false, 37 | } 38 | quicHeaderTruth_Firefox126_0_RTT = &QUICHeader{ 39 | Version: []byte{0x00, 0x00, 0x00, 0x01}, 40 | DCIDLength: 9, 41 | SCIDLength: 3, 42 | PacketNumber: []byte{0x00}, 43 | 44 | HasToken: true, 45 | } 46 | ) 47 | 48 | func testQUICHeaderEqualsTruth(t *testing.T, header, truth *QUICHeader) { 49 | if !bytes.Equal(header.Version, truth.Version) { 50 | t.Errorf("header.Version = %x, want %x", header.Version, truth.Version) 51 | } 52 | 53 | if header.DCIDLength != truth.DCIDLength { 54 | t.Errorf("header.DCIDLength = %d, want %d", header.DCIDLength, truth.DCIDLength) 55 | } 56 | 57 | if header.SCIDLength != truth.SCIDLength { 58 | t.Errorf("header.SCIDLength = %d, want %d", header.SCIDLength, truth.SCIDLength) 59 | } 60 | 61 | if !bytes.Equal(header.PacketNumber, truth.PacketNumber) { 62 | t.Errorf("header.PacketNumber = %x, want %x", header.PacketNumber, truth.PacketNumber) 63 | } 64 | 65 | if header.HasToken != truth.HasToken { 66 | t.Errorf("header.HasToken = %t, want %t", header.HasToken, truth.HasToken) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /quic_transport_parameters.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "bytes" // skipcq: GSC-G505 5 | "errors" 6 | "fmt" 7 | "sort" 8 | 9 | "github.com/gaukas/clienthellod/internal/utils" 10 | "github.com/refraction-networking/utls/dicttls" 11 | ) 12 | 13 | const ( 14 | QTP_GREASE = 27 15 | 16 | UNSET_VLI_BITS = true // if false, unsetVLIBits() will be nop 17 | ) 18 | 19 | // QUICTransportParameters is a struct to hold the parsed QUIC transport parameters 20 | // as a combination. 21 | type QUICTransportParameters struct { 22 | MaxIdleTimeout utils.Uint8Arr `json:"max_idle_timeout,omitempty"` 23 | MaxUDPPayloadSize utils.Uint8Arr `json:"max_udp_payload_size,omitempty"` 24 | InitialMaxData utils.Uint8Arr `json:"initial_max_data,omitempty"` 25 | InitialMaxStreamDataBidiLocal utils.Uint8Arr `json:"initial_max_stream_data_bidi_local,omitempty"` 26 | InitialMaxStreamDataBidiRemote utils.Uint8Arr `json:"initial_max_stream_data_bidi_remote,omitempty"` 27 | InitialMaxStreamDataUni utils.Uint8Arr `json:"initial_max_stream_data_uni,omitempty"` 28 | InitialMaxStreamsBidi utils.Uint8Arr `json:"initial_max_streams_bidi,omitempty"` 29 | InitialMaxStreamsUni utils.Uint8Arr `json:"initial_max_streams_uni,omitempty"` 30 | AckDelayExponent utils.Uint8Arr `json:"ack_delay_exponent,omitempty"` 31 | MaxAckDelay utils.Uint8Arr `json:"max_ack_delay,omitempty"` 32 | 33 | ActiveConnectionIDLimit utils.Uint8Arr `json:"active_connection_id_limit,omitempty"` 34 | QTPIDs []uint64 `json:"tpids,omitempty"` // sorted 35 | 36 | HexID string `json:"hex_id,omitempty"` 37 | NumID uint64 `json:"num_id,omitempty"` 38 | 39 | parseError error 40 | } 41 | 42 | // ParseQUICTransportParameters parses the transport parameters from the extension data of 43 | // TLS Extension "QUIC Transport Parameters" (57) 44 | // 45 | // If any error occurs, the returned struct will have parseError set to the error. 46 | func ParseQUICTransportParameters(extData []byte) *QUICTransportParameters { // skipcq: GO-R1005 47 | qtp := &QUICTransportParameters{ 48 | parseError: errors.New("unknown error"), 49 | } 50 | 51 | r := bytes.NewReader(extData) 52 | var paramType uint64 53 | var paramValLen uint64 54 | var paramData []byte 55 | var n int 56 | for r.Len() > 0 { 57 | paramType, _, qtp.parseError = ReadNextVLI(r) 58 | if qtp.parseError != nil { 59 | qtp.parseError = fmt.Errorf("failed to read transport parameter type: %w", qtp.parseError) 60 | return qtp 61 | } 62 | paramValLen, _, qtp.parseError = ReadNextVLI(r) 63 | if qtp.parseError != nil { 64 | qtp.parseError = fmt.Errorf("failed to read transport parameter value length: %w", qtp.parseError) 65 | return qtp 66 | } 67 | 68 | if IsGREASETransportParameter(paramType) { 69 | qtp.QTPIDs = append(qtp.QTPIDs, QTP_GREASE) // replace with placeholder 70 | } else { 71 | qtp.QTPIDs = append(qtp.QTPIDs, paramType) 72 | } 73 | 74 | if paramValLen == 0 { 75 | continue // skip empty transport parameter, no need to try to read 76 | } 77 | 78 | paramData = make([]byte, paramValLen) 79 | n, qtp.parseError = r.Read(paramData) 80 | if qtp.parseError != nil { 81 | qtp.parseError = fmt.Errorf("failed to read transport parameter value: %w", qtp.parseError) 82 | return qtp 83 | } 84 | if uint64(n) != paramValLen { 85 | qtp.parseError = errors.New("corrupted transport parameter") 86 | return qtp 87 | } 88 | 89 | switch paramType { 90 | case dicttls.QUICTransportParameter_max_idle_timeout: 91 | // qtp.MaxIdleTimeoutLength = uint32(paramValLen) 92 | qtp.MaxIdleTimeout = paramData 93 | unsetVLIBits(qtp.MaxIdleTimeout) // toggle the UNSET_VLI_BITS flag to control behavior 94 | case dicttls.QUICTransportParameter_max_udp_payload_size: 95 | // qtp.MaxUDPPayloadSizeLength = uint32(paramValLen) 96 | qtp.MaxUDPPayloadSize = paramData 97 | unsetVLIBits(qtp.MaxUDPPayloadSize) 98 | case dicttls.QUICTransportParameter_initial_max_data: 99 | // qtp.InitialMaxDataLength = uint32(paramValLen) 100 | qtp.InitialMaxData = paramData 101 | unsetVLIBits(qtp.InitialMaxData) 102 | case dicttls.QUICTransportParameter_initial_max_stream_data_bidi_local: 103 | // qtp.InitialMaxStreamDataBidiLocalLength = uint32(paramValLen) 104 | qtp.InitialMaxStreamDataBidiLocal = paramData 105 | unsetVLIBits(qtp.InitialMaxStreamDataBidiLocal) 106 | case dicttls.QUICTransportParameter_initial_max_stream_data_bidi_remote: 107 | // qtp.InitialMaxStreamDataBidiRemoteLength = uint32(paramValLen) 108 | qtp.InitialMaxStreamDataBidiRemote = paramData 109 | unsetVLIBits(qtp.InitialMaxStreamDataBidiRemote) 110 | case dicttls.QUICTransportParameter_initial_max_stream_data_uni: 111 | // qtp.InitialMaxStreamDataUniLength = uint32(paramValLen) 112 | qtp.InitialMaxStreamDataUni = paramData 113 | unsetVLIBits(qtp.InitialMaxStreamDataUni) 114 | case dicttls.QUICTransportParameter_initial_max_streams_bidi: 115 | // qtp.InitialMaxStreamsBidiLength = uint32(paramValLen) 116 | qtp.InitialMaxStreamsBidi = paramData 117 | unsetVLIBits(qtp.InitialMaxStreamsBidi) 118 | case dicttls.QUICTransportParameter_initial_max_streams_uni: 119 | // qtp.InitialMaxStreamsUniLength = uint32(paramValLen) 120 | qtp.InitialMaxStreamsUni = paramData 121 | unsetVLIBits(qtp.InitialMaxStreamsUni) 122 | case dicttls.QUICTransportParameter_ack_delay_exponent: 123 | // qtp.AckDelayExponentLength = uint32(paramValLen) 124 | qtp.AckDelayExponent = paramData 125 | unsetVLIBits(qtp.AckDelayExponent) 126 | case dicttls.QUICTransportParameter_max_ack_delay: 127 | // qtp.MaxAckDelayLength = uint32(paramValLen) 128 | qtp.MaxAckDelay = paramData 129 | unsetVLIBits(qtp.MaxAckDelay) 130 | case dicttls.QUICTransportParameter_active_connection_id_limit: 131 | // qtp.ActiveConnectionIDLimitLength = uint32(paramValLen) 132 | qtp.ActiveConnectionIDLimit = paramData 133 | unsetVLIBits(qtp.ActiveConnectionIDLimit) 134 | } 135 | 136 | // if IsGREASETransportParameter(paramType) { 137 | // qtp.QTPIDs = append(qtp.QTPIDs, QTP_GREASE) // replace with placeholder 138 | // } else { 139 | // qtp.QTPIDs = append(qtp.QTPIDs, paramType) 140 | // } 141 | } 142 | 143 | // sort QTPIDs 144 | sort.Slice(qtp.QTPIDs, func(i, j int) bool { 145 | return qtp.QTPIDs[i] < qtp.QTPIDs[j] 146 | }) 147 | 148 | qtp.parseError = nil 149 | qtp.NumID = qtp.calcNumericID() 150 | qtp.HexID = FingerprintID(qtp.NumID).AsHex() 151 | return qtp 152 | } 153 | 154 | // ParseError returns the error that occurred during parsing, if any. 155 | func (qtp *QUICTransportParameters) ParseError() error { 156 | return qtp.parseError 157 | } 158 | -------------------------------------------------------------------------------- /quic_transport_parameters_test.go: -------------------------------------------------------------------------------- 1 | package clienthellod_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | . "github.com/gaukas/clienthellod" 8 | 9 | "github.com/refraction-networking/utls/dicttls" 10 | ) 11 | 12 | var ( 13 | rawQTPExtData_Chrome120 = []byte{ 14 | 0x09, 0x02, 0x40, 0x67, // initial_max_streams_uni 15 | 0x0f, 0x00, // initial_source_connection_id 16 | 0x01, 0x04, 0x80, 0x00, 0x75, 0x30, // max_idle_timeout 17 | 0x05, 0x04, 0x80, 0x60, 0x00, 0x00, // initial_max_stream_data_bidi_local 18 | 0xe2, 0xd0, 0x11, 0x38, 0x87, 0x0c, 0x6f, 0x9f, 0x01, 0x96, // GREASE 19 | 0x07, 0x04, 0x80, 0x60, 0x00, 0x00, // initial_max_stream_data_uni 20 | 0x71, 0x28, 0x04, 0x52, 0x56, 0x43, 0x4d, // google_connection_options 21 | 0x03, 0x02, 0x45, 0xc0, // max_udp_payload_size 22 | 0x20, 0x04, 0x80, 0x01, 0x00, 0x00, // max_datagram_frame_size 23 | 0x08, 0x02, 0x40, 0x64, // initial_max_streams_bidi 24 | 0x80, 0xff, 0x73, 0xdb, 0x0c, 0x00, 0x00, 0x00, 0x01, 0xba, 0xca, 0x5a, 0x5a, 0x00, 0x00, 0x00, 0x01, // version_information 25 | 0x80, 0x00, 0x47, 0x52, 0x04, 0x00, 0x00, 0x00, 0x01, // google_quic_version 26 | 0x06, 0x04, 0x80, 0x60, 0x00, 0x00, // initial_max_stream_data_bidi_remote 27 | 0x04, 0x04, 0x80, 0xf0, 0x00, 0x00, // initial_max_data 28 | } 29 | 30 | qtpTruth_Chrome120 *QUICTransportParameters = &QUICTransportParameters{ 31 | MaxIdleTimeout: []byte{0x00, 0x00, 0x75, 0x30}, 32 | MaxUDPPayloadSize: []byte{0x05, 0xc0}, 33 | InitialMaxData: []byte{0x00, 0xf0, 0x00, 0x00}, 34 | InitialMaxStreamDataBidiLocal: []byte{0x00, 0x60, 0x00, 0x00}, 35 | InitialMaxStreamDataBidiRemote: []byte{0x00, 0x60, 0x00, 0x00}, 36 | InitialMaxStreamDataUni: []byte{0x00, 0x60, 0x00, 0x00}, 37 | InitialMaxStreamsBidi: []byte{0x00, 0x64}, 38 | InitialMaxStreamsUni: []byte{0x00, 0x67}, 39 | // AckDelayExponent: []byte{}, // nil 40 | // MaxAckDelay: []byte{}, // nil 41 | // ActiveConnectionIDLimit: []byte{}, // nil 42 | QTPIDs: []uint64{ 43 | dicttls.QUICTransportParameter_max_idle_timeout, 44 | dicttls.QUICTransportParameter_max_udp_payload_size, 45 | dicttls.QUICTransportParameter_initial_max_data, 46 | dicttls.QUICTransportParameter_initial_max_stream_data_bidi_local, 47 | dicttls.QUICTransportParameter_initial_max_stream_data_bidi_remote, 48 | dicttls.QUICTransportParameter_initial_max_stream_data_uni, 49 | dicttls.QUICTransportParameter_initial_max_streams_bidi, 50 | dicttls.QUICTransportParameter_initial_max_streams_uni, 51 | dicttls.QUICTransportParameter_initial_source_connection_id, 52 | QTP_GREASE, 53 | dicttls.QUICTransportParameter_max_datagram_frame_size, 54 | dicttls.QUICTransportParameter_google_connection_options, 55 | dicttls.QUICTransportParameter_google_version, 56 | 0xff73db, // dicttls.QUICTransportParameter_version_information, 57 | }, 58 | 59 | HexID: "89bffb37428ff651", 60 | NumID: 9925928318506366545, 61 | } 62 | ) 63 | 64 | func TestParseQUICTransportParameters(t *testing.T) { 65 | t.Run("Google Chrome", parseQUICTransportParametersGoogleChrome) 66 | } 67 | 68 | func parseQUICTransportParametersGoogleChrome(t *testing.T) { 69 | qtp := ParseQUICTransportParameters(rawQTPExtData_Chrome120) 70 | if qtp == nil { 71 | t.Errorf("ParseQUICTransportParameters failed: got nil") 72 | return 73 | } 74 | 75 | if qtp.ParseError() != nil { 76 | t.Errorf("ParseQUICTransportParameters failed: %v", qtp.ParseError()) 77 | return 78 | } 79 | 80 | if !reflect.DeepEqual(qtp, qtpTruth_Chrome120) { 81 | t.Errorf("ParseQUICTransportParameters failed: expected %v, got %v", qtpTruth_Chrome120, qtp) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tls_fingerprint.go: -------------------------------------------------------------------------------- 1 | package clienthellod 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/gaukas/clienthellod/internal/utils" 12 | ) 13 | 14 | const DEFAULT_TLSFINGERPRINT_EXPIRY = 5 * time.Second 15 | 16 | // TLSFingerprinter can be used to fingerprint TLS connections. 17 | type TLSFingerprinter struct { 18 | mapClientHellos *sync.Map 19 | 20 | timeout time.Duration 21 | closed atomic.Bool 22 | } 23 | 24 | // NewTLSFingerprinter creates a new TLSFingerprinter. 25 | func NewTLSFingerprinter() *TLSFingerprinter { 26 | return &TLSFingerprinter{ 27 | mapClientHellos: new(sync.Map), 28 | closed: atomic.Bool{}, 29 | } 30 | } 31 | 32 | // NewTLSFingerprinterWithTimeout creates a new TLSFingerprinter with a timeout. 33 | func NewTLSFingerprinterWithTimeout(timeout time.Duration) *TLSFingerprinter { 34 | return &TLSFingerprinter{ 35 | mapClientHellos: new(sync.Map), 36 | timeout: timeout, 37 | closed: atomic.Bool{}, 38 | } 39 | } 40 | 41 | // SetTimeout sets the timeout for the TLSFingerprinter. 42 | func (tfp *TLSFingerprinter) SetTimeout(timeout time.Duration) { 43 | tfp.timeout = timeout 44 | } 45 | 46 | // HandleMessage handles a message. 47 | func (tfp *TLSFingerprinter) HandleMessage(from string, p []byte) error { 48 | if tfp.closed.Load() { 49 | return errors.New("TLSFingerprinter closed") 50 | } 51 | 52 | ch, err := UnmarshalClientHello(p) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | tfp.mapClientHellos.Store(from, ch) 58 | go func(timeoutOverride time.Duration, key string, oldCh *ClientHello) { 59 | if timeoutOverride == time.Duration(0) { 60 | <-time.After(DEFAULT_TLSFINGERPRINT_EXPIRY) 61 | } else { 62 | <-time.After(timeoutOverride) 63 | } 64 | // tfp.mapClientHellos.Delete(key) 65 | tfp.mapClientHellos.CompareAndDelete(key, oldCh) 66 | }(tfp.timeout, from, ch) 67 | 68 | return nil 69 | } 70 | 71 | // HandleTCPConn handles a TCP connection. 72 | func (tfp *TLSFingerprinter) HandleTCPConn(conn net.Conn) (rewindConn net.Conn, err error) { 73 | if tfp.closed.Load() { 74 | return nil, errors.New("TLSFingerprinter closed") 75 | } 76 | 77 | ch, err := ReadClientHello(conn) 78 | if err != nil { 79 | return nil, fmt.Errorf("failed to read ClientHello from connection: %w", err) 80 | } 81 | 82 | if err = ch.ParseClientHello(); err != nil { 83 | return nil, fmt.Errorf("failed to parse ClientHello: %w", err) 84 | } 85 | 86 | tfp.mapClientHellos.Store(conn.RemoteAddr().String(), ch) 87 | go func(timeoutOverride time.Duration, key string, oldCh *ClientHello) { 88 | if timeoutOverride == time.Duration(0) { 89 | <-time.After(DEFAULT_TLSFINGERPRINT_EXPIRY) 90 | } else { 91 | <-time.After(timeoutOverride) 92 | } 93 | // tfp.mapClientHellos.Delete(key) 94 | tfp.mapClientHellos.CompareAndDelete(key, oldCh) 95 | }(tfp.timeout, conn.RemoteAddr().String(), ch) 96 | 97 | return utils.RewindConn(conn, ch.Raw()) 98 | } 99 | 100 | // Peek looks up a ClientHello for a given key. 101 | func (tfp *TLSFingerprinter) Peek(from string) *ClientHello { 102 | ch, ok := tfp.mapClientHellos.Load(from) 103 | if !ok { 104 | return nil 105 | } 106 | 107 | clientHello, ok := ch.(*ClientHello) 108 | if !ok { 109 | return nil 110 | } 111 | 112 | return clientHello 113 | } 114 | 115 | // Pop looks up a ClientHello for a given key and deletes it from the 116 | // fingerprinter if found. 117 | func (tfp *TLSFingerprinter) Pop(from string) *ClientHello { 118 | ch, ok := tfp.mapClientHellos.LoadAndDelete(from) 119 | if !ok { 120 | return nil 121 | } 122 | 123 | clientHello, ok := ch.(*ClientHello) 124 | if !ok { 125 | return nil 126 | } 127 | 128 | return clientHello 129 | } 130 | 131 | // Close closes the TLSFingerprinter. 132 | func (tfp *TLSFingerprinter) Close() { 133 | tfp.closed.Store(true) 134 | } 135 | --------------------------------------------------------------------------------