├── .circleci └── config.yml ├── .github ├── CODEOWNERS └── workflows │ ├── archive.yml │ ├── ghpages.yml │ ├── publish.yml │ └── update.yml ├── .gitignore ├── .note.xml ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── draft-ietf-masque-quic-proxy.md └── interop ├── LICENSE ├── asset ├── badge.png ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 └── marked.min.js ├── index.html ├── lib ├── display.mjs ├── modal.mjs └── summary.mjs ├── parse_interop_tags.py ├── results ├── index.mjs ├── nwfw.json └── quiche.json ├── style.css └── tests ├── draft-ietf-masque-connect-udp.mjs ├── draft-pauly-masque-quic-proxy.mjs └── index.mjs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: martinthomson/i-d-template:latest 6 | working_directory: ~/draft 7 | 8 | steps: 9 | - run: 10 | name: "Print Configuration" 11 | command: | 12 | xml2rfc --version 13 | gem list -q kramdown-rfc2629 14 | echo -n 'mmark '; mmark --version 15 | 16 | - restore_cache: 17 | name: "Restoring cache - Git" 18 | keys: 19 | - v2-cache-git-{{ .Branch }}-{{ .Revision }} 20 | - v2-cache-git-{{ .Branch }} 21 | - v2-cache-git- 22 | 23 | - restore_cache: 24 | name: "Restoring cache - References" 25 | keys: 26 | - v1-cache-references-{{ epoch }} 27 | - v1-cache-references- 28 | 29 | # Workaround for https://discuss.circleci.com/t/22437 30 | - run: 31 | name: Tag Checkout 32 | command: | 33 | if [ -n "$CIRCLE_TAG" ] && [ -d .git ]; then 34 | remote=$(echo "$CIRCLE_REPOSITORY_URL" | \ 35 | sed -e 's,/^git.github.com:,https://github.com/,') 36 | git fetch -f "$remote" "refs/tags/$CIRCLE_TAG:refs/tags/$CIRCLE_TAG" || \ 37 | (echo 'Removing .git cache for tag build'; rm -rf .git) 38 | fi 39 | 40 | - checkout 41 | 42 | # Build txt and html versions of drafts 43 | - run: 44 | name: "Build Drafts" 45 | command: "make 'CLONE_ARGS=--reference ~/git-reference'" 46 | 47 | # Update editor's copy on gh-pages 48 | - run: 49 | name: "Update GitHub Pages" 50 | command: | 51 | if [ "${CIRCLE_TAG#draft-}" == "$CIRCLE_TAG" ]; then 52 | make gh-pages 53 | fi 54 | 55 | # For tagged builds, upload to the datatracker. 56 | - deploy: 57 | name: "Upload to Datatracker" 58 | command: | 59 | if [ "${CIRCLE_TAG#draft-}" != "$CIRCLE_TAG" ]; then 60 | make upload 61 | fi 62 | 63 | # Archive GitHub Issues 64 | - run: 65 | name: "Archive GitHub Issues" 66 | command: "make archive || make archive DISABLE_ARCHIVE_FETCH=true && make gh-archive" 67 | 68 | # Create and store artifacts 69 | - run: 70 | name: "Create Artifacts" 71 | command: "make artifacts CI_ARTIFACTS=/tmp/artifacts" 72 | 73 | - store_artifacts: 74 | path: /tmp/artifacts 75 | 76 | - run: 77 | name: "Prepare for Caching" 78 | command: "git reflog expire --expire=now --all && git gc --prune=now" 79 | 80 | - save_cache: 81 | name: "Saving Cache - Git" 82 | key: v2-cache-git-{{ .Branch }}-{{ .Revision }} 83 | paths: 84 | - ~/draft/.git 85 | 86 | - save_cache: 87 | name: "Saving Cache - Drafts" 88 | key: v1-cache-references-{{ epoch }} 89 | paths: 90 | - ~/.cache/xml2rfc 91 | 92 | 93 | workflows: 94 | version: 2 95 | build: 96 | jobs: 97 | - build: 98 | filters: 99 | tags: 100 | only: /.*?/ 101 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Automatically generated CODEOWNERS 2 | # Regenerate with `make update-codeowners` 3 | draft-ietf-masque-quic-proxy.md tpauly@apple.com eric_rosenberg@apple.com dschinazi.ietf@gmail.com 4 | -------------------------------------------------------------------------------- /.github/workflows/archive.yml: -------------------------------------------------------------------------------- 1 | name: "Archive Issues and Pull Requests" 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0,2,4' 6 | repository_dispatch: 7 | types: [archive] 8 | workflow_dispatch: 9 | inputs: 10 | archive_full: 11 | description: 'Recreate the archive from scratch' 12 | default: false 13 | type: boolean 14 | 15 | jobs: 16 | build: 17 | name: "Archive Issues and Pull Requests" 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: "Checkout" 21 | uses: actions/checkout@v4 22 | 23 | # Note: No caching for this build! 24 | 25 | - name: "Update Archive" 26 | uses: martinthomson/i-d-template@v1 27 | env: 28 | ARCHIVE_FULL: ${{ inputs.archive_full }} 29 | with: 30 | make: archive 31 | token: ${{ github.token }} 32 | 33 | - name: "Update GitHub Pages" 34 | uses: martinthomson/i-d-template@v1 35 | with: 36 | make: gh-archive 37 | token: ${{ github.token }} 38 | 39 | - name: "Save Archive" 40 | uses: actions/upload-artifact@v4 41 | with: 42 | path: archive.json 43 | -------------------------------------------------------------------------------- /.github/workflows/ghpages.yml: -------------------------------------------------------------------------------- 1 | name: "Update Editor's Copy" 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - README.md 7 | - CONTRIBUTING.md 8 | - LICENSE.md 9 | - .gitignore 10 | pull_request: 11 | paths-ignore: 12 | - README.md 13 | - CONTRIBUTING.md 14 | - LICENSE.md 15 | - .gitignore 16 | 17 | jobs: 18 | build: 19 | name: "Update Editor's Copy" 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: "Checkout" 23 | uses: actions/checkout@v4 24 | 25 | - name: "Setup" 26 | id: setup 27 | run: date -u "+date=%FT%T" >>"$GITHUB_OUTPUT" 28 | 29 | - name: "Caching" 30 | uses: actions/cache@v4 31 | with: 32 | path: | 33 | .refcache 34 | .venv 35 | .gems 36 | node_modules 37 | .targets.mk 38 | key: i-d-${{ steps.setup.outputs.date }} 39 | restore-keys: i-d- 40 | 41 | - name: "Build Drafts" 42 | uses: martinthomson/i-d-template@v1 43 | with: 44 | token: ${{ github.token }} 45 | 46 | - name: "Update GitHub Pages" 47 | uses: martinthomson/i-d-template@v1 48 | if: ${{ github.event_name == 'push' }} 49 | with: 50 | make: gh-pages 51 | token: ${{ github.token }} 52 | 53 | - name: "Archive Built Drafts" 54 | uses: actions/upload-artifact@v4 55 | with: 56 | path: | 57 | draft-*.html 58 | draft-*.txt 59 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish New Draft Version" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "draft-*" 7 | workflow_dispatch: 8 | inputs: 9 | email: 10 | description: "Submitter email" 11 | default: "" 12 | type: string 13 | 14 | jobs: 15 | build: 16 | name: "Publish New Draft Version" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: "Checkout" 20 | uses: actions/checkout@v4 21 | 22 | # See https://github.com/actions/checkout/issues/290 23 | - name: "Get Tag Annotations" 24 | run: git fetch -f origin ${{ github.ref }}:${{ github.ref }} 25 | 26 | - name: "Setup" 27 | id: setup 28 | run: date -u "+date=%FT%T" >>"$GITHUB_OUTPUT" 29 | 30 | - name: "Caching" 31 | uses: actions/cache@v4 32 | with: 33 | path: | 34 | .refcache 35 | .venv 36 | .gems 37 | node_modules 38 | .targets.mk 39 | key: i-d-${{ steps.setup.outputs.date }} 40 | restore-keys: i-d- 41 | 42 | - name: "Build Drafts" 43 | uses: martinthomson/i-d-template@v1 44 | with: 45 | token: ${{ github.token }} 46 | 47 | - name: "Upload to Datatracker" 48 | uses: martinthomson/i-d-template@v1 49 | with: 50 | make: upload 51 | env: 52 | UPLOAD_EMAIL: ${{ inputs.email }} 53 | 54 | - name: "Archive Submitted Drafts" 55 | uses: actions/upload-artifact@v4 56 | with: 57 | path: "versioned/draft-*-[0-9][0-9].*" 58 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: "Update Generated Files" 2 | # This rule is not run automatically. 3 | # It can be run manually to update all of the files that are part 4 | # of the template, specifically: 5 | # - README.md 6 | # - CONTRIBUTING.md 7 | # - .note.xml 8 | # - .github/CODEOWNERS 9 | # - Makefile 10 | # 11 | # 12 | # This might be useful if you have: 13 | # - added, removed, or renamed drafts (including after adoption) 14 | # - added, removed, or changed draft editors 15 | # - changed the title of drafts 16 | # 17 | # Note that this removes any customizations you have made to 18 | # the affected files. 19 | on: workflow_dispatch 20 | 21 | jobs: 22 | build: 23 | name: "Update Files" 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: "Checkout" 27 | uses: actions/checkout@v4 28 | 29 | - name: "Update Generated Files" 30 | uses: martinthomson/i-d-template@v1 31 | with: 32 | make: update-files 33 | token: ${{ github.token }} 34 | 35 | - name: "Push Update" 36 | run: git push 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.pdf 3 | *.redxml 4 | *.swp 5 | *.txt 6 | *.upload 7 | *~ 8 | .tags 9 | /*-[0-9][0-9].xml 10 | /.gems/ 11 | /.refcache 12 | /.targets.mk 13 | /.venv/ 14 | /.vscode/ 15 | /lib 16 | /node_modules/ 17 | /versioned/ 18 | Gemfile.lock 19 | archive.json 20 | draft-ietf-masque-quic-proxy.xml 21 | package-lock.json 22 | report.xml 23 | !requirements.txt 24 | -------------------------------------------------------------------------------- /.note.xml: -------------------------------------------------------------------------------- 1 | 2 | Source for this draft and an issue tracker can be found at 3 | https://github.com/tfpauly/quic-proxy. 4 | 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository relates to activities in the Internet Engineering Task Force 4 | ([IETF](https://www.ietf.org/)). All material in this repository is considered 5 | Contributions to the IETF Standards Process, as defined in the intellectual 6 | property policies of IETF currently designated as 7 | [BCP 78](https://www.rfc-editor.org/info/bcp78), 8 | [BCP 79](https://www.rfc-editor.org/info/bcp79) and the 9 | [IETF Trust Legal Provisions (TLP) Relating to IETF Documents](http://trustee.ietf.org/trust-legal-provisions.html). 10 | 11 | Any edit, commit, pull request, issue, comment or other change made to this 12 | repository constitutes Contributions to the IETF Standards Process 13 | (https://www.ietf.org/). 14 | 15 | You agree to comply with all applicable IETF policies and procedures, including, 16 | BCP 78, 79, the TLP, and the TLP rules regarding code components (e.g. being 17 | subject to a Simplified BSD License) in Contributions. 18 | ## Working Group Information 19 | 20 | Discussion of this work occurs on the [Multiplexed Application Substrate over QUIC Encryption 21 | Working Group mailing list](mailto:masque@ietf.org) 22 | ([archive](https://mailarchive.ietf.org/arch/browse/masque/), 23 | [subscribe](https://www.ietf.org/mailman/listinfo/masque)). 24 | In addition to contributions in GitHub, you are encouraged to participate in 25 | discussions there. 26 | 27 | **Note**: Some working groups adopt a policy whereby substantive discussion of 28 | technical issues needs to occur on the mailing list. 29 | 30 | You might also like to familiarize yourself with other 31 | [Working Group documents](https://datatracker.ietf.org/wg/masque/documents/). 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | See the 4 | [guidelines for contributions](https://github.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/blob/main/CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBDIR := lib 2 | include $(LIBDIR)/main.mk 3 | 4 | $(LIBDIR)/main.mk: 5 | ifneq (,$(shell grep "path *= *$(LIBDIR)" .gitmodules 2>/dev/null)) 6 | git submodule sync 7 | git submodule update $(CLONE_ARGS) --init 8 | else 9 | git clone -q --depth 10 $(CLONE_ARGS) \ 10 | -b main https://github.com/martinthomson/i-d-template $(LIBDIR) 11 | endif 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QUIC-Aware Proxying Using HTTP 2 | 3 | This is the working area for the IETF [MASQUE Working Group](https://datatracker.ietf.org/wg/masque/documents/) Internet-Draft, "QUIC-Aware Proxying Using HTTP". 4 | 5 | * [Editor's Copy](https://ietf-wg-masque.github.io/draft-ietf-masque-quic-proxy/#go.draft-ietf-masque-quic-proxy.html) 6 | * [Datatracker Page](https://datatracker.ietf.org/doc/draft-ietf-masque-quic-proxy) 7 | * [Working Group Draft](https://datatracker.ietf.org/doc/html/draft-ietf-masque-quic-proxy) 8 | * [Compare Editor's Copy to Working Group Draft](https://ietf-wg-masque.github.io/draft-ietf-masque-quic-proxy/#go.draft-ietf-masque-quic-proxy.diff) 9 | 10 | 11 | ## Contributing 12 | 13 | See the 14 | [guidelines for contributions](https://github.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/blob/main/CONTRIBUTING.md). 15 | 16 | Contributions can be made by creating pull requests. 17 | The GitHub interface supports creating pull requests using the Edit (✏) button. 18 | 19 | 20 | ## Command Line Usage 21 | 22 | Formatted text and HTML versions of the draft can be built using `make`. 23 | 24 | ```sh 25 | $ make 26 | ``` 27 | 28 | Command line usage requires that you have the necessary software installed. See 29 | [the instructions](https://github.com/martinthomson/i-d-template/blob/main/doc/SETUP.md). 30 | 31 | -------------------------------------------------------------------------------- /draft-ietf-masque-quic-proxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: QUIC-Aware Proxying Using HTTP 3 | abbrev: QUIC Proxy 4 | category: exp 5 | docname: draft-ietf-masque-quic-proxy-latest 6 | submissiontype: IETF 7 | number: 8 | date: 9 | consensus: true 10 | v: 3 11 | area: Transport 12 | wg: MASQUE 13 | venue: 14 | group: "MASQUE" 15 | type: "Working Group" 16 | mail: "masque@ietf.org" 17 | arch: "https://mailarchive.ietf.org/arch/browse/masque/" 18 | github: "ietf-wg-masque/draft-ietf-masque-quic-proxy" 19 | latest: "https://ietf-wg-masque.github.io/draft-ietf-masque-quic-proxy/draft-ietf-masque-quic-proxy.html" 20 | keyword: 21 | - quic 22 | - http 23 | - datagram 24 | - udp 25 | - proxy 26 | - tunnels 27 | - quic in quic 28 | - turtles all the way down 29 | - masque 30 | - http-ng 31 | 32 | author: 33 | - 34 | ins: T. Pauly 35 | name: Tommy Pauly 36 | org: Apple Inc. 37 | street: One Apple Park Way 38 | city: Cupertino, California 95014 39 | country: United States of America 40 | email: tpauly@apple.com 41 | 42 | - 43 | ins: E. Rosenberg 44 | name: Eric Rosenberg 45 | org: Apple Inc. 46 | street: One Apple Park Way 47 | city: Cupertino, California 95014 48 | country: United States of America 49 | email: eric_rosenberg@apple.com 50 | 51 | - 52 | ins: "D. Schinazi" 53 | name: "David Schinazi" 54 | organization: "Google LLC" 55 | street: "1600 Amphitheatre Parkway" 56 | city: "Mountain View, California 94043" 57 | country: "United States of America" 58 | email: dschinazi.ietf@gmail.com 59 | 60 | normative: 61 | SP800-38A: 62 | author: 63 | name: Morris Dworkin 64 | org: National Institute of Standards and Technology 65 | title: > 66 | Recommendation for Block Cipher Modes of Operation: Methods and Techniques 67 | date: 2001-12-01 68 | target: https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38a.pdf 69 | 70 | --- abstract 71 | 72 | This document extends UDP Proxying over HTTP to add optimizations for proxied 73 | QUIC connections. Specifically, it allows a proxy to reuse UDP 4-tuples for multiple 74 | proxied connections, and adds a mode of proxying in which QUIC short header packets 75 | can be forwarded and transformed through a HTTP/3 proxy rather than being fully 76 | re-encapsulated and re-encrypted. 77 | 78 | --- middle 79 | 80 | # Introduction {#introduction} 81 | 82 | UDP Proxying over HTTP {{!CONNECT-UDP=RFC9298}} 83 | defines a way to send datagrams through an HTTP proxy, where UDP is used to communicate 84 | between the proxy and a target server. This can be used to proxy QUIC 85 | connections {{!QUIC=RFC9000}}, since QUIC runs over UDP datagrams. 86 | 87 | This document uses the term "target" to refer to the server that a client is 88 | accessing via a proxy. This target may be an origin server hosting content, or 89 | another proxy for cases where proxies are chained together. 90 | 91 | This document extends the UDP proxying protocol to add signalling about QUIC 92 | Connection IDs. QUIC Connection IDs are used to identify QUIC connections in 93 | scenarios where there is not a strict one-to-one mapping between QUIC 94 | connections and UDP 4-tuples (pairs of IP addresses and ports). 95 | 96 | If a client permits proxy port reuse, once a proxy is aware of QUIC Connection IDs, 97 | it can reuse UDP 4-tuples between itself and a target for multiple proxied QUIC connections. 98 | 99 | For proxies that are themselves running on HTTP/3 {{!HTTP3=RFC9114}}, and thus 100 | are accessed by clients over QUIC, QUIC Connection IDs can be used to treat 101 | packets differently on the link between clients and proxies. New QUIC Connection IDs 102 | can be assigned to perform transformations to the packets that allow for efficient 103 | forwarding of packets that don't require full re-encapsulation and re-encryption 104 | of proxied QUIC packets within datagrams inside the QUIC connection between 105 | clients and proxies. 106 | 107 | This document defines two modes for proxying QUIC connections, "tunnelled" and 108 | "forwarded": 109 | 110 | 1. Tunnelled is the default mode for UDP proxying, defined in {{CONNECT-UDP}}. 111 | In this mode, packets in QUIC connection between the client and target are 112 | encapsulated inside the QUIC connection between the client and proxy. 113 | These packets use multiple layers of encryption and congestion control. 114 | 115 | 2. Forwarded is the mode of proxying added by this document. In this mode, 116 | packets in the QUIC connection between the client and target are sent with dedicated 117 | QUIC Connection IDs between the client and proxy, and use special-purpose 118 | tranforms instead of full re-encapsulation and re-encryption. 119 | 120 | QUIC long header packets between clients and targets MUST be proxied in tunnelled 121 | mode. QUIC short header packets between clients and targets MAY be proxied in 122 | forwarded mode, subject to negotiation between a client and a proxy. 123 | 124 | Forwarded mode is an optimization to reduce CPU and memory cost to clients and 125 | proxies and avoid encapsulation overhead for packets on the wire that reduce 126 | the effective MTU (Maximum Transmission Unit). This makes it suitable for 127 | deployment situations that otherwise relied on cleartext TCP 128 | proxies, which cannot support QUIC and have inferior security and privacy 129 | properties. 130 | 131 | The properties provided by the forwarded mode are as follows: 132 | 133 | - All packets sent between the client and the target traverse through the proxy 134 | device. 135 | - The target server cannot know the IP address of the client solely based on the 136 | proxied packets the target receives. 137 | - Observers of either or both of the links between client and proxy and between 138 | proxy and target are not able to learn more about the client-to-target 139 | communication than if no proxy was used. 140 | 141 | Forwarded mode does not prevent correlation of packets on the link between 142 | client and proxy and the link between proxy and target by an entity that 143 | can observe both links. The precise risks depend on the negotiated transform 144 | ({{transforms}}). See {{security}} for further discussion. 145 | 146 | Both clients and proxies can unilaterally choose to disable forwarded mode for 147 | any client-to-target connection. 148 | 149 | The forwarded mode of proxying is only defined for HTTP/3 {{HTTP3}} and not 150 | any earlier versions of HTTP. 151 | 152 | QUIC proxies only need to understand the Header Form bit, and the connection ID 153 | fields from packets in client-to-target QUIC connections. Since these fields 154 | are all in the QUIC invariants header {{!INVARIANTS=RFC8999}}, QUIC proxies can 155 | proxy all versions of QUIC. 156 | 157 | ## Conventions and Definitions {#conventions} 158 | 159 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 160 | "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this 161 | document are to be interpreted as described in BCP 14 {{!RFC2119}} {{!RFC8174}} 162 | when, and only when, they appear in all capitals, as shown here. 163 | 164 | ## Terminology 165 | 166 | This document uses the following terms: 167 | 168 | - Client: the client of all QUIC connections discussed in this document. 169 | - Proxy: the endpoint that responds to the UDP proxying request. 170 | - Target: the server that a client is accessing via a proxy. 171 | - Client-to-proxy 4-tuple: the UDP 4-tuple (client IP address, client UDP port, 172 | proxy IP address, proxy UDP port) used to communicate between the client and 173 | the proxy. 174 | - Proxy-to-target 4-tuple: the UDP 4-tuple (proxy IP address, proxy UDP port, 175 | target IP address, target UDP port) used to communicate between the proxy and 176 | the target. 177 | - Client Connection ID (CID): a QUIC Connection ID that is chosen by the client, and 178 | is used in the Destination Connection ID field of packets from the target to 179 | the client. 180 | - Target Connection ID (CID): a QUIC Connection ID that is chosen by the target, and 181 | is used in the Destination Connection ID field of packets from the client to 182 | the target. 183 | - Virtual Connection ID (VCID): a fake QUIC Connection ID chosen by the proxy 184 | that is used on the client-to-proxy 4-tuple in forwarded mode. 185 | - Client VCID: a VCID used by the proxy to send forwarded packets from the target 186 | to the client. 187 | - Target VCID: a VCID used by the client to send forwarded packets to the target 188 | via the proxy. 189 | - Packet Transform: the procedure used to modify packets before they enter the 190 | client-proxy link. 191 | 192 | # Protocol Overview 193 | 194 | QUIC-aware proxying involves a client, a proxy, and a target. Although 195 | multiple proxies can be chained together in sequence (which is 196 | a main motivation for enabling forwarded mode), each subsequent proxy 197 | is just treated as a target by the preceding proxy. 198 | 199 | ~~~ aasvg 200 | +--------+ +-----------+ +--------+ 201 | | | | | | | 202 | | Client +--------------+ Proxy +--------------+ Target | 203 | | | | | | | 204 | +--------+ +-----------+ +--------+ 205 | 206 | <----------- Client-to-target connection -----------> 207 | 208 | 209 | <-- Client-to-proxy --> <-- Proxy-to-target --> 210 | 4-tuple 4-tuple 211 | ~~~ 212 | {: #overview-diagram title="Diagram of roles in QUIC-aware proxying"} 213 | 214 | All QUIC-aware proxying relies on the proxy learning information about 215 | QUIC connection IDs on the client-to-target QUIC connection ({{cid-awareness}}). 216 | 217 | Forwarded mode adds the concept of Virtual Connection IDs that are assigned 218 | by the proxy to use to identify packets on the client-to-proxy 4-tuple ({{vcids}}). 219 | 220 | Negotation of modes and assignment of connection IDs is described in {{negotiation}}. 221 | 222 | ## Connection ID Awareness {#cid-awareness} 223 | 224 | For a proxy to be aware of proxied QUIC connection IDs, it needs to know and 225 | correlate three values: 226 | 227 | 1. The HTTP stream used to proxy a client-to-target QUIC connection 228 | 1. The client-chosen connection ID on the client-to-target QUIC connection, 229 | or the "client CID" 230 | 1. The target-chosen connection ID on the client-to-target QUIC connection, 231 | or the "target CID" 232 | 233 | For example, consider a proxy using HTTP/3 that has two clients (A and B) connected 234 | simultaneously, each client coming from a different IP address and port. 235 | Each client makes a request to proxy a UDP flow to "target.example.net" on 236 | port 443. If the proxy knows that client A's connection to the target 237 | has negotiated a client CID `AAAA0000` and a target CID `0000AAAA`, and client B's 238 | connection to the target has negotiated a client CID `BBBB0000` and a target CID 239 | `0000BBBB`, then the proxy would be able to use the same proxy-to-target 4-tuple 240 | for both connections, because it can route packets from the target to the 241 | correct client based on CID `AAAA0000` or `BBBB0000`. 242 | 243 | ~~~ aasvg 244 | <---------- Client A-to-target connection ----------> 245 | AAAA0000 0000AAAA 246 | +--------+ +-----------+ 247 | | Client +--------------+ + 248 | | A | | | 249 | +--------+ | | +--------+ 250 | | | Shared | | 251 | | Proxy +--------------+ Target | 252 | | | 4-tuple | | 253 | +--------+ | | +--------+ 254 | | Client | | | 255 | | B +--------------+ | 256 | +--------+ +-----------+ 257 | BBBB0000 0000BBBB 258 | <---------- Client B-to-target connection ----------> 259 | ~~~ 260 | {: #sharing-tuple-diagram title="Example of sharing a proxy-to-target 4-tuple"} 261 | 262 | In order to share a proxy-to-target 4-tuple between multiple proxied connections, 263 | the proxy MUST guarantee that the client CIDs do not conflict. See {{conflicts}} 264 | for more discussion of handlng CID conflicts. 265 | 266 | ## Virtual Connection IDs {#vcids} 267 | 268 | Virtual Connection IDs (VCIDs) are QUIC Connection IDs used on the link between a 269 | client and proxy that do not belong to the QUIC connection between the client 270 | and proxy, but instead are aliases for particular client-to-target connections. 271 | VCIDs are only used in forwarded mode. They are established using HTTP capsules 272 | {{!HTTP-CAPSULES=RFC9297}} as described in {{cid-capsules}}. 273 | 274 | For example, consider a proxy using HTTP/3 that has a single client connected 275 | to it. The client-to-proxy QUIC connection has `CCCC0000` as the client CID 276 | and `0000CCCC` as the proxy CID. The client has connected to a single target 277 | through the proxy, and the client-to-target QUIC connection has `CCCC1111` 278 | as the client CID and `1111CCCC` as the target CID. In order to use 279 | forwarded mode, the proxy assigns VCIDs to use on the client-to-proxy link 280 | to represent the client-to-target connection. In this case, the VCIDs could 281 | be `CCCC2222` for the client's VCID and `2222CCCC` for the proxy's VCID that 282 | it uses to forward to the target. 283 | 284 | ~~~ aasvg 285 | +--------+ +-----------+ +--------+ 286 | | | | | | | 287 | | Client +--------------+ Proxy +--------------+ Target | 288 | | | | | | | 289 | +--------+ +-----------+ +--------+ 290 | 291 | CCCC0000 0000CCCC 292 | <-- Client-to-proxy --> 293 | connection 294 | 295 | CCCC1111 1111CCCC 296 | <----------- Client-to-target connection -----------> 297 | 298 | CCCC2222 (CCCC1111) 2222CCCC (1111CCCC) 299 | <-- Client-to-proxy --> 300 | VCIDs 301 | ~~~ 302 | {: #vcid-diagram title="Diagram of VCIDs in forwarded mode"} 303 | 304 | In order for a proxy to correctly route packets using VCIDs from 305 | client-to-target and target-to-client, the proxy MUST guarantee that 306 | the mappings between VCIDs, CIDs, and 4-tuples are unique. Specifically, 307 | in order to route packets sent by the client, the proxy needs to be able 308 | to observe the VCID and the client-to-proxy 4-tuple, and map them 309 | to a specific target CID and proxy-to-target 4-tuple. In order to route 310 | packets sent by a target, the proxy needs to be able to observe the client 311 | CID and the proxy-to-target 4-tuple, and map them to a specific VCID 312 | and client-to-proxy 4-tuple. Since proxies choose the VCID values, they 313 | can ensure that the VCIDs are distinguishable. 314 | 315 | Servers receiving QUIC packets can employ load balancing 316 | strategies such as those described in {{?QUIC-LB=I-D.ietf-quic-load-balancers}} 317 | that encode routing information in the connection ID. When operating in 318 | forwarded mode, clients send QUIC packets destined for the target directly 319 | to the proxy. Since these packets are generated using the target CID, 320 | load balancers may not have the necessary information to route packets to the correct proxy. The target VCID 321 | is a VCID chosen by the proxy that the client uses when sending 322 | forwarded mode packets. The proxy replaces the target VCID 323 | with the target CID prior to forwarding the packet to the target. 324 | 325 | Similarly, QUIC requires that connection IDs aren't reused over multiple network 326 | paths to avoid linkability. The client VCID is a connection ID 327 | chosen by the proxy that the proxy uses when sending forwarded mode packets. 328 | The proxy replaces the client CID with the client VCID prior to 329 | forwarding the packet to the client. Clients take advantage of this 330 | to avoid linkability when migrating a client to proxy network path. The Virtual 331 | client CID allows the connection ID bytes to change on the wire 332 | without requiring the connection IDs on the client to target connection change. 333 | To reduce the likelihood of connection ID conflicts, the proxy MUST choose a 334 | client VCID that is at least as long as the original client CID. Similarly, 335 | clients multiplexing connections on the same UDP 4-tuple SHOULD 336 | choose a client CID that's sufficiently long to reduce the likelihood 337 | of a conflict with the proxy-chosen client VCID. The client VCID MUST either be 338 | constructed such that it is unpredictable to the client or to guarantee no 339 | conflicts among all proxies sharing an IP address 340 | and port. See {{security}} for more discussion on client VCID 341 | construction. 342 | 343 | Clients and Proxies not implementing forwarded mode do not need to consider 344 | VCIDs since all client-to-target datagrams will be encapsulated 345 | within the client-to-proxy connection. 346 | 347 | ## Negotiating Modes and Connection IDs {#negotiation} 348 | 349 | In order to support QUIC-aware proxying, both clients and proxies need 350 | to support capsules {{HTTP-CAPSULES}}, which is indicated by including 351 | the "Capsule-Protocol" header field in requests and responses. If this header 352 | field is not included, none of the functionality in this document can be used. 353 | 354 | ~~~ 355 | capsule-protocol = ?1 356 | ~~~ 357 | 358 | To permit the proxy to share target-facing ports, the client needs to include the 359 | "Proxy-QUIC-Port-Sharing" header field, as defined in {{port-sharing-header}}. 360 | This indicates that the proxy may share target-facing 4-tuples concurrently with 361 | other QUIC connections. Clients that do not want the target-facing 4-tuple shared 362 | will not include this header or will provide it with a value of "?0". 363 | 364 | ~~~ 365 | proxy-quic-port-sharing = ?1 366 | ~~~ 367 | 368 | To support forwarded mode, both clients and proxies need to include 369 | the "Proxy-QUIC-Forwarding" header field, as defined in {{forwarding-header}}. 370 | This indicates support for forwarded mode, and allows negotiation of 371 | packet transforms to apply when forwarding ({{transforms}}), along 372 | with any parameters for specific transforms. Clients or proxies that 373 | don't support forwarded mode will not include this header field. 374 | 375 | ~~~ 376 | proxy-quic-forwarding = ?1; accept-transform=scramble,identity; \ 377 | scramble-key=:abc...789=: 378 | ~~~ 379 | 380 | If neither header is supplied with a value of "?1", none of the functionality 381 | in this document can be used and handling reduces to normal connect-udp. 382 | 383 | After negotiating support with header fields, clients and proxies use 384 | the capsules defined in {{cid-capsules}} to communicate information 385 | about CIDs and VCIDs. 386 | 387 | For QUIC-aware proxying without forwarded mode, the steps are as follows: 388 | 389 | 1. The client sends the `REGISTER_CLIENT_CID` capsule once it selects a 390 | CID that it will use for receiving packets from a target. 391 | 392 | 1. The proxy sends the `ACK_CLIENT_CID` capsule to acknowledge that CID, 393 | with no associated client VCID; alternatively, the proxy can send the 394 | `CLOSE_CLIENT_CID` if it detects a conflict with another CID. 395 | 396 | 1. The client sends the `REGISTER_TARGET_CID` capsule as soon as it learns 397 | the target CID on the client-to-target connection. 398 | 399 | 1. The proxy sends the `ACK_TARGET_CID` capsule to acknowledge that CID, 400 | with no associated target VCID; alternatively, the proxy can send the 401 | `CLOSE_TARGET_CID` if it detects a conflict with another CID. 402 | 403 | 1. The proxy sends the `MAX_CONNECTION_IDS` capsule to allow additional 404 | registration of new connection IDs via future `REGISTER_CLIENT_CID` and 405 | `REGISTER_TARGET_CID` capsules. 406 | 407 | 1. Whenever a client or target stops uses a particular CID, the client 408 | sends a `CLOSE_CLIENT_CID` or `CLOSE_TARGET_CID` capsule. The client 409 | can also initiate new `REGISTER_CLIENT_CID` or `REGISTER_TARGET_CID` 410 | exchanges at any time. 411 | 412 | For QUIC-aware proxying with forwarded mode, the steps are as follows: 413 | 414 | 1. The client sends the `REGISTER_CLIENT_CID` capsule once it selects a 415 | CID that it will use for receiving packets from a target. 416 | 417 | 1. The proxy sends the `ACK_CLIENT_CID` capsule to acknowledge that CID, 418 | with a client VCID; alternatively, the proxy can send the 419 | `CLOSE_CLIENT_CID` if it detects a conflict with another CID. 420 | 421 | 1. The client sends the `ACK_CLIENT_VCID` capsule to acknowledge the 422 | client VCID, which allows forwarded packets for that VCID to be used. 423 | 424 | 1. The client sends the `REGISTER_TARGET_CID` capsule as soon as it learns 425 | the target CID on the client-to-target connection. 426 | 427 | 1. The proxy sends the `ACK_TARGET_CID` capsule to acknowledge that CID, 428 | with a target VCID; alternatively, the proxy can send the 429 | `CLOSE_TARGET_CID` if it detects a conflict with another CID. Once 430 | the client receives the target VCID, it can start sending forwarded 431 | packets using the target VCID. 432 | 433 | 1. The proxy sends the `MAX_CONNECTION_IDS` capsule to allow additional 434 | registration of new connection IDs via future `REGISTER_CLIENT_CID` and 435 | `REGISTER_TARGET_CID` capsules. 436 | 437 | 1. Whenever a client or target stops uses a particular CID, the client 438 | sends a `CLOSE_CLIENT_CID` or `CLOSE_TARGET_CID` capsule. The client 439 | can also initiate new `REGISTER_CLIENT_CID` or `REGISTER_TARGET_CID` 440 | exchanges at any time. 441 | 442 | # Proxy-QUIC-Forwarding Header {#forwarding-header} 443 | 444 | A client initiates UDP proxying via a CONNECT request as defined 445 | in {{CONNECT-UDP}}. Within its request, it includes the "Proxy-QUIC-Forwarding" 446 | header to indicate whether or not the request should support forwarding. 447 | If this header is not included, the client MUST NOT send any connection ID 448 | capsules. 449 | 450 | "Proxy-QUIC-Forwarding" is an Item Structured Header {{!RFC8941}}. Its 451 | value MUST be a Boolean. 452 | 453 | If the client wants to enable QUIC packet forwarding for this request, it sets 454 | the value to "?1". If it doesn't want to enable forwarding, but instead only 455 | provide information about QUIC Connection IDs for the purpose of allowing 456 | the proxy to share a proxy-to-target 4-tuple, it sets the value to "?0". 457 | 458 | The client MUST add an "accept-transform" parameter whose value is an 459 | `sf-string` containing the supported packet transforms ({{transforms}}) 460 | in order of descending preference, separated by commas. If the proxy receives a 461 | "Proxy-QUIC-Forwarding" header without the "accept-transform" parameters, it 462 | MUST ignore the header and respond as if the client had not sent the 463 | "Proxy-QUIC-Forwarding" header. 464 | 465 | If the proxy supports QUIC-aware proxying, it will include the 466 | "Proxy-QUIC-Forwarding" header in successful HTTP responses. The value 467 | indicates whether or not the proxy supports forwarding. If the client does 468 | not receive this header in responses, the client SHALL assume that the proxy 469 | does not support this extension. 470 | 471 | The proxy MUST include a "transform" parameter whose value is an `sf-string` 472 | indicating the selected transform. If the proxy does not recognize or accept 473 | any of the transforms offered by the client, it MUST omit this parameter and 474 | set the header field value to "?0", or omit the header entirely. 475 | 476 | # Proxy-QUIC-Port-Sharing Header {#port-sharing-header} 477 | 478 | A client may include the "Proxy-QUIC-Port-Sharing" header to indicate whether 479 | or not the proxy is permitted to share ports between this QUIC connection and other 480 | proxied QUIC connections. 481 | 482 | "Proxy-QUIC-Port-Sharing" is an Item Structured Header {{!RFC8941}}. Its value 483 | MUST be a Boolean. 484 | 485 | Clients SHOULD send this with a value of "?1" unless the client wishes to prohibit 486 | this behavior. Permitting the proxy to share ports may allow the proxy to conserve 487 | resources and support more clients. 488 | 489 | A proxy that does not support port sharing, SHOULD send "Proxy-QUIC-Port-Sharing" 490 | with a value of "?0". Doing so allows clients to stop sending capsules for this 491 | extension if forwarding mode is also not supported. Clients who send "?1", but do 492 | not receive any "Proxy-QUIC-Port-Sharing" header in response must assume that port 493 | sharing may be in effect and MUST continue register connection IDs in order for 494 | the proxied connection to continue to work. 495 | 496 | # Connection ID Capsules {#cid-capsules} 497 | 498 | Connection ID awareness relies on using capsules {{HTTP-CAPSULES}} to 499 | signal addition and removal of Connection IDs. Clients send capsules 500 | to let proxies know when Connection IDs on the client-to-target 501 | QUIC connection are changing. Proxies send capsules to acknowledge or 502 | reject these Connection IDs, and in forwarded mode to let clients know 503 | about Virtual Connection IDs to use on the client-to-proxy link. 504 | 505 | Note that these capsules do not register contexts. QUIC packets are encoded 506 | using HTTP Datagrams with the context ID set to zero as defined in 507 | {{CONNECT-UDP}}. 508 | 509 | The REGISTER_CLIENT_CID ({{capsule-reg-client}}) and REGISTER_TARGET_CID 510 | ({{capsule-reg-target}}) capsule types allow a client to inform 511 | the proxy about a new client CID or a new target CID, 512 | respectively. These capsule types MUST only be sent by a client. These capsule 513 | types share a sequence number space which allows the proxy to limit the 514 | number of active registrations. The first registration (of either client CID or target CID) 515 | has sequence number 0, and subsequent registrations increment the sequence number 516 | by 1. 517 | 518 | The ACK_CLIENT_CID ({{capsule-ack-client}}) and ACK_TARGET_CID 519 | ({{capsule-ack-target}}) capsule types are sent by the proxy to the client 520 | to indicate that a mapping was successfully created for a registered 521 | connection ID as well as optionally provide the Virtual Connection IDs that can be 522 | used in forwarded mode. These capsule types MUST only be sent by a proxy. 523 | 524 | The ACK_CLIENT_VCID ({{capsule-ack-virtual}}) capsule type MUST only be sent 525 | by the client and only when forwarded mode is enabled. It is sent by the client 526 | to the proxy in response to an ACK_CLIENT_CID capsule to indicate that the client 527 | is ready to receive forwarded mode packets with the specified virtual connection ID. 528 | The proxy MUST NOT send forwarded mode packets to the client prior to receiving this 529 | acknowledgement. This capsule also contains a Stateless Reset Token the client 530 | may respond with when receiving forwarded mode packets with the specified 531 | virtual connection ID. 532 | 533 | The CLOSE_CLIENT_CID and CLOSE_TARGET_CID capsule types ({{capsule-close}}) 534 | allow either a client or a proxy to remove a mapping for a connection ID. 535 | These capsule types MAY be sent by either a client or the proxy. If a proxy sends a 536 | CLOSE_CLIENT_CID without having sent an ACK_CLIENT_CID, or if a proxy 537 | sends a CLOSE_TARGET_CID without having sent an ACK_TARGET_CID, 538 | it is rejecting a Connection ID registration. Similarly, if a client sends 539 | CLOSE_CLIENT_CID without having sent an ACK_CLIENT_VCID capsule, the client is 540 | either rejecting the proxy-chosen client VCID or no longer 541 | needs the connection ID registered. 542 | 543 | The MAX_CONNECTION_IDS capsule type {{capsule-max-cids}} MUST only be sent by the 544 | proxy. It indicates to the client the maximum permitted sequence number for 545 | connection ID registrations. This allows the proxy to limit the number of active 546 | registrations. The initial maximum is 1, allowing the client to send 2 registrations, 547 | one with sequence number 0 and another with sequence number 1. A proxy MUST NOT 548 | send a MAX_CONNECTION_IDS capsule with a value less than 1. Clients receiving a 549 | MAX_CONNECTION_IDS capsule with a value less than 1 MUST reset the stream with 550 | H3_DATAGRAM_ERROR error code. 551 | 552 | When port sharing {{port-sharing-header}} is supported, the client MUST register 553 | and receive acknowledgements for client and target CIDs before using them. Packets with 554 | unknown connection IDs received by the proxy on a target-facing sockets that support 555 | port sharing MUST be dropped. In order to avoid introducing an additional round trip 556 | on setup, a REGISTER_CLIENT_CID capsule SHOULD be sent at the same time as the client's 557 | first flight. If the proxy rejects the client CID, the proxy MUST drop all packets until 558 | it has sent an ACK_CLIENT_CID capsule and the client MUST NOT send any packets until 559 | receiving an ACK_CLIENT_CID. When port sharing is supported, a proxy SHOULD buffer a 560 | reasonable number of incoming packets while waiting for the first REGISTER_CLIENT_CID 561 | capsule. 562 | 563 | ## REGISTER_CLIENT_CID {#capsule-reg-client} 564 | 565 | The REGISTER_CLIENT_CID capsule is sent by the client and contains a single 566 | connection ID that is the client-provided connection ID on the client-to-target QUIC 567 | connection. 568 | 569 | ~~~ 570 | Register CID Capsule { 571 | Type (i) = see {{iana}} for the value of the capsule type 572 | Length (i), 573 | Connection ID (0..2040), 574 | } 575 | ~~~ 576 | {: #fig-capsule-register-client-cid title="Register CID Capsule Format"} 577 | 578 | Connection ID: 579 | : A connection ID being registered, which is between 0 and 255 bytes in 580 | length. The length of the connection ID is implied by the length of the 581 | capsule. Note that in QUICv1, the length of the Connection ID is limited 582 | to 20 bytes, but QUIC invariants allow up to 255 bytes. 583 | 584 | ## REGISTER_TARGET_CID {#capsule-reg-target} 585 | 586 | The REGISTER_TARGET_CID capsule is sent by the client and includes the 587 | target-provided connection ID on the client-to-target QUIC connection, and 588 | the corresponding Stateless Reset Token. 589 | 590 | ~~~ 591 | Register Target CID Capsule { 592 | Type (i) = see {{iana}} for the value of the capsule type 593 | Length (i), 594 | Connection ID Length (i) 595 | Connection ID (0..2040), 596 | Stateless Reset Token Length (i), 597 | Stateless Reset Token (..), 598 | } 599 | ~~~ 600 | {: #fig-capsule-register-target-cid title="Register Target CID Capsule Format"} 601 | 602 | Connection ID Length 603 | : The length of the connection ID being registered, which is between 0 and 604 | 255. Note that in QUICv1, the length of the Connection ID is limited to 20 605 | bytes, but QUIC invariants allow up to 255 bytes. 606 | 607 | Connection ID 608 | : A connection ID being registered whose length is equal to Connection ID 609 | Length. This is the real target CID. 610 | 611 | Stateless Reset Token Length 612 | : The length of the target-provided Stateless Reset Token. 613 | 614 | Stateless Reset Token 615 | : The target-provided Stateless Reset token allowing the proxy to correctly 616 | recognize Stateless Reset packets to be tunnelled to the client. 617 | 618 | ## ACK_CLIENT_CID {#capsule-ack-client} 619 | 620 | The ACK_CLIENT_CID capsule is sent by the proxy in 621 | response to a REGISTER_CLIENT_CID capsule. It optionally assigns a Virtual 622 | Connection ID when forwarded mode is supported. 623 | 624 | ~~~ 625 | Acknowledge Client CID Capsule { 626 | Type (i) = see {{iana}} for the value of the capsule type 627 | Length (i) 628 | Connection ID Length (i) 629 | Connection ID (0..2040), 630 | Virtual Connection ID Length (i) 631 | Virtual Connection ID (0..2040), 632 | } 633 | ~~~ 634 | {: #fig-capsule-ack-client-cid title="Acknowledge Client CID Capsule Format"} 635 | 636 | Connection ID Length 637 | : The length of the connection ID being acknowledged, which 638 | is between 0 and 255. Note that in QUICv1, the length of the Connection ID 639 | is limited to 20 bytes, but QUIC invariants allow up to 255 bytes. 640 | 641 | Connection ID 642 | : A connection ID being acknowledged whose length is equal to 643 | Connection ID Length. This is the real Cilent Connection ID. 644 | 645 | Virtual Connection ID Length 646 | : The length of the client VCID being provided. This MUST be a 647 | valid connection ID length for the QUIC version used in the client-to-proxy QUIC 648 | connection. When forwarded mode is not negotiated, the length MUST be zero. 649 | The Virtual Connection ID Length and Connection ID Length SHOULD be equal 650 | when possible to avoid the need to resize packets during replacement. The 651 | client VCID Length MUST be at least as large as the 652 | Connection ID to reduce the likelihood of connection ID conflicts. 653 | 654 | Virtual Connection ID 655 | : The proxy-chosen connection ID that the proxy MUST use when sending in 656 | forwarded mode. The proxy rewrites forwarded mode packets to contain the 657 | correct client VCID prior to sending them to the client. 658 | 659 | ## ACK_TARGET_CID {#capsule-ack-target} 660 | 661 | The ACK_TARGET_CID capsule is sent by the proxy in 662 | response to a REGISTER_TARGET_CID capsule. It optionally assigns a Virtual 663 | Connection ID and Stateless Reset Token if forwarded mode is enabled. 664 | 665 | ~~~ 666 | Acknowledge Target CID Capsule { 667 | Type (i) = see {{iana}} for the value of the capsule type 668 | Length (i) 669 | Connection ID Length (i) 670 | Connection ID (0..2040), 671 | Virtual Connection ID Length (i) 672 | Virtual Connection ID (0..2040), 673 | Stateless Reset Token Length (i), 674 | Stateless Reset Token (..), 675 | } 676 | ~~~ 677 | {: #fig-capsule-ack-target-cid title="Acknowledge Target CID Capsule Format"} 678 | 679 | Connection ID Length 680 | : The length of the connection ID being acknowledged, which 681 | is between 0 and 255. Note that in QUICv1, the length of the Connection ID 682 | is limited to 20 bytes, but QUIC invariants allow up to 255 bytes. 683 | 684 | Connection ID 685 | : A connection ID being acknowledged whose length is equal to 686 | Connection ID Length. This is the real target CID. 687 | 688 | Virtual Connection ID Length 689 | : The length of the target VCID being provided. This MUST be a 690 | valid connection ID length for the QUIC version used in the client-to-proxy QUIC 691 | connection. When forwarded mode is not negotiated, the length MUST be zero. 692 | The Virtual Connection ID Length and Connection ID Length SHOULD be equal 693 | when possible to avoid the need to resize packets during replacement. 694 | 695 | Virtual Connection ID 696 | : The proxy-chosen connection ID that the client MUST use when sending in 697 | forwarded mode. The proxy rewrites forwarded mode packets to contain the 698 | correct target CID prior to sending them. 699 | 700 | Stateless Reset Token Length 701 | : The length of the Stateless Reset Token sent by the proxy in response to 702 | forwarded mode packets in order to reset the client-to-target QUIC connection. 703 | When forwarded mode is not negotiated, the length MUST be zero. Proxies choosing 704 | not to support stateless resets MAY set the length to zero. Clients receiving a 705 | zero-length stateless reset token MUST ignore it. 706 | 707 | Stateless Reset Token 708 | : A Stateless Reset Token allowing reset of the client-to-target connection in 709 | response to client-to-target forwarded mode packets. 710 | 711 | ## ACK_CLIENT_VCID {#capsule-ack-virtual} 712 | 713 | The ACK_CLIENT_VCID capsule type is sent by the client in 714 | response to an ACK_TARGET_CID capsule that contains a virtual connection ID. 715 | 716 | ~~~ 717 | Acknowledge Client VCID Capsule { 718 | Type (i) = see {{iana}} for the value of the capsule type 719 | Length (i) 720 | Connection ID Length (i) 721 | Connection ID (0..2040), 722 | Virtual Connection ID Length (i) 723 | Virtual Connection ID (0..2040), 724 | Stateless Reset Token Length (i), 725 | Stateless Reset Token (..), 726 | } 727 | ~~~ 728 | {: #fig-capsule-ack-virtual-client-cid title="Acknowledge Client VCID Capsule Format"} 729 | 730 | Connection ID Length 731 | : The length of the connection ID being acknowledged, which 732 | is between 0 and 255. Note that in QUICv1, the length of the Connection ID 733 | is limited to 20 bytes, but QUIC invariants allow up to 255 bytes. 734 | 735 | Connection ID 736 | : A connection ID being acknowledged whose length is equal to 737 | Connection ID Length. This is the real Cilent Connection ID. 738 | 739 | Virtual Connection ID Length 740 | : The length of the client VCID being acknowledged. 741 | 742 | Virtual Connection ID 743 | : The proxy-chosen virtual connection ID being acknowledged whose length is 744 | equal to Virtual Connection ID Length. 745 | 746 | Stateless Reset Token Length 747 | : The length of the Stateless Reset Token that may be sent by the client in 748 | response to forwarded mode packets to reset the client-to-target connection. 749 | Clients choosing not to support stateless resets MAY set the length to zero. 750 | Proxies receiving a zero-length stateless reset token MUST ignore it. 751 | 752 | Stateless Reset Token 753 | : A Stateless Reset Token allowing reset of the target-to-client forwarding rule 754 | in response to target-to-client forwarded mode packets. 755 | 756 | ## CLOSE_CLIENT_CID and CLOSE_TARGET_CID {#capsule-close} 757 | 758 | CLOSE_CLIENT_CID and CLOSE_TARGET_CID capsule types include a single connection ID to close. They 759 | can be sent by either clients or proxies. 760 | 761 | ~~~ 762 | Close CID Capsule { 763 | Type (i) = see {{iana}} for the values of the capsule types 764 | Length (i), 765 | Connection ID (0..2040), 766 | } 767 | ~~~ 768 | {: #fig-capsule-close-cid title="Close CID Capsule Format"} 769 | 770 | Connection ID: 771 | : A connection ID being closed, which is between 0 and 255 bytes in 772 | length. The length of the connection ID is implied by the length of the 773 | capsule. Note that in QUICv1, the length of the Connection ID is limited 774 | to 20 bytes, but QUIC invariants allow up to 255 bytes. 775 | 776 | ## MAX_CONNECTION_IDS {#capsule-max-cids} 777 | 778 | The MAX_CONNECTION_IDS capsule is sent by the proxy 779 | to permit additional connection ID registrations. 780 | 781 | ~~~ 782 | Maximum Connection IDs Capsule { 783 | Type (i) = see {{iana}} for the value of the capsule type 784 | Length (i) 785 | Maximum Sequence Number (i) 786 | } 787 | ~~~ 788 | {: #fig-capsule-max-connection-ids title="Maximum Connection IDs Capsule Format"} 789 | 790 | Maximum Sequence Number 791 | : The maximum permitted sequence number for connection ID registrations. This MUST 792 | NOT be less than 1. 793 | 794 | ## Detecting Conflicts {#conflicts} 795 | 796 | In order to be able to route packets correctly in both tunnelled and forwarded 797 | mode, proxies check for conflicts before creating a new CID mapping. If a conflict 798 | is detected, the proxy will reject the client's request using a CLOSE_CLIENT_CID 799 | or CLOSE_TARGET_CID capsule. 800 | 801 | Two 4-tuples conflict if and only if all members of the 4-tuple (local IP 802 | address, local UDP port, remote IP address, and remote UDP port) are identical. 803 | 804 | Two Connection IDs conflict if and only if one Connection ID is equal to or a 805 | prefix of another. For example, a zero-length Connection ID conflicts with all 806 | connection IDs. This definition of a conflict originates from the fact that 807 | QUIC short headers do not carry the length of the Destination Connection ID 808 | field, and therefore if two short headers with different Destination Connection 809 | IDs are received on a shared 4-tuple, one being a prefix of the other prevents 810 | the receiver from identifying which mapping this corresponds to. 811 | 812 | The proxy treats two mappings as being in conflict when a conflict is detected 813 | for all elements on the left side of the mapping diagrams above. 814 | 815 | Since very short Connection IDs are more likely to lead to conflicts, 816 | particularly zero-length Connection IDs, a proxy MAY choose to reject all 817 | requests for very short Connection IDs as conflicts, in anticipation of future 818 | conflicts. 819 | 820 | ## Client Considerations 821 | 822 | The client sends a REGISTER_CLIENT_CID capsule before it advertises a new 823 | client CID to the target, and a REGISTER_TARGET_CID capsule when 824 | it has received a new target CID for the target. In order to change 825 | the connection ID bytes on the wire, a client can solicit new virtual connection 826 | IDs by re-registering the same connection IDs. The client may solicit a new 827 | target VCID by sending a REGISTER_TARGET_CID capsule with a 828 | previously registered target CID. Similarly, the client may solicit a 829 | new client VCID by sending a REGISTER_CLIENT_CID with a 830 | previously registered client CID. The client MUST acknowledge the new 831 | client VCID with an ACK_CLIENT_VCID capsule or close the 832 | registration. The proxy MUST NOT send in forwarded mode until ACK_CLIENT_VCID 833 | has been received. Clients are responsible for changing Virtual Connection IDs 834 | when the HTTP stream's network path changes to avoid linkability across network 835 | paths. Note that initial REGISTER_CLIENT_CID capsules MAY be sent prior to 836 | receiving an HTTP response from the proxy. 837 | 838 | Connection ID registrations are subject to a proxy-advertised limit. Each registration 839 | has a corresponding sequence number. The client MUST NOT send a registration 840 | capsule with a sequence number greater than what the proxy advertises via the 841 | MAX_CONNECTION_IDS capsule. The initial MAX_CONNECTION_IDS value is 1, allowing both 842 | sequence numbers 0 and 1 for a total of two registrations without receiving a 843 | MAX_CONNECTION_IDS capsule from the proxy. 844 | 845 | Clients that cannot register new connection IDs within a reasonable time due to 846 | the MAX_CONNECTION_IDS limit SHOULD abort the proxied connection by resetting the HTTP 847 | stream with error code NO_ERROR. This may happen, for example, if the target server 848 | sends a NEW_CONNECTION_ID frame with Sequence Number and Retire Prior To equal to the 849 | same value. 850 | 851 | Clients can cease receiving with forwarded mode over an existing tunnel while 852 | retaining the same client-to-target connection by creating a new tunnel with 853 | "Proxy-QUIC-Forwarding" set to "?0" and migrating the client-to-target connection. 854 | 855 | ### New Proxied Connection Setup 856 | 857 | To initiate QUIC-aware proxying, the client sends a REGISTER_CLIENT_CID 858 | capsule containing the initial client CID that the client has 859 | advertised to the target. 860 | 861 | If the mapping is created successfully, the client will receive a 862 | ACK_CLIENT_CID capsule that contains the same client CID that was 863 | requested as well as a client VCID that the client MUST use 864 | when sending forwarded mode packets, assuming forwarded mode is supported. 865 | 866 | If forwarded mode is supported, the client MUST respond with an 867 | ACK_CLIENT_VCID to signal to the proxy that it may start sending forwarded mode 868 | packets. If forwarded mode is not supported, an ACK_CLIENT_VCID capsule MUST 869 | NOT be sent. 870 | 871 | Since clients are always aware whether or not they are using a QUIC proxy, 872 | clients are expected to cooperate with proxies in selecting client CIDs. 873 | A proxy detects a conflict when it is not able to create a unique mapping 874 | using the client CID ({{conflicts}}). It can reject requests that 875 | would cause a conflict and indicate this to the client by replying with a 876 | CLOSE_CLIENT_CID capsule. In order to avoid conflicts, clients SHOULD select 877 | client CIDs of at least 8 bytes in length with unpredictable values. 878 | A client also SHOULD NOT select a client CID that matches the ID used 879 | for the QUIC connection to the proxy, as this inherently creates a conflict. 880 | 881 | If the rejection indicated a conflict due to the client CID, the 882 | client MUST select a new Connection ID before sending a new request, and 883 | generate a new packet. For example, if a client is sending a QUIC Initial 884 | packet and chooses a Connection ID that conflicts with an existing mapping 885 | to the same target server, it will need to generate a new QUIC Initial. 886 | 887 | ### Adding New Client Connection IDs 888 | 889 | Since QUIC connection IDs are chosen by the receiver, an endpoint needs to 890 | communicate its chosen connection IDs to its peer before the peer can start 891 | using them. In QUICv1, this is performed using the NEW_CONNECTION_ID frame. 892 | 893 | Prior to informing the target of a new chosen client CID, the client 894 | MUST send a REGISTER_CLIENT_CID capsule to the proxy containing the new client 895 | CID. 896 | 897 | The client should only inform the target of the new client CID once an 898 | ACK_CLIENT_CID capsule is received that contains the echoed connection ID. 899 | 900 | If forwarded mode is enabled, the client MUST reply to the ACK_CLIENT_CID with 901 | an ACK_CLIENT_VCID capsule with the real and virtual connection IDs along with 902 | an optional Stateless Reset Token. 903 | 904 | ## Proxy Considerations 905 | 906 | The proxy MUST reply to each REGISTER_CLIENT_CID capsule with either 907 | an ACK_CLIENT_CID or CLOSE_CLIENT_CID capsule containing the 908 | Connection ID that was in the registration capsule. 909 | 910 | Similarly, the proxy MUST reply to each REGISTER_TARGET_CID capsule with 911 | either an ACK_TARGET_CID or CLOSE_TARGET_CID capsule containing the 912 | Connection ID that was in the registration capsule. 913 | 914 | The proxy then determines the proxy-to-target 4-tuple to associate with the 915 | client's request. This will generally involve performing a DNS lookup for 916 | the target hostname in the CONNECT request, or finding an existing proxy-to-target 917 | 4-tuple to the authority. The proxy-to-target 4-tuple might already be open due to a 918 | previous request from this client, or another. If the 4-tuple is not already 919 | created, the proxy creates a new one. Proxies can choose to reuse proxy-to-target 920 | 4-tuples across multiple UDP proxying requests, or have a unique proxy-to-target 4-tuple 921 | for every UDP proxying request. If the client did not send a value of "?1" for the 922 | "Proxy-QUIC-Port-Sharing" header, port reuse is not permitted and the proxy MUST allocate 923 | a new UDP 4-tuple. 924 | 925 | If a proxy reuses proxy-to-target 4-tuples, it SHOULD store which authorities 926 | (which could be a domain name or IP address literal) are being accessed over a 927 | particular proxy-to-target 4-tuple so it can avoid performing a new DNS query and 928 | potentially choosing a different target server IP address which could map to a 929 | different target server. 930 | 931 | Proxy-to-target 4-tuples MUST NOT be reused across QUIC and non-QUIC UDP proxy 932 | requests, since it might not be possible to correctly demultiplex or direct 933 | the traffic. Any packets received on a proxy-to-target 4-tuple used for proxying 934 | QUIC that does not correspond to a known CID MUST be dropped. 935 | 936 | When the proxy recieves a REGISTER_CLIENT_CID capsule, it is receiving a 937 | request to be able to route traffic matching the client CID back to 938 | the client using. If the pair of this client CID and the selected 939 | proxy-to-target 4-tuple does not create a conflict, the proxy creates the mapping 940 | and responds with an ACK_CLIENT_CID capsule. If forwarded mode is enabled, the 941 | capsule contains a proxy-chosen client VCID. If forwarded mode 942 | is enabled, and after receiving an ACK_CLIENT_VCID capsule from the client, any 943 | packets received by the proxy from the proxy-to-target 4-tuple that match the 944 | client CID can to be sent to the client after the proxy has replaced 945 | the CID with the client VCID. If forwarded mode is 946 | not supported, the proxy MUST NOT send a client VCID by setting 947 | the length to zero. The proxy MUST use tunnelled mode (HTTP Datagram frames) for 948 | any long header packets. The proxy SHOULD forward directly to the client for any 949 | matching short header packets if forwarding is supported by the client, but the 950 | proxy MAY tunnel these packets in HTTP Datagram frames instead. If the mapping 951 | would create a conflict, the proxy responds with a CLOSE_CLIENT_CID capsule. 952 | 953 | When the proxy recieves a REGISTER_TARGET_CID capsule, it is receiving a 954 | request to allow the client to forward packets to the target. The proxy 955 | generates a target VCID for the client to use when sending 956 | packets in forwarded mode. If forwarded mode is not supported, the proxy MUST 957 | NOT send a target VCID by setting the length to zero. If 958 | forwarded mode is supported, the proxy MUST use a target VCID 959 | that does not introduce a conflict with any other Connection ID on the 960 | client-to-proxy 4-tuple. The proxy creates the mapping and responds with an 961 | ACK_TARGET_CID capsule. Once the successful response is sent, the proxy will 962 | forward any short header packets received on the client-to-proxy 4-tuple that use 963 | the target VCID using the correct proxy-to-target 4-tuple after 964 | first rewriting the target VCID to be the correct target CID. 965 | 966 | Proxies MUST choose unpredictable client and target VCIDs to 967 | avoid forwarding loop attacks. 968 | 969 | The proxy MUST only forward non-tunnelled packets from the client that are QUIC 970 | short header packets (based on the Header Form bit) and have mapped target VCIDs. 971 | Packets sent by the client that are forwarded SHOULD be 972 | considered as activity for restarting QUIC's Idle Timeout {{QUIC}}. 973 | 974 | In order to permit the client to change client-to-target connection IDs, the proxy 975 | SHOULD send MAX_CONNECTION_IDS capsules allowing the client additional connection ID 976 | registrations. 977 | 978 | ### Closing Proxy State 979 | 980 | For any registration capsule for which the proxy has sent an acknowledgement, any 981 | mappings last until either endpoint sends a close capsule or the either side of the 982 | HTTP stream closes. 983 | 984 | A client that no longer wants a given Connection ID to be forwarded by the 985 | proxy sends a CLOSE_CLIENT_CID or CLOSE_TARGET_CID capsule. 986 | 987 | If a client's connection to the proxy is terminated for any reason, all 988 | mappings associated with all requests are removed. 989 | 990 | A proxy can close its proxy-to-target 4-tuple once all UDP proxying requests mapped to 991 | that 4-tuple have been removed. 992 | 993 | # Using Forwarded Mode 994 | 995 | All packets sent in forwarded mode use a transform in which CIDs are switched 996 | into VCIDs, and the contents of packets are either left the same, or modified 997 | ({{transforms}}). 998 | 999 | Forwarded mode also raises special considerations for handling connection 1000 | maintenance ({{maintenance}}), connection migration ({{migration}}), 1001 | ECN markings ({{ecn}}), and stateless resets ({{resets}}). 1002 | 1003 | ## Sending With Forwarded Mode 1004 | 1005 | Support for forwarded mode is determined by the "Proxy-QUIC-Forwarding" header, 1006 | see {{forwarding-header}}. 1007 | 1008 | Once the client has learned the target server's Connection ID, such as in the 1009 | response to a QUIC Initial packet, it can send a REGISTER_TARGET_CID capsule 1010 | containing the target CID to request the ability to forward packets. 1011 | 1012 | The client MUST wait for an ACK_TARGET_CID capsule that contains the echoed 1013 | connection ID and target VCID before using forwarded mode. 1014 | 1015 | Prior to receiving the proxy server response, the client MUST send short header 1016 | packets tunnelled in HTTP Datagram frames. The client MAY also choose to tunnel 1017 | some short header packets even after receiving the successful response. 1018 | 1019 | If the target CID registration is rejected, for example with a 1020 | CLOSE_TARGET_CID capsule, it MUST NOT forward packets to the requested target CID, 1021 | but only use tunnelled mode. The request might also be rejected 1022 | if the proxy does not support forwarded mode or has it disabled by policy. 1023 | 1024 | QUIC long header packets MUST NOT be forwarded. These packets can only be 1025 | tunnelled within HTTP Datagram frames to avoid exposing unnecessary connection 1026 | metadata. 1027 | 1028 | When forwarding, the client sends a QUIC packet with the target VCID 1029 | in the QUIC short header, using the same 4-tuple between client and 1030 | proxy that was used for the main QUIC connection between client and proxy. 1031 | 1032 | When forwarding, the proxy sends a QUIC packet with the client VCID 1033 | in the QUIC short header, using the same 4-tuple between client 1034 | and proxy that was used for the main QUIC connection between client and proxy. 1035 | 1036 | Prior to sending a forwarded mode packet, the sender MUST replace the Connection 1037 | ID with the Virtual Connection ID. If the Virtual Connection ID is larger than 1038 | the Connection ID, the sender MUST extend the length of the packet by the 1039 | difference between the two lengths, to include the entire Virtual Connection ID. 1040 | If the Virtual Connection ID is smaller than the Connection ID, the sender MUST 1041 | shrink the length of the packet by the difference between the two lengths. 1042 | 1043 | Clients and proxies supporting forwarded mode MUST be able to handle Virtual 1044 | Connection IDs of different lengths than the corresponding Connection IDs. 1045 | 1046 | ## Receiving With Forwarded Mode 1047 | 1048 | If the client has indicated support for forwarded mode with the "Proxy-QUIC-Forwarding" 1049 | header, the proxy MAY use forwarded mode for any client CID for which 1050 | it has a valid mapping. 1051 | 1052 | Once a client has sent an ACK_CLIENT_VCID capsule to the proxy, it MUST be 1053 | prepared to receive forwarded short header packets on the 4-tuple between itself 1054 | and the proxy for the specified client VCID. 1055 | 1056 | The client uses the Destination Connection ID field of the received packet to 1057 | determine if the packet was originated by the proxy, or merely forwarded from 1058 | the target. The client replaces the client VCID with the real 1059 | client CID before processing the packet further. 1060 | 1061 | ## Packet Transforms {#transforms} 1062 | 1063 | A packet transform is the procedure applied to encode packets as they are sent 1064 | on the link between the client and proxy, along with the inverse decode step applied 1065 | on receipt. Simple transforms can be modeled as a function as follows: 1066 | 1067 | Inputs: 1068 | 1069 | 1. A QUIC short header packet (after Connection ID remapping). 1070 | 1. The mode (encode or decode). 1071 | 1. The direction (client-to-proxy or proxy-to-client). 1072 | 1. Any configuration information negotiated at startup. 1073 | 1074 | Output: 1075 | 1076 | * A UDP payload that conforms to the QUIC invariants {{?RFC8999}} and does not 1077 | modify the Connection ID. 1078 | 1079 | More complex transform behaviors could have internal state, but no such transforms 1080 | are presented here. 1081 | 1082 | Packet transforms are identified by an IANA-registered name, and negotiated in 1083 | the HTTP headers (see {{forwarding-header}}). This document defines two initial 1084 | transforms: the `identity` transform and the `scramble` transform. 1085 | 1086 | ### The identify transform {#identity-transform} 1087 | 1088 | The `identity` transform does not modify the packet in any way. When this transform 1089 | is in use, a global passive adversary can trivially correlate pairs of packets 1090 | that crossed the forwarder, providing a compact proof that a specific client 1091 | was communicating to a specific target. 1092 | 1093 | The `identity` transform is identified by the value "identity" {{iana-transforms}}. 1094 | 1095 | Use of this transform is NOT RECOMMENDED if the `scramble` transform is supported 1096 | by both the client and the proxy. Implementations MAY choose to not implement or 1097 | support the `identity` transform, depending on the use cases and privacy requirements of 1098 | the deployment. 1099 | 1100 | ### The scramble transform {#scramble-transform} 1101 | 1102 | The `scramble` transform implements length-preserving unauthenticated 1103 | re-encryption of QUIC packets while preserving the QUIC invariants. When 1104 | the `scramble` transform is in use, a global passive adversary cannot simply compare the packet 1105 | contents on both sides of the proxy 1106 | to link the client and target. However, the `scramble` transform does not defend against 1107 | analysis of packet sizes and timing, nor does it protect privacy against an 1108 | active attacker. 1109 | 1110 | Deployments that implement the version of the `scramble` transform defined in this 1111 | document MUST use the value "scramble-dt". The finalized version is expected 1112 | to use the reserved value "scramble" {{iana-transforms}}. 1113 | 1114 | The `scramble` transform is initialized using a 32-byte random symmetric key. 1115 | When offering or selecting this transform, the client and server each 1116 | generate the key that they will use to encrypt scrambled packets and MUST add it to the 1117 | "Proxy-QUIC-Transform" header in an `sf-binary` parameter named "scramble-key". 1118 | If either side receives a `scramble` transform without the "scramble-key" parameter, 1119 | forwarded mode MUST be disabled. 1120 | 1121 | This transform relies on the AES-128 block cipher, which is represented by the 1122 | syntax `AES-ECB(key, plaintext_block)` as in {{?RFC9001}}. The corresponding 1123 | decryption operation is written here as `AES-ECB-inv(key, ciphertext_block)`. 1124 | It also uses AES in Counter Mode ({{SP800-38A}}, Section 6.5), which is 1125 | represented by the syntax `AES-CTR(key, iv, input)` for encryption and 1126 | decryption (which are identical). In this syntax, `iv` is an array of 16 bytes 1127 | containing the initial counter block. The counter is incremented by the 1128 | standard incrementing function ({{SP800-38A}}, Appendix B.1) on the full block 1129 | width. 1130 | 1131 | In brief, the transform applies AES in counter mode (AES-CTR) using an 1132 | initialization vector drawn from the packet, then encrypts the initialization 1133 | vector with AES-ECB. The detailed procedure is as follows: 1134 | 1135 | 1. Let `k1, k2 = scramble_key[:16], scramble_key[16:32]`. 1136 | 1. Let `L` be the Connection ID length. 1137 | 1. Let `cid = packet[1:L+1]`, i.e., the Connection ID. 1138 | 1. Let `iv = packet[L+1:L+17]`, i.e., the 16 bytes following the Connection ID. 1139 | 1. Let `ctr_input = packet[0] | packet[L+17:]`. 1140 | 1. Let `ctr_output = AES-CTR(k1, iv, ctr_input)`. 1141 | 1. Let `header = ctr_output[0] & 0x7F`. This ensures that the Header Form bit 1142 | is zero, as required by the QUIC invariants ({{?RFC8999}}, Section 5.2). 1143 | 1. Encrypt `iv` with the block cipher: `encrypted_iv = AES-ECB(k2, iv)`. 1144 | 1. Produce the output packet as:\\ 1145 | `header | cid | encrypted_iv | ctr_output[1:]`. 1146 | 1147 | The inverse transform operates as follows: 1148 | 1149 | 1. Decrypt the AES-CTR initialization vector:\\ 1150 | `iv = AES-ECB-inv(k2, packet[L+1:L+17])`. 1151 | 1. Compute the other variables exactly as in the forward transform. 1152 | (AES-CTR encryption and decryption are identical.) 1153 | 1. Produce the output: `header | cid | iv | ctr_output[1:]`. 1154 | 1155 | The encryption keys used in this procedure do not depend on the packet contents, 1156 | so each party only needs to perform AES initialization once for each connection. 1157 | 1158 | NOTE: The security of this arrangement relies on every short-header QUIC packet 1159 | containing a distinct 16 bytes following the Connection ID. This is true 1160 | for the original ciphersuites of QUICv1, but it is not guaranteed by the QUIC 1161 | Invariants. Future ciphersuites and QUIC versions could in principle produce 1162 | packets that are too short or repeat the values at this location. When using the 1163 | `scramble` transform, clients MUST NOT offer any configuration that could 1164 | cause the client or target to violate this requirement. 1165 | 1166 | ## Connection Maintenance in Forwarded Mode {#maintenance} 1167 | 1168 | When a client and proxy are using forwarded mode, it is possible that there can be 1169 | long periods of time in which no ack-eliciting packets 1170 | (see {{Section 2 of !QUIC-RETRANSMISSION=RFC9002}}) are exchanged 1171 | between the client and proxy. If these periods extend beyond the effective idle 1172 | timeout for the client-to-proxy QUIC connection (see {{Section 10.1 of QUIC}}), 1173 | the QUIC connection might be closed by the proxy if the proxy does not use 1174 | forwarded packets as an explicit liveness signal. To avoid this, clients SHOULD 1175 | send keepalive packets to the proxy before the idle timeouts would be reached, 1176 | which can be done using a PING frame or another ack-eliciting frame as described 1177 | in {{Section 10.1.1 of QUIC}}. 1178 | 1179 | ## Handling Connection Migration {#migration} 1180 | 1181 | If a proxy supports QUIC connection migration, it needs to ensure that a migration 1182 | event does not end up sending too many tunnelled or forwarded packets on a new 1183 | path prior to path validation. 1184 | 1185 | Specifically, the proxy MUST limit the number of packets that it will proxy 1186 | to an unvalidated client address to the size of an initial congestion window. 1187 | Proxies additionally SHOULD pace the rate at which packets are sent over a new 1188 | path to avoid creating unintentional congestion on the new path. 1189 | 1190 | When operating in forwarded mode, the proxy reconfigures or removes forwarding 1191 | rules as the network path between the client and proxy changes. In the event of 1192 | passive migration, the proxy automatically reconfigures forwarding rules to use 1193 | the latest active and validated network path for the HTTP stream. In the event of 1194 | active migration, the proxy removes forwarding rules in order to not send 1195 | packets with the same connection ID bytes over multiple network paths. After 1196 | initiating active migration, clients are no longer able to send forwarded mode 1197 | packets since the proxy will have removed forwarding rules. Clients can proceed with 1198 | tunnelled mode or can request new forwarding rules via REGISTER_CLIENT_CID and 1199 | REGISTER_TARGET_CID capsules. Each of the acknowledging capsules will contain new 1200 | virtual connection IDs to prevent packets with the same connection ID bytes being 1201 | used over multiple network paths. Note that the client CID and target CID 1202 | can stay the same while the target VCID and client VCID change. 1203 | 1204 | ## Handling ECN Marking {#ecn} 1205 | 1206 | Explicit Congestion Notification marking {{!ECN=RFC3168}} uses two bits in the IP 1207 | header to signal congestion from a network to endpoints. When using forwarded mode, 1208 | the proxy replaces IP headers for packets exchanged between the client and target; 1209 | these headers can include ECN markings. Proxies SHOULD preserve ECN markings on 1210 | forwarded packets in both directions, to allow ECN to function end-to-end. If the proxy does not 1211 | preserve ECN markings, it MUST set ECN marks to zero on the IP headers it generates. 1212 | 1213 | Forwarded mode does not create an IP-in-IP tunnel, so the guidance in 1214 | {{?ECN-TUNNEL=RFC6040}} about transferring ECN markings between inner and outer IP 1215 | headers does not apply. 1216 | 1217 | A proxy MAY additionally add ECN markings to signal congestion being experienced 1218 | on the proxy itself. 1219 | 1220 | ## Stateless Resets for Forwarded Mode QUIC Packets {#resets} 1221 | 1222 | While the lifecycle of forwarding rules are bound to the lifecycle of the 1223 | client-to-proxy HTTP stream, a peer may not be aware that the stream has 1224 | terminated. If the above mappings are lost or removed without the peer's 1225 | knowledge, they may send forwarded mode packets even though the client 1226 | or proxy no longer has state for that connection. To allow the client or 1227 | proxy to reset the client-to-target connection in the absence of the mappings 1228 | above, a stateless reset token corresponding to the Virtual Connection ID 1229 | can be provided. 1230 | 1231 | Consider a proxy that initiates closure of a client-to-proxy QUIC connection. 1232 | If the client is temporarily unresponsive or unreachable, the proxy might have 1233 | considered the connection closed and removed all connection state (including 1234 | the stream mappings used for forwarding). If the client never learned about the closure, it 1235 | might send forwarded mode packets to the proxy, assuming the stream mappings 1236 | and client-to-proxy connection are still intact. The proxy will receive these 1237 | forwarded mode packets, but won't have any state corresponding to the 1238 | destination connection ID in the packet. If the proxy has provided a stateless 1239 | reset token for the target VCID, it can send a stateless reset 1240 | packet to quickly notify the client that the client-to-target connection is 1241 | broken. 1242 | 1243 | ### Stateless Resets from the Target 1244 | 1245 | Reuse of proxy-to-target 4-tuples is only possible because QUIC connection IDs 1246 | allow distinguishing packets for multiple QUIC connections received with the 1247 | same 5-tuple. One exception to this is Stateless Reset packets, in which the 1248 | connection ID is not used, but rather populated with unpredictable bits followed 1249 | by a Stateless Reset token, to make it indistinguishable from a regular packet 1250 | with a short header. In order for the proxy to correctly recognize Stateless 1251 | Reset packets, the client SHOULD share the Stateless Reset token for each 1252 | registered target CID. When the proxy receives a Stateless Reset packet, 1253 | it can send the packet to the client as a tunnelled datagram. Although Stateless Reset packets 1254 | look like short header packets, they are not technically short header packets and do not contain 1255 | negotiated connection IDs, and thus are not eligible for forwarded mode. 1256 | 1257 | # Example Exchange 1258 | 1259 | Consider a client that is establishing a new QUIC connection through the proxy. 1260 | In this example, the client prefers the `scramble` transform, but also offers the `identity` 1261 | transform. It has selected a client CID of `0x31323334`. In order to inform a proxy 1262 | of the new QUIC client CID, the client also sends a 1263 | REGISTER_CLIENT_CID capsule. 1264 | 1265 | The client will also send the initial QUIC packet with the Long Header form in 1266 | an HTTP datagram. 1267 | 1268 | ~~~ 1269 | Client Server 1270 | 1271 | STREAM(44): HEADERS --------> 1272 | :method = CONNECT 1273 | :protocol = connect-udp 1274 | :scheme = https 1275 | :path = /target.example.com/443/ 1276 | :authority = proxy.example.org 1277 | proxy-quic-port-sharing = ?1 1278 | proxy-quic-forwarding = ?1; accept-transform=scramble,identity; \ 1279 | scramble-key=:abc...789=: 1280 | capsule-protocol = ?1 1281 | 1282 | STREAM(44): DATA --------> 1283 | Capsule Type = REGISTER_CLIENT_CID 1284 | Connection ID = 0x31323334 1285 | Stateless Reset Token = Token 1286 | 1287 | <-------- STREAM(44): DATA 1288 | Capsule Type = MAX_CONNECTION_IDS 1289 | Maximum Sequence Number = 3 1290 | 1291 | DATAGRAM --------> 1292 | Quarter Stream ID = 11 1293 | Context ID = 0 1294 | Payload = Encapsulated QUIC initial 1295 | 1296 | <-------- STREAM(44): HEADERS 1297 | :status = 200 1298 | proxy-quic-forwarding = ?1; \ 1299 | transform=scramble; \ 1300 | scramble-key=:ABC...321=: 1301 | capsule-protocol = ?1 1302 | 1303 | <-------- STREAM(44): DATA 1304 | Capsule Type = ACK_CLIENT_CID 1305 | Connection ID = 0x31323334 1306 | Virtual CID = 0x62646668 1307 | ~~~ 1308 | 1309 | The proxy has acknowledged the client CID and provided a client VCID. 1310 | Even if there were Short Header packets to send, the proxy 1311 | cannot send forwarded mode packets because the client hasn't acknowledged the 1312 | client VCID. 1313 | 1314 | The proxy indicates to the client that it will allow connection ID registrations 1315 | with sequence numbers 0-3, allowing for registrations beyond the initial maximum 1316 | of 1. 1317 | 1318 | ~~~ 1319 | STREAM(44): DATA --------> 1320 | Capsule Type = ACK_CLIENT_VCID 1321 | Connection ID = 0x31323334 1322 | Virtual CID = 0x62646668 1323 | Stateless Reset Token = Token 1324 | ~~~ 1325 | 1326 | The client acknowledges the client VCID. The proxy still 1327 | doesn't have any Short Header Packets to send, but, if it did, it would be able 1328 | to send with forwarded mode. 1329 | 1330 | ~~~ 1331 | /* Wait for target server to respond to UDP packet. */ 1332 | 1333 | <-------- DATAGRAM 1334 | Quarter Stream ID = 11 1335 | Context ID = 0 1336 | Payload = Encapsulated QUIC initial 1337 | 1338 | /* All Client -> Target QUIC packets must still be encapsulated */ 1339 | 1340 | DATAGRAM --------> 1341 | Quarter Stream ID = 11 1342 | Context ID = 0 1343 | Payload = Encapsulated QUIC packet 1344 | 1345 | /* Forwarded mode packets possible in Target -> Client direction */ 1346 | 1347 | <-------- UDP Datagram 1348 | Payload = Forwarded QUIC SH packet 1349 | 1350 | ~~~ 1351 | 1352 | The client may receive forwarded mode packets from the proxy with a Virtual 1353 | client CID of 0x62646668 which it will replace with the real client CID 1354 | of 0x31323334. All forwarded mode packets sent by the proxy 1355 | will have been modified to contain the client VCID instead 1356 | of the client CID, and processed by the negotiated "scramble" 1357 | packet transform. However, in the unlikely event that a forwarded packet 1358 | arrives before the proxy's HTTP response, the client will not know which 1359 | transform the proxy selected. In this case, the client will have to ignore 1360 | the packet or buffer it until the HTTP response is received. 1361 | 1362 | Once the client learns which Connection ID has been selected by the target 1363 | server, it can send a new request to the proxy to establish a mapping for 1364 | forwarding. In this case, that ID is 0x61626364. The client sends the 1365 | following capsule: 1366 | 1367 | ~~~ 1368 | STREAM(44): DATA --------> 1369 | Capsule Type = REGISTER_TARGET_CID 1370 | Connection ID = 0x61626364 1371 | 1372 | <-------- STREAM(44): DATA 1373 | Capsule Type = ACK_TARGET_CID 1374 | Connection ID = 0x61626364 1375 | Virtual Connection ID = 0x123412341234 1376 | Stateless Reset Token = Token 1377 | 1378 | /* Client -> Target QUIC short header packets may use forwarded mode */ 1379 | 1380 | UDP Datagram --------> 1381 | Payload = Forwarded QUIC SH packet 1382 | 1383 | ~~~ 1384 | 1385 | Upon receiving an ACK_TARGET_CID capsule, the client starts sending Short Header 1386 | packets with a Destination Connection ID of 0x123412341234 directly to the proxy 1387 | (not tunnelled), and these are rewritten by the proxy to have the Destination 1388 | Connection ID 0x61626364 prior to being forwarded directly to the target. In the 1389 | reverse direction, Short Header packets from the target with a Destination 1390 | Connection ID of 0x31323334 are modified to replace the Destination Connection 1391 | ID with the client VCID of 0x62646668 and forwarded directly to 1392 | the client. 1393 | 1394 | # Packet Size Considerations 1395 | 1396 | Since Initial QUIC packets must be at least 1200 bytes in length, the HTTP 1397 | Datagram frames that are used for a QUIC-aware proxy MUST be able to carry at least 1398 | 1200 bytes. 1399 | 1400 | Additionally, clients that connect to a proxy for purpose of proxying QUIC 1401 | SHOULD start their connection with a larger packet size than 1200 bytes, to 1402 | account for the overhead of tunnelling an Initial QUIC packet within an 1403 | HTTP Datagram frame. If the client does not begin with a larger packet size than 1404 | 1200 bytes, it will need to perform Path MTU (Maximum Transmission Unit) 1405 | discovery to discover a larger path size prior to sending any tunnelled Initial 1406 | QUIC packets. 1407 | 1408 | Once a proxied QUIC connections moves into forwarded mode, the client SHOULD 1409 | initiate Path MTU discovery to increase its end-to-end MTU. 1410 | 1411 | # Security Considerations {#security} 1412 | 1413 | Proxies that support this extension SHOULD provide protections to rate-limit 1414 | or restrict clients from opening an excessive number of proxied connections, so 1415 | as to limit abuse or use of proxies to launch Denial-of-Service attacks. 1416 | 1417 | Sending QUIC packets by forwarding through a proxy without tunnelling exposes 1418 | clients to additional information exposure and deanonymization attacks which 1419 | need to be carefully considered. Analysis should consider both passive and 1420 | active attackers which may be global or localized to the network paths used 1421 | on one side of the proxy. The following sections highlight deanonymization risks with 1422 | using forwarded mode. 1423 | 1424 | ## Passive Attacks 1425 | 1426 | A passive attacker aims to deanonymize a client by correlating traffic across 1427 | both sides of the proxy. When using forwarded mode with the `identity` packet 1428 | transform (see {{identity-transform}}), such correlation is trivial by matching 1429 | a subset of QUIC packet bytes as packets enter the proxy on one side and exit 1430 | on the other. Packet transforms such as `scramble` mitigate this by 1431 | cryptographically preventing such byte comparisons 1432 | (see {{!scramble-transform=scramble-transform}}). 1433 | 1434 | Regardless of which packet transform is used, both tunnelled and forwarded mode 1435 | are still vulnerable to size and timing attacks, without the addition of techniques that go beyond the analysis 1436 | in this document, such as padding and adding chaff packets. Such techniques could be supported 1437 | in future packet transforms, subject to additional security analysis. 1438 | 1439 | Unlike tunnelled mode where packets are fully encapsulated in the client-to-proxy 1440 | connection, clients using forwarded mode to access multiple target servers 1441 | over the same client-to-proxy connection expose the number of target servers 1442 | they are communicating with on each connection to passive attackers that can 1443 | observe the client-to-proxy traffic. This additional metadata revealed on each 1444 | packet simplifies size and timing attacks. 1445 | 1446 | ## Active Attacks 1447 | 1448 | An active attacker is an adversary that can inject, modify, drop, and view 1449 | packets in the network. Some active attacks have different effects between 1450 | forwarded mode and tunnelled mode, but active attacks can be used to correlate 1451 | flows in either mode. 1452 | 1453 | Both tunnelled mode and forwarded mode (regardless of packet transform) are 1454 | vulnerable to packet injection in the target-to-client direction. An attacker 1455 | can inject a burst of packets with a known QUIC Connection ID and see which 1456 | Connection ID is used for the corresponding burst on the proxy-to-client network path. 1457 | 1458 | Packet injection with a known QUIC Connection ID can also happen in the 1459 | client-to-proxy direction, which only affects forwarded mode since 1460 | tunnelled mode sends packets within an authenticated and integrity protected 1461 | QUIC connection to the proxy (see {{?RFC9001}}). None of the packet transforms 1462 | defined in this document provide integrity protection. Even if a packet 1463 | transform did provide integrity protection, attackers can inject replayed 1464 | packets. Protection against replayed packets is similarly provided by QUIC in 1465 | tunnelled mode, but not provided by any of the forwarded mode packet transforms 1466 | defined in this document. 1467 | 1468 | An active attacker can modify packets in the client-to-proxy direction, which 1469 | would cause a tunnelling proxy to silently drop packets, while a forwarding proxy 1470 | would forward the packets. In this way, forwarded mode is less vulnerable to 1471 | flow recognition based on corrupting a portion of packets in a burst. 1472 | 1473 | Chaining of proxies using forwarded mode introduces the risk of forwarding loop 1474 | attacks. Preventing client VCID conflicts across proxies 1475 | sharing an IP address and port mitigates one such forwarding loop attack. 1476 | Conflicts can be avoided by partitioning the client VCID space 1477 | across proxies, using sufficiently long and random values, or by other means. 1478 | 1479 | [comment1]: # OPEN ISSUE: Figure out how clients and proxies could interact to 1480 | [comment2]: # learn whether an adversary is injecting malicious forwarded 1481 | [comment3]: # packets to induce rate limiting. 1482 | 1483 | # IANA Considerations {#iana} 1484 | 1485 | ## HTTP Header Field {#iana-header} 1486 | 1487 | This document registers the "Proxy-QUIC-Forwarding" header field in the 1488 | "Hypertext Transfer Protocol (HTTP) Field Name Registry" 1489 | <[](https://www.iana.org/assignments/http-fields)>. 1490 | 1491 | ~~~ 1492 | +-----------------------+-----------+-----------------+---------------+----------+ 1493 | | Field Name | Status | Structured Type | Reference | Comments | 1494 | +-----------------------+-----------+-----------------+---------------+----------+ 1495 | | Proxy-QUIC-Forwarding | permanent | Item | This document | None | 1496 | +-----------------------+-----------+-----------------+---------------+----------+ 1497 | ~~~ 1498 | {: #iana-header-type-table title="Registered HTTP Header Field"} 1499 | 1500 | ## Proxy QUIC Forwarding Parameter Names 1501 | 1502 | This document establishes a new registry, "Proxy QUIC Forwarding Parameter Names", 1503 | for parameter names to use with the "Proxy-QUIC-Forwarding" header field, 1504 | in <[](https://www.iana.org/assignments/masque/masque.xhtml)>. 1505 | Registrations in this registry are assigned using the 1506 | Specification Required policy (Section 4.6 of [IANA-POLICY]). 1507 | 1508 | ~~~ 1509 | +-----------------------+-------------------------------------+---------------+--------------------------------+ 1510 | | Parameter Name | Description | Reference | Notes | 1511 | +-----------------------+-------------------------------------+---------------+--------------------------------+ 1512 | | accept-transform | contains supported transforms | This document | Section {{forwarding-header}} | 1513 | +-----------------------+-------------------------------------+---------------+--------------------------------+ 1514 | | transform | indicates selected transforms | This document | Section {{forwarding-header}} | 1515 | +-----------------------+-------------------------------------+---------------+--------------------------------+ 1516 | | scramble-key | contains key for scramble transform | This document | Section {{scramble-transform}} | 1517 | +-----------------------+-------------------------------------+---------------+--------------------------------+ 1518 | ~~~ 1519 | {: #iana-parameter-names-table title="Initial Proxy QUIC Forwarding Parameter Names"} 1520 | 1521 | ## Packet Transform Names {#iana-transforms} 1522 | 1523 | This document establishes a new registry for packet transform names 1524 | in <[](https://www.iana.org/assignments/masque/masque.xhtml)> 1525 | and defines two initial transforms: "identity" and "scramble". 1526 | Prior to finalization, deployments that implement the version of 1527 | the `scramble` transform defined in this document should use the value 1528 | "scramble-dt". Once the design team proposal is adopted and a new draft is submitted, 1529 | the wire identifier will become "scramble-XX" where XX is the draft number. 1530 | Registrations in this registry are assigned using the 1531 | Specification Required policy (Section 4.6 of [IANA-POLICY]). 1532 | 1533 | | Transform Name | Description | Specification | Notes | 1534 | |:---------------|:------------------|:--------------|--------------------------------| 1535 | | identity | no transformation | This Document | Section {{identity-transform}} | 1536 | | scramble | Reserved (will be used for final version) | This Document | Section {{scramble-transform}} | 1537 | {: #iana-packet-transforms-table title="Initial Packet Transform Names"} 1538 | 1539 | ## Capsule Types {#iana-capsule-types} 1540 | 1541 | This document registers six new values in the "HTTP Capsule Types" 1542 | registry established by {{HTTP-CAPSULES}}. Note that the codepoints below 1543 | will be replaced with lower values before publication. 1544 | 1545 | | Capule Type | Value | Specification | 1546 | |:--------------------|:----------|:--------------| 1547 | | REGISTER_CLIENT_CID | 0xffe600 | This Document | 1548 | | REGISTER_TARGET_CID | 0xffe601 | This Document | 1549 | | ACK_CLIENT_CID | 0xffe602 | This Document | 1550 | | ACK_CLIENT_VCID | 0xffe603 | This Document | 1551 | | ACK_TARGET_CID | 0xffe604 | This Document | 1552 | | CLOSE_CLIENT_CID | 0xffe605 | This Document | 1553 | | CLOSE_TARGET_CID | 0xffe606 | This Document | 1554 | | MAX_CONNECTION_IDS | 0xffe607 | This Document | 1555 | {: #iana-capsule-type-table title="Registered Capsule Types"} 1556 | 1557 | All of these new entries use the following values for these fields: 1558 | 1559 | Status: 1560 | 1561 | : provisional (permanent when this document is published) 1562 | 1563 | Reference: 1564 | 1565 | : This document 1566 | 1567 | Change Controller: 1568 | 1569 | : IETF 1570 | 1571 | Contact: 1572 | 1573 | : masque@ietf.org 1574 | 1575 | Notes: 1576 | 1577 | : None 1578 | {: spacing="compact" newline="false"} 1579 | 1580 | --- back 1581 | 1582 | # Acknowledgments {#acknowledgments} 1583 | {:numbered="false"} 1584 | 1585 | Thanks to Lucas Pardue, Ryan Hamilton, and Mirja Kühlewind for their inputs 1586 | on this document. 1587 | -------------------------------------------------------------------------------- /interop/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Mark Nottingham 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /interop/asset/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/099c163983dd6512f7db641a36b09c0abb6d5368/interop/asset/badge.png -------------------------------------------------------------------------------- /interop/asset/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/099c163983dd6512f7db641a36b09c0abb6d5368/interop/asset/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /interop/asset/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/099c163983dd6512f7db641a36b09c0abb6d5368/interop/asset/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /interop/asset/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/099c163983dd6512f7db641a36b09c0abb6d5368/interop/asset/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /interop/asset/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/099c163983dd6512f7db641a36b09c0abb6d5368/interop/asset/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /interop/asset/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ietf-wg-masque/draft-ietf-masque-quic-proxy/099c163983dd6512f7db641a36b09c0abb6d5368/interop/asset/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /interop/asset/marked.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * marked - a markdown parser 3 | * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT Licensed) 4 | * https://github.com/markedjs/marked 5 | */ 6 | !function(e){"use strict";var k={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:f,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,nptable:f,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|\\n*|\\n*|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)|(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,table:f,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/,text:/^[^\n]+/};function a(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||m.defaults,this.rules=k.normal,this.options.pedantic?this.rules=k.pedantic:this.options.gfm&&(this.options.tables?this.rules=k.tables:this.rules=k.gfm)}k._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,k._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,k.def=i(k.def).replace("label",k._label).replace("title",k._title).getRegex(),k.bullet=/(?:[*+-]|\d{1,9}\.)/,k.item=/^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/,k.item=i(k.item,"gm").replace(/bull/g,k.bullet).getRegex(),k.list=i(k.list).replace(/bull/g,k.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+k.def.source+")").getRegex(),k._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",k._comment=//,k.html=i(k.html,"i").replace("comment",k._comment).replace("tag",k._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),k.paragraph=i(k.paragraph).replace("hr",k.hr).replace("heading",k.heading).replace("lheading",k.lheading).replace("tag",k._tag).getRegex(),k.blockquote=i(k.blockquote).replace("paragraph",k.paragraph).getRegex(),k.normal=d({},k),k.gfm=d({},k.normal,{fences:/^ {0,3}(`{3,}|~{3,})([^`\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),k.gfm.paragraph=i(k.paragraph).replace("(?!","(?!"+k.gfm.fences.source.replace("\\1","\\2")+"|"+k.list.source.replace("\\1","\\3")+"|").getRegex(),k.tables=d({},k.gfm,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),k.pedantic=d({},k.normal,{html:i("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",k._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/}),a.rules=k,a.lex=function(e,t){return new a(t).lex(e)},a.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},a.prototype.token=function(e,t){var n,r,s,i,l,o,a,h,p,u,c,g,f,d,m,b;for(e=e.replace(/^ +$/gm,"");e;)if((s=this.rules.newline.exec(e))&&(e=e.substring(s[0].length),1 ?/gm,""),this.token(s,t),this.tokens.push({type:"blockquote_end"});else if(s=this.rules.list.exec(e)){for(e=e.substring(s[0].length),a={type:"list_start",ordered:d=1<(i=s[2]).length,start:d?+i:"",loose:!1},this.tokens.push(a),n=!(h=[]),f=(s=s[0].match(this.rules.item)).length,c=0;c?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:f,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(href(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*"<\[])\*(?!\*)|^_([^\s][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s"<\[][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:f,text:/^(`+|[^`])[\s\S]*?(?=[\\?@\\[^_{|}~",n.em=i(n.em).replace(/punctuation/g,n._punctuation).getRegex(),n._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=i(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=i(n.tag).replace("comment",k._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?/,n._href=/\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f\\]*\)|[^\s\x00-\x1f()\\])*?)/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=i(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=i(n.reflink).replace("label",n._label).getRegex(),n.normal=d({},n),n.pedantic=d({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:i(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:i(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=d({},n.normal,{escape:i(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:i(n.text).replace("]|","~]|").replace("|$","|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&'*+/=?^_`{\\|}~-]+@|$").getRegex()}),n.gfm.url=i(n.gfm.url,"i").replace("email",n.gfm._extended_email).getRegex(),n.breaks=d({},n.gfm,{br:i(n.br).replace("{2,}","*").getRegex(),text:i(n.gfm.text).replace("{2,}","*").getRegex()}),h.rules=n,h.output=function(e,t,n){return new h(t,n).output(e)},h.prototype.output=function(e){for(var t,n,r,s,i,l,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=u(i[1]);else if(i=this.rules.tag.exec(e))!this.inLink&&/^/i.test(i[0])&&(this.inLink=!1),!this.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(i[0])?this.inRawBlock=!0:this.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(i[0])&&(this.inRawBlock=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):u(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,r=i[2],this.options.pedantic?(t=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r))?(r=t[1],s=t[3]):s="":s=i[3]?i[3].slice(1,-1):"",r=r.trim().replace(/^<([\s\S]*)>$/,"$1"),o+=this.outputLink(i,{href:h.escapes(r),title:h.escapes(s)}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[4]||i[3]||i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[6]||i[5]||i[4]||i[3]||i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(u(i[2].trim(),!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),r="@"===i[2]?"mailto:"+(n=u(this.mangle(i[1]))):n=u(i[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.text.exec(e))e=e.substring(i[0].length),this.inRawBlock?o+=this.renderer.text(i[0]):o+=this.renderer.text(u(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===i[2])r="mailto:"+(n=u(i[0]));else{for(;l=i[0],i[0]=this.rules._backpedal.exec(i[0])[0],l!==i[0];);n=u(i[0]),r="www."===i[1]?"http://"+n:n}e=e.substring(i[0].length),o+=this.renderer.link(r,null,n)}return o},h.escapes=function(e){return e?e.replace(h.rules._escapes,"$1"):e},h.prototype.outputLink=function(e,t){var n=t.href,r=t.title?u(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,u(e[1]))},h.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},h.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,s=0;s'+(n?e:u(e,!0))+"\n":"
"+(n?e:u(e,!0))+"
"},r.prototype.blockquote=function(e){return"
\n"+e+"
\n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n,r){return this.options.headerIds?"'+e+"\n":""+e+"\n"},r.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},r.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},r.prototype.checkbox=function(e){return" "},r.prototype.paragraph=function(e){return"

    "+e+"

    \n"},r.prototype.table=function(e,t){return t&&(t=""+t+""),"\n\n"+e+"\n"+t+"
    \n"},r.prototype.tablerow=function(e){return"\n"+e+"\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"},r.prototype.strong=function(e){return""+e+""},r.prototype.em=function(e){return""+e+""},r.prototype.codespan=function(e){return""+e+""},r.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},r.prototype.del=function(e){return""+e+""},r.prototype.link=function(e,t,n){if(null===(e=l(this.options.sanitize,this.options.baseUrl,e)))return n;var r='
    "},r.prototype.image=function(e,t,n){if(null===(e=l(this.options.sanitize,this.options.baseUrl,e)))return n;var r=''+n+'":">"},r.prototype.text=function(e){return e},s.prototype.strong=s.prototype.em=s.prototype.codespan=s.prototype.del=s.prototype.text=function(e){return e},s.prototype.link=s.prototype.image=function(e,t,n){return""+n},s.prototype.br=function(){return""},p.parse=function(e,t){return new p(t).parse(e)},p.prototype.parse=function(e){this.inline=new h(e.links,this.options),this.inlineText=new h(e.links,d({},this.options,{renderer:new s})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},p.prototype.next=function(){return this.token=this.tokens.pop()},p.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},p.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},p.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,c(this.inlineText.output(this.token.text)),this.slugger);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s="",i="";for(n="",e=0;e?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t))for(var n=t;this.seen[n]++,t=n+"-"+this.seen[n],this.seen.hasOwnProperty(t););return this.seen[t]=0,t},u.escapeTest=/[&<>"']/,u.escapeReplace=/[&<>"']/g,u.replacements={"&":"&","<":"<",">":">",'"':""","'":"'"},u.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,u.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var o={},g=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function f(){}function d(e){for(var t,n,r=1;rt)n.splice(t);else for(;n.lengthAn error occurred:

    "+u(e.message+"",!0)+"
    ";throw e}}f.exec=f,m.options=m.setOptions=function(e){return d(m.defaults,e),m},m.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tables:!0,xhtml:!1}},m.defaults=m.getDefaults(),m.Parser=p,m.parser=p.parse,m.Renderer=r,m.TextRenderer=s,m.Lexer=a,m.lexer=a.lex,m.InlineLexer=h,m.inlineLexer=h.output,m.Slugger=t,m.parse=m,"undefined"!=typeof module&&"object"==typeof exports?module.exports=m:"function"==typeof define&&define.amd?define(function(){return m}):e.marked=m}(this||("undefined"!=typeof window?window:global)); -------------------------------------------------------------------------------- /interop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MASQUE Interoperability Tests 6 | 7 | 42 | 43 | 44 |

    MASQUE Interoperability Tests

    45 | 46 |

    These results are used for reporting interop testing only. This is intended for use tracking the results of IETF hackathon events.

    47 | 48 |

    49 | Requirement passed   50 | SHOULD requirement failed   51 | MUST requirement failed   52 | Feature supported   53 | Feature not supported   54 | Not tested 55 |

    56 | 57 |
      58 |
    59 | 60 |
    61 | 62 |
    63 |
    64 | 65 | 66 | -------------------------------------------------------------------------------- /interop/lib/display.mjs: -------------------------------------------------------------------------------- 1 | /* global Blob marked */ 2 | 3 | import '../asset/marked.min.js' 4 | 5 | export function downloadTestResults (target, fileName, data, auto) { 6 | const dataBlob = new Blob([JSON.stringify(data, null, 2)], { type: 'text/json' }) 7 | target.setAttribute('href', window.URL.createObjectURL(dataBlob)) 8 | target.setAttribute('download', fileName) 9 | target.style.display = 'inherit' 10 | if (auto) { 11 | target.click() 12 | } 13 | } 14 | 15 | export function renderTestResults (testSuites, testResults, testUUIDs, target, useBrowserCache) { 16 | let totalTests = 0 17 | let totalPassed = 0 18 | testSuites.forEach(testSuite => { 19 | const headerElement = document.createElement('h3') 20 | target.appendChild(headerElement) 21 | const headerText = document.createTextNode(testSuite.name) 22 | headerElement.appendChild(headerText) 23 | const listElement = document.createElement('ul') 24 | const resultList = target.appendChild(listElement) 25 | let tests = 0 26 | let passed = 0 27 | testSuite.tests.forEach(test => { 28 | if (test.browser_only === true && !useBrowserCache === true) return 29 | if (test.browser_skip === true && useBrowserCache === true) return 30 | test.suiteName = testSuite.name 31 | const testElement = resultList.appendChild(document.createElement('li')) 32 | testElement.appendChild(showTestResult(testSuites, test.id, testResults)) 33 | testElement.appendChild(showTestName(test, testUUIDs[test.id])) 34 | tests++ 35 | if (testResults[test.id] === true) { 36 | passed++ 37 | } 38 | }) 39 | const summaryElement = document.createElement('p') 40 | const suiteSummary = target.appendChild(summaryElement) 41 | suiteSummary.appendChild(document.createTextNode(tests + ' tests, ' + passed + ' passed.')) 42 | totalTests += tests 43 | totalPassed += passed 44 | }) 45 | const totalElement = document.createElement('p') 46 | const totalSummary = target.appendChild(totalElement) 47 | const totalText = document.createTextNode('Total ' + totalTests + ' tests, ' + totalPassed + ' passed.') 48 | totalSummary.appendChild(totalText) 49 | } 50 | 51 | export function showTestName (test, uuid) { 52 | const wrapper = document.createElement('span') 53 | const span = document.createElement('span') 54 | span.innerHTML = marked.parse(test.name).slice(3, -5) 55 | wrapper.appendChild(span) 56 | 57 | if (uuid) { 58 | const uuidLinkElement = document.createElement('a') 59 | uuidLinkElement.appendChild(document.createTextNode('⚙︎')) 60 | uuidLinkElement.setAttribute('class', 'clickhint') 61 | uuidLinkElement.title = 'Test UUID (click to copy)' 62 | wrapper.appendChild(uuidLinkElement) 63 | } 64 | return wrapper 65 | } 66 | 67 | export function showKey (element) { 68 | const spans = element.getElementsByClassName('fa') 69 | for (const span of spans) { 70 | const kind = span.getAttribute('data-kind') 71 | const styling = resultTypes[kind] 72 | const contentNode = document.createTextNode(styling[0]) 73 | span.style.color = styling[1] 74 | span.appendChild(contentNode) 75 | } 76 | } 77 | 78 | export function showTestResult (testSuites, testId, testResults) { 79 | const result = testResults[testId] 80 | const resultValue = determineTestResult(testSuites, testId, testResults) 81 | const resultNode = document.createTextNode(` ${resultValue[0]} `) 82 | const span = document.createElement('span') 83 | span.className = 'fa' 84 | span.style.color = resultValue[1] 85 | span.appendChild(resultNode) 86 | if (result && typeof (result[1]) === 'string') { 87 | span.title = result[1] 88 | } 89 | return span 90 | } 91 | 92 | const resultTypes = { 93 | untested: ['-', '', '-'], 94 | pass: ['\uf058', '#1aa123', '✅'], 95 | must_fail: ['\uf057', '#c33131', '⛔️'], 96 | should_fail: ['\uf05a', '#bbbd15', '⚠️'], 97 | yes: ['\uf055', '#999696', 'Y'], 98 | no: ['\uf056', '#999696', 'N'] 99 | } 100 | const passTypes = [resultTypes.pass, resultTypes.yes] 101 | 102 | export function determineTestResult (testSuites, testId, testResults) { 103 | const test = testLookup(testSuites, testId) 104 | const result = testResults[testId] 105 | if (result === undefined) { 106 | return resultTypes.untested 107 | } 108 | if (test.kind === 'must' || test.kind === undefined) { 109 | if (result === true) { 110 | return resultTypes.pass 111 | } else { 112 | return resultTypes.must_fail 113 | } 114 | } else if (test.kind === 'should') { 115 | if (result === true) { 116 | return resultTypes.pass 117 | } else { 118 | return resultTypes.should_fail 119 | } 120 | } else if (test.kind === 'supported') { 121 | if (result === true) { 122 | return resultTypes.yes 123 | } else { 124 | return resultTypes.no 125 | } 126 | } else { 127 | throw new Error(`Unrecognised test kind ${test.kind}`) 128 | } 129 | } 130 | 131 | export function testLookup (testSuites, testId) { 132 | for (const testSuite of testSuites) { 133 | for (const test of testSuite.tests) { 134 | if (test.id === testId) { 135 | return test 136 | } 137 | } 138 | } 139 | throw new Error(`Cannot find test ${testId}`) 140 | } 141 | -------------------------------------------------------------------------------- /interop/lib/modal.mjs: -------------------------------------------------------------------------------- 1 | 2 | export function modalOpen (content) { 3 | let modal = document.getElementById('modal') 4 | if (!modal) { 5 | modal = document.createElement('div') 6 | modal.classList.add('modal') 7 | modal.id = 'modal' 8 | } 9 | modal.classList.add('modal-open') 10 | modal.innerHTML = content 11 | const closeButton = document.createElement('button') 12 | const closeText = document.createTextNode('❎') 13 | closeButton.appendChild(closeText) 14 | closeButton.classList.add('modal-exit') 15 | closeButton.addEventListener('click', function (event) { 16 | event.preventDefault() 17 | modal.classList.remove('modal-open') 18 | }) 19 | modal.appendChild(closeButton) 20 | document.body.appendChild(modal) 21 | document.onkeydown = function (evt) { 22 | evt = evt || window.event 23 | if (evt.key === 'Escape' || evt.key === 'Esc') { 24 | modal.classList.remove('modal-open') 25 | document.onkeydown = function () {} 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /interop/lib/summary.mjs: -------------------------------------------------------------------------------- 1 | /* global fetch marked */ 2 | 3 | import '../asset/marked.min.js' 4 | import * as display from './display.mjs' 5 | 6 | export function loadResults (index) { 7 | return Promise.all(index.map(item => 8 | fetch(`results/${item.file}`) 9 | .then(response => { 10 | return response.json() 11 | }) 12 | .then(results => { 13 | item.results = results 14 | return item 15 | } 16 | )) 17 | ) 18 | } 19 | 20 | export function showResults (target, testSuites, results, testIds, suiteIds) { 21 | const isDefault = testIds.length === 0 && suiteIds.length === 0 22 | testSuites.forEach(testSuite => { 23 | const selectedTests = [] 24 | const suiteTestIds = [] 25 | testSuite.tests.forEach(test => { 26 | if (isDefault || suiteIds.includes(testSuite.id)) { 27 | selectedTests.push(test) 28 | suiteTestIds.push(test.id) 29 | } 30 | if (isDefault === 0 || testIds.includes(test.id)) { 31 | if (!suiteTestIds.includes(test.id)) { 32 | selectedTests.push(test) 33 | } 34 | } 35 | }) 36 | if (selectedTests.length) { 37 | showHeader(testSuite, results).forEach(row => { 38 | target.appendChild(row) 39 | }) 40 | selectedTests.forEach(test => { 41 | const result = showTest(testSuites, test.id, results) 42 | if (target.childElementCount % 2) { 43 | result.setAttribute('class', 'shade') 44 | } 45 | target.appendChild(result) 46 | }) 47 | } 48 | }) 49 | } 50 | 51 | export function showToC (target, testSuites) { 52 | testSuites.forEach(testSuite => { 53 | const suiteLink = document.createElement('a') 54 | suiteLink.href = '#' + testSuite.id 55 | suiteLink.appendChild(document.createTextNode(testSuite.name)) 56 | const suiteLi = document.createElement('li') 57 | suiteLi.appendChild(suiteLink) 58 | target.appendChild(suiteLi) 59 | }) 60 | } 61 | 62 | function showHeader (testSuite, results) { 63 | const rows = [] 64 | const numCols = results.length + 2 65 | const blankRow = tableRow() 66 | blankRow.appendChild(emptyCell(numCols)) 67 | rows.push(blankRow) 68 | const headerRow = tableRow() 69 | headerRow.appendChild(tableCell('th', '\xa0', 'name category')) 70 | const headerLink = document.createElement('a') 71 | headerLink.href = '#' + testSuite.id 72 | headerLink.appendChild(document.createTextNode(testSuite.name)) 73 | const firstHeader = tableCell('th', headerLink, 'name category') 74 | firstHeader.id = testSuite.id 75 | headerRow.appendChild(firstHeader) 76 | results.forEach(implementation => { 77 | headerRow.appendChild(tableCell('th', implementation.name, 'category', implementation.version, implementation.link)) 78 | }) 79 | rows.push(headerRow) 80 | if (testSuite.description !== undefined) { 81 | const descriptionRow = tableRow() 82 | const drCells = emptyCell(numCols) 83 | drCells.innerHTML = marked.parse(testSuite.description).slice(3, -5) 84 | descriptionRow.appendChild(drCells) 85 | rows.push(descriptionRow) 86 | } 87 | return rows 88 | } 89 | 90 | function showTest (testSuites, testId, results) { 91 | const test = display.testLookup(testSuites, testId) 92 | const testRow = tableRow() 93 | testRow.appendChild(tableCell('td', testSelector(test.id))) 94 | testRow.appendChild(tableCell('th', display.showTestName(test), 'name')) 95 | results.forEach(implementation => { 96 | testRow.appendChild( 97 | tableCell('th', display.showTestResult(testSuites, test.id, implementation.results))) 98 | }) 99 | return testRow 100 | } 101 | 102 | function tableRow (CssClass) { 103 | const rowElement = document.createElement('tr') 104 | if (CssClass) { 105 | rowElement.setAttribute('class', CssClass) 106 | } 107 | return rowElement 108 | } 109 | 110 | function tableCell (cellType, content, CssClass, hint, link, colspan) { 111 | const cellElement = document.createElement(cellType) 112 | if (CssClass) { 113 | cellElement.setAttribute('class', CssClass) 114 | } 115 | if (colspan) { 116 | cellElement.colSpan = colspan 117 | } 118 | let contentNode 119 | if (typeof (content) === 'string') { 120 | contentNode = document.createTextNode(content) 121 | } else { 122 | contentNode = content 123 | } 124 | if (link) { 125 | const linkElement = document.createElement('a') 126 | linkElement.setAttribute('href', link) 127 | linkElement.appendChild(contentNode) 128 | cellElement.appendChild(linkElement) 129 | } else { 130 | cellElement.appendChild(contentNode) 131 | } 132 | if (hint) { 133 | cellElement.title = hint 134 | } 135 | return cellElement 136 | } 137 | 138 | function testSelector (testId) { 139 | const checkbox = document.createElement('input') 140 | checkbox.type = 'checkbox' 141 | checkbox.name = 'id' 142 | checkbox.value = testId 143 | checkbox.style.display = 'none' 144 | checkbox.setAttribute('class', 'select') 145 | return checkbox 146 | } 147 | 148 | function emptyCell (numCols = 1) { 149 | return tableCell('td', '\xa0', undefined, undefined, undefined, numCols) 150 | } 151 | -------------------------------------------------------------------------------- /interop/parse_interop_tags.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import sys 4 | import json 5 | 6 | def is_valid_option(option): 7 | return ("req" in option and "req-id" in option and "req-type" in option) 8 | 9 | def create_test_from_option(option): 10 | assert(is_valid_option(option)) 11 | return { 12 | "name": option["req"], 13 | "id": option["req-id"], 14 | "kind": option["req-type"], 15 | } 16 | 17 | def create_test_summary_from_options(fname, options): 18 | valid_options = filter(lambda option : is_valid_option(option), options) 19 | tests = [create_test_from_option(option) for option in valid_options] 20 | data = { 21 | "name": draft_title(fname), 22 | "id": draft_to_test_name(fname), 23 | "description": "Interoperability test results for " + draft_title(fname), 24 | "tests": tests, 25 | } 26 | return data 27 | 28 | def draft_to_test_name(fname): 29 | return os.path.basename(fname).split(".")[0] + ".mjs" 30 | 31 | def draft_title(fname): 32 | with open(fname, "r") as fh: 33 | return re.search(r'title: (.*?)\n', fh.read()).group(1).strip() 34 | 35 | def parse_options_from_draft(draft_name): 36 | with open(draft_name, "r") as fh: 37 | filedata = fh.read() 38 | option_pattern = re.compile(r'\{::options(.*?)/\}') 39 | options = [] 40 | for option in re.findall(option_pattern, filedata): 41 | data = option.split(" ") 42 | entry = {} 43 | pattern = re.compile(r' (.*?)=\"(.*?)\"') 44 | for k, v in re.findall(pattern, option): 45 | entry[k] = v 46 | options.append(entry) 47 | return options 48 | 49 | def parse_options(draft_name, output_path): 50 | options = parse_options_from_draft(draft_name) 51 | test_summary = create_test_summary_from_options(draft_name, options) 52 | with open(os.path.join(output_path, draft_to_test_name(draft_name)), "w") as fh: 53 | fh.write("export default\n") 54 | fh.write(json.dumps(test_summary, indent=4)) 55 | 56 | if __name__ == '__main__': 57 | draft_name = sys.argv[1] 58 | output_path = sys.argv[2] 59 | parse_options(draft_name, output_path) -------------------------------------------------------------------------------- /interop/results/index.mjs: -------------------------------------------------------------------------------- 1 | 2 | export default [ 3 | { 4 | file: 'nwfw.json', 5 | name: 'Network.framework', 6 | }, 7 | { 8 | file: 'quiche.json', 9 | name: 'Quiche', 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /interop/results/nwfw.json: -------------------------------------------------------------------------------- 1 | { 2 | "connect-udp-authority": true, 3 | "connect-udp-no-scheme-path": true, 4 | "connect-udp-no-content-length": true, 5 | "connect-udp-datagram": false, 6 | "connect-udp-flow-id-echo": true, 7 | "connect-udp-lv": false, 8 | "connect-udp-validate-tuple": true, 9 | "connect-udp-datagram-flow-id": true, 10 | "connect-udp-datagram-flow-id-match": true, 11 | "connect-udp-quic-cids": true, 12 | "connect-udp-quic-client-cid": true, 13 | "connect-udp-early-data": true, 14 | "connect-udp-forwarded-mode": true, 15 | "connect-udp-forwarded-sh": true, 16 | "connect-udp-forwarded-sh-wait": true, 17 | "connect-udp-forwarded-rejected": true, 18 | "connect-udp-forwarded-rejected": false, 19 | "connect-udp-forwarded-receive": true, 20 | "connect-udp-headers-validate": true, 21 | "connect-udp-proxy-forwarded-mode": true, 22 | "connect-udp-headers-echo": true, 23 | "connect-udp-proxy-idle": false, 24 | "connect-udp-close-stream": true 25 | } 26 | -------------------------------------------------------------------------------- /interop/results/quiche.json: -------------------------------------------------------------------------------- 1 | { 2 | "connect-udp-authority": true, 3 | "connect-udp-no-scheme-path": true, 4 | "connect-udp-no-content-length": true, 5 | "connect-udp-datagram": false, 6 | "connect-udp-flow-id-echo": true, 7 | "connect-udp-lv": false, 8 | "connect-udp-validate-tuple": true 9 | } 10 | -------------------------------------------------------------------------------- /interop/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 22px; 5 | } 6 | 7 | body { 8 | margin: 2em 4em 10em 4em; 9 | } 10 | 11 | h1 { 12 | line-height: 1.2em; 13 | } 14 | 15 | table { 16 | border-spacing: 0; 17 | } 18 | 19 | th { 20 | font-weight: normal; 21 | padding: 0.2em 0.4em; 22 | } 23 | 24 | ul { 25 | list-style-type: none; 26 | } 27 | 28 | ul#ToC { 29 | column-count: 4; 30 | } 31 | 32 | th.category a { 33 | color: white; 34 | } 35 | 36 | .category { 37 | background-color: black; 38 | color: white; 39 | font-weight: bold; 40 | } 41 | 42 | .name { 43 | margin: 0; 44 | text-align: right; 45 | } 46 | 47 | .clickhint { 48 | color: #999; 49 | margin-left: 0.5em; 50 | } 51 | 52 | .hint { 53 | color: #999; 54 | font-style: normal; 55 | } 56 | 57 | .shade { 58 | background-color: #fafafa; 59 | } 60 | 61 | .description { 62 | background-color: #eee; 63 | padding: 3px 6px; 64 | } 65 | 66 | .warning { 67 | padding: 0.5em; 68 | margin: 0.5em auto; 69 | background-color: #fde2e3; 70 | border-left: solid 4px #f47477; 71 | } 72 | 73 | .key-item { 74 | white-space: nowrap; 75 | } 76 | 77 | #download { 78 | display: none; 79 | } 80 | 81 | #key { 82 | position: fixed; 83 | bottom: 0; 84 | width: 80vw; 85 | left: 10vw; 86 | background-color: white; 87 | margin-bottom: 0; 88 | padding: 10px; 89 | border: 1px solid #999; 90 | border-bottom: none; 91 | } 92 | 93 | input.select { 94 | margin-left: 6px; 95 | } 96 | 97 | code { 98 | white-space: nowrap; 99 | } 100 | 101 | pre code { 102 | white-space: pre; 103 | } 104 | 105 | @font-face { 106 | font-family: 'FontAwesome'; 107 | src: url('asset/fonts/fontawesome-webfont.eot?v=4.7.0'); 108 | src: url('asset/fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('asset/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('asset/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('asset/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('asset/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); 109 | font-weight: normal; 110 | font-style: normal; 111 | } 112 | .fa { 113 | display: inline-block; 114 | font: normal normal normal 18px/22px FontAwesome; 115 | text-rendering: auto; 116 | -webkit-font-smoothing: antialiased; 117 | -moz-osx-font-smoothing: grayscale; 118 | margin: 0 10px; 119 | } 120 | 121 | .modal { 122 | position: fixed; 123 | top: 50%; 124 | left: 50%; 125 | width: 50vw; 126 | height: 70vh; 127 | transform: translate(-50%, -50%); 128 | background-color: #fafafa; 129 | opacity: 0; 130 | visibility: hidden; 131 | transition: all 0.3s ease; 132 | align-items: center; 133 | justify-content: center; 134 | padding: 2em; 135 | border: 2px solid #aaa; 136 | border-radius: 0.5em; 137 | overflow-y: auto; 138 | } 139 | .modal-open { 140 | visibility: visible; 141 | opacity: 1; 142 | transition-delay: 0s; 143 | } 144 | 145 | .modal-exit { 146 | position: absolute; 147 | right: 15px; 148 | top: 15px; 149 | outline: none; 150 | appearance: none; 151 | color: red; 152 | background: none; 153 | border: 0px; 154 | font-weight: bold; 155 | font-size: 1.5em; 156 | cursor: pointer; 157 | } 158 | 159 | .modal ul { 160 | list-style-type: disc; 161 | } 162 | 163 | .modal pre { 164 | background-color: #ddd; 165 | padding: 0.5em 1em; 166 | width: auto; 167 | } 168 | 169 | .noFrame { 170 | display: none; 171 | } 172 | -------------------------------------------------------------------------------- /interop/tests/draft-ietf-masque-connect-udp.mjs: -------------------------------------------------------------------------------- 1 | export default 2 | { 3 | "description": "Interoperability test results for The CONNECT-UDP HTTP Method", 4 | "tests": [ 5 | { 6 | "kind": "must", 7 | "name": "Client sends `:authority` in `CONNECT-UDP` requests", 8 | "id": "connect-udp-authority" 9 | }, 10 | { 11 | "kind": "must", 12 | "name": "Client does not include `:scheme` and `:path` in `CONNECT-UDP` requests", 13 | "id": "connect-udp-no-scheme-path" 14 | }, 15 | { 16 | "kind": "must", 17 | "name": "Server does not include `Transfer-Encoding` or `Content-Length` in `CONNECT-UDP` responses", 18 | "id": "connect-udp-no-content-length" 19 | }, 20 | { 21 | "kind": "must", 22 | "name": "Client rejects responses that include `Transfer-Encoding` or `Content-Length` in `CONNECT-UDP` responses", 23 | "id": "connect-udp-reject-content-length" 24 | }, 25 | { 26 | "kind": "supported", 27 | "name": "Supports HTTP/3 datagrams by sending `H3_DATAGRAM` in `SETTINGS`", 28 | "id": "connect-udp-datagram" 29 | }, 30 | { 31 | "kind": "must", 32 | "name": "Proxy echoes `Datagram-Flow-Id` header in successful replies", 33 | "id": "connect-udp-flow-id-echo" 34 | }, 35 | { 36 | "kind": "supported", 37 | "name": "When HTTP/3 datagrams are not supported, sends datagrams on a stream using a 16-bit length field", 38 | "id": "connect-udp-lv" 39 | }, 40 | { 41 | "kind": "must", 42 | "name": "Server discards UDP packets that do not match any client's request", 43 | "id": "connect-udp-validate-tuple" 44 | } 45 | ], 46 | "name": "The CONNECT-UDP HTTP Method", 47 | "id": "draft-ietf-masque-connect-udp.mjs" 48 | } -------------------------------------------------------------------------------- /interop/tests/draft-pauly-masque-quic-proxy.mjs: -------------------------------------------------------------------------------- 1 | export default 2 | { 3 | "description": "Interoperability test results for QUIC-Aware Proxying Using CONNECT-UDP", 4 | "tests": [ 5 | { 6 | "kind": "must", 7 | "name": "Client sends `Datagram-Flow-Id` in all `CONNECT-UDP` requests", 8 | "id": "connect-udp-datagram-flow-id" 9 | }, 10 | { 11 | "kind": "must", 12 | "name": "Client sends the same `:authority` in all requests for the same connection", 13 | "id": "connect-udp-authority" 14 | }, 15 | { 16 | "kind": "should", 17 | "name": "Client sends the same `Datagram-Flow-Id` in all requests for the same connection", 18 | "id": "connect-udp-datagram-flow-id-match" 19 | }, 20 | { 21 | "kind": "must", 22 | "name": "Client sends exactly one `*-Connection-Id ` in each request", 23 | "id": "connect-udp-quic-cids" 24 | }, 25 | { 26 | "kind": "must", 27 | "name": "Client sends `Client-Connection-Id` in first `CONNECT-UDP` request", 28 | "id": "connect-udp-quic-client-cid" 29 | }, 30 | { 31 | "kind": "supported", 32 | "name": "Client sends `DATAGRAM` frames in the same flight as the `CONNECT-UDP` request", 33 | "id": "connect-udp-early-data" 34 | }, 35 | { 36 | "kind": "must", 37 | "name": "Client does not reuse a `Datagram-Flow-Id` that has been rejected", 38 | "id": "connect-udp-rejected-flow-id" 39 | }, 40 | { 41 | "kind": "must", 42 | "name": "Client does not reuse a `Client-Connection-Id` that has been rejected", 43 | "id": "connect-udp-rejected-conn-id" 44 | }, 45 | { 46 | "kind": "must", 47 | "name": "Client sends a new request prior to sending `NEW_CONNECTION_ID` frames", 48 | "id": "connect-udp-new-client-cid" 49 | }, 50 | { 51 | "kind": "supported", 52 | "name": "Client supports forwarded mode", 53 | "id": "connect-udp-forwarded-mode" 54 | }, 55 | { 56 | "kind": "must", 57 | "name": "Client only uses forwarded mode with short header packets", 58 | "id": "connect-udp-forwarded-sh" 59 | }, 60 | { 61 | "kind": "must", 62 | "name": "Client does not use forwarded mode prior to receiving a `2xx (OK)`", 63 | "id": "connect-udp-forwarded-sh-wait" 64 | }, 65 | { 66 | "kind": "must", 67 | "name": "Client does not use forwarded mode if the server sends a `409 (Conflict)`", 68 | "id": "connect-udp-forwarded-rejected" 69 | }, 70 | { 71 | "kind": "should", 72 | "name": "Client stops sending requests for forwarded mode if the server sends an error other than `409 (Conflict)`", 73 | "id": "connect-udp-forwarded-stop" 74 | }, 75 | { 76 | "kind": "must", 77 | "name": "Client supports receiving forwarded packets from proxy", 78 | "id": "connect-udp-forwarded-receive" 79 | }, 80 | { 81 | "kind": "must", 82 | "name": "Proxy rejects requests without correct headers with `400 (Bad Request)`", 83 | "id": "connect-udp-headers-validate" 84 | }, 85 | { 86 | "kind": "supported", 87 | "name": "Proxy supports forwarded mode", 88 | "id": "connect-udp-proxy-forwarded-mode" 89 | }, 90 | { 91 | "kind": "must", 92 | "name": "Proxy echoes `Datagram-Flow-Id` and `*-Connection-Id` headers in successful replies", 93 | "id": "connect-udp-headers-echo" 94 | }, 95 | { 96 | "kind": "should", 97 | "name": "Proxy uses forwarded packets to restart its idle timeout", 98 | "id": "connect-udp-proxy-idle" 99 | }, 100 | { 101 | "kind": "must", 102 | "name": "Proxy closes HTTP stream when it rejects a `CONNECT-UDP` request", 103 | "id": "connect-udp-close-stream" 104 | } 105 | ], 106 | "name": "QUIC-Aware Proxying Using CONNECT-UDP", 107 | "id": "draft-pauly-masque-quic-proxy.mjs" 108 | } 109 | -------------------------------------------------------------------------------- /interop/tests/index.mjs: -------------------------------------------------------------------------------- 1 | import connectUDP from './draft-ietf-masque-connect-udp.mjs' 2 | import quicProxy from './draft-pauly-masque-quic-proxy.mjs' 3 | 4 | export default [connectUDP, quicProxy] 5 | --------------------------------------------------------------------------------