pax_global_header 0000666 0000000 0000000 00000000064 15074232574 0014523 g ustar 00root root 0000000 0000000 52 comment=843469cff83e8e11da5554d0296ecd11f45b8b50
golang-github-lucas-clemente-quic-go-0.55.0/ 0000775 0000000 0000000 00000000000 15074232574 0020602 5 ustar 00root root 0000000 0000000 golang-github-lucas-clemente-quic-go-0.55.0/.clusterfuzzlite/ 0000775 0000000 0000000 00000000000 15074232574 0024136 5 ustar 00root root 0000000 0000000 golang-github-lucas-clemente-quic-go-0.55.0/.clusterfuzzlite/Dockerfile 0000664 0000000 0000000 00000001113 15074232574 0026124 0 ustar 00root root 0000000 0000000 FROM gcr.io/oss-fuzz-base/base-builder-go:v1
ARG TARGETPLATFORM
RUN echo "TARGETPLATFORM: ${TARGETPLATFORM}"
ENV GOVERSION=1.25.0
RUN platform=$(echo ${TARGETPLATFORM} | tr '/' '-') && \
filename="go${GOVERSION}.${platform}.tar.gz" && \
wget https://dl.google.com/go/${filename} && \
mkdir temp-go && \
rm -rf /root/.go/* && \
tar -C temp-go/ -xzf ${filename} && \
mv temp-go/go/* /root/.go/ && \
rm -r ${filename} temp-go
RUN apt-get update && apt-get install -y make autoconf automake libtool
COPY . $SRC/quic-go
WORKDIR quic-go
COPY .clusterfuzzlite/build.sh $SRC/
golang-github-lucas-clemente-quic-go-0.55.0/.clusterfuzzlite/build.sh 0000775 0000000 0000000 00000000755 15074232574 0025603 0 ustar 00root root 0000000 0000000 #!/bin/bash -eu
export CXX="${CXX} -lresolv" # required by Go 1.20
compile_go_fuzzer github.com/quic-go/quic-go/fuzzing/frames Fuzz frame_fuzzer
compile_go_fuzzer github.com/quic-go/quic-go/fuzzing/header Fuzz header_fuzzer
compile_go_fuzzer github.com/quic-go/quic-go/fuzzing/transportparameters Fuzz transportparameter_fuzzer
compile_go_fuzzer github.com/quic-go/quic-go/fuzzing/tokens Fuzz token_fuzzer
compile_go_fuzzer github.com/quic-go/quic-go/fuzzing/handshake Fuzz handshake_fuzzer
golang-github-lucas-clemente-quic-go-0.55.0/.clusterfuzzlite/project.yaml 0000664 0000000 0000000 00000000015 15074232574 0026464 0 ustar 00root root 0000000 0000000 language: go
golang-github-lucas-clemente-quic-go-0.55.0/.githooks/ 0000775 0000000 0000000 00000000000 15074232574 0022507 5 ustar 00root root 0000000 0000000 golang-github-lucas-clemente-quic-go-0.55.0/.githooks/README.md 0000664 0000000 0000000 00000000231 15074232574 0023762 0 ustar 00root root 0000000 0000000 # Git Hooks
This directory contains useful Git hooks for working with quic-go.
Install them by running
```bash
git config core.hooksPath .githooks
```
golang-github-lucas-clemente-quic-go-0.55.0/.githooks/pre-commit 0000775 0000000 0000000 00000001622 15074232574 0024512 0 ustar 00root root 0000000 0000000 #!/bin/bash
# Check that test files don't contain focussed test cases.
errored=false
for f in $(git diff --diff-filter=d --cached --name-only); do
if [[ $f != *_test.go ]]; then continue; fi
output=$(git show :"$f" | grep -n -e "FIt(" -e "FContext(" -e "FDescribe(")
if [ $? -eq 0 ]; then
echo "$f contains a focussed test:"
echo "$output"
echo ""
errored=true
fi
done
pushd ./integrationtests/gomodvendor > /dev/null
go mod tidy
if [[ -n $(git diff --diff-filter=d --name-only -- "go.mod" "go.sum") ]]; then
echo "go.mod / go.sum in integrationtests/gomodvendor not tidied"
errored=true
fi
popd > /dev/null
# Check that all Go files are properly gofumpt-ed.
output=$(gofumpt -d $(git diff --diff-filter=d --cached --name-only -- '*.go'))
if [ -n "$output" ]; then
echo "Found files that are not properly gofumpt-ed."
echo "$output"
errored=true
fi
if [ "$errored" = true ]; then
exit 1
fi
golang-github-lucas-clemente-quic-go-0.55.0/.github/ 0000775 0000000 0000000 00000000000 15074232574 0022142 5 ustar 00root root 0000000 0000000 golang-github-lucas-clemente-quic-go-0.55.0/.github/FUNDING.yml 0000664 0000000 0000000 00000001464 15074232574 0023764 0 ustar 00root root 0000000 0000000 # These are supported funding model platforms
github: [marten-seemann] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
golang-github-lucas-clemente-quic-go-0.55.0/.github/dependabot.yml 0000664 0000000 0000000 00000000166 15074232574 0024775 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15074232574 0024177 5 ustar 00root root 0000000 0000000 golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/build-interop-docker.yml 0000664 0000000 0000000 00000002773 15074232574 0030755 0 ustar 00root root 0000000 0000000 name: Build interop Docker image
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' }}
jobs:
interop:
runs-on: ${{ fromJSON(vars['DOCKER_RUNNER_UBUNTU'] || '"ubuntu-latest"') }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Login to Docker Hub
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: set tag name
id: tag
# Tagged releases won't be picked up by the interop runner automatically,
# but they can be useful when debugging regressions.
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "tag=${GITHUB_REF#refs/tags/}" | tee -a $GITHUB_OUTPUT;
else
echo 'tag=latest' | tee -a $GITHUB_OUTPUT;
fi
- uses: docker/build-push-action@v6
with:
context: "."
file: "interop/Dockerfile"
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'push' }}
tags: martenseemann/quic-go-interop:${{ steps.tag.outputs.tag }}
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/clusterfuzz-lite-pr.yml 0000664 0000000 0000000 00000003501 15074232574 0030673 0 ustar 00root root 0000000 0000000 name: ClusterFuzzLite PR fuzzing
on:
pull_request:
paths:
- '**'
permissions: read-all
jobs:
PR:
runs-on: ${{ fromJSON(vars['CLUSTERFUZZ_LITE_RUNNER_UBUNTU'] || '"ubuntu-latest"') }}
concurrency:
group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
sanitizer:
- address
steps:
- name: Build Fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: go
github-token: ${{ secrets.GITHUB_TOKEN }}
sanitizer: ${{ matrix.sanitizer }}
# Optional but recommended: used to only run fuzzers that are affected
# by the PR.
# See later section on "Git repo for storage".
# storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git
# storage-repo-branch: main # Optional. Defaults to "main"
# storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages".
- name: Run Fuzzers (${{ matrix.sanitizer }})
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 480
mode: 'code-change'
sanitizer: ${{ matrix.sanitizer }}
output-sarif: true
parallel-fuzzing: true
# Optional but recommended: used to download the corpus produced by
# batch fuzzing.
# See later section on "Git repo for storage".
# storage-repo: https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/OWNER/STORAGE-REPO-NAME.git
# storage-repo-branch: main # Optional. Defaults to "main"
# storage-repo-branch-coverage: gh-pages # Optional. Defaults to "gh-pages".
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/cross-compile.sh 0000775 0000000 0000000 00000001502 15074232574 0027313 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -e
dist="$1"
goos=$(echo "$dist" | cut -d "/" -f1)
goarch=$(echo "$dist" | cut -d "/" -f2)
# cross-compiling for android is a pain...
if [[ "$goos" == "android" ]]; then exit; fi
# iOS builds require Cgo, see https://github.com/golang/go/issues/43343
# Cgo would then need a C cross compilation setup. Not worth the hassle.
if [[ "$goos" == "ios" ]]; then exit; fi
# Write all log output to a temporary file instead of to stdout.
# That allows running this script in parallel, while preserving the correct order of the output.
log_file=$(mktemp)
error_handler() {
cat "$log_file" >&2
rm "$log_file"
exit 1
}
trap 'error_handler' ERR
echo "$dist" >> "$log_file"
out="main-$goos-$goarch"
GOOS=$goos GOARCH=$goarch go build -o $out example/main.go >> "$log_file" 2>&1
rm $out
cat "$log_file"
rm "$log_file"
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/cross-compile.yml 0000664 0000000 0000000 00000003273 15074232574 0027506 0 ustar 00root root 0000000 0000000 on: [push, pull_request]
jobs:
crosscompile:
strategy:
fail-fast: false
matrix:
go: [ "1.24.x", "1.25.x" ]
runs-on: ${{ fromJSON(vars['CROSS_COMPILE_RUNNER_UBUNTU'] || '"ubuntu-latest"') }}
name: "Cross Compilation (Go ${{matrix.go}})"
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Get Date
id: get-date
run: echo "date=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_OUTPUT
- name: Load Go build cache
id: load-go-cache
uses: actions/cache/restore@v4
with:
path: ~/.cache/go-build
key: go-${{ matrix.go }}-crosscompile-${{ steps.get-date.outputs.date }}
restore-keys: go-${{ matrix.go }}-crosscompile-
- name: Install build utils
run: |
sudo apt-get update
sudo apt-get install -y gcc-multilib
- name: Install dependencies
run: go build example/main.go
- name: Run cross compilation
# run in parallel on as many cores as are available on the machine
run: go tool dist list | xargs -I % -P "$(nproc)" .github/workflows/cross-compile.sh %
- name: Save Go build cache
# only store cache when on master
if: github.event_name == 'push' && github.ref_name == 'master'
uses: actions/cache/save@v4
with:
path: ~/.cache/go-build
# Caches are immutable, so we only update it once per day (at most).
# See https://github.com/actions/cache/blob/main/tips-and-workarounds.md#update-a-cache
key: go-${{ matrix.go }}-crosscompile-${{ steps.get-date.outputs.date }}
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/go-generate.sh 0000775 0000000 0000000 00000001114 15074232574 0026730 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
set -e
# delete all go-generated files (that adhere to the comment convention)
git ls-files -z | grep --include \*.go -lrIZ "^// Code generated .* DO NOT EDIT\.$" | tr '\0' '\n' | xargs rm -f
# First regenerate sys_conn_buffers_write.go.
# If it doesn't exist, the following mockgen calls will fail.
go generate -run "sys_conn_buffers_write.go"
# now generate everything
go generate ./...
# Check if any files were changed
git diff --exit-code || (
echo "Generated files are not up to date. Please run 'go generate ./...' and commit the changes."
exit 1
)
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/integration.yml 0000664 0000000 0000000 00000010632 15074232574 0027247 0 ustar 00root root 0000000 0000000 on: [push, pull_request]
jobs:
integration:
strategy:
fail-fast: false
matrix:
os: [ "ubuntu" ]
go: [ "1.24.x", "1.25.x" ]
race: [ false ]
use32bit: [ false ]
include:
- os: "ubuntu"
go: "1.25.x"
race: true
- os: "ubuntu"
go: "1.25.x"
use32bit: true
- os: "windows"
go: "1.25.x"
race: false
- os: "macos"
go: "1.25.x"
race: false
runs-on: ${{ fromJSON(vars[format('INTEGRATION_RUNNER_{0}', matrix.os)] || format('"{0}-latest"', matrix.os)) }}
timeout-minutes: 30
defaults:
run:
shell: bash # by default Windows uses PowerShell, which uses a different syntax for setting environment variables
env:
DEBUG: false # set this to true to export qlogs and save them as artifacts
TIMESCALE_FACTOR: 3
name: "Integration (${{ matrix.os }}, Go ${{ matrix.go }}${{ matrix.race && ', race' || '' }}${{ matrix.use32bit && ', 32-bit' || '' }})"
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Install go-junit-report
run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0
- name: Set qlogger
if: env.DEBUG == 'true'
run: echo "QLOGFLAG= -qlog" >> $GITHUB_ENV
- name: Enable race detector
if: ${{ matrix.race }}
run: echo "RACEFLAG= -race" >> $GITHUB_ENV
- name: Enable 32-bit build
if: ${{ matrix.use32bit }}
run: echo "GOARCH=386" >> $GITHUB_ENV
- run: go version
- name: Run tools tests
run: go test ${{ env.RACEFLAG }} -v -timeout 30s -shuffle=on ./integrationtests/tools/... 2>&1 | go-junit-report -set-exit-code -iocopy -out report_tools.xml
- name: Run version negotiation tests
run: go test ${{ env.RACEFLAG }} -v -timeout 30s -shuffle=on ./integrationtests/versionnegotiation ${{ env.QLOGFLAG }} 2>&1 | go-junit-report -set-exit-code -iocopy -out report_versionnegotiation.xml
- name: Run self tests, using QUIC v1
if: success() || failure() # run this step even if the previous one failed
run: go test ${{ env.RACEFLAG }} -v -timeout 5m -shuffle=on ./integrationtests/self -version=1 ${{ env.QLOGFLAG }} 2>&1 | go-junit-report -set-exit-code -iocopy -out report_self.xml
- name: Run self tests, using QUIC v2
if: ${{ !matrix.race && (success() || failure()) }} # run this step even if the previous one failed
run: go test ${{ env.RACEFLAG }} -v -timeout 5m -shuffle=on ./integrationtests/self -version=2 ${{ env.QLOGFLAG }} 2>&1 | go-junit-report -set-exit-code -iocopy -out report_self_v2.xml
- name: Run self tests, with GSO disabled
if: ${{ matrix.os == 'ubuntu' && (success() || failure()) }} # run this step even if the previous one failed
env:
QUIC_GO_DISABLE_GSO: true
run: go test ${{ env.RACEFLAG }} -v -timeout 5m -shuffle=on ./integrationtests/self -version=1 ${{ env.QLOGFLAG }} 2>&1 | go-junit-report -set-exit-code -iocopy -out report_self_nogso.xml
- name: Run self tests, with ECN disabled
if: ${{ !matrix.race && matrix.os == 'ubuntu' && (success() || failure()) }} # run this step even if the previous one failed
env:
QUIC_GO_DISABLE_ECN: true
run: go test ${{ env.RACEFLAG }} -v -timeout 5m -shuffle=on ./integrationtests/self -version=1 ${{ env.QLOGFLAG }} 2>&1 | go-junit-report -set-exit-code -iocopy -out report_self_noecn.xml
- name: Run benchmarks
if: ${{ !matrix.race }}
run: go test -v -run=^$ -timeout 5m -shuffle=on -bench=. ./integrationtests/self
- name: save qlogs
if: ${{ always() && env.DEBUG == 'true' }}
uses: actions/upload-artifact@v4
with:
name: qlogs-${{ matrix.os }}-go${{ matrix.go }}-race${{ matrix.race }}${{ matrix.use32bit && '-32bit' || '' }}
path: integrationtests/self/*.qlog
retention-days: 7
- name: Upload report to Codecov
if: ${{ !cancelled() && !matrix.race && !matrix.use32bit }}
uses: codecov/test-results-action@v1
with:
name: Unit tests
files: report_tools.xml,report_versionnegotiation.xml,report_self.xml,report_self_v2.xml,report_self_nogso.xml,report_self_noecn.xml
env_vars: OS,GO
token: ${{ secrets.CODECOV_TOKEN }}
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/lint.yml 0000664 0000000 0000000 00000005627 15074232574 0025702 0 ustar 00root root 0000000 0000000 on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
GOEXPERIMENT: ${{ matrix.go == '1.24.x' && 'synctest' || '' }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- name: Check for //go:build ignore in .go files
run: |
IGNORED_FILES=$(grep -rl '//go:build ignore' . --include='*.go') || true
if [ -n "$IGNORED_FILES" ]; then
echo "::error::Found ignored Go files: $IGNORED_FILES"
exit 1
fi
- name: Check that go.mod is tidied
if: success() || failure() # run this step even if the previous one failed
run: go mod tidy -diff
- name: Run code generators
if: success() || failure() # run this step even if the previous one failed
run: .github/workflows/go-generate.sh
- name: Check that go mod vendor works
if: success() || failure() # run this step even if the previous one failed
run: |
cd integrationtests/gomodvendor
go mod vendor
golangci-lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go: [ "1.24.x", "1.25.x" ]
env:
GOLANGCI_LINT_VERSION: v2.4.0
GOEXPERIMENT: ${{ matrix.go == '1.24.x' && 'synctest' || '' }}
name: golangci-lint (Go ${{ matrix.go }})
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: golangci-lint (Linux)
uses: golangci/golangci-lint-action@v8
with:
args: --timeout=3m
version: ${{ env.GOLANGCI_LINT_VERSION }}
- name: golangci-lint (Windows)
if: success() || failure() # run this step even if the previous one failed
uses: golangci/golangci-lint-action@v8
env:
GOOS: "windows"
with:
args: --timeout=3m
version: ${{ env.GOLANGCI_LINT_VERSION }}
- name: golangci-lint (OSX)
if: success() || failure() # run this step even if the previous one failed
uses: golangci/golangci-lint-action@v8
env:
GOOS: "darwin"
with:
args: --timeout=3m
version: ${{ env.GOLANGCI_LINT_VERSION }}
- name: golangci-lint (FreeBSD)
if: success() || failure() # run this step even if the previous one failed
uses: golangci/golangci-lint-action@v8
env:
GOOS: "freebsd"
with:
args: --timeout=3m
version: ${{ env.GOLANGCI_LINT_VERSION }}
- name: golangci-lint (others)
if: success() || failure() # run this step even if the previous one failed
uses: golangci/golangci-lint-action@v8
env:
GOOS: "solaris" # some OS that we don't have any build tags for
with:
args: --timeout=3m
version: ${{ env.GOLANGCI_LINT_VERSION }}
golang-github-lucas-clemente-quic-go-0.55.0/.github/workflows/unit.yml 0000664 0000000 0000000 00000005325 15074232574 0025706 0 ustar 00root root 0000000 0000000 on: [push, pull_request]
jobs:
unit:
strategy:
fail-fast: false
matrix:
os: [ "ubuntu", "windows", "macos" ]
go: [ "1.24.x", "1.25.x" ]
runs-on: ${{ fromJSON(vars[format('UNIT_RUNNER_{0}', matrix.os)] || format('"{0}-latest"', matrix.os)) }}
name: Unit tests (${{ matrix.os}}, Go ${{ matrix.go }})
timeout-minutes: 30
env:
GOEXPERIMENT: ${{ matrix.go == '1.24.x' && 'synctest' || '' }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- run: go version
- name: Install go-junit-report
run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0
- name: Remove integrationtests
shell: bash
run: rm -rf integrationtests
- name: Run tests
env:
TIMESCALE_FACTOR: 10
run: go test -v -shuffle on -cover -coverprofile coverage.txt ./... 2>&1 | go-junit-report -set-exit-code -iocopy -out report.xml
- name: Run tests as root
if: ${{ matrix.os == 'ubuntu' }}
env:
TIMESCALE_FACTOR: 10
FILE: sys_conn_helper_linux_test.go
run: |
test -f $FILE # make sure the file actually exists
TEST_NAMES=$(grep '^func Test' "$FILE" | sed 's/^func \([A-Za-z0-9_]*\)(.*/\1/' | tr '\n' '|')
go test -c -cover -tags root -o quic-go.test .
sudo ./quic-go.test -test.v -test.run "${TEST_NAMES%|}" -test.coverprofile coverage-root.txt 2>&1 | go-junit-report -set-exit-code -iocopy -package-name github.com/quic-go/quic-go -out report_root.xml
rm quic-go.test
- name: Run tests (32 bit)
if: ${{ matrix.os != 'macos' }} # can't run 32 bit tests on macOS
env:
TIMESCALE_FACTOR: 10
GOARCH: 386
run: go test -v -shuffle on ./...
- name: Run tests with race detector
if: ${{ matrix.os == 'ubuntu' }} # speed things up. Windows and OSX VMs are slow
env:
TIMESCALE_FACTOR: 20
run: go test -v -shuffle on ./...
- name: Run benchmark tests
run: go test -v -run=^$ -benchtime 0.5s -bench=. ./...
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
env:
OS: ${{ matrix.os }}
GO: ${{ matrix.go }}
with:
files: coverage.txt,coverage-root.txt
env_vars: OS,GO
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload report to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
name: Unit tests
files: report.xml,report_root.xml
env_vars: OS,GO
token: ${{ secrets.CODECOV_TOKEN }}
golang-github-lucas-clemente-quic-go-0.55.0/.gitignore 0000664 0000000 0000000 00000000332 15074232574 0022570 0 ustar 00root root 0000000 0000000 debug
debug.test
main
mockgen_tmp.go
*.qtr
*.qlog
*.sqlog
*.txt
race.[0-9]*
fuzzing/*/*.zip
fuzzing/*/coverprofile
fuzzing/*/crashers
fuzzing/*/sonarprofile
fuzzing/*/suppressions
fuzzing/*/corpus/
gomock_reflect_*/
golang-github-lucas-clemente-quic-go-0.55.0/.golangci.yml 0000664 0000000 0000000 00000005134 15074232574 0023171 0 ustar 00root root 0000000 0000000 version: "2"
linters:
default: none
enable:
- asciicheck
- copyloopvar
- depguard
- exhaustive
- govet
- ineffassign
- misspell
- nolintlint
- prealloc
- staticcheck
- unconvert
- unparam
- unused
- usetesting
settings:
depguard:
rules:
random:
deny:
- pkg: "math/rand$"
desc: use math/rand/v2
- pkg: "golang.org/x/exp/rand"
desc: use math/rand/v2
quicvarint:
list-mode: strict
files:
- '**/github.com/quic-go/quic-go/quicvarint/*'
- '!$test'
allow:
- $gostd
rsa:
list-mode: original
deny:
- pkg: crypto/rsa
desc: "use crypto/ed25519 instead"
ginkgo:
list-mode: original
deny:
- pkg: github.com/onsi/ginkgo
desc: "use standard Go tests"
- pkg: github.com/onsi/ginkgo/v2
desc: "use standard Go tests"
- pkg: github.com/onsi/gomega
desc: "use standard Go tests"
http3-internal:
list-mode: lax
files:
- '**/http3/**'
deny:
- pkg: 'github.com/quic-go/quic-go/internal'
desc: 'no dependency on quic-go/internal'
misspell:
ignore-rules:
- ect
# see https://github.com/ldez/usetesting/issues/10
usetesting:
context-background: false
context-todo: false
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- depguard
path: internal/qtls
- linters:
- exhaustive
- prealloc
- unparam
path: _test\.go
- linters:
- staticcheck
path: _test\.go
text: 'SA1029:' # inappropriate key in call to context.WithValue
# WebTransport still relies on the ConnectionTracingID and ConnectionTracingKey.
# See https://github.com/quic-go/quic-go/issues/4405 for more details.
- linters:
- staticcheck
paths:
- http3/
- integrationtests/self/http_test.go
text: 'SA1019:.+quic\.ConnectionTracing(ID|Key)'
paths:
- internal/handshake/cipher_suite.go
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- internal/handshake/cipher_suite.go
- third_party$
- builtin$
- examples$
golang-github-lucas-clemente-quic-go-0.55.0/LICENSE 0000664 0000000 0000000 00000002103 15074232574 0021603 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2016 the quic-go authors & Google, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
golang-github-lucas-clemente-quic-go-0.55.0/README.md 0000664 0000000 0000000 00000020575 15074232574 0022072 0 ustar 00root root 0000000 0000000 # A QUIC implementation in pure Go
[](https://quic-go.net/docs/)
[](https://pkg.go.dev/github.com/quic-go/quic-go)
[](https://codecov.io/gh/quic-go/quic-go/)
[](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:quic-go)
quic-go is an implementation of the QUIC protocol ([RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000), [RFC 9001](https://datatracker.ietf.org/doc/html/rfc9001), [RFC 9002](https://datatracker.ietf.org/doc/html/rfc9002)) in Go. It has support for HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)), including QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)) and HTTP Datagrams ([RFC 9297](https://datatracker.ietf.org/doc/html/rfc9297)).
In addition to these base RFCs, it also implements the following RFCs:
* Unreliable Datagram Extension ([RFC 9221](https://datatracker.ietf.org/doc/html/rfc9221))
* Datagram Packetization Layer Path MTU Discovery (DPLPMTUD, [RFC 8899](https://datatracker.ietf.org/doc/html/rfc8899))
* QUIC Version 2 ([RFC 9369](https://datatracker.ietf.org/doc/html/rfc9369))
* QUIC Event Logging using qlog ([draft-ietf-quic-qlog-main-schema](https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-main-schema/) and [draft-ietf-quic-qlog-quic-events](https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-quic-events/))
* QUIC Stream Resets with Partial Delivery ([draft-ietf-quic-reliable-stream-reset](https://datatracker.ietf.org/doc/html/draft-ietf-quic-reliable-stream-reset-07))
Support for WebTransport over HTTP/3 ([draft-ietf-webtrans-http3](https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/)) is implemented in [webtransport-go](https://github.com/quic-go/webtransport-go).
Detailed documentation can be found on [quic-go.net](https://quic-go.net/docs/).
## Projects using quic-go
| Project | Description | Stars |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) | Free and open source, powerful network-wide ads & trackers blocking DNS server. |  |
| [algernon](https://github.com/xyproto/algernon) | Small self-contained pure-Go web server with Lua, Markdown, HTTP/2, QUIC, Redis and PostgreSQL support |  |
| [caddy](https://github.com/caddyserver/caddy/) | Fast, multi-platform web server with automatic HTTPS |  |
| [cloudflared](https://github.com/cloudflare/cloudflared) | A tunneling daemon that proxies traffic from the Cloudflare network to your origins |  |
| [frp](https://github.com/fatedier/frp) | A fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet |  |
| [go-libp2p](https://github.com/libp2p/go-libp2p) | libp2p implementation in Go, powering [Kubo](https://github.com/ipfs/kubo) (IPFS) and [Lotus](https://github.com/filecoin-project/lotus) (Filecoin), among others |  |
| [gost](https://github.com/go-gost/gost) | A simple security tunnel written in Go |  |
| [Hysteria](https://github.com/apernet/hysteria) | A powerful, lightning fast and censorship resistant proxy |  |
| [Mercure](https://github.com/dunglas/mercure) | An open, easy, fast, reliable and battery-efficient solution for real-time communications |  |
| [OONI Probe](https://github.com/ooni/probe-cli) | Next generation OONI Probe. Library and CLI tool. |  |
| [reverst](https://github.com/flipt-io/reverst) | Reverse Tunnels in Go over HTTP/3 and QUIC |  |
| [RoadRunner](https://github.com/roadrunner-server/roadrunner) | High-performance PHP application server, process manager written in Go and powered with plugins |  |
| [syncthing](https://github.com/syncthing/syncthing/) | Open Source Continuous File Synchronization |  |
| [traefik](https://github.com/traefik/traefik) | The Cloud Native Application Proxy |  |
| [v2ray-core](https://github.com/v2fly/v2ray-core) | A platform for building proxies to bypass network restrictions |  |
| [YoMo](https://github.com/yomorun/yomo) | Streaming Serverless Framework for Geo-distributed System |  |
If you'd like to see your project added to this list, please send us a PR.
## Release Policy
quic-go always aims to support the latest two Go releases.
## Contributing
We are always happy to welcome new contributors! We have a number of self-contained issues that are suitable for first-time contributors, they are tagged with [help wanted](https://github.com/quic-go/quic-go/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22). If you have any questions, please feel free to reach out by opening an issue or leaving a comment.
golang-github-lucas-clemente-quic-go-0.55.0/SECURITY.md 0000664 0000000 0000000 00000001507 15074232574 0022376 0 ustar 00root root 0000000 0000000 # Security Policy
quic-go still in development. This means that there may be problems in our protocols,
or there may be mistakes in our implementations.
We take security vulnerabilities very seriously. If you discover a security issue,
please bring it to our attention right away!
## Reporting a Vulnerability
If you find a vulnerability that may affect live deployments -- for example, by exposing
a remote execution exploit -- please [**report privately**](https://github.com/quic-go/quic-go/security/advisories/new).
Please **DO NOT file a public issue**.
If the issue is an implementation weakness that cannot be immediately exploited or
something not yet deployed, just discuss it openly.
## Reporting a non security bug
For non-security bugs, please simply file a GitHub [issue](https://github.com/quic-go/quic-go/issues/new).
golang-github-lucas-clemente-quic-go-0.55.0/buffer_pool.go 0000664 0000000 0000000 00000004324 15074232574 0023436 0 ustar 00root root 0000000 0000000 package quic
import (
"sync"
"github.com/quic-go/quic-go/internal/protocol"
)
type packetBuffer struct {
Data []byte
// refCount counts how many packets Data is used in.
// It doesn't support concurrent use.
// It is > 1 when used for coalesced packet.
refCount int
}
// Split increases the refCount.
// It must be called when a packet buffer is used for more than one packet,
// e.g. when splitting coalesced packets.
func (b *packetBuffer) Split() {
b.refCount++
}
// Decrement decrements the reference counter.
// It doesn't put the buffer back into the pool.
func (b *packetBuffer) Decrement() {
b.refCount--
if b.refCount < 0 {
panic("negative packetBuffer refCount")
}
}
// MaybeRelease puts the packet buffer back into the pool,
// if the reference counter already reached 0.
func (b *packetBuffer) MaybeRelease() {
// only put the packetBuffer back if it's not used any more
if b.refCount == 0 {
b.putBack()
}
}
// Release puts back the packet buffer into the pool.
// It should be called when processing is definitely finished.
func (b *packetBuffer) Release() {
b.Decrement()
if b.refCount != 0 {
panic("packetBuffer refCount not zero")
}
b.putBack()
}
// Len returns the length of Data
func (b *packetBuffer) Len() protocol.ByteCount { return protocol.ByteCount(len(b.Data)) }
func (b *packetBuffer) Cap() protocol.ByteCount { return protocol.ByteCount(cap(b.Data)) }
func (b *packetBuffer) putBack() {
if cap(b.Data) == protocol.MaxPacketBufferSize {
bufferPool.Put(b)
return
}
if cap(b.Data) == protocol.MaxLargePacketBufferSize {
largeBufferPool.Put(b)
return
}
panic("putPacketBuffer called with packet of wrong size!")
}
var bufferPool, largeBufferPool sync.Pool
func getPacketBuffer() *packetBuffer {
buf := bufferPool.Get().(*packetBuffer)
buf.refCount = 1
buf.Data = buf.Data[:0]
return buf
}
func getLargePacketBuffer() *packetBuffer {
buf := largeBufferPool.Get().(*packetBuffer)
buf.refCount = 1
buf.Data = buf.Data[:0]
return buf
}
func init() {
bufferPool.New = func() any {
return &packetBuffer{Data: make([]byte, 0, protocol.MaxPacketBufferSize)}
}
largeBufferPool.New = func() any {
return &packetBuffer{Data: make([]byte, 0, protocol.MaxLargePacketBufferSize)}
}
}
golang-github-lucas-clemente-quic-go-0.55.0/buffer_pool_test.go 0000664 0000000 0000000 00000002111 15074232574 0024465 0 ustar 00root root 0000000 0000000 package quic
import (
"testing"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/stretchr/testify/require"
)
func TestBufferPoolSizes(t *testing.T) {
buf1 := getPacketBuffer()
require.Equal(t, protocol.MaxPacketBufferSize, cap(buf1.Data))
require.Zero(t, buf1.Len())
buf1.Data = append(buf1.Data, []byte("foobar")...)
require.Equal(t, protocol.ByteCount(6), buf1.Len())
buf2 := getLargePacketBuffer()
require.Equal(t, protocol.MaxLargePacketBufferSize, cap(buf2.Data))
require.Zero(t, buf2.Len())
}
func TestBufferPoolRelease(t *testing.T) {
buf1 := getPacketBuffer()
buf1.Release()
// panics if released twice
require.Panics(t, func() { buf1.Release() })
// panics if wrong-sized buffers are passed
buf2 := getLargePacketBuffer()
buf2.Data = make([]byte, 10) // replace the underlying slice
require.Panics(t, func() { buf2.Release() })
}
func TestBufferPoolSplitting(t *testing.T) {
buf := getPacketBuffer()
buf.Split()
buf.Split()
// now we have 3 parts
buf.Decrement()
buf.Decrement()
buf.Decrement()
require.Panics(t, func() { buf.Decrement() })
}
golang-github-lucas-clemente-quic-go-0.55.0/client.go 0000664 0000000 0000000 00000006566 15074232574 0022424 0 ustar 00root root 0000000 0000000 package quic
import (
"context"
"crypto/tls"
"errors"
"net"
"github.com/quic-go/quic-go/internal/protocol"
)
// make it possible to mock connection ID for initial generation in the tests
var generateConnectionIDForInitial = protocol.GenerateConnectionIDForInitial
// DialAddr establishes a new QUIC connection to a server.
// It resolves the address, and then creates a new UDP connection to dial the QUIC server.
// When the QUIC connection is closed, this UDP connection is closed.
// See [Dial] for more details.
func DialAddr(ctx context.Context, addr string, tlsConf *tls.Config, conf *Config) (*Conn, error) {
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
return nil, err
}
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
tr, err := setupTransport(udpConn, tlsConf, true)
if err != nil {
return nil, err
}
conn, err := tr.dial(ctx, udpAddr, addr, tlsConf, conf, false)
if err != nil {
tr.Close()
return nil, err
}
return conn, nil
}
// DialAddrEarly establishes a new 0-RTT QUIC connection to a server.
// See [DialAddr] for more details.
func DialAddrEarly(ctx context.Context, addr string, tlsConf *tls.Config, conf *Config) (*Conn, error) {
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
return nil, err
}
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
tr, err := setupTransport(udpConn, tlsConf, true)
if err != nil {
return nil, err
}
conn, err := tr.dial(ctx, udpAddr, addr, tlsConf, conf, true)
if err != nil {
tr.Close()
return nil, err
}
return conn, nil
}
// DialEarly establishes a new 0-RTT QUIC connection to a server using a net.PacketConn.
// See [Dial] for more details.
func DialEarly(ctx context.Context, c net.PacketConn, addr net.Addr, tlsConf *tls.Config, conf *Config) (*Conn, error) {
dl, err := setupTransport(c, tlsConf, false)
if err != nil {
return nil, err
}
conn, err := dl.DialEarly(ctx, addr, tlsConf, conf)
if err != nil {
dl.Close()
return nil, err
}
return conn, nil
}
// Dial establishes a new QUIC connection to a server using a net.PacketConn.
// If the PacketConn satisfies the [OOBCapablePacketConn] interface (as a [net.UDPConn] does),
// ECN and packet info support will be enabled. In this case, ReadMsgUDP and WriteMsgUDP
// will be used instead of ReadFrom and WriteTo to read/write packets.
// The [tls.Config] must define an application protocol (using tls.Config.NextProtos).
//
// This is a convenience function. More advanced use cases should instantiate a [Transport],
// which offers configuration options for a more fine-grained control of the connection establishment,
// including reusing the underlying UDP socket for multiple QUIC connections.
func Dial(ctx context.Context, c net.PacketConn, addr net.Addr, tlsConf *tls.Config, conf *Config) (*Conn, error) {
dl, err := setupTransport(c, tlsConf, false)
if err != nil {
return nil, err
}
conn, err := dl.Dial(ctx, addr, tlsConf, conf)
if err != nil {
dl.Close()
return nil, err
}
return conn, nil
}
func setupTransport(c net.PacketConn, tlsConf *tls.Config, createdPacketConn bool) (*Transport, error) {
if tlsConf == nil {
return nil, errors.New("quic: tls.Config not set")
}
return &Transport{
Conn: c,
createdConn: createdPacketConn,
isSingleUse: true,
}, nil
}
golang-github-lucas-clemente-quic-go-0.55.0/client_test.go 0000664 0000000 0000000 00000004767 15074232574 0023464 0 ustar 00root root 0000000 0000000 package quic
import (
"context"
"crypto/tls"
"net"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestDial(t *testing.T) {
t.Run("Dial", func(t *testing.T) {
testDial(t,
func(ctx context.Context, addr net.Addr) error {
conn := newUDPConnLocalhost(t)
_, err := Dial(ctx, conn, addr, &tls.Config{}, nil)
return err
},
false,
)
})
t.Run("DialEarly", func(t *testing.T) {
testDial(t,
func(ctx context.Context, addr net.Addr) error {
conn := newUDPConnLocalhost(t)
_, err := DialEarly(ctx, conn, addr, &tls.Config{}, nil)
return err
},
false,
)
})
t.Run("DialAddr", func(t *testing.T) {
testDial(t,
func(ctx context.Context, addr net.Addr) error {
_, err := DialAddr(ctx, addr.String(), &tls.Config{}, nil)
return err
},
true,
)
})
t.Run("DialAddrEarly", func(t *testing.T) {
testDial(t,
func(ctx context.Context, addr net.Addr) error {
_, err := DialAddrEarly(ctx, addr.String(), &tls.Config{}, nil)
return err
},
true,
)
})
}
func testDial(t *testing.T,
dialFn func(context.Context, net.Addr) error,
shouldCloseConn bool,
) {
server := newUDPConnLocalhost(t)
ctx, cancel := context.WithCancel(context.Background())
errChan := make(chan error, 1)
go func() { errChan <- dialFn(ctx, server.LocalAddr()) }()
server.SetReadDeadline(time.Now().Add(time.Second))
_, addr, err := server.ReadFrom(make([]byte, 1500))
require.NoError(t, err)
cancel()
select {
case err := <-errChan:
require.ErrorIs(t, err, context.Canceled)
case <-time.After(time.Second):
t.Fatal("timeout")
}
if shouldCloseConn {
// The socket that the client used for dialing should be closed now.
// Binding to the same address would error if the address was still in use.
require.Eventually(t, func() bool {
conn, err := net.ListenUDP("udp", addr.(*net.UDPAddr))
if err != nil {
return false
}
conn.Close()
return true
}, scaleDuration(200*time.Millisecond), scaleDuration(10*time.Millisecond))
require.False(t, areTransportsRunning())
return
}
// The socket that the client used for dialing should not be closed now.
// Binding to the same address will error if the address was still in use.
_, err = net.ListenUDP("udp", addr.(*net.UDPAddr))
require.Error(t, err)
if runtime.GOOS == "windows" {
require.ErrorContains(t, err, "bind: Only one usage of each socket address")
} else {
require.ErrorContains(t, err, "address already in use")
}
require.False(t, areTransportsRunning())
}
golang-github-lucas-clemente-quic-go-0.55.0/closed_conn.go 0000664 0000000 0000000 00000003430 15074232574 0023417 0 ustar 00root root 0000000 0000000 package quic
import (
"math/bits"
"net"
"sync/atomic"
"github.com/quic-go/quic-go/internal/utils"
)
// A closedLocalConn is a connection that we closed locally.
// When receiving packets for such a connection, we need to retransmit the packet containing the CONNECTION_CLOSE frame,
// with an exponential backoff.
type closedLocalConn struct {
counter atomic.Uint32
logger utils.Logger
sendPacket func(net.Addr, packetInfo)
}
var _ packetHandler = &closedLocalConn{}
// newClosedLocalConn creates a new closedLocalConn and runs it.
func newClosedLocalConn(sendPacket func(net.Addr, packetInfo), logger utils.Logger) packetHandler {
return &closedLocalConn{
sendPacket: sendPacket,
logger: logger,
}
}
func (c *closedLocalConn) handlePacket(p receivedPacket) {
n := c.counter.Add(1)
// exponential backoff
// only send a CONNECTION_CLOSE for the 1st, 2nd, 4th, 8th, 16th, ... packet arriving
if bits.OnesCount32(n) != 1 {
return
}
c.logger.Debugf("Received %d packets after sending CONNECTION_CLOSE. Retransmitting.", n)
c.sendPacket(p.remoteAddr, p.info)
}
func (c *closedLocalConn) destroy(error) {}
func (c *closedLocalConn) closeWithTransportError(TransportErrorCode) {}
// A closedRemoteConn is a connection that was closed remotely.
// For such a connection, we might receive reordered packets that were sent before the CONNECTION_CLOSE.
// We can just ignore those packets.
type closedRemoteConn struct{}
var _ packetHandler = &closedRemoteConn{}
func newClosedRemoteConn() packetHandler {
return &closedRemoteConn{}
}
func (c *closedRemoteConn) handlePacket(receivedPacket) {}
func (c *closedRemoteConn) destroy(error) {}
func (c *closedRemoteConn) closeWithTransportError(TransportErrorCode) {}
golang-github-lucas-clemente-quic-go-0.55.0/closed_conn_test.go 0000664 0000000 0000000 00000001540 15074232574 0024456 0 ustar 00root root 0000000 0000000 package quic
import (
"net"
"testing"
"github.com/quic-go/quic-go/internal/utils"
"github.com/stretchr/testify/require"
)
func TestClosedLocalConnection(t *testing.T) {
written := make(chan net.Addr, 1)
conn := newClosedLocalConn(func(addr net.Addr, _ packetInfo) { written <- addr }, utils.DefaultLogger)
addr := &net.UDPAddr{IP: net.IPv4(127, 1, 2, 3), Port: 1337}
for i := 1; i <= 20; i++ {
conn.handlePacket(receivedPacket{remoteAddr: addr})
if i == 1 || i == 2 || i == 4 || i == 8 || i == 16 {
select {
case gotAddr := <-written:
require.Equal(t, addr, gotAddr) // receive the CONNECTION_CLOSE
default:
t.Fatal("expected to receive address")
}
} else {
select {
case gotAddr := <-written:
t.Fatalf("unexpected address received: %v", gotAddr)
default:
// Nothing received, which is expected
}
}
}
}
golang-github-lucas-clemente-quic-go-0.55.0/codecov.yml 0000664 0000000 0000000 00000000713 15074232574 0022750 0 ustar 00root root 0000000 0000000 coverage:
round: nearest
ignore:
- http3/gzip_reader.go
- example/
- interop/
- internal/handshake/cipher_suite.go
- internal/mocks/
- internal/utils/linkedlist/linkedlist.go
- internal/testdata
- internal/synctest
- logging/connection_tracer_multiplexer.go
- logging/tracer_multiplexer.go
- testutils/
- fuzzing/
- metrics/
status:
project:
default:
threshold: 0.5
patch: false
golang-github-lucas-clemente-quic-go-0.55.0/config.go 0000664 0000000 0000000 00000010472 15074232574 0022402 0 ustar 00root root 0000000 0000000 package quic
import (
"fmt"
"time"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/quicvarint"
)
// Clone clones a Config.
func (c *Config) Clone() *Config {
copy := *c
return ©
}
func (c *Config) handshakeTimeout() time.Duration {
return 2 * c.HandshakeIdleTimeout
}
func (c *Config) maxRetryTokenAge() time.Duration {
return c.handshakeTimeout()
}
func validateConfig(config *Config) error {
if config == nil {
return nil
}
const maxStreams = 1 << 60
if config.MaxIncomingStreams > maxStreams {
config.MaxIncomingStreams = maxStreams
}
if config.MaxIncomingUniStreams > maxStreams {
config.MaxIncomingUniStreams = maxStreams
}
if config.MaxStreamReceiveWindow > quicvarint.Max {
config.MaxStreamReceiveWindow = quicvarint.Max
}
if config.MaxConnectionReceiveWindow > quicvarint.Max {
config.MaxConnectionReceiveWindow = quicvarint.Max
}
if config.InitialPacketSize > 0 && config.InitialPacketSize < protocol.MinInitialPacketSize {
config.InitialPacketSize = protocol.MinInitialPacketSize
}
if config.InitialPacketSize > protocol.MaxPacketBufferSize {
config.InitialPacketSize = protocol.MaxPacketBufferSize
}
// check that all QUIC versions are actually supported
for _, v := range config.Versions {
if !protocol.IsValidVersion(v) {
return fmt.Errorf("invalid QUIC version: %s", v)
}
}
return nil
}
// populateConfig populates fields in the quic.Config with their default values, if none are set
// it may be called with nil
func populateConfig(config *Config) *Config {
if config == nil {
config = &Config{}
}
versions := config.Versions
if len(versions) == 0 {
versions = protocol.SupportedVersions
}
handshakeIdleTimeout := protocol.DefaultHandshakeIdleTimeout
if config.HandshakeIdleTimeout != 0 {
handshakeIdleTimeout = config.HandshakeIdleTimeout
}
idleTimeout := protocol.DefaultIdleTimeout
if config.MaxIdleTimeout != 0 {
idleTimeout = config.MaxIdleTimeout
}
initialStreamReceiveWindow := config.InitialStreamReceiveWindow
if initialStreamReceiveWindow == 0 {
initialStreamReceiveWindow = protocol.DefaultInitialMaxStreamData
}
maxStreamReceiveWindow := config.MaxStreamReceiveWindow
if maxStreamReceiveWindow == 0 {
maxStreamReceiveWindow = protocol.DefaultMaxReceiveStreamFlowControlWindow
}
initialConnectionReceiveWindow := config.InitialConnectionReceiveWindow
if initialConnectionReceiveWindow == 0 {
initialConnectionReceiveWindow = protocol.DefaultInitialMaxData
}
maxConnectionReceiveWindow := config.MaxConnectionReceiveWindow
if maxConnectionReceiveWindow == 0 {
maxConnectionReceiveWindow = protocol.DefaultMaxReceiveConnectionFlowControlWindow
}
maxIncomingStreams := config.MaxIncomingStreams
if maxIncomingStreams == 0 {
maxIncomingStreams = protocol.DefaultMaxIncomingStreams
} else if maxIncomingStreams < 0 {
maxIncomingStreams = 0
}
maxIncomingUniStreams := config.MaxIncomingUniStreams
if maxIncomingUniStreams == 0 {
maxIncomingUniStreams = protocol.DefaultMaxIncomingUniStreams
} else if maxIncomingUniStreams < 0 {
maxIncomingUniStreams = 0
}
initialPacketSize := config.InitialPacketSize
if initialPacketSize == 0 {
initialPacketSize = protocol.InitialPacketSize
}
return &Config{
GetConfigForClient: config.GetConfigForClient,
Versions: versions,
HandshakeIdleTimeout: handshakeIdleTimeout,
MaxIdleTimeout: idleTimeout,
KeepAlivePeriod: config.KeepAlivePeriod,
InitialStreamReceiveWindow: initialStreamReceiveWindow,
MaxStreamReceiveWindow: maxStreamReceiveWindow,
InitialConnectionReceiveWindow: initialConnectionReceiveWindow,
MaxConnectionReceiveWindow: maxConnectionReceiveWindow,
AllowConnectionWindowIncrease: config.AllowConnectionWindowIncrease,
MaxIncomingStreams: maxIncomingStreams,
MaxIncomingUniStreams: maxIncomingUniStreams,
TokenStore: config.TokenStore,
EnableDatagrams: config.EnableDatagrams,
InitialPacketSize: initialPacketSize,
DisablePathMTUDiscovery: config.DisablePathMTUDiscovery,
EnableStreamResetPartialDelivery: config.EnableStreamResetPartialDelivery,
Allow0RTT: config.Allow0RTT,
Tracer: config.Tracer,
}
}
golang-github-lucas-clemente-quic-go-0.55.0/config_test.go 0000664 0000000 0000000 00000015040 15074232574 0023435 0 ustar 00root root 0000000 0000000 package quic
import (
"context"
"reflect"
"testing"
"time"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/logging"
"github.com/quic-go/quic-go/quicvarint"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConfigValidation(t *testing.T) {
t.Run("nil config", func(t *testing.T) {
require.NoError(t, validateConfig(nil))
})
t.Run("config with a few values set", func(t *testing.T) {
conf := populateConfig(&Config{
MaxIncomingStreams: 5,
MaxStreamReceiveWindow: 10,
})
require.NoError(t, validateConfig(conf))
require.Equal(t, int64(5), conf.MaxIncomingStreams)
require.Equal(t, uint64(10), conf.MaxStreamReceiveWindow)
})
t.Run("stream limits", func(t *testing.T) {
conf := &Config{
MaxIncomingStreams: 1<<60 + 1,
MaxIncomingUniStreams: 1<<60 + 2,
}
require.NoError(t, validateConfig(conf))
require.Equal(t, int64(1<<60), conf.MaxIncomingStreams)
require.Equal(t, int64(1<<60), conf.MaxIncomingUniStreams)
})
t.Run("flow control windows", func(t *testing.T) {
conf := &Config{
MaxStreamReceiveWindow: quicvarint.Max + 1,
MaxConnectionReceiveWindow: quicvarint.Max + 2,
}
require.NoError(t, validateConfig(conf))
require.Equal(t, uint64(quicvarint.Max), conf.MaxStreamReceiveWindow)
require.Equal(t, uint64(quicvarint.Max), conf.MaxConnectionReceiveWindow)
})
t.Run("initial packet size", func(t *testing.T) {
// not set
conf := &Config{InitialPacketSize: 0}
require.NoError(t, validateConfig(conf))
require.Zero(t, conf.InitialPacketSize)
// too small
conf = &Config{InitialPacketSize: 10}
require.NoError(t, validateConfig(conf))
require.Equal(t, uint16(1200), conf.InitialPacketSize)
// too large
conf = &Config{InitialPacketSize: protocol.MaxPacketBufferSize + 1}
require.NoError(t, validateConfig(conf))
require.Equal(t, uint16(protocol.MaxPacketBufferSize), conf.InitialPacketSize)
})
}
func TestConfigHandshakeIdleTimeout(t *testing.T) {
c := &Config{HandshakeIdleTimeout: time.Second * 11 / 2}
require.Equal(t, 11*time.Second, c.handshakeTimeout())
}
func configWithNonZeroNonFunctionFields(t *testing.T) *Config {
t.Helper()
c := &Config{}
v := reflect.ValueOf(c).Elem()
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
f := v.Field(i)
if !f.CanSet() {
// unexported field; not cloned.
continue
}
switch fn := typ.Field(i).Name; fn {
case "GetConfigForClient", "RequireAddressValidation", "GetLogWriter", "AllowConnectionWindowIncrease", "Tracer":
// Can't compare functions.
case "Versions":
f.Set(reflect.ValueOf([]Version{1, 2, 3}))
case "ConnectionIDLength":
f.Set(reflect.ValueOf(8))
case "ConnectionIDGenerator":
f.Set(reflect.ValueOf(&protocol.DefaultConnectionIDGenerator{ConnLen: protocol.DefaultConnectionIDLength}))
case "HandshakeIdleTimeout":
f.Set(reflect.ValueOf(time.Second))
case "MaxIdleTimeout":
f.Set(reflect.ValueOf(time.Hour))
case "TokenStore":
f.Set(reflect.ValueOf(NewLRUTokenStore(2, 3)))
case "InitialStreamReceiveWindow":
f.Set(reflect.ValueOf(uint64(1234)))
case "MaxStreamReceiveWindow":
f.Set(reflect.ValueOf(uint64(9)))
case "InitialConnectionReceiveWindow":
f.Set(reflect.ValueOf(uint64(4321)))
case "MaxConnectionReceiveWindow":
f.Set(reflect.ValueOf(uint64(10)))
case "MaxIncomingStreams":
f.Set(reflect.ValueOf(int64(11)))
case "MaxIncomingUniStreams":
f.Set(reflect.ValueOf(int64(12)))
case "StatelessResetKey":
f.Set(reflect.ValueOf(&StatelessResetKey{1, 2, 3, 4}))
case "KeepAlivePeriod":
f.Set(reflect.ValueOf(time.Second))
case "EnableDatagrams":
f.Set(reflect.ValueOf(true))
case "DisableVersionNegotiationPackets":
f.Set(reflect.ValueOf(true))
case "InitialPacketSize":
f.Set(reflect.ValueOf(uint16(1350)))
case "DisablePathMTUDiscovery":
f.Set(reflect.ValueOf(true))
case "Allow0RTT":
f.Set(reflect.ValueOf(true))
case "EnableStreamResetPartialDelivery":
f.Set(reflect.ValueOf(true))
default:
t.Fatalf("all fields must be accounted for, but saw unknown field %q", fn)
}
}
return c
}
func TestConfigClone(t *testing.T) {
t.Run("function fields", func(t *testing.T) {
var calledAllowConnectionWindowIncrease, calledTracer bool
c1 := &Config{
GetConfigForClient: func(info *ClientInfo) (*Config, error) { return nil, assert.AnError },
AllowConnectionWindowIncrease: func(*Conn, uint64) bool { calledAllowConnectionWindowIncrease = true; return true },
Tracer: func(context.Context, logging.Perspective, ConnectionID) *logging.ConnectionTracer {
calledTracer = true
return nil
},
}
c2 := c1.Clone()
c2.AllowConnectionWindowIncrease(nil, 1234)
require.True(t, calledAllowConnectionWindowIncrease)
_, err := c2.GetConfigForClient(&ClientInfo{})
require.ErrorIs(t, err, assert.AnError)
c2.Tracer(context.Background(), logging.PerspectiveClient, protocol.ConnectionID{})
require.True(t, calledTracer)
})
t.Run("non-function fields", func(t *testing.T) {
c := configWithNonZeroNonFunctionFields(t)
require.Equal(t, c, c.Clone())
})
t.Run("returns a copy", func(t *testing.T) {
c1 := &Config{MaxIncomingStreams: 100}
c2 := c1.Clone()
c2.MaxIncomingStreams = 200
require.EqualValues(t, 100, c1.MaxIncomingStreams)
})
}
func TestConfigDefaultValues(t *testing.T) {
// if set, the values should be copied
c := configWithNonZeroNonFunctionFields(t)
require.Equal(t, c, populateConfig(c))
// if not set, some fields use default values
c = populateConfig(&Config{})
require.Equal(t, protocol.SupportedVersions, c.Versions)
require.Equal(t, protocol.DefaultHandshakeIdleTimeout, c.HandshakeIdleTimeout)
require.Equal(t, protocol.DefaultIdleTimeout, c.MaxIdleTimeout)
require.EqualValues(t, protocol.DefaultInitialMaxStreamData, c.InitialStreamReceiveWindow)
require.EqualValues(t, protocol.DefaultMaxReceiveStreamFlowControlWindow, c.MaxStreamReceiveWindow)
require.EqualValues(t, protocol.DefaultInitialMaxData, c.InitialConnectionReceiveWindow)
require.EqualValues(t, protocol.DefaultMaxReceiveConnectionFlowControlWindow, c.MaxConnectionReceiveWindow)
require.EqualValues(t, protocol.DefaultMaxIncomingStreams, c.MaxIncomingStreams)
require.EqualValues(t, protocol.DefaultMaxIncomingUniStreams, c.MaxIncomingUniStreams)
require.False(t, c.DisablePathMTUDiscovery)
require.Nil(t, c.GetConfigForClient)
}
func TestConfigZeroLimits(t *testing.T) {
config := &Config{
MaxIncomingStreams: -1,
MaxIncomingUniStreams: -1,
}
c := populateConfig(config)
require.Zero(t, c.MaxIncomingStreams)
require.Zero(t, c.MaxIncomingUniStreams)
}
golang-github-lucas-clemente-quic-go-0.55.0/conn_id_generator.go 0000664 0000000 0000000 00000014530 15074232574 0024613 0 ustar 00root root 0000000 0000000 package quic
import (
"fmt"
"slices"
"time"
"github.com/quic-go/quic-go/internal/monotime"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/wire"
)
type connRunnerCallbacks struct {
AddConnectionID func(protocol.ConnectionID)
RemoveConnectionID func(protocol.ConnectionID)
ReplaceWithClosed func([]protocol.ConnectionID, []byte, time.Duration)
}
// The memory address of the Transport is used as the key.
type connRunners map[connRunner]connRunnerCallbacks
func (cr connRunners) AddConnectionID(id protocol.ConnectionID) {
for _, c := range cr {
c.AddConnectionID(id)
}
}
func (cr connRunners) RemoveConnectionID(id protocol.ConnectionID) {
for _, c := range cr {
c.RemoveConnectionID(id)
}
}
func (cr connRunners) ReplaceWithClosed(ids []protocol.ConnectionID, b []byte, expiry time.Duration) {
for _, c := range cr {
c.ReplaceWithClosed(ids, b, expiry)
}
}
type connIDToRetire struct {
t monotime.Time
connID protocol.ConnectionID
}
type connIDGenerator struct {
generator ConnectionIDGenerator
highestSeq uint64
connRunners connRunners
activeSrcConnIDs map[uint64]protocol.ConnectionID
connIDsToRetire []connIDToRetire // sorted by t
initialClientDestConnID *protocol.ConnectionID // nil for the client
statelessResetter *statelessResetter
queueControlFrame func(wire.Frame)
}
func newConnIDGenerator(
runner connRunner,
initialConnectionID protocol.ConnectionID,
initialClientDestConnID *protocol.ConnectionID, // nil for the client
statelessResetter *statelessResetter,
callbacks connRunnerCallbacks,
queueControlFrame func(wire.Frame),
generator ConnectionIDGenerator,
) *connIDGenerator {
m := &connIDGenerator{
generator: generator,
activeSrcConnIDs: make(map[uint64]protocol.ConnectionID),
statelessResetter: statelessResetter,
connRunners: map[connRunner]connRunnerCallbacks{runner: callbacks},
queueControlFrame: queueControlFrame,
}
m.activeSrcConnIDs[0] = initialConnectionID
m.initialClientDestConnID = initialClientDestConnID
return m
}
func (m *connIDGenerator) SetMaxActiveConnIDs(limit uint64) error {
if m.generator.ConnectionIDLen() == 0 {
return nil
}
// The active_connection_id_limit transport parameter is the number of
// connection IDs the peer will store. This limit includes the connection ID
// used during the handshake, and the one sent in the preferred_address
// transport parameter.
// We currently don't send the preferred_address transport parameter,
// so we can issue (limit - 1) connection IDs.
for i := uint64(len(m.activeSrcConnIDs)); i < min(limit, protocol.MaxIssuedConnectionIDs); i++ {
if err := m.issueNewConnID(); err != nil {
return err
}
}
return nil
}
func (m *connIDGenerator) Retire(seq uint64, sentWithDestConnID protocol.ConnectionID, expiry monotime.Time) error {
if seq > m.highestSeq {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: fmt.Sprintf("retired connection ID %d (highest issued: %d)", seq, m.highestSeq),
}
}
connID, ok := m.activeSrcConnIDs[seq]
// We might already have deleted this connection ID, if this is a duplicate frame.
if !ok {
return nil
}
if connID == sentWithDestConnID {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: fmt.Sprintf("retired connection ID %d (%s), which was used as the Destination Connection ID on this packet", seq, connID),
}
}
m.queueConnIDForRetiring(connID, expiry)
delete(m.activeSrcConnIDs, seq)
// Don't issue a replacement for the initial connection ID.
if seq == 0 {
return nil
}
return m.issueNewConnID()
}
func (m *connIDGenerator) queueConnIDForRetiring(connID protocol.ConnectionID, expiry monotime.Time) {
idx := slices.IndexFunc(m.connIDsToRetire, func(c connIDToRetire) bool {
return c.t.After(expiry)
})
if idx == -1 {
idx = len(m.connIDsToRetire)
}
m.connIDsToRetire = slices.Insert(m.connIDsToRetire, idx, connIDToRetire{t: expiry, connID: connID})
}
func (m *connIDGenerator) issueNewConnID() error {
connID, err := m.generator.GenerateConnectionID()
if err != nil {
return err
}
m.activeSrcConnIDs[m.highestSeq+1] = connID
m.connRunners.AddConnectionID(connID)
m.queueControlFrame(&wire.NewConnectionIDFrame{
SequenceNumber: m.highestSeq + 1,
ConnectionID: connID,
StatelessResetToken: m.statelessResetter.GetStatelessResetToken(connID),
})
m.highestSeq++
return nil
}
func (m *connIDGenerator) SetHandshakeComplete(connIDExpiry monotime.Time) {
if m.initialClientDestConnID != nil {
m.queueConnIDForRetiring(*m.initialClientDestConnID, connIDExpiry)
m.initialClientDestConnID = nil
}
}
func (m *connIDGenerator) NextRetireTime() monotime.Time {
if len(m.connIDsToRetire) == 0 {
return 0
}
return m.connIDsToRetire[0].t
}
func (m *connIDGenerator) RemoveRetiredConnIDs(now monotime.Time) {
if len(m.connIDsToRetire) == 0 {
return
}
for _, c := range m.connIDsToRetire {
if c.t.After(now) {
break
}
m.connRunners.RemoveConnectionID(c.connID)
m.connIDsToRetire = m.connIDsToRetire[1:]
}
}
func (m *connIDGenerator) RemoveAll() {
if m.initialClientDestConnID != nil {
m.connRunners.RemoveConnectionID(*m.initialClientDestConnID)
}
for _, connID := range m.activeSrcConnIDs {
m.connRunners.RemoveConnectionID(connID)
}
for _, c := range m.connIDsToRetire {
m.connRunners.RemoveConnectionID(c.connID)
}
}
func (m *connIDGenerator) ReplaceWithClosed(connClose []byte, expiry time.Duration) {
connIDs := make([]protocol.ConnectionID, 0, len(m.activeSrcConnIDs)+len(m.connIDsToRetire)+1)
if m.initialClientDestConnID != nil {
connIDs = append(connIDs, *m.initialClientDestConnID)
}
for _, connID := range m.activeSrcConnIDs {
connIDs = append(connIDs, connID)
}
for _, c := range m.connIDsToRetire {
connIDs = append(connIDs, c.connID)
}
m.connRunners.ReplaceWithClosed(connIDs, connClose, expiry)
}
func (m *connIDGenerator) AddConnRunner(runner connRunner, r connRunnerCallbacks) {
// The transport might have already been added earlier.
// This happens if the application migrates back to and old path.
if _, ok := m.connRunners[runner]; ok {
return
}
m.connRunners[runner] = r
if m.initialClientDestConnID != nil {
r.AddConnectionID(*m.initialClientDestConnID)
}
for _, connID := range m.activeSrcConnIDs {
r.AddConnectionID(connID)
}
}
golang-github-lucas-clemente-quic-go-0.55.0/conn_id_generator_test.go 0000664 0000000 0000000 00000031656 15074232574 0025662 0 ustar 00root root 0000000 0000000 package quic
import (
"math/rand/v2"
"testing"
"time"
"github.com/quic-go/quic-go/internal/monotime"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/wire"
"github.com/stretchr/testify/require"
)
func TestConnIDGeneratorIssueAndRetire(t *testing.T) {
t.Run("with initial client destination connection ID", func(t *testing.T) {
testConnIDGeneratorIssueAndRetire(t, true)
})
t.Run("without initial client destination connection ID", func(t *testing.T) {
testConnIDGeneratorIssueAndRetire(t, false)
})
}
func testConnIDGeneratorIssueAndRetire(t *testing.T, hasInitialClientDestConnID bool) {
var (
added []protocol.ConnectionID
removed []protocol.ConnectionID
)
var queuedFrames []wire.Frame
sr := newStatelessResetter(&StatelessResetKey{1, 2, 3, 4})
var initialClientDestConnID *protocol.ConnectionID
if hasInitialClientDestConnID {
connID := protocol.ParseConnectionID([]byte{2, 2, 2, 2})
initialClientDestConnID = &connID
}
g := newConnIDGenerator(
&packetHandlerMap{},
protocol.ParseConnectionID([]byte{1, 1, 1, 1}),
initialClientDestConnID,
sr,
connRunnerCallbacks{
AddConnectionID: func(c protocol.ConnectionID) { added = append(added, c) },
RemoveConnectionID: func(c protocol.ConnectionID) { removed = append(removed, c) },
ReplaceWithClosed: func([]protocol.ConnectionID, []byte, time.Duration) {},
},
func(f wire.Frame) { queuedFrames = append(queuedFrames, f) },
&protocol.DefaultConnectionIDGenerator{ConnLen: 5},
)
require.Empty(t, added)
require.NoError(t, g.SetMaxActiveConnIDs(4))
require.Len(t, added, 3)
require.Len(t, queuedFrames, 3)
require.Empty(t, removed)
connIDs := make(map[uint64]protocol.ConnectionID)
// connection IDs 1, 2 and 3 were issued
for i, f := range queuedFrames {
ncid := f.(*wire.NewConnectionIDFrame)
require.EqualValues(t, i+1, ncid.SequenceNumber)
require.Equal(t, ncid.ConnectionID, added[i])
require.Equal(t, ncid.StatelessResetToken, sr.GetStatelessResetToken(ncid.ConnectionID))
connIDs[ncid.SequenceNumber] = ncid.ConnectionID
}
// completing the handshake retires the initial client destination connection ID
added = added[:0]
queuedFrames = queuedFrames[:0]
now := monotime.Now()
g.SetHandshakeComplete(now)
require.Empty(t, added)
require.Empty(t, queuedFrames)
require.Empty(t, removed)
g.RemoveRetiredConnIDs(now)
if hasInitialClientDestConnID {
require.Equal(t, []protocol.ConnectionID{*initialClientDestConnID}, removed)
removed = removed[:0]
} else {
require.Empty(t, removed)
}
// it's invalid to retire a connection ID that hasn't been issued yet
err := g.Retire(4, protocol.ParseConnectionID([]byte{3, 3, 3, 3}), monotime.Now())
require.ErrorIs(t, &qerr.TransportError{ErrorCode: qerr.ProtocolViolation}, err)
require.ErrorContains(t, err, "retired connection ID 4 (highest issued: 3)")
// it's invalid to retire a connection ID in a packet that uses that connection ID
err = g.Retire(3, connIDs[3], monotime.Now())
require.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.ProtocolViolation})
require.ErrorContains(t, err, "was used as the Destination Connection ID on this packet")
// retiring a connection ID makes us issue a new one
require.NoError(t, g.Retire(2, protocol.ParseConnectionID([]byte{3, 3, 3, 3}), monotime.Now()))
g.RemoveRetiredConnIDs(monotime.Now())
require.Equal(t, []protocol.ConnectionID{connIDs[2]}, removed)
require.Len(t, queuedFrames, 1)
require.EqualValues(t, 4, queuedFrames[0].(*wire.NewConnectionIDFrame).SequenceNumber)
queuedFrames = queuedFrames[:0]
removed = removed[:0]
// duplicate retirements don't do anything
require.NoError(t, g.Retire(2, protocol.ParseConnectionID([]byte{3, 3, 3, 3}), monotime.Now()))
g.RemoveRetiredConnIDs(monotime.Now())
require.Empty(t, queuedFrames)
require.Empty(t, removed)
}
func TestConnIDGeneratorRetiring(t *testing.T) {
initialConnID := protocol.ParseConnectionID([]byte{2, 2, 2, 2})
var added, removed []protocol.ConnectionID
g := newConnIDGenerator(
&packetHandlerMap{},
protocol.ParseConnectionID([]byte{1, 1, 1, 1}),
&initialConnID,
newStatelessResetter(&StatelessResetKey{1, 2, 3, 4}),
connRunnerCallbacks{
AddConnectionID: func(c protocol.ConnectionID) { added = append(added, c) },
RemoveConnectionID: func(c protocol.ConnectionID) { removed = append(removed, c) },
ReplaceWithClosed: func([]protocol.ConnectionID, []byte, time.Duration) {},
},
func(f wire.Frame) {},
&protocol.DefaultConnectionIDGenerator{ConnLen: 5},
)
require.NoError(t, g.SetMaxActiveConnIDs(6))
require.Empty(t, removed)
require.Len(t, added, 5)
now := monotime.Now()
retirements := map[protocol.ConnectionID]monotime.Time{}
t1 := now.Add(time.Duration(rand.IntN(1000)) * time.Millisecond)
retirements[initialConnID] = t1
g.SetHandshakeComplete(t1)
for i := range 5 {
t2 := now.Add(time.Duration(rand.IntN(1000)) * time.Millisecond)
require.NoError(t, g.Retire(uint64(i+1), protocol.ParseConnectionID([]byte{9, 9, 9, 9}), t2))
retirements[added[i]] = t2
var nextRetirement monotime.Time
for _, r := range retirements {
if nextRetirement.IsZero() || r.Before(nextRetirement) {
nextRetirement = r
}
}
require.Equal(t, nextRetirement, g.NextRetireTime())
if rand.IntN(2) == 0 {
now = now.Add(time.Duration(rand.IntN(500)) * time.Millisecond)
g.RemoveRetiredConnIDs(now)
for _, r := range removed {
require.Contains(t, retirements, r)
require.LessOrEqual(t, retirements[r], now)
delete(retirements, r)
}
removed = removed[:0]
for _, r := range retirements {
require.Greater(t, r, now)
}
}
}
}
func TestConnIDGeneratorRemoveAll(t *testing.T) {
t.Run("with initial client destination connection ID", func(t *testing.T) {
testConnIDGeneratorRemoveAll(t, true)
})
t.Run("without initial client destination connection ID", func(t *testing.T) {
testConnIDGeneratorRemoveAll(t, false)
})
}
func testConnIDGeneratorRemoveAll(t *testing.T, hasInitialClientDestConnID bool) {
var initialClientDestConnID *protocol.ConnectionID
if hasInitialClientDestConnID {
connID := protocol.ParseConnectionID([]byte{2, 2, 2, 2})
initialClientDestConnID = &connID
}
var (
added []protocol.ConnectionID
removed []protocol.ConnectionID
)
g := newConnIDGenerator(
&packetHandlerMap{},
protocol.ParseConnectionID([]byte{1, 1, 1, 1}),
initialClientDestConnID,
newStatelessResetter(&StatelessResetKey{1, 2, 3, 4}),
connRunnerCallbacks{
AddConnectionID: func(c protocol.ConnectionID) { added = append(added, c) },
RemoveConnectionID: func(c protocol.ConnectionID) { removed = append(removed, c) },
ReplaceWithClosed: func([]protocol.ConnectionID, []byte, time.Duration) {},
},
func(f wire.Frame) {},
&protocol.DefaultConnectionIDGenerator{ConnLen: 5},
)
require.NoError(t, g.SetMaxActiveConnIDs(1000))
require.Len(t, added, protocol.MaxIssuedConnectionIDs-1)
g.RemoveAll()
if hasInitialClientDestConnID {
require.Len(t, removed, protocol.MaxIssuedConnectionIDs+1)
require.Contains(t, removed, *initialClientDestConnID)
} else {
require.Len(t, removed, protocol.MaxIssuedConnectionIDs)
}
for _, id := range added {
require.Contains(t, removed, id)
}
require.Contains(t, removed, protocol.ParseConnectionID([]byte{1, 1, 1, 1}))
}
func TestConnIDGeneratorReplaceWithClosed(t *testing.T) {
t.Run("with initial client destination connection ID", func(t *testing.T) {
testConnIDGeneratorReplaceWithClosed(t, true)
})
t.Run("without initial client destination connection ID", func(t *testing.T) {
testConnIDGeneratorReplaceWithClosed(t, false)
})
}
func testConnIDGeneratorReplaceWithClosed(t *testing.T, hasInitialClientDestConnID bool) {
var initialClientDestConnID *protocol.ConnectionID
if hasInitialClientDestConnID {
connID := protocol.ParseConnectionID([]byte{2, 2, 2, 2})
initialClientDestConnID = &connID
}
var (
added []protocol.ConnectionID
replaced []protocol.ConnectionID
replacedWith []byte
)
g := newConnIDGenerator(
&packetHandlerMap{},
protocol.ParseConnectionID([]byte{1, 1, 1, 1}),
initialClientDestConnID,
newStatelessResetter(&StatelessResetKey{1, 2, 3, 4}),
connRunnerCallbacks{
AddConnectionID: func(c protocol.ConnectionID) { added = append(added, c) },
RemoveConnectionID: func(c protocol.ConnectionID) { t.Fatal("didn't expect conn ID removals") },
ReplaceWithClosed: func(connIDs []protocol.ConnectionID, b []byte, _ time.Duration) {
replaced = connIDs
replacedWith = b
},
},
func(f wire.Frame) {},
&protocol.DefaultConnectionIDGenerator{ConnLen: 5},
)
require.NoError(t, g.SetMaxActiveConnIDs(1000))
require.Len(t, added, protocol.MaxIssuedConnectionIDs-1)
// Retire two of these connection ID.
// This makes us issue two more connection IDs.
require.NoError(t, g.Retire(3, protocol.ParseConnectionID([]byte{1, 1, 1, 1}), monotime.Now()))
require.NoError(t, g.Retire(4, protocol.ParseConnectionID([]byte{1, 1, 1, 1}), monotime.Now()))
require.Len(t, added, protocol.MaxIssuedConnectionIDs+1)
g.ReplaceWithClosed([]byte("foobar"), time.Second)
if hasInitialClientDestConnID {
require.Len(t, replaced, protocol.MaxIssuedConnectionIDs+3)
require.Contains(t, replaced, *initialClientDestConnID)
} else {
require.Len(t, replaced, protocol.MaxIssuedConnectionIDs+2)
}
for _, id := range added {
require.Contains(t, replaced, id)
}
require.Contains(t, replaced, protocol.ParseConnectionID([]byte{1, 1, 1, 1}))
require.Equal(t, []byte("foobar"), replacedWith)
}
func TestConnIDGeneratorAddConnRunner(t *testing.T) {
initialConnID := protocol.ParseConnectionID([]byte{1, 1, 1, 1})
clientDestConnID := protocol.ParseConnectionID([]byte{2, 2, 2, 2})
type connIDTracker struct {
added, removed, replaced []protocol.ConnectionID
}
var tracker1, tracker2, tracker3 connIDTracker
runner1 := connRunnerCallbacks{
AddConnectionID: func(c protocol.ConnectionID) { tracker1.added = append(tracker1.added, c) },
RemoveConnectionID: func(c protocol.ConnectionID) { tracker1.removed = append(tracker1.removed, c) },
ReplaceWithClosed: func(connIDs []protocol.ConnectionID, _ []byte, _ time.Duration) {
tracker1.replaced = append(tracker1.replaced, connIDs...)
},
}
runner2 := connRunnerCallbacks{
AddConnectionID: func(c protocol.ConnectionID) { tracker2.added = append(tracker2.added, c) },
RemoveConnectionID: func(c protocol.ConnectionID) { tracker2.removed = append(tracker2.removed, c) },
ReplaceWithClosed: func(connIDs []protocol.ConnectionID, _ []byte, _ time.Duration) {
tracker2.replaced = append(tracker2.replaced, connIDs...)
},
}
runner3 := connRunnerCallbacks{
AddConnectionID: func(c protocol.ConnectionID) { tracker3.added = append(tracker3.added, c) },
RemoveConnectionID: func(c protocol.ConnectionID) { tracker3.removed = append(tracker3.removed, c) },
ReplaceWithClosed: func(connIDs []protocol.ConnectionID, _ []byte, _ time.Duration) {
tracker3.replaced = append(tracker3.replaced, connIDs...)
},
}
sr := newStatelessResetter(&StatelessResetKey{1, 2, 3, 4})
var queuedFrames []wire.Frame
tr := &packetHandlerMap{}
g := newConnIDGenerator(
tr,
initialConnID,
&clientDestConnID,
sr,
runner1,
func(f wire.Frame) { queuedFrames = append(queuedFrames, f) },
&protocol.DefaultConnectionIDGenerator{ConnLen: 5},
)
require.NoError(t, g.SetMaxActiveConnIDs(3))
require.Len(t, tracker1.added, 2)
// add the second runner - it should get all existing connection IDs
g.AddConnRunner(&packetHandlerMap{}, runner2)
require.Len(t, tracker1.added, 2) // unchanged
require.Len(t, tracker2.added, 4)
require.Contains(t, tracker2.added, initialConnID)
require.Contains(t, tracker2.added, clientDestConnID)
require.Contains(t, tracker2.added, tracker1.added[0])
require.Contains(t, tracker2.added, tracker1.added[1])
// adding the same transport again doesn't do anything
trCopy := tr
g.AddConnRunner(trCopy, runner3)
require.Empty(t, tracker3.added)
var connIDToRetire protocol.ConnectionID
var seqToRetire uint64
ncid := queuedFrames[0].(*wire.NewConnectionIDFrame)
connIDToRetire = ncid.ConnectionID
seqToRetire = ncid.SequenceNumber
require.NoError(t, g.Retire(seqToRetire, protocol.ParseConnectionID([]byte{3, 3, 3, 3}), monotime.Now()))
g.RemoveRetiredConnIDs(monotime.Now())
require.Equal(t, []protocol.ConnectionID{connIDToRetire}, tracker1.removed)
require.Equal(t, []protocol.ConnectionID{connIDToRetire}, tracker2.removed)
tracker1.removed = nil
tracker2.removed = nil
g.SetHandshakeComplete(monotime.Now())
g.RemoveRetiredConnIDs(monotime.Now())
require.Equal(t, []protocol.ConnectionID{clientDestConnID}, tracker1.removed)
require.Equal(t, []protocol.ConnectionID{clientDestConnID}, tracker2.removed)
g.ReplaceWithClosed([]byte("connection closed"), time.Second)
require.True(t, len(tracker1.replaced) > 0)
require.Equal(t, tracker1.replaced, tracker2.replaced)
tracker1.removed = nil
tracker2.removed = nil
g.RemoveAll()
require.NotEmpty(t, tracker1.removed)
require.Equal(t, tracker1.removed, tracker2.removed)
}
golang-github-lucas-clemente-quic-go-0.55.0/conn_id_manager.go 0000664 0000000 0000000 00000023441 15074232574 0024240 0 ustar 00root root 0000000 0000000 package quic
import (
"fmt"
"slices"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/internal/wire"
)
type newConnID struct {
SequenceNumber uint64
ConnectionID protocol.ConnectionID
StatelessResetToken protocol.StatelessResetToken
}
type connIDManager struct {
queue []newConnID
highestProbingID uint64
pathProbing map[pathID]newConnID // initialized lazily
handshakeComplete bool
activeSequenceNumber uint64
highestRetired uint64
activeConnectionID protocol.ConnectionID
activeStatelessResetToken *protocol.StatelessResetToken
// We change the connection ID after sending on average
// protocol.PacketsPerConnectionID packets. The actual value is randomized
// hide the packet loss rate from on-path observers.
rand utils.Rand
packetsSinceLastChange uint32
packetsPerConnectionID uint32
addStatelessResetToken func(protocol.StatelessResetToken)
removeStatelessResetToken func(protocol.StatelessResetToken)
queueControlFrame func(wire.Frame)
closed bool
}
func newConnIDManager(
initialDestConnID protocol.ConnectionID,
addStatelessResetToken func(protocol.StatelessResetToken),
removeStatelessResetToken func(protocol.StatelessResetToken),
queueControlFrame func(wire.Frame),
) *connIDManager {
return &connIDManager{
activeConnectionID: initialDestConnID,
addStatelessResetToken: addStatelessResetToken,
removeStatelessResetToken: removeStatelessResetToken,
queueControlFrame: queueControlFrame,
queue: make([]newConnID, 0, protocol.MaxActiveConnectionIDs),
}
}
func (h *connIDManager) AddFromPreferredAddress(connID protocol.ConnectionID, resetToken protocol.StatelessResetToken) error {
return h.addConnectionID(1, connID, resetToken)
}
func (h *connIDManager) Add(f *wire.NewConnectionIDFrame) error {
if err := h.add(f); err != nil {
return err
}
if len(h.queue) >= protocol.MaxActiveConnectionIDs {
return &qerr.TransportError{ErrorCode: qerr.ConnectionIDLimitError}
}
return nil
}
func (h *connIDManager) add(f *wire.NewConnectionIDFrame) error {
if h.activeConnectionID.Len() == 0 {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "received NEW_CONNECTION_ID frame but zero-length connection IDs are in use",
}
}
// If the NEW_CONNECTION_ID frame is reordered, such that its sequence number is smaller than the currently active
// connection ID or if it was already retired, send the RETIRE_CONNECTION_ID frame immediately.
if f.SequenceNumber < max(h.activeSequenceNumber, h.highestProbingID) || f.SequenceNumber < h.highestRetired {
h.queueControlFrame(&wire.RetireConnectionIDFrame{
SequenceNumber: f.SequenceNumber,
})
return nil
}
if f.RetirePriorTo != 0 && h.pathProbing != nil {
for id, entry := range h.pathProbing {
if entry.SequenceNumber < f.RetirePriorTo {
h.queueControlFrame(&wire.RetireConnectionIDFrame{
SequenceNumber: entry.SequenceNumber,
})
h.removeStatelessResetToken(entry.StatelessResetToken)
delete(h.pathProbing, id)
}
}
}
// Retire elements in the queue.
// Doesn't retire the active connection ID.
if f.RetirePriorTo > h.highestRetired {
var newQueue []newConnID
for _, entry := range h.queue {
if entry.SequenceNumber >= f.RetirePriorTo {
newQueue = append(newQueue, entry)
} else {
h.queueControlFrame(&wire.RetireConnectionIDFrame{SequenceNumber: entry.SequenceNumber})
}
}
h.queue = newQueue
h.highestRetired = f.RetirePriorTo
}
if f.SequenceNumber == h.activeSequenceNumber {
return nil
}
if err := h.addConnectionID(f.SequenceNumber, f.ConnectionID, f.StatelessResetToken); err != nil {
return err
}
// Retire the active connection ID, if necessary.
if h.activeSequenceNumber < f.RetirePriorTo {
// The queue is guaranteed to have at least one element at this point.
h.updateConnectionID()
}
return nil
}
func (h *connIDManager) addConnectionID(seq uint64, connID protocol.ConnectionID, resetToken protocol.StatelessResetToken) error {
// fast path: add to the end of the queue
if len(h.queue) == 0 || h.queue[len(h.queue)-1].SequenceNumber < seq {
h.queue = append(h.queue, newConnID{
SequenceNumber: seq,
ConnectionID: connID,
StatelessResetToken: resetToken,
})
return nil
}
// slow path: insert in the middle
for i, entry := range h.queue {
if entry.SequenceNumber == seq {
if entry.ConnectionID != connID {
return fmt.Errorf("received conflicting connection IDs for sequence number %d", seq)
}
if entry.StatelessResetToken != resetToken {
return fmt.Errorf("received conflicting stateless reset tokens for sequence number %d", seq)
}
return nil
}
// insert at the correct position to maintain sorted order
if entry.SequenceNumber > seq {
h.queue = slices.Insert(h.queue, i, newConnID{
SequenceNumber: seq,
ConnectionID: connID,
StatelessResetToken: resetToken,
})
return nil
}
}
return nil // unreachable
}
func (h *connIDManager) updateConnectionID() {
h.assertNotClosed()
h.queueControlFrame(&wire.RetireConnectionIDFrame{
SequenceNumber: h.activeSequenceNumber,
})
h.highestRetired = max(h.highestRetired, h.activeSequenceNumber)
if h.activeStatelessResetToken != nil {
h.removeStatelessResetToken(*h.activeStatelessResetToken)
}
front := h.queue[0]
h.queue = h.queue[1:]
h.activeSequenceNumber = front.SequenceNumber
h.activeConnectionID = front.ConnectionID
h.activeStatelessResetToken = &front.StatelessResetToken
h.packetsSinceLastChange = 0
h.packetsPerConnectionID = protocol.PacketsPerConnectionID/2 + uint32(h.rand.Int31n(protocol.PacketsPerConnectionID))
h.addStatelessResetToken(*h.activeStatelessResetToken)
}
func (h *connIDManager) Close() {
h.closed = true
if h.activeStatelessResetToken != nil {
h.removeStatelessResetToken(*h.activeStatelessResetToken)
}
if h.pathProbing != nil {
for _, entry := range h.pathProbing {
h.removeStatelessResetToken(entry.StatelessResetToken)
}
}
}
// is called when the server performs a Retry
// and when the server changes the connection ID in the first Initial sent
func (h *connIDManager) ChangeInitialConnID(newConnID protocol.ConnectionID) {
if h.activeSequenceNumber != 0 {
panic("expected first connection ID to have sequence number 0")
}
h.activeConnectionID = newConnID
}
// is called when the server provides a stateless reset token in the transport parameters
func (h *connIDManager) SetStatelessResetToken(token protocol.StatelessResetToken) {
h.assertNotClosed()
if h.activeSequenceNumber != 0 {
panic("expected first connection ID to have sequence number 0")
}
h.activeStatelessResetToken = &token
h.addStatelessResetToken(token)
}
func (h *connIDManager) SentPacket() {
h.packetsSinceLastChange++
}
func (h *connIDManager) shouldUpdateConnID() bool {
if !h.handshakeComplete {
return false
}
// initiate the first change as early as possible (after handshake completion)
if len(h.queue) > 0 && h.activeSequenceNumber == 0 {
return true
}
// For later changes, only change if
// 1. The queue of connection IDs is filled more than 50%.
// 2. We sent at least PacketsPerConnectionID packets
return 2*len(h.queue) >= protocol.MaxActiveConnectionIDs &&
h.packetsSinceLastChange >= h.packetsPerConnectionID
}
func (h *connIDManager) Get() protocol.ConnectionID {
h.assertNotClosed()
if h.shouldUpdateConnID() {
h.updateConnectionID()
}
return h.activeConnectionID
}
func (h *connIDManager) SetHandshakeComplete() {
h.handshakeComplete = true
}
// GetConnIDForPath retrieves a connection ID for a new path (i.e. not the active one).
// Once a connection ID is allocated for a path, it cannot be used for a different path.
// When called with the same pathID, it will return the same connection ID,
// unless the peer requested that this connection ID be retired.
func (h *connIDManager) GetConnIDForPath(id pathID) (protocol.ConnectionID, bool) {
h.assertNotClosed()
// if we're using zero-length connection IDs, we don't need to change the connection ID
if h.activeConnectionID.Len() == 0 {
return protocol.ConnectionID{}, true
}
if h.pathProbing == nil {
h.pathProbing = make(map[pathID]newConnID)
}
entry, ok := h.pathProbing[id]
if ok {
return entry.ConnectionID, true
}
if len(h.queue) == 0 {
return protocol.ConnectionID{}, false
}
front := h.queue[0]
h.queue = h.queue[1:]
h.pathProbing[id] = front
h.highestProbingID = front.SequenceNumber
h.addStatelessResetToken(front.StatelessResetToken)
return front.ConnectionID, true
}
func (h *connIDManager) RetireConnIDForPath(pathID pathID) {
h.assertNotClosed()
// if we're using zero-length connection IDs, we don't need to change the connection ID
if h.activeConnectionID.Len() == 0 {
return
}
entry, ok := h.pathProbing[pathID]
if !ok {
return
}
h.queueControlFrame(&wire.RetireConnectionIDFrame{
SequenceNumber: entry.SequenceNumber,
})
h.removeStatelessResetToken(entry.StatelessResetToken)
delete(h.pathProbing, pathID)
}
func (h *connIDManager) IsActiveStatelessResetToken(token protocol.StatelessResetToken) bool {
if h.activeStatelessResetToken != nil {
if *h.activeStatelessResetToken == token {
return true
}
}
if h.pathProbing != nil {
for _, entry := range h.pathProbing {
if entry.StatelessResetToken == token {
return true
}
}
}
return false
}
// Using the connIDManager after it has been closed can have disastrous effects:
// If the connection ID is rotated, a new entry would be inserted into the packet handler map,
// leading to a memory leak of the connection struct.
// See https://github.com/quic-go/quic-go/pull/4852 for more details.
func (h *connIDManager) assertNotClosed() {
if h.closed {
panic("connection ID manager is closed")
}
}
golang-github-lucas-clemente-quic-go-0.55.0/conn_id_manager_test.go 0000664 0000000 0000000 00000042515 15074232574 0025302 0 ustar 00root root 0000000 0000000 package quic
import (
"crypto/rand"
"testing"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/wire"
"github.com/stretchr/testify/require"
)
func TestConnIDManagerInitialConnID(t *testing.T) {
m := newConnIDManager(protocol.ParseConnectionID([]byte{1, 2, 3, 4}), nil, nil, nil)
require.Equal(t, protocol.ParseConnectionID([]byte{1, 2, 3, 4}), m.Get())
require.Equal(t, protocol.ParseConnectionID([]byte{1, 2, 3, 4}), m.Get())
m.ChangeInitialConnID(protocol.ParseConnectionID([]byte{5, 6, 7, 8}))
require.Equal(t, protocol.ParseConnectionID([]byte{5, 6, 7, 8}), m.Get())
}
func TestConnIDManagerAddConnIDs(t *testing.T) {
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(protocol.StatelessResetToken) {},
func(protocol.StatelessResetToken) {},
func(wire.Frame) {},
)
f1 := &wire.NewConnectionIDFrame{
SequenceNumber: 1,
ConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
}
f2 := &wire.NewConnectionIDFrame{
SequenceNumber: 2,
ConnectionID: protocol.ParseConnectionID([]byte{0xba, 0xad, 0xf0, 0x0d}),
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
}
require.NoError(t, m.Add(f2))
require.NoError(t, m.Add(f1)) // receiving reordered frames is fine
require.NoError(t, m.Add(f2)) // receiving a duplicate is fine
require.Equal(t, protocol.ParseConnectionID([]byte{1, 2, 3, 4}), m.Get())
m.updateConnectionID()
require.Equal(t, protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}), m.Get())
m.updateConnectionID()
require.Equal(t, protocol.ParseConnectionID([]byte{0xba, 0xad, 0xf0, 0x0d}), m.Get())
require.NoError(t, m.Add(f2)) // receiving a duplicate for the current connection ID is fine as well
require.Equal(t, protocol.ParseConnectionID([]byte{0xba, 0xad, 0xf0, 0x0d}), m.Get())
// receiving mismatching connection IDs is not fine
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 3,
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}), // mismatching connection ID
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
}))
require.EqualError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 3,
ConnectionID: protocol.ParseConnectionID([]byte{2, 3, 4, 5}), // mismatching connection ID
StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe},
}), "received conflicting connection IDs for sequence number 3")
// receiving mismatching stateless reset tokens is not fine either
require.EqualError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 3,
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
StatelessResetToken: protocol.StatelessResetToken{1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0},
}), "received conflicting stateless reset tokens for sequence number 3")
}
func TestConnIDManagerLimit(t *testing.T) {
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(protocol.StatelessResetToken) {},
func(protocol.StatelessResetToken) {},
func(f wire.Frame) {},
)
for i := uint8(1); i < protocol.MaxActiveConnectionIDs; i++ {
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: uint64(i),
ConnectionID: protocol.ParseConnectionID([]byte{i, i, i, i}),
StatelessResetToken: protocol.StatelessResetToken{i, i, i, i, i, i, i, i, i, i, i, i, i, i, i, i},
}))
}
require.Equal(t, &qerr.TransportError{ErrorCode: qerr.ConnectionIDLimitError}, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: uint64(9999),
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
StatelessResetToken: protocol.StatelessResetToken{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
}))
}
func TestConnIDManagerRetiringConnectionIDs(t *testing.T) {
var frameQueue []wire.Frame
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(protocol.StatelessResetToken) {},
func(protocol.StatelessResetToken) {},
func(f wire.Frame) { frameQueue = append(frameQueue, f) },
)
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 10,
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
}))
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 13,
ConnectionID: protocol.ParseConnectionID([]byte{2, 3, 4, 5}),
}))
require.Empty(t, frameQueue)
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
RetirePriorTo: 14,
SequenceNumber: 17,
ConnectionID: protocol.ParseConnectionID([]byte{3, 4, 5, 6}),
}))
require.Equal(t, []wire.Frame{
&wire.RetireConnectionIDFrame{SequenceNumber: 10},
&wire.RetireConnectionIDFrame{SequenceNumber: 13},
&wire.RetireConnectionIDFrame{SequenceNumber: 0},
}, frameQueue)
require.Equal(t, protocol.ParseConnectionID([]byte{3, 4, 5, 6}), m.Get())
frameQueue = nil
// a reordered connection ID is immediately retired
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 12,
ConnectionID: protocol.ParseConnectionID([]byte{5, 6, 7, 8}),
}))
require.Equal(t, []wire.Frame{&wire.RetireConnectionIDFrame{SequenceNumber: 12}}, frameQueue)
require.Equal(t, protocol.ParseConnectionID([]byte{3, 4, 5, 6}), m.Get())
}
func TestConnIDManagerHandshakeCompletion(t *testing.T) {
var frameQueue []wire.Frame
var addedTokens, removedTokens []protocol.StatelessResetToken
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(token protocol.StatelessResetToken) { addedTokens = append(addedTokens, token) },
func(token protocol.StatelessResetToken) { removedTokens = append(removedTokens, token) },
func(f wire.Frame) { frameQueue = append(frameQueue, f) },
)
m.SetStatelessResetToken(protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1})
require.Equal(t, []protocol.StatelessResetToken{{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}}, addedTokens)
require.Empty(t, removedTokens)
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 1,
ConnectionID: protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
}))
require.Equal(t, protocol.ParseConnectionID([]byte{1, 2, 3, 4}), m.Get())
m.SetHandshakeComplete()
require.Equal(t, protocol.ParseConnectionID([]byte{4, 3, 2, 1}), m.Get())
require.Equal(t, []wire.Frame{&wire.RetireConnectionIDFrame{SequenceNumber: 0}}, frameQueue)
require.Equal(t, []protocol.StatelessResetToken{{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}}, removedTokens)
}
func TestConnIDManagerConnIDRotation(t *testing.T) {
toToken := func(connID protocol.ConnectionID) protocol.StatelessResetToken {
var token protocol.StatelessResetToken
copy(token[:], connID.Bytes())
copy(token[connID.Len():], connID.Bytes())
return token
}
var frameQueue []wire.Frame
var addedTokens, removedTokens []protocol.StatelessResetToken
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(token protocol.StatelessResetToken) { addedTokens = append(addedTokens, token) },
func(token protocol.StatelessResetToken) { removedTokens = append(removedTokens, token) },
func(f wire.Frame) { frameQueue = append(frameQueue, f) },
)
// the first connection ID is used as soon as the handshake is complete
m.SetHandshakeComplete()
firstConnID := protocol.ParseConnectionID([]byte{4, 3, 2, 1})
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 1,
ConnectionID: firstConnID,
StatelessResetToken: toToken(protocol.ParseConnectionID([]byte{4, 3, 2, 1})),
}))
require.Equal(t, firstConnID, m.Get())
frameQueue = nil
require.True(t, m.IsActiveStatelessResetToken(toToken(firstConnID)))
require.Equal(t, addedTokens, []protocol.StatelessResetToken{toToken(firstConnID)})
addedTokens = addedTokens[:0]
// Note that we're missing the connection ID with sequence number 2.
// It will be received later.
var queuedConnIDs []protocol.ConnectionID
for i := 0; i < protocol.MaxActiveConnectionIDs-1; i++ {
b := make([]byte, 4)
rand.Read(b)
connID := protocol.ParseConnectionID(b)
queuedConnIDs = append(queuedConnIDs, connID)
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: uint64(3 + i),
ConnectionID: connID,
StatelessResetToken: toToken(connID),
}))
require.False(t, m.IsActiveStatelessResetToken(toToken(connID)))
}
var counter int
for {
require.Empty(t, frameQueue)
m.SentPacket()
counter++
if connID := m.Get(); connID != firstConnID {
require.Equal(t, queuedConnIDs[0], m.Get())
require.Equal(t, []wire.Frame{&wire.RetireConnectionIDFrame{SequenceNumber: 1}}, frameQueue)
require.Equal(t, removedTokens, []protocol.StatelessResetToken{toToken(firstConnID)})
require.Equal(t, addedTokens, []protocol.StatelessResetToken{toToken(connID)})
addedTokens = addedTokens[:0]
removedTokens = removedTokens[:0]
require.True(t, m.IsActiveStatelessResetToken(toToken(connID)))
require.False(t, m.IsActiveStatelessResetToken(toToken(firstConnID)))
break
}
require.True(t, m.IsActiveStatelessResetToken(toToken(firstConnID)))
require.Empty(t, addedTokens)
}
require.GreaterOrEqual(t, counter, protocol.PacketsPerConnectionID/2)
require.LessOrEqual(t, counter, protocol.PacketsPerConnectionID*3/2)
frameQueue = nil
// now receive connection ID 2
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 2,
ConnectionID: protocol.ParseConnectionID([]byte{2, 3, 4, 5}),
}))
require.Equal(t, []wire.Frame{&wire.RetireConnectionIDFrame{SequenceNumber: 2}}, frameQueue)
}
func TestConnIDManagerPathMigration(t *testing.T) {
var frameQueue []wire.Frame
var addedTokens, removedTokens []protocol.StatelessResetToken
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(token protocol.StatelessResetToken) { addedTokens = append(addedTokens, token) },
func(token protocol.StatelessResetToken) { removedTokens = append(removedTokens, token) },
func(f wire.Frame) { frameQueue = append(frameQueue, f) },
)
// no connection ID available yet
_, ok := m.GetConnIDForPath(1)
require.False(t, ok)
// add two connection IDs
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 1,
ConnectionID: protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
StatelessResetToken: protocol.StatelessResetToken{4, 3, 2, 1, 4, 3, 2, 1},
}))
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 2,
ConnectionID: protocol.ParseConnectionID([]byte{5, 4, 3, 2}),
StatelessResetToken: protocol.StatelessResetToken{5, 4, 3, 2, 5, 4, 3, 2},
}))
connID, ok := m.GetConnIDForPath(1)
require.True(t, ok)
require.Equal(t, protocol.ParseConnectionID([]byte{4, 3, 2, 1}), connID)
require.Equal(t, []protocol.StatelessResetToken{{4, 3, 2, 1, 4, 3, 2, 1}}, addedTokens)
require.Empty(t, removedTokens)
addedTokens = addedTokens[:0]
require.False(t, m.IsActiveStatelessResetToken(protocol.StatelessResetToken{5, 4, 3, 2, 5, 4, 3, 2}))
connID, ok = m.GetConnIDForPath(2)
require.True(t, ok)
require.Equal(t, protocol.ParseConnectionID([]byte{5, 4, 3, 2}), connID)
require.Equal(t, []protocol.StatelessResetToken{{5, 4, 3, 2, 5, 4, 3, 2}}, addedTokens)
require.Empty(t, removedTokens)
require.True(t, m.IsActiveStatelessResetToken(protocol.StatelessResetToken{5, 4, 3, 2, 5, 4, 3, 2}))
addedTokens = addedTokens[:0]
// asking for the connection for path 1 again returns the same connection ID
connID, ok = m.GetConnIDForPath(1)
require.True(t, ok)
require.Equal(t, protocol.ParseConnectionID([]byte{4, 3, 2, 1}), connID)
require.Empty(t, addedTokens)
// if the connection ID is retired, the path will use another connection ID
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 3,
RetirePriorTo: 2,
ConnectionID: protocol.ParseConnectionID([]byte{6, 5, 4, 3}),
StatelessResetToken: protocol.StatelessResetToken{6, 5, 4, 3, 6, 5, 4, 3},
}))
require.Len(t, frameQueue, 2)
require.Equal(t, []protocol.StatelessResetToken{{4, 3, 2, 1, 4, 3, 2, 1}}, removedTokens)
frameQueue = nil
removedTokens = removedTokens[:0]
require.Equal(t, protocol.ParseConnectionID([]byte{6, 5, 4, 3}), m.Get())
require.Equal(t, []protocol.StatelessResetToken{{6, 5, 4, 3, 6, 5, 4, 3}}, addedTokens)
require.Empty(t, removedTokens)
addedTokens = addedTokens[:0]
// the connection ID is not used for new paths
_, ok = m.GetConnIDForPath(3)
require.False(t, ok)
// Manually retiring the connection ID does nothing.
// Path 1 doesn't have a connection ID anymore.
m.RetireConnIDForPath(1)
require.Empty(t, frameQueue)
_, ok = m.GetConnIDForPath(1)
require.False(t, ok)
require.Empty(t, removedTokens)
// only after a new connection ID is added, it will be used for path 1
require.NoError(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 4,
ConnectionID: protocol.ParseConnectionID([]byte{7, 6, 5, 4}),
StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13},
}))
connID, ok = m.GetConnIDForPath(1)
require.True(t, ok)
require.Equal(t, protocol.ParseConnectionID([]byte{7, 6, 5, 4}), connID)
require.Equal(t, []protocol.StatelessResetToken{{16, 15, 14, 13}}, addedTokens)
require.Empty(t, removedTokens)
require.True(t, m.IsActiveStatelessResetToken(protocol.StatelessResetToken{16, 15, 14, 13}))
// a RETIRE_CONNECTION_ID frame for path 1 is queued when retiring the connection ID
m.RetireConnIDForPath(1)
require.Equal(t, []wire.Frame{&wire.RetireConnectionIDFrame{SequenceNumber: 4}}, frameQueue)
require.Equal(t, []protocol.StatelessResetToken{{16, 15, 14, 13}}, removedTokens)
removedTokens = removedTokens[:0]
require.False(t, m.IsActiveStatelessResetToken(protocol.StatelessResetToken{16, 15, 14, 13}))
m.Close()
require.Equal(t, []protocol.StatelessResetToken{
{6, 5, 4, 3, 6, 5, 4, 3}, // currently active connection ID
{5, 4, 3, 2, 5, 4, 3, 2}, // path 2
}, removedTokens)
}
func TestConnIDManagerZeroLengthConnectionID(t *testing.T) {
m := newConnIDManager(
protocol.ConnectionID{},
func(protocol.StatelessResetToken) {},
func(protocol.StatelessResetToken) {},
func(f wire.Frame) {},
)
require.Equal(t, protocol.ConnectionID{}, m.Get())
for range 5 * protocol.PacketsPerConnectionID {
m.SentPacket()
require.Equal(t, protocol.ConnectionID{}, m.Get())
}
// for path probing, we don't need to change the connection ID
for id := pathID(1); id < 10; id++ {
connID, ok := m.GetConnIDForPath(id)
require.True(t, ok)
require.Equal(t, protocol.ConnectionID{}, connID)
}
// retiring a connection ID for a path is also a no-op
for id := pathID(1); id < 20; id++ {
m.RetireConnIDForPath(id)
}
require.ErrorIs(t, m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: 1,
ConnectionID: protocol.ConnectionID{},
StatelessResetToken: protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
}), &qerr.TransportError{ErrorCode: qerr.ProtocolViolation})
}
func TestConnIDManagerClose(t *testing.T) {
var addedTokens, removedTokens []protocol.StatelessResetToken
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(token protocol.StatelessResetToken) { addedTokens = append(addedTokens, token) },
func(token protocol.StatelessResetToken) { removedTokens = append(removedTokens, token) },
func(f wire.Frame) {},
)
m.SetStatelessResetToken(protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1})
require.Equal(t, []protocol.StatelessResetToken{{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}}, addedTokens)
require.Empty(t, removedTokens)
m.Close()
require.Equal(t, []protocol.StatelessResetToken{{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}}, removedTokens)
require.Panics(t, func() { m.Get() })
require.Panics(t, func() { m.SetStatelessResetToken(protocol.StatelessResetToken{}) })
}
func BenchmarkConnIDManagerReordered(b *testing.B) {
benchmarkConnIDManager(b, true)
}
func BenchmarkConnIDManagerInOrder(b *testing.B) {
benchmarkConnIDManager(b, false)
}
func benchmarkConnIDManager(b *testing.B, reordered bool) {
m := newConnIDManager(
protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
func(protocol.StatelessResetToken) {},
func(protocol.StatelessResetToken) {},
func(f wire.Frame) {},
)
connIDs := make([]protocol.ConnectionID, 0, protocol.MaxActiveConnectionIDs)
statelessResetTokens := make([]protocol.StatelessResetToken, 0, protocol.MaxActiveConnectionIDs)
for range protocol.MaxActiveConnectionIDs {
b := make([]byte, 8)
rand.Read(b)
connIDs = append(connIDs, protocol.ParseConnectionID(b))
var statelessResetToken protocol.StatelessResetToken
rand.Read(statelessResetToken[:])
statelessResetTokens = append(statelessResetTokens, statelessResetToken)
}
// 1 -> 3
// 2 -> 1
// 3 -> 2
// 4 -> 4
offsets := []int{2, -1, -1, 0}
b.ResetTimer()
for i := range b.N {
seq := i
if reordered {
seq += offsets[i%len(offsets)]
}
m.Add(&wire.NewConnectionIDFrame{
SequenceNumber: uint64(seq),
ConnectionID: connIDs[i%len(connIDs)],
StatelessResetToken: statelessResetTokens[i%len(statelessResetTokens)],
})
if i > protocol.MaxActiveConnectionIDs-2 {
m.updateConnectionID()
}
}
}
golang-github-lucas-clemente-quic-go-0.55.0/conn_wrapped_test.go 0000664 0000000 0000000 00000002633 15074232574 0024653 0 ustar 00root root 0000000 0000000 package quic
import "context"
func (c *wrappedConn) run() error {
if c.testHooks == nil {
return c.Conn.run()
}
if c.testHooks.run != nil {
return c.testHooks.run()
}
return nil
}
func (c *wrappedConn) earlyConnReady() <-chan struct{} {
if c.testHooks == nil {
return c.Conn.earlyConnReady()
}
if c.testHooks.earlyConnReady != nil {
return c.testHooks.earlyConnReady()
}
return nil
}
func (c *wrappedConn) Context() context.Context {
if c.testHooks == nil {
return c.Conn.Context()
}
if c.testHooks.context != nil {
return c.testHooks.context()
}
return context.Background()
}
func (c *wrappedConn) HandshakeComplete() <-chan struct{} {
if c.testHooks == nil {
return c.Conn.HandshakeComplete()
}
if c.testHooks.handshakeComplete != nil {
return c.testHooks.handshakeComplete()
}
return nil
}
func (c *wrappedConn) closeWithTransportError(code TransportErrorCode) {
if c.testHooks == nil {
c.Conn.closeWithTransportError(code)
return
}
if c.testHooks.closeWithTransportError != nil {
c.testHooks.closeWithTransportError(code)
}
}
func (c *wrappedConn) destroy(e error) {
if c.testHooks == nil {
c.Conn.destroy(e)
return
}
if c.testHooks.destroy != nil {
c.testHooks.destroy(e)
}
}
func (c *wrappedConn) handlePacket(p receivedPacket) {
if c.testHooks == nil {
c.Conn.handlePacket(p)
return
}
if c.testHooks.handlePacket != nil {
c.testHooks.handlePacket(p)
}
}
golang-github-lucas-clemente-quic-go-0.55.0/connection.go 0000664 0000000 0000000 00000276025 15074232574 0023304 0 ustar 00root root 0000000 0000000 package quic
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"reflect"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/quic-go/quic-go/internal/ackhandler"
"github.com/quic-go/quic-go/internal/flowcontrol"
"github.com/quic-go/quic-go/internal/handshake"
"github.com/quic-go/quic-go/internal/monotime"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/internal/utils/ringbuffer"
"github.com/quic-go/quic-go/internal/wire"
"github.com/quic-go/quic-go/logging"
)
type unpacker interface {
UnpackLongHeader(hdr *wire.Header, data []byte) (*unpackedPacket, error)
UnpackShortHeader(rcvTime monotime.Time, data []byte) (protocol.PacketNumber, protocol.PacketNumberLen, protocol.KeyPhaseBit, []byte, error)
}
type cryptoStreamHandler interface {
StartHandshake(context.Context) error
ChangeConnectionID(protocol.ConnectionID)
SetLargest1RTTAcked(protocol.PacketNumber) error
SetHandshakeConfirmed()
GetSessionTicket() ([]byte, error)
NextEvent() handshake.Event
DiscardInitialKeys()
HandleMessage([]byte, protocol.EncryptionLevel) error
io.Closer
ConnectionState() handshake.ConnectionState
}
type receivedPacket struct {
buffer *packetBuffer
remoteAddr net.Addr
rcvTime monotime.Time
data []byte
ecn protocol.ECN
info packetInfo // only valid if the contained IP address is valid
}
func (p *receivedPacket) Size() protocol.ByteCount { return protocol.ByteCount(len(p.data)) }
func (p *receivedPacket) Clone() *receivedPacket {
return &receivedPacket{
remoteAddr: p.remoteAddr,
rcvTime: p.rcvTime,
data: p.data,
buffer: p.buffer,
ecn: p.ecn,
info: p.info,
}
}
type connRunner interface {
Add(protocol.ConnectionID, packetHandler) bool
Remove(protocol.ConnectionID)
ReplaceWithClosed([]protocol.ConnectionID, []byte, time.Duration)
AddResetToken(protocol.StatelessResetToken, packetHandler)
RemoveResetToken(protocol.StatelessResetToken)
}
type closeError struct {
err error
immediate bool
}
type errCloseForRecreating struct {
nextPacketNumber protocol.PacketNumber
nextVersion protocol.Version
}
func (e *errCloseForRecreating) Error() string {
return "closing connection in order to recreate it"
}
var deadlineSendImmediately = monotime.Time(42 * time.Millisecond) // any value > time.Time{} and before time.Now() is fine
var connTracingID atomic.Uint64 // to be accessed atomically
func nextConnTracingID() ConnectionTracingID { return ConnectionTracingID(connTracingID.Add(1)) }
type blockMode uint8
const (
// blockModeNone means that the connection is not blocked.
blockModeNone blockMode = iota
// blockModeCongestionLimited means that the connection is congestion limited.
// In that case, we can still send acknowledgments and PTO probe packets.
blockModeCongestionLimited
// blockModeHardBlocked means that no packet can be sent, under no circumstances. This can happen when:
// * the send queue is full
// * the SentPacketHandler returns SendNone, e.g. when we are tracking the maximum number of packets
// In that case, the timer will be set to the idle timeout.
blockModeHardBlocked
)
// A Conn is a QUIC connection between two peers.
// Calls to the connection (and to streams) can return the following types of errors:
// - [ApplicationError]: for errors triggered by the application running on top of QUIC
// - [TransportError]: for errors triggered by the QUIC transport (in many cases a misbehaving peer)
// - [IdleTimeoutError]: when the peer goes away unexpectedly (this is a [net.Error] timeout error)
// - [HandshakeTimeoutError]: when the cryptographic handshake takes too long (this is a [net.Error] timeout error)
// - [StatelessResetError]: when we receive a stateless reset
// - [VersionNegotiationError]: returned by the client, when there's no version overlap between the peers
type Conn struct {
// Destination connection ID used during the handshake.
// Used to check source connection ID on incoming packets.
handshakeDestConnID protocol.ConnectionID
// Set for the client. Destination connection ID used on the first Initial sent.
origDestConnID protocol.ConnectionID
retrySrcConnID *protocol.ConnectionID // only set for the client (and if a Retry was performed)
srcConnIDLen int
perspective protocol.Perspective
version protocol.Version
config *Config
conn sendConn
sendQueue sender
// lazily initialzed: most connections never migrate
pathManager *pathManager
largestRcvdAppData protocol.PacketNumber
pathManagerOutgoing atomic.Pointer[pathManagerOutgoing]
streamsMap *streamsMap
connIDManager *connIDManager
connIDGenerator *connIDGenerator
rttStats *utils.RTTStats
connStats utils.ConnectionStats
cryptoStreamManager *cryptoStreamManager
sentPacketHandler ackhandler.SentPacketHandler
receivedPacketHandler ackhandler.ReceivedPacketHandler
retransmissionQueue *retransmissionQueue
framer *framer
connFlowController flowcontrol.ConnectionFlowController
tokenStoreKey string // only set for the client
tokenGenerator *handshake.TokenGenerator // only set for the server
unpacker unpacker
frameParser wire.FrameParser
packer packer
mtuDiscoverer mtuDiscoverer // initialized when the transport parameters are received
currentMTUEstimate atomic.Uint32
initialStream *initialCryptoStream
handshakeStream *cryptoStream
oneRTTStream *cryptoStream // only set for the server
cryptoStreamHandler cryptoStreamHandler
notifyReceivedPacket chan struct{}
sendingScheduled chan struct{}
receivedPacketMx sync.Mutex
receivedPackets ringbuffer.RingBuffer[receivedPacket]
// closeChan is used to notify the run loop that it should terminate
closeChan chan struct{}
closeErr atomic.Pointer[closeError]
ctx context.Context
ctxCancel context.CancelCauseFunc
handshakeCompleteChan chan struct{}
undecryptablePackets []receivedPacket // undecryptable packets, waiting for a change in encryption level
undecryptablePacketsToProcess []receivedPacket
earlyConnReadyChan chan struct{}
sentFirstPacket bool
droppedInitialKeys bool
handshakeComplete bool
handshakeConfirmed bool
receivedRetry bool
versionNegotiated bool
receivedFirstPacket bool
blocked blockMode
// the minimum of the max_idle_timeout values advertised by both endpoints
idleTimeout time.Duration
creationTime monotime.Time
// The idle timeout is set based on the max of the time we received the last packet...
lastPacketReceivedTime monotime.Time
// ... and the time we sent a new ack-eliciting packet after receiving a packet.
firstAckElicitingPacketAfterIdleSentTime monotime.Time
// pacingDeadline is the time when the next packet should be sent
pacingDeadline monotime.Time
peerParams *wire.TransportParameters
timer *time.Timer
// keepAlivePingSent stores whether a keep alive PING is in flight.
// It is reset as soon as we receive a packet from the peer.
keepAlivePingSent bool
keepAliveInterval time.Duration
datagramQueue *datagramQueue
connStateMutex sync.Mutex
connState ConnectionState
logID string
tracer *logging.ConnectionTracer
logger utils.Logger
}
var _ streamSender = &Conn{}
type connTestHooks struct {
run func() error
earlyConnReady func() <-chan struct{}
context func() context.Context
handshakeComplete func() <-chan struct{}
closeWithTransportError func(TransportErrorCode)
destroy func(error)
handlePacket func(receivedPacket)
}
type wrappedConn struct {
testHooks *connTestHooks
*Conn
}
var newConnection = func(
ctx context.Context,
ctxCancel context.CancelCauseFunc,
conn sendConn,
runner connRunner,
origDestConnID protocol.ConnectionID,
retrySrcConnID *protocol.ConnectionID,
clientDestConnID protocol.ConnectionID,
destConnID protocol.ConnectionID,
srcConnID protocol.ConnectionID,
connIDGenerator ConnectionIDGenerator,
statelessResetter *statelessResetter,
conf *Config,
tlsConf *tls.Config,
tokenGenerator *handshake.TokenGenerator,
clientAddressValidated bool,
rtt time.Duration,
tracer *logging.ConnectionTracer,
logger utils.Logger,
v protocol.Version,
) *wrappedConn {
s := &Conn{
ctx: ctx,
ctxCancel: ctxCancel,
conn: conn,
config: conf,
handshakeDestConnID: destConnID,
srcConnIDLen: srcConnID.Len(),
tokenGenerator: tokenGenerator,
oneRTTStream: newCryptoStream(),
perspective: protocol.PerspectiveServer,
tracer: tracer,
logger: logger,
version: v,
}
if origDestConnID.Len() > 0 {
s.logID = origDestConnID.String()
} else {
s.logID = destConnID.String()
}
s.connIDManager = newConnIDManager(
destConnID,
func(token protocol.StatelessResetToken) { runner.AddResetToken(token, s) },
runner.RemoveResetToken,
s.queueControlFrame,
)
s.connIDGenerator = newConnIDGenerator(
runner,
srcConnID,
&clientDestConnID,
statelessResetter,
connRunnerCallbacks{
AddConnectionID: func(connID protocol.ConnectionID) { runner.Add(connID, s) },
RemoveConnectionID: runner.Remove,
ReplaceWithClosed: runner.ReplaceWithClosed,
},
s.queueControlFrame,
connIDGenerator,
)
s.preSetup()
s.rttStats.SetInitialRTT(rtt)
s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
0,
protocol.ByteCount(s.config.InitialPacketSize),
s.rttStats,
&s.connStats,
clientAddressValidated,
s.conn.capabilities().ECN,
s.perspective,
s.tracer,
s.logger,
)
s.currentMTUEstimate.Store(uint32(estimateMaxPayloadSize(protocol.ByteCount(s.config.InitialPacketSize))))
statelessResetToken := statelessResetter.GetStatelessResetToken(srcConnID)
params := &wire.TransportParameters{
InitialMaxStreamDataBidiLocal: protocol.ByteCount(s.config.InitialStreamReceiveWindow),
InitialMaxStreamDataBidiRemote: protocol.ByteCount(s.config.InitialStreamReceiveWindow),
InitialMaxStreamDataUni: protocol.ByteCount(s.config.InitialStreamReceiveWindow),
InitialMaxData: protocol.ByteCount(s.config.InitialConnectionReceiveWindow),
MaxIdleTimeout: s.config.MaxIdleTimeout,
MaxBidiStreamNum: protocol.StreamNum(s.config.MaxIncomingStreams),
MaxUniStreamNum: protocol.StreamNum(s.config.MaxIncomingUniStreams),
MaxAckDelay: protocol.MaxAckDelayInclGranularity,
AckDelayExponent: protocol.AckDelayExponent,
MaxUDPPayloadSize: protocol.MaxPacketBufferSize,
StatelessResetToken: &statelessResetToken,
OriginalDestinationConnectionID: origDestConnID,
// For interoperability with quic-go versions before May 2023, this value must be set to a value
// different from protocol.DefaultActiveConnectionIDLimit.
// If set to the default value, it will be omitted from the transport parameters, which will make
// old quic-go versions interpret it as 0, instead of the default value of 2.
// See https://github.com/quic-go/quic-go/pull/3806.
ActiveConnectionIDLimit: protocol.MaxActiveConnectionIDs,
InitialSourceConnectionID: srcConnID,
RetrySourceConnectionID: retrySrcConnID,
EnableResetStreamAt: conf.EnableStreamResetPartialDelivery,
}
if s.config.EnableDatagrams {
params.MaxDatagramFrameSize = wire.MaxDatagramSize
} else {
params.MaxDatagramFrameSize = protocol.InvalidByteCount
}
if s.tracer != nil && s.tracer.SentTransportParameters != nil {
s.tracer.SentTransportParameters(params)
}
cs := handshake.NewCryptoSetupServer(
clientDestConnID,
conn.LocalAddr(),
conn.RemoteAddr(),
params,
tlsConf,
conf.Allow0RTT,
s.rttStats,
tracer,
logger,
s.version,
)
s.cryptoStreamHandler = cs
s.packer = newPacketPacker(srcConnID, s.connIDManager.Get, s.initialStream, s.handshakeStream, s.sentPacketHandler, s.retransmissionQueue, cs, s.framer, s.receivedPacketHandler, s.datagramQueue, s.perspective)
s.unpacker = newPacketUnpacker(cs, s.srcConnIDLen)
s.cryptoStreamManager = newCryptoStreamManager(s.initialStream, s.handshakeStream, s.oneRTTStream)
return &wrappedConn{Conn: s}
}
// declare this as a variable, such that we can it mock it in the tests
var newClientConnection = func(
ctx context.Context,
conn sendConn,
runner connRunner,
destConnID protocol.ConnectionID,
srcConnID protocol.ConnectionID,
connIDGenerator ConnectionIDGenerator,
statelessResetter *statelessResetter,
conf *Config,
tlsConf *tls.Config,
initialPacketNumber protocol.PacketNumber,
enable0RTT bool,
hasNegotiatedVersion bool,
tracer *logging.ConnectionTracer,
logger utils.Logger,
v protocol.Version,
) *wrappedConn {
s := &Conn{
conn: conn,
config: conf,
origDestConnID: destConnID,
handshakeDestConnID: destConnID,
srcConnIDLen: srcConnID.Len(),
perspective: protocol.PerspectiveClient,
logID: destConnID.String(),
logger: logger,
tracer: tracer,
versionNegotiated: hasNegotiatedVersion,
version: v,
}
s.connIDManager = newConnIDManager(
destConnID,
func(token protocol.StatelessResetToken) { runner.AddResetToken(token, s) },
runner.RemoveResetToken,
s.queueControlFrame,
)
s.connIDGenerator = newConnIDGenerator(
runner,
srcConnID,
nil,
statelessResetter,
connRunnerCallbacks{
AddConnectionID: func(connID protocol.ConnectionID) { runner.Add(connID, s) },
RemoveConnectionID: runner.Remove,
ReplaceWithClosed: runner.ReplaceWithClosed,
},
s.queueControlFrame,
connIDGenerator,
)
s.ctx, s.ctxCancel = context.WithCancelCause(ctx)
s.preSetup()
s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
initialPacketNumber,
protocol.ByteCount(s.config.InitialPacketSize),
s.rttStats,
&s.connStats,
false, // has no effect
s.conn.capabilities().ECN,
s.perspective,
s.tracer,
s.logger,
)
s.currentMTUEstimate.Store(uint32(estimateMaxPayloadSize(protocol.ByteCount(s.config.InitialPacketSize))))
oneRTTStream := newCryptoStream()
params := &wire.TransportParameters{
InitialMaxStreamDataBidiRemote: protocol.ByteCount(s.config.InitialStreamReceiveWindow),
InitialMaxStreamDataBidiLocal: protocol.ByteCount(s.config.InitialStreamReceiveWindow),
InitialMaxStreamDataUni: protocol.ByteCount(s.config.InitialStreamReceiveWindow),
InitialMaxData: protocol.ByteCount(s.config.InitialConnectionReceiveWindow),
MaxIdleTimeout: s.config.MaxIdleTimeout,
MaxBidiStreamNum: protocol.StreamNum(s.config.MaxIncomingStreams),
MaxUniStreamNum: protocol.StreamNum(s.config.MaxIncomingUniStreams),
MaxAckDelay: protocol.MaxAckDelayInclGranularity,
MaxUDPPayloadSize: protocol.MaxPacketBufferSize,
AckDelayExponent: protocol.AckDelayExponent,
// For interoperability with quic-go versions before May 2023, this value must be set to a value
// different from protocol.DefaultActiveConnectionIDLimit.
// If set to the default value, it will be omitted from the transport parameters, which will make
// old quic-go versions interpret it as 0, instead of the default value of 2.
// See https://github.com/quic-go/quic-go/pull/3806.
ActiveConnectionIDLimit: protocol.MaxActiveConnectionIDs,
InitialSourceConnectionID: srcConnID,
EnableResetStreamAt: conf.EnableStreamResetPartialDelivery,
}
if s.config.EnableDatagrams {
params.MaxDatagramFrameSize = wire.MaxDatagramSize
} else {
params.MaxDatagramFrameSize = protocol.InvalidByteCount
}
if s.tracer != nil && s.tracer.SentTransportParameters != nil {
s.tracer.SentTransportParameters(params)
}
cs := handshake.NewCryptoSetupClient(
destConnID,
params,
tlsConf,
enable0RTT,
s.rttStats,
tracer,
logger,
s.version,
)
s.cryptoStreamHandler = cs
s.cryptoStreamManager = newCryptoStreamManager(s.initialStream, s.handshakeStream, oneRTTStream)
s.unpacker = newPacketUnpacker(cs, s.srcConnIDLen)
s.packer = newPacketPacker(srcConnID, s.connIDManager.Get, s.initialStream, s.handshakeStream, s.sentPacketHandler, s.retransmissionQueue, cs, s.framer, s.receivedPacketHandler, s.datagramQueue, s.perspective)
if len(tlsConf.ServerName) > 0 {
s.tokenStoreKey = tlsConf.ServerName
} else {
s.tokenStoreKey = conn.RemoteAddr().String()
}
if s.config.TokenStore != nil {
if token := s.config.TokenStore.Pop(s.tokenStoreKey); token != nil {
s.packer.SetToken(token.data)
s.rttStats.SetInitialRTT(token.rtt)
}
}
return &wrappedConn{Conn: s}
}
func (c *Conn) preSetup() {
c.largestRcvdAppData = protocol.InvalidPacketNumber
c.initialStream = newInitialCryptoStream(c.perspective == protocol.PerspectiveClient)
c.handshakeStream = newCryptoStream()
c.sendQueue = newSendQueue(c.conn)
c.retransmissionQueue = newRetransmissionQueue()
c.frameParser = *wire.NewFrameParser(
c.config.EnableDatagrams,
c.config.EnableStreamResetPartialDelivery,
false, // ACK_FREQUENCY is not supported yet
)
c.rttStats = &utils.RTTStats{}
c.connFlowController = flowcontrol.NewConnectionFlowController(
protocol.ByteCount(c.config.InitialConnectionReceiveWindow),
protocol.ByteCount(c.config.MaxConnectionReceiveWindow),
func(size protocol.ByteCount) bool {
if c.config.AllowConnectionWindowIncrease == nil {
return true
}
return c.config.AllowConnectionWindowIncrease(c, uint64(size))
},
c.rttStats,
c.logger,
)
c.earlyConnReadyChan = make(chan struct{})
c.streamsMap = newStreamsMap(
c.ctx,
c,
c.queueControlFrame,
c.newFlowController,
uint64(c.config.MaxIncomingStreams),
uint64(c.config.MaxIncomingUniStreams),
c.perspective,
)
c.framer = newFramer(c.connFlowController)
c.receivedPackets.Init(8)
c.notifyReceivedPacket = make(chan struct{}, 1)
c.closeChan = make(chan struct{}, 1)
c.sendingScheduled = make(chan struct{}, 1)
c.handshakeCompleteChan = make(chan struct{})
now := monotime.Now()
c.lastPacketReceivedTime = now
c.creationTime = now
c.datagramQueue = newDatagramQueue(c.scheduleSending, c.logger)
c.connState.Version = c.version
}
// run the connection main loop
func (c *Conn) run() (err error) {
defer func() { c.ctxCancel(err) }()
defer func() {
// drain queued packets that will never be processed
c.receivedPacketMx.Lock()
defer c.receivedPacketMx.Unlock()
for !c.receivedPackets.Empty() {
p := c.receivedPackets.PopFront()
p.buffer.Decrement()
p.buffer.MaybeRelease()
}
}()
c.timer = time.NewTimer(monotime.Until(c.idleTimeoutStartTime().Add(c.config.HandshakeIdleTimeout)))
if err := c.cryptoStreamHandler.StartHandshake(c.ctx); err != nil {
return err
}
if err := c.handleHandshakeEvents(monotime.Now()); err != nil {
return err
}
go func() {
if err := c.sendQueue.Run(); err != nil {
c.destroyImpl(err)
}
}()
if c.perspective == protocol.PerspectiveClient {
c.scheduleSending() // so the ClientHello actually gets sent
}
var sendQueueAvailable <-chan struct{}
runLoop:
for {
if c.framer.QueuedTooManyControlFrames() {
c.setCloseError(&closeError{err: &qerr.TransportError{ErrorCode: InternalError}})
break runLoop
}
// Close immediately if requested
select {
case <-c.closeChan:
break runLoop
default:
}
// no need to set a timer if we can send packets immediately
if c.pacingDeadline != deadlineSendImmediately {
c.maybeResetTimer()
}
// 1st: handle undecryptable packets, if any.
// This can only occur before completion of the handshake.
if len(c.undecryptablePacketsToProcess) > 0 {
var processedUndecryptablePacket bool
queue := c.undecryptablePacketsToProcess
c.undecryptablePacketsToProcess = nil
for _, p := range queue {
processed, err := c.handleOnePacket(p)
if err != nil {
c.setCloseError(&closeError{err: err})
break runLoop
}
if processed {
processedUndecryptablePacket = true
}
}
if processedUndecryptablePacket {
// if we processed any undecryptable packets, jump to the resetting of the timers directly
continue
}
}
// 2nd: receive packets.
processed, err := c.handlePackets() // don't check receivedPackets.Len() in the run loop to avoid locking the mutex
if err != nil {
c.setCloseError(&closeError{err: err})
break runLoop
}
// We don't need to wait for new events if:
// * we processed packets: we probably need to send an ACK, and potentially more data
// * the pacer allows us to send more packets immediately
shouldProceedImmediately := sendQueueAvailable == nil && (processed || c.pacingDeadline.Equal(deadlineSendImmediately))
if !shouldProceedImmediately {
// 3rd: wait for something to happen:
// * closing of the connection
// * timer firing
// * sending scheduled
// * send queue available
// * received packets
select {
case <-c.closeChan:
break runLoop
case <-c.timer.C:
case <-c.sendingScheduled:
case <-sendQueueAvailable:
case <-c.notifyReceivedPacket:
wasProcessed, err := c.handlePackets()
if err != nil {
c.setCloseError(&closeError{err: err})
break runLoop
}
// if we processed any undecryptable packets, jump to the resetting of the timers directly
if !wasProcessed {
continue
}
}
}
// Check for loss detection timeout.
// This could cause packets to be declared lost, and retransmissions to be enqueued.
now := monotime.Now()
if timeout := c.sentPacketHandler.GetLossDetectionTimeout(); !timeout.IsZero() && timeout.Before(now) {
if err := c.sentPacketHandler.OnLossDetectionTimeout(now); err != nil {
c.setCloseError(&closeError{err: err})
break runLoop
}
}
if keepAliveTime := c.nextKeepAliveTime(); !keepAliveTime.IsZero() && !now.Before(keepAliveTime) {
// send a PING frame since there is no activity in the connection
c.logger.Debugf("Sending a keep-alive PING to keep the connection alive.")
c.framer.QueueControlFrame(&wire.PingFrame{})
c.keepAlivePingSent = true
} else if !c.handshakeComplete && now.Sub(c.creationTime) >= c.config.handshakeTimeout() {
c.destroyImpl(qerr.ErrHandshakeTimeout)
break runLoop
} else {
idleTimeoutStartTime := c.idleTimeoutStartTime()
if (!c.handshakeComplete && now.Sub(idleTimeoutStartTime) >= c.config.HandshakeIdleTimeout) ||
(c.handshakeComplete && !now.Before(c.nextIdleTimeoutTime())) {
c.destroyImpl(qerr.ErrIdleTimeout)
break runLoop
}
}
c.connIDGenerator.RemoveRetiredConnIDs(now)
if c.perspective == protocol.PerspectiveClient {
pm := c.pathManagerOutgoing.Load()
if pm != nil {
tr, ok := pm.ShouldSwitchPath()
if ok {
c.switchToNewPath(tr, now)
}
}
}
if c.sendQueue.WouldBlock() {
// The send queue is still busy sending out packets. Wait until there's space to enqueue new packets.
sendQueueAvailable = c.sendQueue.Available()
// Cancel the pacing timer, as we can't send any more packets until the send queue is available again.
c.pacingDeadline = 0
c.blocked = blockModeHardBlocked
continue
}
if c.closeErr.Load() != nil {
break runLoop
}
c.blocked = blockModeNone // sending might set it back to true if we're congestion limited
if err := c.triggerSending(now); err != nil {
c.setCloseError(&closeError{err: err})
break runLoop
}
if c.sendQueue.WouldBlock() {
// The send queue is still busy sending out packets. Wait until there's space to enqueue new packets.
sendQueueAvailable = c.sendQueue.Available()
// Cancel the pacing timer, as we can't send any more packets until the send queue is available again.
c.pacingDeadline = 0
c.blocked = blockModeHardBlocked
} else {
sendQueueAvailable = nil
}
}
closeErr := c.closeErr.Load()
c.cryptoStreamHandler.Close()
c.sendQueue.Close() // close the send queue before sending the CONNECTION_CLOSE
c.handleCloseError(closeErr)
if c.tracer != nil && c.tracer.Close != nil {
if e := (&errCloseForRecreating{}); !errors.As(closeErr.err, &e) {
c.tracer.Close()
}
}
c.logger.Infof("Connection %s closed.", c.logID)
c.timer.Stop()
return closeErr.err
}
// blocks until the early connection can be used
func (c *Conn) earlyConnReady() <-chan struct{} {
return c.earlyConnReadyChan
}
// Context returns a context that is cancelled when the connection is closed.
// The cancellation cause is set to the error that caused the connection to close.
func (c *Conn) Context() context.Context {
return c.ctx
}
func (c *Conn) supportsDatagrams() bool {
return c.peerParams.MaxDatagramFrameSize > 0
}
// ConnectionState returns basic details about the QUIC connection.
func (c *Conn) ConnectionState() ConnectionState {
c.connStateMutex.Lock()
defer c.connStateMutex.Unlock()
cs := c.cryptoStreamHandler.ConnectionState()
c.connState.TLS = cs.ConnectionState
c.connState.Used0RTT = cs.Used0RTT
c.connState.SupportsStreamResetPartialDelivery = c.peerParams.EnableResetStreamAt
c.connState.GSO = c.conn.capabilities().GSO
return c.connState
}
// ConnectionStats contains statistics about the QUIC connection
type ConnectionStats struct {
// MinRTT is the estimate of the minimum RTT observed on the active network
// path.
MinRTT time.Duration
// LatestRTT is the last RTT sample observed on the active network path.
LatestRTT time.Duration
// SmoothedRTT is an exponentially weighted moving average of an endpoint's
// RTT samples. See https://www.rfc-editor.org/rfc/rfc9002#section-5.3
SmoothedRTT time.Duration
// MeanDeviation estimates the variation in the RTT samples using a mean
// variation. See https://www.rfc-editor.org/rfc/rfc9002#section-5.3
MeanDeviation time.Duration
// BytesSent is the number of bytes sent on the underlying connection,
// including retransmissions. Does not include UDP or any other outer
// framing.
BytesSent uint64
// PacketsSent is the number of packets sent on the underlying connection,
// including those that are determined to have been lost.
PacketsSent uint64
// BytesReceived is the number of total bytes received on the underlying
// connection, including duplicate data for streams. Does not include UDP or
// any other outer framing.
BytesReceived uint64
// PacketsReceived is the number of total packets received on the underlying
// connection, including packets that were not processable.
PacketsReceived uint64
// BytesLost is the number of bytes lost on the underlying connection (does
// not monotonically increase, because packets that are declared lost can
// subsequently be received). Does not include UDP or any other outer
// framing.
BytesLost uint64
// PacketsLost is the number of packets lost on the underlying connection
// (does not monotonically increase, because packets that are declared lost
// can subsequently be received).
PacketsLost uint64
}
func (c *Conn) ConnectionStats() ConnectionStats {
return ConnectionStats{
MinRTT: c.rttStats.MinRTT(),
LatestRTT: c.rttStats.LatestRTT(),
SmoothedRTT: c.rttStats.SmoothedRTT(),
MeanDeviation: c.rttStats.MeanDeviation(),
BytesSent: c.connStats.BytesSent.Load(),
PacketsSent: c.connStats.PacketsSent.Load(),
BytesReceived: c.connStats.BytesReceived.Load(),
PacketsReceived: c.connStats.PacketsReceived.Load(),
BytesLost: c.connStats.BytesLost.Load(),
PacketsLost: c.connStats.PacketsLost.Load(),
}
}
// Time when the connection should time out
func (c *Conn) nextIdleTimeoutTime() monotime.Time {
idleTimeout := max(c.idleTimeout, c.rttStats.PTO(true)*3)
return c.idleTimeoutStartTime().Add(idleTimeout)
}
// Time when the next keep-alive packet should be sent.
// It returns a zero time if no keep-alive should be sent.
func (c *Conn) nextKeepAliveTime() monotime.Time {
if c.config.KeepAlivePeriod == 0 || c.keepAlivePingSent {
return 0
}
keepAliveInterval := max(c.keepAliveInterval, c.rttStats.PTO(true)*3/2)
return c.lastPacketReceivedTime.Add(keepAliveInterval)
}
func (c *Conn) maybeResetTimer() {
var deadline monotime.Time
if !c.handshakeComplete {
deadline = c.creationTime.Add(c.config.handshakeTimeout())
if t := c.idleTimeoutStartTime().Add(c.config.HandshakeIdleTimeout); t.Before(deadline) {
deadline = t
}
} else {
// A keep-alive packet is ack-eliciting, so it can only be sent if the connection is
// neither congestion limited nor hard-blocked.
if c.blocked != blockModeNone {
deadline = c.nextIdleTimeoutTime()
} else {
if keepAliveTime := c.nextKeepAliveTime(); !keepAliveTime.IsZero() {
deadline = keepAliveTime
} else {
deadline = c.nextIdleTimeoutTime()
}
}
}
// If the connection is hard-blocked, we can't even send acknowledgments,
// nor can we send PTO probe packets.
if c.blocked == blockModeHardBlocked {
c.timer.Reset(monotime.Until(deadline))
return
}
if t := c.receivedPacketHandler.GetAlarmTimeout(); !t.IsZero() && t.Before(deadline) {
deadline = t
}
if t := c.sentPacketHandler.GetLossDetectionTimeout(); !t.IsZero() && t.Before(deadline) {
deadline = t
}
if c.blocked == blockModeCongestionLimited {
c.timer.Reset(monotime.Until(deadline))
return
}
if t := c.connIDGenerator.NextRetireTime(); !t.IsZero() && t.Before(deadline) {
deadline = t
}
if !c.pacingDeadline.IsZero() && c.pacingDeadline.Before(deadline) {
deadline = c.pacingDeadline
}
c.timer.Reset(monotime.Until(deadline))
}
func (c *Conn) idleTimeoutStartTime() monotime.Time {
startTime := c.lastPacketReceivedTime
if t := c.firstAckElicitingPacketAfterIdleSentTime; !t.IsZero() && t.After(startTime) {
startTime = t
}
return startTime
}
func (c *Conn) switchToNewPath(tr *Transport, now monotime.Time) {
initialPacketSize := protocol.ByteCount(c.config.InitialPacketSize)
c.sentPacketHandler.MigratedPath(now, initialPacketSize)
maxPacketSize := protocol.ByteCount(protocol.MaxPacketBufferSize)
if c.peerParams.MaxUDPPayloadSize > 0 && c.peerParams.MaxUDPPayloadSize < maxPacketSize {
maxPacketSize = c.peerParams.MaxUDPPayloadSize
}
c.mtuDiscoverer.Reset(now, initialPacketSize, maxPacketSize)
c.conn = newSendConn(tr.conn, c.conn.RemoteAddr(), packetInfo{}, utils.DefaultLogger) // TODO: find a better way
c.sendQueue.Close()
c.sendQueue = newSendQueue(c.conn)
go func() {
if err := c.sendQueue.Run(); err != nil {
c.destroyImpl(err)
}
}()
}
func (c *Conn) handleHandshakeComplete(now monotime.Time) error {
defer close(c.handshakeCompleteChan)
// Once the handshake completes, we have derived 1-RTT keys.
// There's no point in queueing undecryptable packets for later decryption anymore.
c.undecryptablePackets = nil
c.connIDManager.SetHandshakeComplete()
c.connIDGenerator.SetHandshakeComplete(now.Add(3 * c.rttStats.PTO(false)))
if c.tracer != nil && c.tracer.ChoseALPN != nil {
c.tracer.ChoseALPN(c.cryptoStreamHandler.ConnectionState().NegotiatedProtocol)
}
// The server applies transport parameters right away, but the client side has to wait for handshake completion.
// During a 0-RTT connection, the client is only allowed to use the new transport parameters for 1-RTT packets.
if c.perspective == protocol.PerspectiveClient {
c.applyTransportParameters()
return nil
}
// All these only apply to the server side.
if err := c.handleHandshakeConfirmed(now); err != nil {
return err
}
ticket, err := c.cryptoStreamHandler.GetSessionTicket()
if err != nil {
return err
}
if ticket != nil { // may be nil if session tickets are disabled via tls.Config.SessionTicketsDisabled
c.oneRTTStream.Write(ticket)
for c.oneRTTStream.HasData() {
if cf := c.oneRTTStream.PopCryptoFrame(protocol.MaxPostHandshakeCryptoFrameSize); cf != nil {
c.queueControlFrame(cf)
}
}
}
token, err := c.tokenGenerator.NewToken(c.conn.RemoteAddr(), c.rttStats.SmoothedRTT())
if err != nil {
return err
}
c.queueControlFrame(&wire.NewTokenFrame{Token: token})
c.queueControlFrame(&wire.HandshakeDoneFrame{})
return nil
}
func (c *Conn) handleHandshakeConfirmed(now monotime.Time) error {
// Drop initial keys.
// On the client side, this should have happened when sending the first Handshake packet,
// but this is not guaranteed if the server misbehaves.
// See CVE-2025-59530 for more details.
if err := c.dropEncryptionLevel(protocol.EncryptionInitial, now); err != nil {
return err
}
if err := c.dropEncryptionLevel(protocol.EncryptionHandshake, now); err != nil {
return err
}
c.handshakeConfirmed = true
c.cryptoStreamHandler.SetHandshakeConfirmed()
if !c.config.DisablePathMTUDiscovery && c.conn.capabilities().DF {
c.mtuDiscoverer.Start(now)
}
return nil
}
func (c *Conn) handlePackets() (wasProcessed bool, _ error) {
// Now process all packets in the receivedPackets channel.
// Limit the number of packets to the length of the receivedPackets channel,
// so we eventually get a chance to send out an ACK when receiving a lot of packets.
c.receivedPacketMx.Lock()
numPackets := c.receivedPackets.Len()
if numPackets == 0 {
c.receivedPacketMx.Unlock()
return false, nil
}
var hasMorePackets bool
for i := 0; i < numPackets; i++ {
if i > 0 {
c.receivedPacketMx.Lock()
}
p := c.receivedPackets.PopFront()
hasMorePackets = !c.receivedPackets.Empty()
c.receivedPacketMx.Unlock()
processed, err := c.handleOnePacket(p)
if err != nil {
return false, err
}
if processed {
wasProcessed = true
}
if !hasMorePackets {
break
}
// only process a single packet at a time before handshake completion
if !c.handshakeComplete {
break
}
}
if hasMorePackets {
select {
case c.notifyReceivedPacket <- struct{}{}:
default:
}
}
return wasProcessed, nil
}
func (c *Conn) handleOnePacket(rp receivedPacket) (wasProcessed bool, _ error) {
c.sentPacketHandler.ReceivedBytes(rp.Size(), rp.rcvTime)
if wire.IsVersionNegotiationPacket(rp.data) {
c.handleVersionNegotiationPacket(rp)
return false, nil
}
var counter uint8
var lastConnID protocol.ConnectionID
data := rp.data
p := rp
for len(data) > 0 {
if counter > 0 {
p = *(p.Clone())
p.data = data
destConnID, err := wire.ParseConnectionID(p.data, c.srcConnIDLen)
if err != nil {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), logging.PacketDropHeaderParseError)
}
c.logger.Debugf("error parsing packet, couldn't parse connection ID: %s", err)
break
}
if destConnID != lastConnID {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), logging.PacketDropUnknownConnectionID)
}
c.logger.Debugf("coalesced packet has different destination connection ID: %s, expected %s", destConnID, lastConnID)
break
}
}
if wire.IsLongHeaderPacket(p.data[0]) {
hdr, packetData, rest, err := wire.ParsePacket(p.data)
if err != nil {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
dropReason := logging.PacketDropHeaderParseError
if err == wire.ErrUnsupportedVersion {
dropReason = logging.PacketDropUnsupportedVersion
}
c.tracer.DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), dropReason)
}
c.logger.Debugf("error parsing packet: %s", err)
break
}
lastConnID = hdr.DestConnectionID
if hdr.Version != c.version {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeFromHeader(hdr), protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), logging.PacketDropUnexpectedVersion)
}
c.logger.Debugf("Dropping packet with version %x. Expected %x.", hdr.Version, c.version)
break
}
if counter > 0 {
p.buffer.Split()
}
counter++
// only log if this actually a coalesced packet
if c.logger.Debug() && (counter > 1 || len(rest) > 0) {
c.logger.Debugf("Parsed a coalesced packet. Part %d: %d bytes. Remaining: %d bytes.", counter, len(packetData), len(rest))
}
p.data = packetData
processed, err := c.handleLongHeaderPacket(p, hdr)
if err != nil {
return false, err
}
if processed {
wasProcessed = true
}
data = rest
} else {
if counter > 0 {
p.buffer.Split()
}
processed, err := c.handleShortHeaderPacket(p, counter > 0)
if err != nil {
return false, err
}
if processed {
wasProcessed = true
}
break
}
}
p.buffer.MaybeRelease()
c.blocked = blockModeNone
return wasProcessed, nil
}
func (c *Conn) handleShortHeaderPacket(p receivedPacket, isCoalesced bool) (wasProcessed bool, _ error) {
var wasQueued bool
defer func() {
// Put back the packet buffer if the packet wasn't queued for later decryption.
if !wasQueued {
p.buffer.Decrement()
}
}()
destConnID, err := wire.ParseConnectionID(p.data, c.srcConnIDLen)
if err != nil {
c.tracer.DroppedPacket(logging.PacketType1RTT, protocol.InvalidPacketNumber, protocol.ByteCount(len(p.data)), logging.PacketDropHeaderParseError)
return false, nil
}
pn, pnLen, keyPhase, data, err := c.unpacker.UnpackShortHeader(p.rcvTime, p.data)
if err != nil {
// Stateless reset packets (see RFC 9000, section 10.3):
// * fill the entire UDP datagram (i.e. they cannot be part of a coalesced packet)
// * are short header packets (first bit is 0)
// * have the QUIC bit set (second bit is 1)
// * are at least 21 bytes long
if !isCoalesced && len(p.data) >= protocol.MinReceivedStatelessResetSize && p.data[0]&0b11000000 == 0b01000000 {
token := protocol.StatelessResetToken(p.data[len(p.data)-16:])
if c.connIDManager.IsActiveStatelessResetToken(token) {
return false, &StatelessResetError{}
}
}
wasQueued, err = c.handleUnpackError(err, p, logging.PacketType1RTT)
return false, err
}
c.largestRcvdAppData = max(c.largestRcvdAppData, pn)
if c.logger.Debug() {
c.logger.Debugf("<- Reading packet %d (%d bytes) for connection %s, 1-RTT", pn, p.Size(), destConnID)
wire.LogShortHeader(c.logger, destConnID, pn, pnLen, keyPhase)
}
if c.receivedPacketHandler.IsPotentiallyDuplicate(pn, protocol.Encryption1RTT) {
c.logger.Debugf("Dropping (potentially) duplicate packet.")
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketType1RTT, pn, p.Size(), logging.PacketDropDuplicate)
}
return false, nil
}
var log func([]logging.Frame)
if c.tracer != nil && c.tracer.ReceivedShortHeaderPacket != nil {
log = func(frames []logging.Frame) {
c.tracer.ReceivedShortHeaderPacket(
&logging.ShortHeader{
DestConnectionID: destConnID,
PacketNumber: pn,
PacketNumberLen: pnLen,
KeyPhase: keyPhase,
},
p.Size(),
p.ecn,
frames,
)
}
}
isNonProbing, pathChallenge, err := c.handleUnpackedShortHeaderPacket(destConnID, pn, data, p.ecn, p.rcvTime, log)
if err != nil {
return false, err
}
// In RFC 9000, only the client can migrate between paths.
if c.perspective == protocol.PerspectiveClient {
return true, nil
}
if addrsEqual(p.remoteAddr, c.RemoteAddr()) {
return true, nil
}
var shouldSwitchPath bool
if c.pathManager == nil {
c.pathManager = newPathManager(
c.connIDManager.GetConnIDForPath,
c.connIDManager.RetireConnIDForPath,
c.logger,
)
}
destConnID, frames, shouldSwitchPath := c.pathManager.HandlePacket(p.remoteAddr, p.rcvTime, pathChallenge, isNonProbing)
if len(frames) > 0 {
probe, buf, err := c.packer.PackPathProbePacket(destConnID, frames, c.version)
if err != nil {
return true, err
}
c.logger.Debugf("sending path probe packet to %s", p.remoteAddr)
c.logShortHeaderPacket(probe.DestConnID, probe.Ack, probe.Frames, probe.StreamFrames, probe.PacketNumber, probe.PacketNumberLen, probe.KeyPhase, protocol.ECNNon, buf.Len(), false)
c.registerPackedShortHeaderPacket(probe, protocol.ECNNon, p.rcvTime)
c.sendQueue.SendProbe(buf, p.remoteAddr)
}
// We only switch paths in response to the highest-numbered non-probing packet,
// see section 9.3 of RFC 9000.
if !shouldSwitchPath || pn != c.largestRcvdAppData {
return true, nil
}
c.pathManager.SwitchToPath(p.remoteAddr)
c.sentPacketHandler.MigratedPath(p.rcvTime, protocol.ByteCount(c.config.InitialPacketSize))
maxPacketSize := protocol.ByteCount(protocol.MaxPacketBufferSize)
if c.peerParams.MaxUDPPayloadSize > 0 && c.peerParams.MaxUDPPayloadSize < maxPacketSize {
maxPacketSize = c.peerParams.MaxUDPPayloadSize
}
c.mtuDiscoverer.Reset(
p.rcvTime,
protocol.ByteCount(c.config.InitialPacketSize),
maxPacketSize,
)
c.conn.ChangeRemoteAddr(p.remoteAddr, p.info)
return true, nil
}
func (c *Conn) handleLongHeaderPacket(p receivedPacket, hdr *wire.Header) (wasProcessed bool, _ error) {
var wasQueued bool
defer func() {
// Put back the packet buffer if the packet wasn't queued for later decryption.
if !wasQueued {
p.buffer.Decrement()
}
}()
if hdr.Type == protocol.PacketTypeRetry {
return c.handleRetryPacket(hdr, p.data, p.rcvTime), nil
}
// The server can change the source connection ID with the first Handshake packet.
// After this, all packets with a different source connection have to be ignored.
if c.receivedFirstPacket && hdr.Type == protocol.PacketTypeInitial && hdr.SrcConnectionID != c.handshakeDestConnID {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeInitial, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropUnknownConnectionID)
}
c.logger.Debugf("Dropping Initial packet (%d bytes) with unexpected source connection ID: %s (expected %s)", p.Size(), hdr.SrcConnectionID, c.handshakeDestConnID)
return false, nil
}
// drop 0-RTT packets, if we are a client
if c.perspective == protocol.PerspectiveClient && hdr.Type == protocol.PacketType0RTT {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketType0RTT, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropUnexpectedPacket)
}
return false, nil
}
packet, err := c.unpacker.UnpackLongHeader(hdr, p.data)
if err != nil {
wasQueued, err = c.handleUnpackError(err, p, logging.PacketTypeFromHeader(hdr))
return false, err
}
if c.logger.Debug() {
c.logger.Debugf("<- Reading packet %d (%d bytes) for connection %s, %s", packet.hdr.PacketNumber, p.Size(), hdr.DestConnectionID, packet.encryptionLevel)
packet.hdr.Log(c.logger)
}
if pn := packet.hdr.PacketNumber; c.receivedPacketHandler.IsPotentiallyDuplicate(pn, packet.encryptionLevel) {
c.logger.Debugf("Dropping (potentially) duplicate packet.")
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeFromHeader(hdr), pn, p.Size(), logging.PacketDropDuplicate)
}
return false, nil
}
if err := c.handleUnpackedLongHeaderPacket(packet, p.ecn, p.rcvTime, p.Size()); err != nil {
return false, err
}
return true, nil
}
func (c *Conn) handleUnpackError(err error, p receivedPacket, pt logging.PacketType) (wasQueued bool, _ error) {
switch err {
case handshake.ErrKeysDropped:
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(pt, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropKeyUnavailable)
}
c.logger.Debugf("Dropping %s packet (%d bytes) because we already dropped the keys.", pt, p.Size())
return false, nil
case handshake.ErrKeysNotYetAvailable:
// Sealer for this encryption level not yet available.
// Try again later.
c.tryQueueingUndecryptablePacket(p, pt)
return true, nil
case wire.ErrInvalidReservedBits:
return false, &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: err.Error(),
}
case handshake.ErrDecryptionFailed:
// This might be a packet injected by an attacker. Drop it.
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(pt, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropPayloadDecryptError)
}
c.logger.Debugf("Dropping %s packet (%d bytes) that could not be unpacked. Error: %s", pt, p.Size(), err)
return false, nil
default:
var headerErr *headerParseError
if errors.As(err, &headerErr) {
// This might be a packet injected by an attacker. Drop it.
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(pt, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropHeaderParseError)
}
c.logger.Debugf("Dropping %s packet (%d bytes) for which we couldn't unpack the header. Error: %s", pt, p.Size(), err)
return false, nil
}
// This is an error returned by the AEAD (other than ErrDecryptionFailed).
// For example, a PROTOCOL_VIOLATION due to key updates.
return false, err
}
}
func (c *Conn) handleRetryPacket(hdr *wire.Header, data []byte, rcvTime monotime.Time) bool /* was this a valid Retry */ {
if c.perspective == protocol.PerspectiveServer {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeRetry, protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), logging.PacketDropUnexpectedPacket)
}
c.logger.Debugf("Ignoring Retry.")
return false
}
if c.receivedFirstPacket {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeRetry, protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), logging.PacketDropUnexpectedPacket)
}
c.logger.Debugf("Ignoring Retry, since we already received a packet.")
return false
}
destConnID := c.connIDManager.Get()
if hdr.SrcConnectionID == destConnID {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeRetry, protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), logging.PacketDropUnexpectedPacket)
}
c.logger.Debugf("Ignoring Retry, since the server didn't change the Source Connection ID.")
return false
}
// If a token is already set, this means that we already received a Retry from the server.
// Ignore this Retry packet.
if c.receivedRetry {
c.logger.Debugf("Ignoring Retry, since a Retry was already received.")
return false
}
tag := handshake.GetRetryIntegrityTag(data[:len(data)-16], destConnID, hdr.Version)
if !bytes.Equal(data[len(data)-16:], tag[:]) {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeRetry, protocol.InvalidPacketNumber, protocol.ByteCount(len(data)), logging.PacketDropPayloadDecryptError)
}
c.logger.Debugf("Ignoring spoofed Retry. Integrity Tag doesn't match.")
return false
}
newDestConnID := hdr.SrcConnectionID
c.receivedRetry = true
c.sentPacketHandler.ResetForRetry(rcvTime)
c.handshakeDestConnID = newDestConnID
c.retrySrcConnID = &newDestConnID
c.cryptoStreamHandler.ChangeConnectionID(newDestConnID)
c.packer.SetToken(hdr.Token)
c.connIDManager.ChangeInitialConnID(newDestConnID)
if c.logger.Debug() {
c.logger.Debugf("<- Received Retry:")
(&wire.ExtendedHeader{Header: *hdr}).Log(c.logger)
c.logger.Debugf("Switching destination connection ID to: %s", hdr.SrcConnectionID)
}
if c.tracer != nil && c.tracer.ReceivedRetry != nil {
c.tracer.ReceivedRetry(hdr)
}
c.scheduleSending()
return true
}
func (c *Conn) handleVersionNegotiationPacket(p receivedPacket) {
if c.perspective == protocol.PerspectiveServer || // servers never receive version negotiation packets
c.receivedFirstPacket || c.versionNegotiated { // ignore delayed / duplicated version negotiation packets
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeVersionNegotiation, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropUnexpectedPacket)
}
return
}
src, dest, supportedVersions, err := wire.ParseVersionNegotiationPacket(p.data)
if err != nil {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeVersionNegotiation, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropHeaderParseError)
}
c.logger.Debugf("Error parsing Version Negotiation packet: %s", err)
return
}
if slices.Contains(supportedVersions, c.version) {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeVersionNegotiation, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropUnexpectedVersion)
}
// The Version Negotiation packet contains the version that we offered.
// This might be a packet sent by an attacker, or it was corrupted.
return
}
c.logger.Infof("Received a Version Negotiation packet. Supported Versions: %s", supportedVersions)
if c.tracer != nil && c.tracer.ReceivedVersionNegotiationPacket != nil {
c.tracer.ReceivedVersionNegotiationPacket(dest, src, supportedVersions)
}
newVersion, ok := protocol.ChooseSupportedVersion(c.config.Versions, supportedVersions)
if !ok {
c.destroyImpl(&VersionNegotiationError{
Ours: c.config.Versions,
Theirs: supportedVersions,
})
c.logger.Infof("No compatible QUIC version found.")
return
}
if c.tracer != nil && c.tracer.NegotiatedVersion != nil {
c.tracer.NegotiatedVersion(newVersion, c.config.Versions, supportedVersions)
}
c.logger.Infof("Switching to QUIC version %s.", newVersion)
nextPN, _ := c.sentPacketHandler.PeekPacketNumber(protocol.EncryptionInitial)
c.destroyImpl(&errCloseForRecreating{
nextPacketNumber: nextPN,
nextVersion: newVersion,
})
}
func (c *Conn) handleUnpackedLongHeaderPacket(
packet *unpackedPacket,
ecn protocol.ECN,
rcvTime monotime.Time,
packetSize protocol.ByteCount, // only for logging
) error {
if !c.receivedFirstPacket {
c.receivedFirstPacket = true
if !c.versionNegotiated && c.tracer != nil && c.tracer.NegotiatedVersion != nil {
var clientVersions, serverVersions []protocol.Version
switch c.perspective {
case protocol.PerspectiveClient:
clientVersions = c.config.Versions
case protocol.PerspectiveServer:
serverVersions = c.config.Versions
}
c.tracer.NegotiatedVersion(c.version, clientVersions, serverVersions)
}
// The server can change the source connection ID with the first Handshake packet.
if c.perspective == protocol.PerspectiveClient && packet.hdr.SrcConnectionID != c.handshakeDestConnID {
cid := packet.hdr.SrcConnectionID
c.logger.Debugf("Received first packet. Switching destination connection ID to: %s", cid)
c.handshakeDestConnID = cid
c.connIDManager.ChangeInitialConnID(cid)
}
// We create the connection as soon as we receive the first packet from the client.
// We do that before authenticating the packet.
// That means that if the source connection ID was corrupted,
// we might have created a connection with an incorrect source connection ID.
// Once we authenticate the first packet, we need to update it.
if c.perspective == protocol.PerspectiveServer {
if packet.hdr.SrcConnectionID != c.handshakeDestConnID {
c.handshakeDestConnID = packet.hdr.SrcConnectionID
c.connIDManager.ChangeInitialConnID(packet.hdr.SrcConnectionID)
}
if c.tracer != nil && c.tracer.StartedConnection != nil {
c.tracer.StartedConnection(
c.conn.LocalAddr(),
c.conn.RemoteAddr(),
packet.hdr.SrcConnectionID,
packet.hdr.DestConnectionID,
)
}
}
}
if c.perspective == protocol.PerspectiveServer && packet.encryptionLevel == protocol.EncryptionHandshake &&
!c.droppedInitialKeys {
// On the server side, Initial keys are dropped as soon as the first Handshake packet is received.
// See Section 4.9.1 of RFC 9001.
if err := c.dropEncryptionLevel(protocol.EncryptionInitial, rcvTime); err != nil {
return err
}
}
c.lastPacketReceivedTime = rcvTime
c.firstAckElicitingPacketAfterIdleSentTime = 0
c.keepAlivePingSent = false
if packet.hdr.Type == protocol.PacketType0RTT {
c.largestRcvdAppData = max(c.largestRcvdAppData, packet.hdr.PacketNumber)
}
var log func([]logging.Frame)
if c.tracer != nil && c.tracer.ReceivedLongHeaderPacket != nil {
log = func(frames []logging.Frame) {
c.tracer.ReceivedLongHeaderPacket(packet.hdr, packetSize, ecn, frames)
}
}
isAckEliciting, _, _, err := c.handleFrames(packet.data, packet.hdr.DestConnectionID, packet.encryptionLevel, log, rcvTime)
if err != nil {
return err
}
return c.receivedPacketHandler.ReceivedPacket(packet.hdr.PacketNumber, ecn, packet.encryptionLevel, rcvTime, isAckEliciting)
}
func (c *Conn) handleUnpackedShortHeaderPacket(
destConnID protocol.ConnectionID,
pn protocol.PacketNumber,
data []byte,
ecn protocol.ECN,
rcvTime monotime.Time,
log func([]logging.Frame),
) (isNonProbing bool, pathChallenge *wire.PathChallengeFrame, _ error) {
c.lastPacketReceivedTime = rcvTime
c.firstAckElicitingPacketAfterIdleSentTime = 0
c.keepAlivePingSent = false
isAckEliciting, isNonProbing, pathChallenge, err := c.handleFrames(data, destConnID, protocol.Encryption1RTT, log, rcvTime)
if err != nil {
return false, nil, err
}
if err := c.receivedPacketHandler.ReceivedPacket(pn, ecn, protocol.Encryption1RTT, rcvTime, isAckEliciting); err != nil {
return false, nil, err
}
return isNonProbing, pathChallenge, nil
}
// handleFrames parses the frames, one after the other, and handles them.
// It returns the last PATH_CHALLENGE frame contained in the packet, if any.
func (c *Conn) handleFrames(
data []byte,
destConnID protocol.ConnectionID,
encLevel protocol.EncryptionLevel,
log func([]logging.Frame),
rcvTime monotime.Time,
) (isAckEliciting, isNonProbing bool, pathChallenge *wire.PathChallengeFrame, _ error) {
// Only used for tracing.
// If we're not tracing, this slice will always remain empty.
var frames []logging.Frame
if log != nil {
frames = make([]logging.Frame, 0, 4)
}
handshakeWasComplete := c.handshakeComplete
var handleErr error
var skipHandling bool
for len(data) > 0 {
frameType, l, err := c.frameParser.ParseType(data, encLevel)
if err != nil {
// The frame parser skips over PADDING frames, and returns an io.EOF if the PADDING
// frames were the last frames in this packet.
if err == io.EOF {
break
}
return false, false, nil, err
}
data = data[l:]
if ackhandler.IsFrameTypeAckEliciting(frameType) {
isAckEliciting = true
}
if !wire.IsProbingFrameType(frameType) {
isNonProbing = true
}
// We're inlining common cases, to avoid using interfaces
// Fast path: STREAM, DATAGRAM and ACK
if frameType.IsStreamFrameType() {
streamFrame, l, err := c.frameParser.ParseStreamFrame(frameType, data, c.version)
if err != nil {
return false, false, nil, err
}
data = data[l:]
if log != nil {
frames = append(frames, toLoggingFrame(streamFrame))
}
// an error occurred handling a previous frame, don't handle the current frame
if skipHandling {
continue
}
wire.LogFrame(c.logger, streamFrame, false)
handleErr = c.streamsMap.HandleStreamFrame(streamFrame, rcvTime)
} else if frameType.IsAckFrameType() {
ackFrame, l, err := c.frameParser.ParseAckFrame(frameType, data, encLevel, c.version)
if err != nil {
return false, false, nil, err
}
data = data[l:]
if log != nil {
frames = append(frames, toLoggingFrame(ackFrame))
}
// an error occurred handling a previous frame, don't handle the current frame
if skipHandling {
continue
}
wire.LogFrame(c.logger, ackFrame, false)
handleErr = c.handleAckFrame(ackFrame, encLevel, rcvTime)
} else if frameType.IsDatagramFrameType() {
datagramFrame, l, err := c.frameParser.ParseDatagramFrame(frameType, data, c.version)
if err != nil {
return false, false, nil, err
}
data = data[l:]
if log != nil {
frames = append(frames, toLoggingFrame(datagramFrame))
}
// an error occurred handling a previous frame, don't handle the current frame
if skipHandling {
continue
}
wire.LogFrame(c.logger, datagramFrame, false)
handleErr = c.handleDatagramFrame(datagramFrame)
} else {
frame, l, err := c.frameParser.ParseLessCommonFrame(frameType, data, c.version)
if err != nil {
return false, false, nil, err
}
data = data[l:]
if log != nil {
frames = append(frames, toLoggingFrame(frame))
}
// an error occurred handling a previous frame, don't handle the current frame
if skipHandling {
continue
}
pc, err := c.handleFrame(frame, encLevel, destConnID, rcvTime)
if pc != nil {
pathChallenge = pc
}
handleErr = err
}
if handleErr != nil {
// if we're logging, we need to keep parsing (but not handling) all frames
skipHandling = true
if log == nil {
return false, false, nil, handleErr
}
}
}
if log != nil {
log(frames)
if handleErr != nil {
return false, false, nil, handleErr
}
}
// Handle completion of the handshake after processing all the frames.
// This ensures that we correctly handle the following case on the server side:
// We receive a Handshake packet that contains the CRYPTO frame that allows us to complete the handshake,
// and an ACK serialized after that CRYPTO frame. In this case, we still want to process the ACK frame.
if !handshakeWasComplete && c.handshakeComplete {
if err := c.handleHandshakeComplete(rcvTime); err != nil {
return false, false, nil, err
}
}
return
}
func (c *Conn) handleFrame(
f wire.Frame,
encLevel protocol.EncryptionLevel,
destConnID protocol.ConnectionID,
rcvTime monotime.Time,
) (pathChallenge *wire.PathChallengeFrame, _ error) {
var err error
wire.LogFrame(c.logger, f, false)
switch frame := f.(type) {
case *wire.CryptoFrame:
err = c.handleCryptoFrame(frame, encLevel, rcvTime)
case *wire.ConnectionCloseFrame:
err = c.handleConnectionCloseFrame(frame)
case *wire.ResetStreamFrame:
err = c.streamsMap.HandleResetStreamFrame(frame, rcvTime)
case *wire.MaxDataFrame:
c.connFlowController.UpdateSendWindow(frame.MaximumData)
case *wire.MaxStreamDataFrame:
err = c.streamsMap.HandleMaxStreamDataFrame(frame)
case *wire.MaxStreamsFrame:
c.streamsMap.HandleMaxStreamsFrame(frame)
case *wire.DataBlockedFrame:
case *wire.StreamDataBlockedFrame:
err = c.streamsMap.HandleStreamDataBlockedFrame(frame)
case *wire.StreamsBlockedFrame:
case *wire.StopSendingFrame:
err = c.streamsMap.HandleStopSendingFrame(frame)
case *wire.PingFrame:
case *wire.PathChallengeFrame:
c.handlePathChallengeFrame(frame)
pathChallenge = frame
case *wire.PathResponseFrame:
err = c.handlePathResponseFrame(frame)
case *wire.NewTokenFrame:
err = c.handleNewTokenFrame(frame)
case *wire.NewConnectionIDFrame:
err = c.connIDManager.Add(frame)
case *wire.RetireConnectionIDFrame:
err = c.connIDGenerator.Retire(frame.SequenceNumber, destConnID, rcvTime.Add(3*c.rttStats.PTO(false)))
case *wire.HandshakeDoneFrame:
err = c.handleHandshakeDoneFrame(rcvTime)
default:
err = fmt.Errorf("unexpected frame type: %s", reflect.ValueOf(&frame).Elem().Type().Name())
}
return pathChallenge, err
}
// handlePacket is called by the server with a new packet
func (c *Conn) handlePacket(p receivedPacket) {
c.receivedPacketMx.Lock()
// Discard packets once the amount of queued packets is larger than
// the channel size, protocol.MaxConnUnprocessedPackets
if c.receivedPackets.Len() >= protocol.MaxConnUnprocessedPackets {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropDOSPrevention)
}
c.receivedPacketMx.Unlock()
return
}
c.receivedPackets.PushBack(p)
c.receivedPacketMx.Unlock()
select {
case c.notifyReceivedPacket <- struct{}{}:
default:
}
}
func (c *Conn) handleConnectionCloseFrame(frame *wire.ConnectionCloseFrame) error {
if frame.IsApplicationError {
return &qerr.ApplicationError{
Remote: true,
ErrorCode: qerr.ApplicationErrorCode(frame.ErrorCode),
ErrorMessage: frame.ReasonPhrase,
}
}
return &qerr.TransportError{
Remote: true,
ErrorCode: qerr.TransportErrorCode(frame.ErrorCode),
FrameType: frame.FrameType,
ErrorMessage: frame.ReasonPhrase,
}
}
func (c *Conn) handleCryptoFrame(frame *wire.CryptoFrame, encLevel protocol.EncryptionLevel, rcvTime monotime.Time) error {
if err := c.cryptoStreamManager.HandleCryptoFrame(frame, encLevel); err != nil {
return err
}
for {
data := c.cryptoStreamManager.GetCryptoData(encLevel)
if data == nil {
break
}
if err := c.cryptoStreamHandler.HandleMessage(data, encLevel); err != nil {
return err
}
}
return c.handleHandshakeEvents(rcvTime)
}
func (c *Conn) handleHandshakeEvents(now monotime.Time) error {
for {
ev := c.cryptoStreamHandler.NextEvent()
var err error
switch ev.Kind {
case handshake.EventNoEvent:
return nil
case handshake.EventHandshakeComplete:
// Don't call handleHandshakeComplete yet.
// It's advantageous to process ACK frames that might be serialized after the CRYPTO frame first.
c.handshakeComplete = true
case handshake.EventReceivedTransportParameters:
err = c.handleTransportParameters(ev.TransportParameters)
case handshake.EventRestoredTransportParameters:
c.restoreTransportParameters(ev.TransportParameters)
close(c.earlyConnReadyChan)
case handshake.EventReceivedReadKeys:
// queue all previously undecryptable packets
c.undecryptablePacketsToProcess = append(c.undecryptablePacketsToProcess, c.undecryptablePackets...)
c.undecryptablePackets = nil
case handshake.EventDiscard0RTTKeys:
err = c.dropEncryptionLevel(protocol.Encryption0RTT, now)
case handshake.EventWriteInitialData:
_, err = c.initialStream.Write(ev.Data)
case handshake.EventWriteHandshakeData:
_, err = c.handshakeStream.Write(ev.Data)
}
if err != nil {
return err
}
}
}
func (c *Conn) handlePathChallengeFrame(f *wire.PathChallengeFrame) {
if c.perspective == protocol.PerspectiveClient {
c.queueControlFrame(&wire.PathResponseFrame{Data: f.Data})
}
}
func (c *Conn) handlePathResponseFrame(f *wire.PathResponseFrame) error {
switch c.perspective {
case protocol.PerspectiveClient:
return c.handlePathResponseFrameClient(f)
case protocol.PerspectiveServer:
return c.handlePathResponseFrameServer(f)
default:
panic("unreachable")
}
}
func (c *Conn) handlePathResponseFrameClient(f *wire.PathResponseFrame) error {
pm := c.pathManagerOutgoing.Load()
if pm == nil {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "unexpected PATH_RESPONSE frame",
}
}
pm.HandlePathResponseFrame(f)
return nil
}
func (c *Conn) handlePathResponseFrameServer(f *wire.PathResponseFrame) error {
if c.pathManager == nil {
// since we didn't send PATH_CHALLENGEs yet, we don't expect PATH_RESPONSEs
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "unexpected PATH_RESPONSE frame",
}
}
c.pathManager.HandlePathResponseFrame(f)
return nil
}
func (c *Conn) handleNewTokenFrame(frame *wire.NewTokenFrame) error {
if c.perspective == protocol.PerspectiveServer {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "received NEW_TOKEN frame from the client",
}
}
if c.config.TokenStore != nil {
c.config.TokenStore.Put(c.tokenStoreKey, &ClientToken{data: frame.Token, rtt: c.rttStats.SmoothedRTT()})
}
return nil
}
func (c *Conn) handleHandshakeDoneFrame(rcvTime monotime.Time) error {
if c.perspective == protocol.PerspectiveServer {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "received a HANDSHAKE_DONE frame",
}
}
if !c.handshakeConfirmed {
return c.handleHandshakeConfirmed(rcvTime)
}
return nil
}
func (c *Conn) handleAckFrame(frame *wire.AckFrame, encLevel protocol.EncryptionLevel, rcvTime monotime.Time) error {
acked1RTTPacket, err := c.sentPacketHandler.ReceivedAck(frame, encLevel, c.lastPacketReceivedTime)
if err != nil {
return err
}
if !acked1RTTPacket {
return nil
}
// On the client side: If the packet acknowledged a 1-RTT packet, this confirms the handshake.
// This is only possible if the ACK was sent in a 1-RTT packet.
// This is an optimization over simply waiting for a HANDSHAKE_DONE frame, see section 4.1.2 of RFC 9001.
if c.perspective == protocol.PerspectiveClient && !c.handshakeConfirmed {
if err := c.handleHandshakeConfirmed(rcvTime); err != nil {
return err
}
}
// If one of the acknowledged packets was a Path MTU probe packet, this might have increased the Path MTU estimate.
if c.mtuDiscoverer != nil {
if mtu := c.mtuDiscoverer.CurrentSize(); mtu > protocol.ByteCount(c.currentMTUEstimate.Load()) {
c.currentMTUEstimate.Store(uint32(mtu))
c.sentPacketHandler.SetMaxDatagramSize(mtu)
}
}
return c.cryptoStreamHandler.SetLargest1RTTAcked(frame.LargestAcked())
}
func (c *Conn) handleDatagramFrame(f *wire.DatagramFrame) error {
if f.Length(c.version) > wire.MaxDatagramSize {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "DATAGRAM frame too large",
}
}
c.datagramQueue.HandleDatagramFrame(f)
return nil
}
func (c *Conn) setCloseError(e *closeError) {
c.closeErr.CompareAndSwap(nil, e)
select {
case c.closeChan <- struct{}{}:
default:
}
}
// closeLocal closes the connection and send a CONNECTION_CLOSE containing the error
func (c *Conn) closeLocal(e error) {
c.setCloseError(&closeError{err: e, immediate: false})
}
// destroy closes the connection without sending the error on the wire
func (c *Conn) destroy(e error) {
c.destroyImpl(e)
<-c.ctx.Done()
}
func (c *Conn) destroyImpl(e error) {
c.setCloseError(&closeError{err: e, immediate: true})
}
// CloseWithError closes the connection with an error.
// The error string will be sent to the peer.
func (c *Conn) CloseWithError(code ApplicationErrorCode, desc string) error {
c.closeLocal(&qerr.ApplicationError{
ErrorCode: code,
ErrorMessage: desc,
})
<-c.ctx.Done()
return nil
}
func (c *Conn) closeWithTransportError(code TransportErrorCode) {
c.closeLocal(&qerr.TransportError{ErrorCode: code})
<-c.ctx.Done()
}
func (c *Conn) handleCloseError(closeErr *closeError) {
if closeErr.immediate {
if nerr, ok := closeErr.err.(net.Error); ok && nerr.Timeout() {
c.logger.Errorf("Destroying connection: %s", closeErr.err)
} else {
c.logger.Errorf("Destroying connection with error: %s", closeErr.err)
}
} else {
if closeErr.err == nil {
c.logger.Infof("Closing connection.")
} else {
c.logger.Errorf("Closing connection with error: %s", closeErr.err)
}
}
e := closeErr.err
if e == nil {
e = &qerr.ApplicationError{}
} else {
defer func() { closeErr.err = e }()
}
var (
statelessResetErr *StatelessResetError
versionNegotiationErr *VersionNegotiationError
recreateErr *errCloseForRecreating
applicationErr *ApplicationError
transportErr *TransportError
)
var isRemoteClose bool
switch {
case errors.Is(e, qerr.ErrIdleTimeout),
errors.Is(e, qerr.ErrHandshakeTimeout),
errors.As(e, &statelessResetErr),
errors.As(e, &versionNegotiationErr),
errors.As(e, &recreateErr):
case errors.As(e, &applicationErr):
isRemoteClose = applicationErr.Remote
case errors.As(e, &transportErr):
isRemoteClose = transportErr.Remote
case closeErr.immediate:
e = closeErr.err
default:
e = &qerr.TransportError{
ErrorCode: qerr.InternalError,
ErrorMessage: e.Error(),
}
}
c.streamsMap.CloseWithError(e)
if c.datagramQueue != nil {
c.datagramQueue.CloseWithError(e)
}
// In rare instances, the connection ID manager might switch to a new connection ID
// when sending the CONNECTION_CLOSE frame.
// The connection ID manager removes the active stateless reset token from the packet
// handler map when it is closed, so we need to make sure that this happens last.
defer c.connIDManager.Close()
if c.tracer != nil && c.tracer.ClosedConnection != nil && !errors.As(e, &recreateErr) {
c.tracer.ClosedConnection(e)
}
// If this is a remote close we're done here
if isRemoteClose {
c.connIDGenerator.ReplaceWithClosed(nil, 3*c.rttStats.PTO(false))
return
}
if closeErr.immediate {
c.connIDGenerator.RemoveAll()
return
}
// Don't send out any CONNECTION_CLOSE if this is an error that occurred
// before we even sent out the first packet.
if c.perspective == protocol.PerspectiveClient && !c.sentFirstPacket {
c.connIDGenerator.RemoveAll()
return
}
connClosePacket, err := c.sendConnectionClose(e)
if err != nil {
c.logger.Debugf("Error sending CONNECTION_CLOSE: %s", err)
}
c.connIDGenerator.ReplaceWithClosed(connClosePacket, 3*c.rttStats.PTO(false))
}
func (c *Conn) dropEncryptionLevel(encLevel protocol.EncryptionLevel, now monotime.Time) error {
if c.tracer != nil && c.tracer.DroppedEncryptionLevel != nil {
c.tracer.DroppedEncryptionLevel(encLevel)
}
c.sentPacketHandler.DropPackets(encLevel, now)
c.receivedPacketHandler.DropPackets(encLevel)
//nolint:exhaustive // only Initial and 0-RTT need special treatment
switch encLevel {
case protocol.EncryptionInitial:
c.droppedInitialKeys = true
c.cryptoStreamHandler.DiscardInitialKeys()
case protocol.Encryption0RTT:
c.streamsMap.ResetFor0RTT()
c.framer.Handle0RTTRejection()
return c.connFlowController.Reset()
}
return c.cryptoStreamManager.Drop(encLevel)
}
// is called for the client, when restoring transport parameters saved for 0-RTT
func (c *Conn) restoreTransportParameters(params *wire.TransportParameters) {
if c.logger.Debug() {
c.logger.Debugf("Restoring Transport Parameters: %s", params)
}
if c.tracer != nil && c.tracer.RestoredTransportParameters != nil {
c.tracer.RestoredTransportParameters(params)
}
c.peerParams = params
c.connIDGenerator.SetMaxActiveConnIDs(params.ActiveConnectionIDLimit)
c.connFlowController.UpdateSendWindow(params.InitialMaxData)
c.streamsMap.HandleTransportParameters(params)
c.connStateMutex.Lock()
c.connState.SupportsDatagrams = c.supportsDatagrams()
c.connStateMutex.Unlock()
}
func (c *Conn) handleTransportParameters(params *wire.TransportParameters) error {
if c.tracer != nil && c.tracer.ReceivedTransportParameters != nil {
c.tracer.ReceivedTransportParameters(params)
}
if err := c.checkTransportParameters(params); err != nil {
return &qerr.TransportError{
ErrorCode: qerr.TransportParameterError,
ErrorMessage: err.Error(),
}
}
if c.perspective == protocol.PerspectiveClient && c.peerParams != nil && c.ConnectionState().Used0RTT && !params.ValidForUpdate(c.peerParams) {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "server sent reduced limits after accepting 0-RTT data",
}
}
c.peerParams = params
// On the client side we have to wait for handshake completion.
// During a 0-RTT connection, we are only allowed to use the new transport parameters for 1-RTT packets.
if c.perspective == protocol.PerspectiveServer {
c.applyTransportParameters()
// On the server side, the early connection is ready as soon as we processed
// the client's transport parameters.
close(c.earlyConnReadyChan)
}
c.connStateMutex.Lock()
c.connState.SupportsDatagrams = c.supportsDatagrams()
c.connStateMutex.Unlock()
return nil
}
func (c *Conn) checkTransportParameters(params *wire.TransportParameters) error {
if c.logger.Debug() {
c.logger.Debugf("Processed Transport Parameters: %s", params)
}
// check the initial_source_connection_id
if params.InitialSourceConnectionID != c.handshakeDestConnID {
return fmt.Errorf("expected initial_source_connection_id to equal %s, is %s", c.handshakeDestConnID, params.InitialSourceConnectionID)
}
if c.perspective == protocol.PerspectiveServer {
return nil
}
// check the original_destination_connection_id
if params.OriginalDestinationConnectionID != c.origDestConnID {
return fmt.Errorf("expected original_destination_connection_id to equal %s, is %s", c.origDestConnID, params.OriginalDestinationConnectionID)
}
if c.retrySrcConnID != nil { // a Retry was performed
if params.RetrySourceConnectionID == nil {
return errors.New("missing retry_source_connection_id")
}
if *params.RetrySourceConnectionID != *c.retrySrcConnID {
return fmt.Errorf("expected retry_source_connection_id to equal %s, is %s", c.retrySrcConnID, *params.RetrySourceConnectionID)
}
} else if params.RetrySourceConnectionID != nil {
return errors.New("received retry_source_connection_id, although no Retry was performed")
}
return nil
}
func (c *Conn) applyTransportParameters() {
params := c.peerParams
// Our local idle timeout will always be > 0.
c.idleTimeout = c.config.MaxIdleTimeout
// If the peer advertised an idle timeout, take the minimum of the values.
if params.MaxIdleTimeout > 0 {
c.idleTimeout = min(c.idleTimeout, params.MaxIdleTimeout)
}
c.keepAliveInterval = min(c.config.KeepAlivePeriod, c.idleTimeout/2)
c.streamsMap.HandleTransportParameters(params)
c.frameParser.SetAckDelayExponent(params.AckDelayExponent)
c.connFlowController.UpdateSendWindow(params.InitialMaxData)
c.rttStats.SetMaxAckDelay(params.MaxAckDelay)
c.connIDGenerator.SetMaxActiveConnIDs(params.ActiveConnectionIDLimit)
if params.StatelessResetToken != nil {
c.connIDManager.SetStatelessResetToken(*params.StatelessResetToken)
}
// We don't support connection migration yet, so we don't have any use for the preferred_address.
if params.PreferredAddress != nil {
// Retire the connection ID.
c.connIDManager.AddFromPreferredAddress(params.PreferredAddress.ConnectionID, params.PreferredAddress.StatelessResetToken)
}
maxPacketSize := protocol.ByteCount(protocol.MaxPacketBufferSize)
if params.MaxUDPPayloadSize > 0 && params.MaxUDPPayloadSize < maxPacketSize {
maxPacketSize = params.MaxUDPPayloadSize
}
c.mtuDiscoverer = newMTUDiscoverer(
c.rttStats,
protocol.ByteCount(c.config.InitialPacketSize),
maxPacketSize,
c.tracer,
)
}
func (c *Conn) triggerSending(now monotime.Time) error {
c.pacingDeadline = 0
sendMode := c.sentPacketHandler.SendMode(now)
switch sendMode {
case ackhandler.SendAny:
return c.sendPackets(now)
case ackhandler.SendNone:
c.blocked = blockModeHardBlocked
return nil
case ackhandler.SendPacingLimited:
deadline := c.sentPacketHandler.TimeUntilSend()
if deadline.IsZero() {
deadline = deadlineSendImmediately
}
c.pacingDeadline = deadline
// Allow sending of an ACK if we're pacing limit.
// This makes sure that a peer that is mostly receiving data (and thus has an inaccurate cwnd estimate)
// sends enough ACKs to allow its peer to utilize the bandwidth.
return c.maybeSendAckOnlyPacket(now)
case ackhandler.SendAck:
// We can at most send a single ACK only packet.
// There will only be a new ACK after receiving new packets.
// SendAck is only returned when we're congestion limited, so we don't need to set the pacing timer.
c.blocked = blockModeCongestionLimited
return c.maybeSendAckOnlyPacket(now)
case ackhandler.SendPTOInitial, ackhandler.SendPTOHandshake, ackhandler.SendPTOAppData:
if err := c.sendProbePacket(sendMode, now); err != nil {
return err
}
if c.sendQueue.WouldBlock() {
c.scheduleSending()
return nil
}
return c.triggerSending(now)
default:
return fmt.Errorf("BUG: invalid send mode %d", sendMode)
}
}
func (c *Conn) sendPackets(now monotime.Time) error {
if c.perspective == protocol.PerspectiveClient && c.handshakeConfirmed {
if pm := c.pathManagerOutgoing.Load(); pm != nil {
connID, frame, tr, ok := pm.NextPathToProbe()
if ok {
probe, buf, err := c.packer.PackPathProbePacket(connID, []ackhandler.Frame{frame}, c.version)
if err != nil {
return err
}
c.logger.Debugf("sending path probe packet from %s", c.LocalAddr())
c.logShortHeaderPacket(probe.DestConnID, probe.Ack, probe.Frames, probe.StreamFrames, probe.PacketNumber, probe.PacketNumberLen, probe.KeyPhase, protocol.ECNNon, buf.Len(), false)
c.registerPackedShortHeaderPacket(probe, protocol.ECNNon, now)
tr.WriteTo(buf.Data, c.conn.RemoteAddr())
// There's (likely) more data to send. Loop around again.
c.scheduleSending()
return nil
}
}
}
// Path MTU Discovery
// Can't use GSO, since we need to send a single packet that's larger than our current maximum size.
// Performance-wise, this doesn't matter, since we only send a very small (<10) number of
// MTU probe packets per connection.
if c.handshakeConfirmed && c.mtuDiscoverer != nil && c.mtuDiscoverer.ShouldSendProbe(now) {
ping, size := c.mtuDiscoverer.GetPing(now)
p, buf, err := c.packer.PackMTUProbePacket(ping, size, c.version)
if err != nil {
return err
}
ecn := c.sentPacketHandler.ECNMode(true)
c.logShortHeaderPacket(p.DestConnID, p.Ack, p.Frames, p.StreamFrames, p.PacketNumber, p.PacketNumberLen, p.KeyPhase, ecn, buf.Len(), false)
c.registerPackedShortHeaderPacket(p, ecn, now)
c.sendQueue.Send(buf, 0, ecn)
// There's (likely) more data to send. Loop around again.
c.scheduleSending()
return nil
}
if offset := c.connFlowController.GetWindowUpdate(now); offset > 0 {
c.framer.QueueControlFrame(&wire.MaxDataFrame{MaximumData: offset})
}
if cf := c.cryptoStreamManager.GetPostHandshakeData(protocol.MaxPostHandshakeCryptoFrameSize); cf != nil {
c.queueControlFrame(cf)
}
if !c.handshakeConfirmed {
packet, err := c.packer.PackCoalescedPacket(false, c.maxPacketSize(), now, c.version)
if err != nil || packet == nil {
return err
}
c.sentFirstPacket = true
if err := c.sendPackedCoalescedPacket(packet, c.sentPacketHandler.ECNMode(packet.IsOnlyShortHeaderPacket()), now); err != nil {
return err
}
//nolint:exhaustive // only need to handle pacing-related events here
switch c.sentPacketHandler.SendMode(now) {
case ackhandler.SendPacingLimited:
c.resetPacingDeadline()
case ackhandler.SendAny:
c.pacingDeadline = deadlineSendImmediately
}
return nil
}
if c.conn.capabilities().GSO {
return c.sendPacketsWithGSO(now)
}
return c.sendPacketsWithoutGSO(now)
}
func (c *Conn) sendPacketsWithoutGSO(now monotime.Time) error {
for {
buf := getPacketBuffer()
ecn := c.sentPacketHandler.ECNMode(true)
if _, err := c.appendOneShortHeaderPacket(buf, c.maxPacketSize(), ecn, now); err != nil {
if err == errNothingToPack {
buf.Release()
return nil
}
return err
}
c.sendQueue.Send(buf, 0, ecn)
if c.sendQueue.WouldBlock() {
return nil
}
sendMode := c.sentPacketHandler.SendMode(now)
if sendMode == ackhandler.SendPacingLimited {
c.resetPacingDeadline()
return nil
}
if sendMode != ackhandler.SendAny {
return nil
}
// Prioritize receiving of packets over sending out more packets.
c.receivedPacketMx.Lock()
hasPackets := !c.receivedPackets.Empty()
c.receivedPacketMx.Unlock()
if hasPackets {
c.pacingDeadline = deadlineSendImmediately
return nil
}
}
}
func (c *Conn) sendPacketsWithGSO(now monotime.Time) error {
buf := getLargePacketBuffer()
maxSize := c.maxPacketSize()
ecn := c.sentPacketHandler.ECNMode(true)
for {
var dontSendMore bool
size, err := c.appendOneShortHeaderPacket(buf, maxSize, ecn, now)
if err != nil {
if err != errNothingToPack {
return err
}
if buf.Len() == 0 {
buf.Release()
return nil
}
dontSendMore = true
}
if !dontSendMore {
sendMode := c.sentPacketHandler.SendMode(now)
if sendMode == ackhandler.SendPacingLimited {
c.resetPacingDeadline()
}
if sendMode != ackhandler.SendAny {
dontSendMore = true
}
}
// Don't send more packets in this batch if they require a different ECN marking than the previous ones.
nextECN := c.sentPacketHandler.ECNMode(true)
// Append another packet if
// 1. The congestion controller and pacer allow sending more
// 2. The last packet appended was a full-size packet
// 3. The next packet will have the same ECN marking
// 4. We still have enough space for another full-size packet in the buffer
if !dontSendMore && size == maxSize && nextECN == ecn && buf.Len()+maxSize <= buf.Cap() {
continue
}
c.sendQueue.Send(buf, uint16(maxSize), ecn)
if dontSendMore {
return nil
}
if c.sendQueue.WouldBlock() {
return nil
}
// Prioritize receiving of packets over sending out more packets.
c.receivedPacketMx.Lock()
hasPackets := !c.receivedPackets.Empty()
c.receivedPacketMx.Unlock()
if hasPackets {
c.pacingDeadline = deadlineSendImmediately
return nil
}
ecn = nextECN
buf = getLargePacketBuffer()
}
}
func (c *Conn) resetPacingDeadline() {
deadline := c.sentPacketHandler.TimeUntilSend()
if deadline.IsZero() {
deadline = deadlineSendImmediately
}
c.pacingDeadline = deadline
}
func (c *Conn) maybeSendAckOnlyPacket(now monotime.Time) error {
if !c.handshakeConfirmed {
ecn := c.sentPacketHandler.ECNMode(false)
packet, err := c.packer.PackCoalescedPacket(true, c.maxPacketSize(), now, c.version)
if err != nil {
return err
}
if packet == nil {
return nil
}
return c.sendPackedCoalescedPacket(packet, ecn, now)
}
ecn := c.sentPacketHandler.ECNMode(true)
p, buf, err := c.packer.PackAckOnlyPacket(c.maxPacketSize(), now, c.version)
if err != nil {
if err == errNothingToPack {
return nil
}
return err
}
c.logShortHeaderPacket(p.DestConnID, p.Ack, p.Frames, p.StreamFrames, p.PacketNumber, p.PacketNumberLen, p.KeyPhase, ecn, buf.Len(), false)
c.registerPackedShortHeaderPacket(p, ecn, now)
c.sendQueue.Send(buf, 0, ecn)
return nil
}
func (c *Conn) sendProbePacket(sendMode ackhandler.SendMode, now monotime.Time) error {
var encLevel protocol.EncryptionLevel
//nolint:exhaustive // We only need to handle the PTO send modes here.
switch sendMode {
case ackhandler.SendPTOInitial:
encLevel = protocol.EncryptionInitial
case ackhandler.SendPTOHandshake:
encLevel = protocol.EncryptionHandshake
case ackhandler.SendPTOAppData:
encLevel = protocol.Encryption1RTT
default:
return fmt.Errorf("connection BUG: unexpected send mode: %d", sendMode)
}
// Queue probe packets until we actually send out a packet,
// or until there are no more packets to queue.
var packet *coalescedPacket
for packet == nil {
if wasQueued := c.sentPacketHandler.QueueProbePacket(encLevel); !wasQueued {
break
}
var err error
packet, err = c.packer.PackPTOProbePacket(encLevel, c.maxPacketSize(), false, now, c.version)
if err != nil {
return err
}
}
if packet == nil {
var err error
packet, err = c.packer.PackPTOProbePacket(encLevel, c.maxPacketSize(), true, now, c.version)
if err != nil {
return err
}
}
if packet == nil || (len(packet.longHdrPackets) == 0 && packet.shortHdrPacket == nil) {
return fmt.Errorf("connection BUG: couldn't pack %s probe packet: %v", encLevel, packet)
}
return c.sendPackedCoalescedPacket(packet, c.sentPacketHandler.ECNMode(packet.IsOnlyShortHeaderPacket()), now)
}
// appendOneShortHeaderPacket appends a new packet to the given packetBuffer.
// If there was nothing to pack, the returned size is 0.
func (c *Conn) appendOneShortHeaderPacket(buf *packetBuffer, maxSize protocol.ByteCount, ecn protocol.ECN, now monotime.Time) (protocol.ByteCount, error) {
startLen := buf.Len()
p, err := c.packer.AppendPacket(buf, maxSize, now, c.version)
if err != nil {
return 0, err
}
size := buf.Len() - startLen
c.logShortHeaderPacket(p.DestConnID, p.Ack, p.Frames, p.StreamFrames, p.PacketNumber, p.PacketNumberLen, p.KeyPhase, ecn, size, false)
c.registerPackedShortHeaderPacket(p, ecn, now)
return size, nil
}
func (c *Conn) registerPackedShortHeaderPacket(p shortHeaderPacket, ecn protocol.ECN, now monotime.Time) {
if p.IsPathProbePacket {
c.sentPacketHandler.SentPacket(
now,
p.PacketNumber,
protocol.InvalidPacketNumber,
p.StreamFrames,
p.Frames,
protocol.Encryption1RTT,
ecn,
p.Length,
p.IsPathMTUProbePacket,
true,
)
return
}
if c.firstAckElicitingPacketAfterIdleSentTime.IsZero() && (len(p.StreamFrames) > 0 || ackhandler.HasAckElicitingFrames(p.Frames)) {
c.firstAckElicitingPacketAfterIdleSentTime = now
}
largestAcked := protocol.InvalidPacketNumber
if p.Ack != nil {
largestAcked = p.Ack.LargestAcked()
}
c.sentPacketHandler.SentPacket(
now,
p.PacketNumber,
largestAcked,
p.StreamFrames,
p.Frames,
protocol.Encryption1RTT,
ecn,
p.Length,
p.IsPathMTUProbePacket,
false,
)
c.connIDManager.SentPacket()
}
func (c *Conn) sendPackedCoalescedPacket(packet *coalescedPacket, ecn protocol.ECN, now monotime.Time) error {
c.logCoalescedPacket(packet, ecn)
for _, p := range packet.longHdrPackets {
if c.firstAckElicitingPacketAfterIdleSentTime.IsZero() && p.IsAckEliciting() {
c.firstAckElicitingPacketAfterIdleSentTime = now
}
largestAcked := protocol.InvalidPacketNumber
if p.ack != nil {
largestAcked = p.ack.LargestAcked()
}
c.sentPacketHandler.SentPacket(
now,
p.header.PacketNumber,
largestAcked,
p.streamFrames,
p.frames,
p.EncryptionLevel(),
ecn,
p.length,
false,
false,
)
if c.perspective == protocol.PerspectiveClient && p.EncryptionLevel() == protocol.EncryptionHandshake &&
!c.droppedInitialKeys {
// On the client side, Initial keys are dropped as soon as the first Handshake packet is sent.
// See Section 4.9.1 of RFC 9001.
if err := c.dropEncryptionLevel(protocol.EncryptionInitial, now); err != nil {
return err
}
}
}
if p := packet.shortHdrPacket; p != nil {
if c.firstAckElicitingPacketAfterIdleSentTime.IsZero() && p.IsAckEliciting() {
c.firstAckElicitingPacketAfterIdleSentTime = now
}
largestAcked := protocol.InvalidPacketNumber
if p.Ack != nil {
largestAcked = p.Ack.LargestAcked()
}
c.sentPacketHandler.SentPacket(
now,
p.PacketNumber,
largestAcked,
p.StreamFrames,
p.Frames,
protocol.Encryption1RTT,
ecn,
p.Length,
p.IsPathMTUProbePacket,
false,
)
}
c.connIDManager.SentPacket()
c.sendQueue.Send(packet.buffer, 0, ecn)
return nil
}
func (c *Conn) sendConnectionClose(e error) ([]byte, error) {
var packet *coalescedPacket
var err error
var transportErr *qerr.TransportError
var applicationErr *qerr.ApplicationError
if errors.As(e, &transportErr) {
packet, err = c.packer.PackConnectionClose(transportErr, c.maxPacketSize(), c.version)
} else if errors.As(e, &applicationErr) {
packet, err = c.packer.PackApplicationClose(applicationErr, c.maxPacketSize(), c.version)
} else {
packet, err = c.packer.PackConnectionClose(&qerr.TransportError{
ErrorCode: qerr.InternalError,
ErrorMessage: fmt.Sprintf("connection BUG: unspecified error type (msg: %s)", e.Error()),
}, c.maxPacketSize(), c.version)
}
if err != nil {
return nil, err
}
ecn := c.sentPacketHandler.ECNMode(packet.IsOnlyShortHeaderPacket())
c.logCoalescedPacket(packet, ecn)
return packet.buffer.Data, c.conn.Write(packet.buffer.Data, 0, ecn)
}
func (c *Conn) maxPacketSize() protocol.ByteCount {
if c.mtuDiscoverer == nil {
// Use the configured packet size on the client side.
// If the server sends a max_udp_payload_size that's smaller than this size, we can ignore this:
// Apparently the server still processed the (fully padded) Initial packet anyway.
if c.perspective == protocol.PerspectiveClient {
return protocol.ByteCount(c.config.InitialPacketSize)
}
// On the server side, there's no downside to using 1200 bytes until we received the client's transport
// parameters:
// * If the first packet didn't contain the entire ClientHello, all we can do is ACK that packet. We don't
// need a lot of bytes for that.
// * If it did, we will have processed the transport parameters and initialized the MTU discoverer.
return protocol.MinInitialPacketSize
}
return c.mtuDiscoverer.CurrentSize()
}
// AcceptStream returns the next stream opened by the peer, blocking until one is available.
func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error) {
return c.streamsMap.AcceptStream(ctx)
}
// AcceptUniStream returns the next unidirectional stream opened by the peer, blocking until one is available.
func (c *Conn) AcceptUniStream(ctx context.Context) (*ReceiveStream, error) {
return c.streamsMap.AcceptUniStream(ctx)
}
// OpenStream opens a new bidirectional QUIC stream.
// There is no signaling to the peer about new streams:
// The peer can only accept the stream after data has been sent on the stream,
// or the stream has been reset or closed.
// When reaching the peer's stream limit, it is not possible to open a new stream until the
// peer raises the stream limit. In that case, a [StreamLimitReachedError] is returned.
func (c *Conn) OpenStream() (*Stream, error) {
return c.streamsMap.OpenStream()
}
// OpenStreamSync opens a new bidirectional QUIC stream.
// It blocks until a new stream can be opened.
// There is no signaling to the peer about new streams:
// The peer can only accept the stream after data has been sent on the stream,
// or the stream has been reset or closed.
func (c *Conn) OpenStreamSync(ctx context.Context) (*Stream, error) {
return c.streamsMap.OpenStreamSync(ctx)
}
// OpenUniStream opens a new outgoing unidirectional QUIC stream.
// There is no signaling to the peer about new streams:
// The peer can only accept the stream after data has been sent on the stream,
// or the stream has been reset or closed.
// When reaching the peer's stream limit, it is not possible to open a new stream until the
// peer raises the stream limit. In that case, a [StreamLimitReachedError] is returned.
func (c *Conn) OpenUniStream() (*SendStream, error) {
return c.streamsMap.OpenUniStream()
}
// OpenUniStreamSync opens a new outgoing unidirectional QUIC stream.
// It blocks until a new stream can be opened.
// There is no signaling to the peer about new streams:
// The peer can only accept the stream after data has been sent on the stream,
// or the stream has been reset or closed.
func (c *Conn) OpenUniStreamSync(ctx context.Context) (*SendStream, error) {
return c.streamsMap.OpenUniStreamSync(ctx)
}
func (c *Conn) newFlowController(id protocol.StreamID) flowcontrol.StreamFlowController {
initialSendWindow := c.peerParams.InitialMaxStreamDataUni
if id.Type() == protocol.StreamTypeBidi {
if id.InitiatedBy() == c.perspective {
initialSendWindow = c.peerParams.InitialMaxStreamDataBidiRemote
} else {
initialSendWindow = c.peerParams.InitialMaxStreamDataBidiLocal
}
}
return flowcontrol.NewStreamFlowController(
id,
c.connFlowController,
protocol.ByteCount(c.config.InitialStreamReceiveWindow),
protocol.ByteCount(c.config.MaxStreamReceiveWindow),
initialSendWindow,
c.rttStats,
c.logger,
)
}
// scheduleSending signals that we have data for sending
func (c *Conn) scheduleSending() {
select {
case c.sendingScheduled <- struct{}{}:
default:
}
}
// tryQueueingUndecryptablePacket queues a packet for which we're missing the decryption keys.
// The logging.PacketType is only used for logging purposes.
func (c *Conn) tryQueueingUndecryptablePacket(p receivedPacket, pt logging.PacketType) {
if c.handshakeComplete {
panic("shouldn't queue undecryptable packets after handshake completion")
}
if len(c.undecryptablePackets)+1 > protocol.MaxUndecryptablePackets {
if c.tracer != nil && c.tracer.DroppedPacket != nil {
c.tracer.DroppedPacket(pt, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropDOSPrevention)
}
c.logger.Infof("Dropping undecryptable packet (%d bytes). Undecryptable packet queue full.", p.Size())
return
}
c.logger.Infof("Queueing packet (%d bytes) for later decryption", p.Size())
if c.tracer != nil && c.tracer.BufferedPacket != nil {
c.tracer.BufferedPacket(pt, p.Size())
}
c.undecryptablePackets = append(c.undecryptablePackets, p)
}
func (c *Conn) queueControlFrame(f wire.Frame) {
c.framer.QueueControlFrame(f)
c.scheduleSending()
}
func (c *Conn) onHasConnectionData() { c.scheduleSending() }
func (c *Conn) onHasStreamData(id protocol.StreamID, str *SendStream) {
c.framer.AddActiveStream(id, str)
c.scheduleSending()
}
func (c *Conn) onHasStreamControlFrame(id protocol.StreamID, str streamControlFrameGetter) {
c.framer.AddStreamWithControlFrames(id, str)
c.scheduleSending()
}
func (c *Conn) onStreamCompleted(id protocol.StreamID) {
if err := c.streamsMap.DeleteStream(id); err != nil {
c.closeLocal(err)
}
c.framer.RemoveActiveStream(id)
}
// SendDatagram sends a message using a QUIC datagram, as specified in RFC 9221,
// if the peer enabled datagram support.
// There is no delivery guarantee for DATAGRAM frames, they are not retransmitted if lost.
// The payload of the datagram needs to fit into a single QUIC packet.
// In addition, a datagram may be dropped before being sent out if the available packet size suddenly decreases.
// If the payload is too large to be sent at the current time, a DatagramTooLargeError is returned.
func (c *Conn) SendDatagram(p []byte) error {
if !c.supportsDatagrams() {
return errors.New("datagram support disabled")
}
f := &wire.DatagramFrame{DataLenPresent: true}
// The payload size estimate is conservative.
// Under many circumstances we could send a few more bytes.
maxDataLen := min(
f.MaxDataLen(c.peerParams.MaxDatagramFrameSize, c.version),
protocol.ByteCount(c.currentMTUEstimate.Load()),
)
if protocol.ByteCount(len(p)) > maxDataLen {
return &DatagramTooLargeError{MaxDatagramPayloadSize: int64(maxDataLen)}
}
f.Data = make([]byte, len(p))
copy(f.Data, p)
return c.datagramQueue.Add(f)
}
// ReceiveDatagram gets a message received in a QUIC datagram, as specified in RFC 9221.
func (c *Conn) ReceiveDatagram(ctx context.Context) ([]byte, error) {
if !c.config.EnableDatagrams {
return nil, errors.New("datagram support disabled")
}
return c.datagramQueue.Receive(ctx)
}
// LocalAddr returns the local address of the QUIC connection.
func (c *Conn) LocalAddr() net.Addr { return c.conn.LocalAddr() }
// RemoteAddr returns the remote address of the QUIC connection.
func (c *Conn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() }
// getPathManager lazily initializes the Conn's pathManagerOutgoing.
// May create multiple pathManagerOutgoing objects if called concurrently.
func (c *Conn) getPathManager() *pathManagerOutgoing {
old := c.pathManagerOutgoing.Load()
if old != nil {
// Path manager is already initialized
return old
}
// Initialize the path manager
new := newPathManagerOutgoing(
c.connIDManager.GetConnIDForPath,
c.connIDManager.RetireConnIDForPath,
c.scheduleSending,
)
if c.pathManagerOutgoing.CompareAndSwap(old, new) {
return new
}
// Swap failed. A concurrent writer wrote first, use their value.
return c.pathManagerOutgoing.Load()
}
func (c *Conn) AddPath(t *Transport) (*Path, error) {
if c.perspective == protocol.PerspectiveServer {
return nil, errors.New("server cannot initiate connection migration")
}
if c.peerParams.DisableActiveMigration {
return nil, errors.New("server disabled connection migration")
}
if err := t.init(false); err != nil {
return nil, err
}
return c.getPathManager().NewPath(
t,
200*time.Millisecond, // initial RTT estimate
func() {
runner := (*packetHandlerMap)(t)
c.connIDGenerator.AddConnRunner(
runner,
connRunnerCallbacks{
AddConnectionID: func(connID protocol.ConnectionID) { runner.Add(connID, c) },
RemoveConnectionID: runner.Remove,
ReplaceWithClosed: runner.ReplaceWithClosed,
},
)
},
), nil
}
// HandshakeComplete blocks until the handshake completes (or fails).
// For the client, data sent before completion of the handshake is encrypted with 0-RTT keys.
// For the server, data sent before completion of the handshake is encrypted with 1-RTT keys,
// however the client's identity is only verified once the handshake completes.
func (c *Conn) HandshakeComplete() <-chan struct{} {
return c.handshakeCompleteChan
}
func (c *Conn) NextConnection(ctx context.Context) (*Conn, error) {
// The handshake might fail after the server rejected 0-RTT.
// This could happen if the Finished message is malformed or never received.
select {
case <-ctx.Done():
return nil, context.Cause(ctx)
case <-c.Context().Done():
case <-c.HandshakeComplete():
c.streamsMap.UseResetMaps()
}
return c, nil
}
// estimateMaxPayloadSize estimates the maximum payload size for short header packets.
// It is not very sophisticated: it just subtracts the size of header (assuming the maximum
// connection ID length), and the size of the encryption tag.
func estimateMaxPayloadSize(mtu protocol.ByteCount) protocol.ByteCount {
return mtu - 1 /* type byte */ - 20 /* maximum connection ID length */ - 16 /* tag size */
}
golang-github-lucas-clemente-quic-go-0.55.0/connection_logging.go 0000664 0000000 0000000 00000011605 15074232574 0025001 0 ustar 00root root 0000000 0000000 package quic
import (
"slices"
"github.com/quic-go/quic-go/internal/ackhandler"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/wire"
"github.com/quic-go/quic-go/logging"
)
// ConvertFrame converts a wire.Frame into a logging.Frame.
// This makes it possible for external packages to access the frames.
// Furthermore, it removes the data slices from CRYPTO and STREAM frames.
func toLoggingFrame(frame wire.Frame) logging.Frame {
switch f := frame.(type) {
case *wire.AckFrame:
// We use a pool for ACK frames.
// Implementations of the tracer interface may hold on to frames, so we need to make a copy here.
return toLoggingAckFrame(f)
case *wire.CryptoFrame:
return &logging.CryptoFrame{
Offset: f.Offset,
Length: protocol.ByteCount(len(f.Data)),
}
case *wire.StreamFrame:
return &logging.StreamFrame{
StreamID: f.StreamID,
Offset: f.Offset,
Length: f.DataLen(),
Fin: f.Fin,
}
case *wire.DatagramFrame:
return &logging.DatagramFrame{
Length: logging.ByteCount(len(f.Data)),
}
default:
return logging.Frame(frame)
}
}
func toLoggingAckFrame(f *wire.AckFrame) *logging.AckFrame {
ack := &logging.AckFrame{
AckRanges: slices.Clone(f.AckRanges),
DelayTime: f.DelayTime,
ECNCE: f.ECNCE,
ECT0: f.ECT0,
ECT1: f.ECT1,
}
return ack
}
func (c *Conn) logLongHeaderPacket(p *longHeaderPacket, ecn protocol.ECN) {
// quic-go logging
if c.logger.Debug() {
p.header.Log(c.logger)
if p.ack != nil {
wire.LogFrame(c.logger, p.ack, true)
}
for _, frame := range p.frames {
wire.LogFrame(c.logger, frame.Frame, true)
}
for _, frame := range p.streamFrames {
wire.LogFrame(c.logger, frame.Frame, true)
}
}
// tracing
if c.tracer != nil && c.tracer.SentLongHeaderPacket != nil {
frames := make([]logging.Frame, 0, len(p.frames))
for _, f := range p.frames {
frames = append(frames, toLoggingFrame(f.Frame))
}
for _, f := range p.streamFrames {
frames = append(frames, toLoggingFrame(f.Frame))
}
var ack *logging.AckFrame
if p.ack != nil {
ack = toLoggingAckFrame(p.ack)
}
c.tracer.SentLongHeaderPacket(p.header, p.length, ecn, ack, frames)
}
}
func (c *Conn) logShortHeaderPacket(
destConnID protocol.ConnectionID,
ackFrame *wire.AckFrame,
frames []ackhandler.Frame,
streamFrames []ackhandler.StreamFrame,
pn protocol.PacketNumber,
pnLen protocol.PacketNumberLen,
kp protocol.KeyPhaseBit,
ecn protocol.ECN,
size protocol.ByteCount,
isCoalesced bool,
) {
if c.logger.Debug() && !isCoalesced {
c.logger.Debugf("-> Sending packet %d (%d bytes) for connection %s, 1-RTT (ECN: %s)", pn, size, c.logID, ecn)
}
// quic-go logging
if c.logger.Debug() {
wire.LogShortHeader(c.logger, destConnID, pn, pnLen, kp)
if ackFrame != nil {
wire.LogFrame(c.logger, ackFrame, true)
}
for _, f := range frames {
wire.LogFrame(c.logger, f.Frame, true)
}
for _, f := range streamFrames {
wire.LogFrame(c.logger, f.Frame, true)
}
}
// tracing
if c.tracer != nil && c.tracer.SentShortHeaderPacket != nil {
fs := make([]logging.Frame, 0, len(frames)+len(streamFrames))
for _, f := range frames {
fs = append(fs, toLoggingFrame(f.Frame))
}
for _, f := range streamFrames {
fs = append(fs, toLoggingFrame(f.Frame))
}
var ack *logging.AckFrame
if ackFrame != nil {
ack = toLoggingAckFrame(ackFrame)
}
c.tracer.SentShortHeaderPacket(
&logging.ShortHeader{DestConnectionID: destConnID, PacketNumber: pn, PacketNumberLen: pnLen, KeyPhase: kp},
size,
ecn,
ack,
fs,
)
}
}
func (c *Conn) logCoalescedPacket(packet *coalescedPacket, ecn protocol.ECN) {
if c.logger.Debug() {
// There's a short period between dropping both Initial and Handshake keys and completion of the handshake,
// during which we might call PackCoalescedPacket but just pack a short header packet.
if len(packet.longHdrPackets) == 0 && packet.shortHdrPacket != nil {
c.logShortHeaderPacket(
packet.shortHdrPacket.DestConnID,
packet.shortHdrPacket.Ack,
packet.shortHdrPacket.Frames,
packet.shortHdrPacket.StreamFrames,
packet.shortHdrPacket.PacketNumber,
packet.shortHdrPacket.PacketNumberLen,
packet.shortHdrPacket.KeyPhase,
ecn,
packet.shortHdrPacket.Length,
false,
)
return
}
if len(packet.longHdrPackets) > 1 {
c.logger.Debugf("-> Sending coalesced packet (%d parts, %d bytes) for connection %s", len(packet.longHdrPackets), packet.buffer.Len(), c.logID)
} else {
c.logger.Debugf("-> Sending packet %d (%d bytes) for connection %s, %s", packet.longHdrPackets[0].header.PacketNumber, packet.buffer.Len(), c.logID, packet.longHdrPackets[0].EncryptionLevel())
}
}
for _, p := range packet.longHdrPackets {
c.logLongHeaderPacket(p, ecn)
}
if p := packet.shortHdrPacket; p != nil {
c.logShortHeaderPacket(p.DestConnID, p.Ack, p.Frames, p.StreamFrames, p.PacketNumber, p.PacketNumberLen, p.KeyPhase, ecn, p.Length, true)
}
}
golang-github-lucas-clemente-quic-go-0.55.0/connection_logging_test.go 0000664 0000000 0000000 00000003157 15074232574 0026043 0 ustar 00root root 0000000 0000000 package quic
import (
"testing"
"github.com/quic-go/quic-go/internal/wire"
"github.com/quic-go/quic-go/logging"
"github.com/stretchr/testify/require"
)
func TestConnectionLoggingCryptoFrame(t *testing.T) {
f := toLoggingFrame(&wire.CryptoFrame{
Offset: 1234,
Data: []byte("foobar"),
})
require.Equal(t, &logging.CryptoFrame{
Offset: 1234,
Length: 6,
}, f)
}
func TestConnectionLoggingStreamFrame(t *testing.T) {
f := toLoggingFrame(&wire.StreamFrame{
StreamID: 42,
Offset: 1234,
Data: []byte("foo"),
Fin: true,
})
require.Equal(t, &logging.StreamFrame{
StreamID: 42,
Offset: 1234,
Length: 3,
Fin: true,
}, f)
}
func TestConnectionLoggingAckFrame(t *testing.T) {
ack := &wire.AckFrame{
AckRanges: []wire.AckRange{
{Smallest: 1, Largest: 3},
{Smallest: 6, Largest: 7},
},
DelayTime: 42,
ECNCE: 123,
ECT0: 456,
ECT1: 789,
}
f := toLoggingFrame(ack)
// now modify the ACK range in the original frame
ack.AckRanges[0].Smallest = 2
require.Equal(t, &logging.AckFrame{
AckRanges: []wire.AckRange{
{Smallest: 1, Largest: 3}, // unchanged, since the ACK ranges were cloned
{Smallest: 6, Largest: 7},
},
DelayTime: 42,
ECNCE: 123,
ECT0: 456,
ECT1: 789,
}, f)
}
func TestConnectionLoggingDatagramFrame(t *testing.T) {
f := toLoggingFrame(&wire.DatagramFrame{Data: []byte("foobar")})
require.Equal(t, &logging.DatagramFrame{Length: 6}, f)
}
func TestConnectionLoggingOtherFrames(t *testing.T) {
f := toLoggingFrame(&wire.MaxDataFrame{MaximumData: 1234})
require.Equal(t, &logging.MaxDataFrame{MaximumData: 1234}, f)
}
golang-github-lucas-clemente-quic-go-0.55.0/connection_test.go 0000664 0000000 0000000 00000327014 15074232574 0024336 0 ustar 00root root 0000000 0000000 package quic
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"errors"
"net"
"net/netip"
"strconv"
"testing"
"time"
"github.com/quic-go/quic-go/internal/ackhandler"
"github.com/quic-go/quic-go/internal/flowcontrol"
"github.com/quic-go/quic-go/internal/handshake"
"github.com/quic-go/quic-go/internal/mocks"
mockackhandler "github.com/quic-go/quic-go/internal/mocks/ackhandler"
mocklogging "github.com/quic-go/quic-go/internal/mocks/logging"
"github.com/quic-go/quic-go/internal/monotime"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/synctest"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/internal/wire"
"github.com/quic-go/quic-go/logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
type testConnectionOpt func(*Conn)
func connectionOptCryptoSetup(cs *mocks.MockCryptoSetup) testConnectionOpt {
return func(conn *Conn) { conn.cryptoStreamHandler = cs }
}
func connectionOptConnFlowController(cfc flowcontrol.ConnectionFlowController) testConnectionOpt {
return func(conn *Conn) { conn.connFlowController = cfc }
}
func connectionOptTracer(tr *logging.ConnectionTracer) testConnectionOpt {
return func(conn *Conn) { conn.tracer = tr }
}
func connectionOptSentPacketHandler(sph ackhandler.SentPacketHandler) testConnectionOpt {
return func(conn *Conn) { conn.sentPacketHandler = sph }
}
func connectionOptReceivedPacketHandler(rph ackhandler.ReceivedPacketHandler) testConnectionOpt {
return func(conn *Conn) { conn.receivedPacketHandler = rph }
}
func connectionOptUnpacker(u unpacker) testConnectionOpt {
return func(conn *Conn) { conn.unpacker = u }
}
func connectionOptSender(s sender) testConnectionOpt {
return func(conn *Conn) { conn.sendQueue = s }
}
func connectionOptHandshakeConfirmed() testConnectionOpt {
return func(conn *Conn) {
conn.handshakeComplete = true
conn.handshakeConfirmed = true
}
}
func connectionOptRTT(rtt time.Duration) testConnectionOpt {
var rttStats utils.RTTStats
rttStats.UpdateRTT(rtt, 0)
return func(conn *Conn) { conn.rttStats = &rttStats }
}
func connectionOptRetrySrcConnID(rcid protocol.ConnectionID) testConnectionOpt {
return func(conn *Conn) { conn.retrySrcConnID = &rcid }
}
type testConnection struct {
conn *Conn
connRunner *MockConnRunner
sendConn *MockSendConn
packer *MockPacker
destConnID protocol.ConnectionID
srcConnID protocol.ConnectionID
remoteAddr *net.UDPAddr
}
func newServerTestConnection(
t *testing.T,
mockCtrl *gomock.Controller,
config *Config,
gso bool,
opts ...testConnectionOpt,
) *testConnection {
if mockCtrl == nil {
mockCtrl = gomock.NewController(t)
}
remoteAddr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 4321}
localAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234}
connRunner := NewMockConnRunner(mockCtrl)
sendConn := NewMockSendConn(mockCtrl)
sendConn.EXPECT().capabilities().Return(connCapabilities{GSO: gso}).AnyTimes()
sendConn.EXPECT().RemoteAddr().Return(remoteAddr).AnyTimes()
sendConn.EXPECT().LocalAddr().Return(localAddr).AnyTimes()
packer := NewMockPacker(mockCtrl)
b := make([]byte, 12)
rand.Read(b)
origDestConnID := protocol.ParseConnectionID(b[:6])
srcConnID := protocol.ParseConnectionID(b[6:12])
ctx, cancel := context.WithCancelCause(context.Background())
if config == nil {
config = &Config{DisablePathMTUDiscovery: true}
}
wc := newConnection(
ctx,
cancel,
sendConn,
connRunner,
origDestConnID,
nil,
protocol.ConnectionID{},
protocol.ConnectionID{},
srcConnID,
&protocol.DefaultConnectionIDGenerator{},
newStatelessResetter(nil),
populateConfig(config),
&tls.Config{},
handshake.NewTokenGenerator(handshake.TokenProtectorKey{}),
false,
1337*time.Millisecond,
nil,
utils.DefaultLogger,
protocol.Version1,
)
require.Nil(t, wc.testHooks)
conn := wc.Conn
conn.packer = packer
for _, opt := range opts {
opt(conn)
}
return &testConnection{
conn: conn,
connRunner: connRunner,
sendConn: sendConn,
packer: packer,
destConnID: origDestConnID,
srcConnID: srcConnID,
remoteAddr: remoteAddr,
}
}
func newClientTestConnection(
t *testing.T,
mockCtrl *gomock.Controller,
config *Config,
enable0RTT bool,
opts ...testConnectionOpt,
) *testConnection {
if mockCtrl == nil {
mockCtrl = gomock.NewController(t)
}
remoteAddr := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 4321}
localAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234}
connRunner := NewMockConnRunner(mockCtrl)
sendConn := NewMockSendConn(mockCtrl)
sendConn.EXPECT().capabilities().Return(connCapabilities{}).AnyTimes()
sendConn.EXPECT().RemoteAddr().Return(remoteAddr).AnyTimes()
sendConn.EXPECT().LocalAddr().Return(localAddr).AnyTimes()
packer := NewMockPacker(mockCtrl)
b := make([]byte, 12)
rand.Read(b)
destConnID := protocol.ParseConnectionID(b[:6])
srcConnID := protocol.ParseConnectionID(b[6:12])
if config == nil {
config = &Config{DisablePathMTUDiscovery: true}
}
conn := newClientConnection(
context.Background(),
sendConn,
connRunner,
destConnID,
srcConnID,
&protocol.DefaultConnectionIDGenerator{},
newStatelessResetter(nil),
populateConfig(config),
&tls.Config{ServerName: "quic-go.net"},
0,
enable0RTT,
false,
nil,
utils.DefaultLogger,
protocol.Version1,
)
require.Nil(t, conn.testHooks)
conn.packer = packer
for _, opt := range opts {
opt(conn.Conn)
}
return &testConnection{
conn: conn.Conn,
connRunner: connRunner,
sendConn: sendConn,
packer: packer,
destConnID: destConnID,
srcConnID: srcConnID,
}
}
func TestConnectionHandleStreamRelatedFrames(t *testing.T) {
const id protocol.StreamID = 5
connID := protocol.ConnectionID{}
tests := []struct {
name string
frame wire.Frame
}{
{name: "RESET_STREAM", frame: &wire.ResetStreamFrame{StreamID: id, ErrorCode: 42, FinalSize: 1337}},
{name: "STOP_SENDING", frame: &wire.StopSendingFrame{StreamID: id, ErrorCode: 42}},
{name: "MAX_STREAM_DATA", frame: &wire.MaxStreamDataFrame{StreamID: id, MaximumStreamData: 1337}},
{name: "STREAM_DATA_BLOCKED", frame: &wire.StreamDataBlockedFrame{StreamID: id, MaximumStreamData: 42}},
{name: "STREAM_FRAME", frame: &wire.StreamFrame{StreamID: id, Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, Offset: 1337}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tc := newServerTestConnection(t, gomock.NewController(t), nil, false)
data, err := test.frame.Append(nil, protocol.Version1)
require.NoError(t, err)
_, _, _, err = tc.conn.handleFrames(data, connID, protocol.Encryption1RTT, nil, monotime.Now())
require.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.StreamStateError})
})
}
}
func TestConnectionHandleConnectionFlowControlFrames(t *testing.T) {
mockCtrl := gomock.NewController(t)
connFC := flowcontrol.NewConnectionFlowController(0, 0, nil, &utils.RTTStats{}, utils.DefaultLogger)
require.Zero(t, connFC.SendWindowSize())
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptConnFlowController(connFC))
now := monotime.Now()
connID := protocol.ConnectionID{}
// MAX_DATA frame
_, err := tc.conn.handleFrame(&wire.MaxDataFrame{MaximumData: 1337}, protocol.Encryption1RTT, connID, now)
require.NoError(t, err)
require.Equal(t, protocol.ByteCount(1337), connFC.SendWindowSize())
// DATA_BLOCKED frame
_, err = tc.conn.handleFrame(&wire.DataBlockedFrame{MaximumData: 1337}, protocol.Encryption1RTT, connID, now)
require.NoError(t, err)
}
func TestConnectionServerInvalidFrames(t *testing.T) {
mockCtrl := gomock.NewController(t)
tc := newServerTestConnection(t, mockCtrl, nil, false)
for _, test := range []struct {
Name string
Frame wire.Frame
}{
{Name: "NEW_TOKEN", Frame: &wire.NewTokenFrame{Token: []byte("foobar")}},
{Name: "HANDSHAKE_DONE", Frame: &wire.HandshakeDoneFrame{}},
{Name: "PATH_RESPONSE", Frame: &wire.PathResponseFrame{Data: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}}},
} {
t.Run(test.Name, func(t *testing.T) {
_, err := tc.conn.handleFrame(test.Frame, protocol.Encryption1RTT, protocol.ConnectionID{}, monotime.Now())
require.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.ProtocolViolation})
})
}
}
func TestConnectionTransportError(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
errChan := make(chan error, 1)
expectedErr := &qerr.TransportError{
ErrorCode: 1337,
FrameType: 42,
ErrorMessage: "foobar",
}
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
b := getPacketBuffer()
b.Data = append(b.Data, []byte("connection close")...)
tc.packer.EXPECT().PackConnectionClose(expectedErr, gomock.Any(), protocol.Version1).Return(&coalescedPacket{buffer: b}, nil)
tc.sendConn.EXPECT().Write([]byte("connection close"), gomock.Any(), gomock.Any())
tc.connRunner.EXPECT().ReplaceWithClosed(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
gomock.InOrder(
tracer.EXPECT().ClosedConnection(expectedErr),
tracer.EXPECT().Close(),
)
go func() { errChan <- tc.conn.run() }()
tc.conn.closeLocal(expectedErr)
select {
case err := <-errChan:
require.ErrorIs(t, err, expectedErr)
case <-time.After(time.Second):
t.Fatal("timeout")
}
// further calls to CloseWithError don't do anything
tc.conn.CloseWithError(42, "another error")
}
func TestConnectionApplicationClose(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
errChan := make(chan error, 1)
expectedErr := &qerr.ApplicationError{
ErrorCode: 1337,
ErrorMessage: "foobar",
}
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
b := getPacketBuffer()
b.Data = append(b.Data, []byte("connection close")...)
tc.packer.EXPECT().PackApplicationClose(expectedErr, gomock.Any(), protocol.Version1).Return(&coalescedPacket{buffer: b}, nil)
tc.sendConn.EXPECT().Write([]byte("connection close"), gomock.Any(), gomock.Any())
tc.connRunner.EXPECT().ReplaceWithClosed(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
gomock.InOrder(
tracer.EXPECT().ClosedConnection(expectedErr),
tracer.EXPECT().Close(),
)
go func() { errChan <- tc.conn.run() }()
tc.conn.CloseWithError(1337, "foobar")
select {
case err := <-errChan:
require.ErrorIs(t, err, expectedErr)
case <-time.After(time.Second):
t.Fatal("timeout")
}
// further calls to CloseWithError don't do anything
tc.conn.CloseWithError(42, "another error")
}
func TestConnectionStatelessReset(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
errChan := make(chan error, 1)
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
gomock.InOrder(
tracer.EXPECT().ClosedConnection(&StatelessResetError{}),
tracer.EXPECT().Close(),
)
go func() { errChan <- tc.conn.run() }()
tc.conn.destroy(&StatelessResetError{})
select {
case err := <-errChan:
require.ErrorIs(t, err, &StatelessResetError{})
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func getLongHeaderPacket(t *testing.T, remoteAddr net.Addr, extHdr *wire.ExtendedHeader, data []byte) receivedPacket {
t.Helper()
b, err := extHdr.Append(nil, protocol.Version1)
require.NoError(t, err)
return receivedPacket{
remoteAddr: remoteAddr,
data: append(b, data...),
buffer: getPacketBuffer(),
rcvTime: monotime.Now(),
}
}
func getShortHeaderPacket(t *testing.T, remoteAddr net.Addr, connID protocol.ConnectionID, pn protocol.PacketNumber, data []byte) receivedPacket {
t.Helper()
b, err := wire.AppendShortHeader(nil, connID, pn, protocol.PacketNumberLen2, protocol.KeyPhaseOne)
require.NoError(t, err)
return receivedPacket{
remoteAddr: remoteAddr,
data: append(b, data...),
buffer: getPacketBuffer(),
rcvTime: monotime.Now(),
}
}
func TestConnectionServerInvalidPackets(t *testing.T) {
t.Run("Retry", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
p := getLongHeaderPacket(t,
tc.remoteAddr,
&wire.ExtendedHeader{Header: wire.Header{
Type: protocol.PacketTypeRetry,
DestConnectionID: tc.conn.origDestConnID,
SrcConnectionID: tc.srcConnID,
Version: tc.conn.version,
Token: []byte("foobar"),
}},
make([]byte, 16), /* Retry integrity tag */
)
tracer.EXPECT().DroppedPacket(logging.PacketTypeRetry, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropUnexpectedPacket)
wasProcessed, err := tc.conn.handleOnePacket(p)
require.NoError(t, err)
require.False(t, wasProcessed)
})
t.Run("version negotiation", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
b := wire.ComposeVersionNegotiation(
protocol.ArbitraryLenConnectionID(tc.srcConnID.Bytes()),
protocol.ArbitraryLenConnectionID(tc.conn.origDestConnID.Bytes()),
[]Version{Version1},
)
tracer.EXPECT().DroppedPacket(logging.PacketTypeVersionNegotiation, protocol.InvalidPacketNumber, protocol.ByteCount(len(b)), logging.PacketDropUnexpectedPacket)
wasProcessed, err := tc.conn.handleOnePacket(receivedPacket{data: b, buffer: getPacketBuffer()})
require.NoError(t, err)
require.False(t, wasProcessed)
})
t.Run("unsupported version", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
p := getLongHeaderPacket(t,
tc.remoteAddr,
&wire.ExtendedHeader{
Header: wire.Header{Type: protocol.PacketTypeHandshake, Version: 1234},
PacketNumberLen: protocol.PacketNumberLen2,
},
nil,
)
tracer.EXPECT().DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropUnsupportedVersion)
wasProcessed, err := tc.conn.handleOnePacket(p)
require.NoError(t, err)
require.False(t, wasProcessed)
})
t.Run("invalid header", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
p := getLongHeaderPacket(t,
tc.remoteAddr,
&wire.ExtendedHeader{
Header: wire.Header{Type: protocol.PacketTypeHandshake, Version: Version1},
PacketNumberLen: protocol.PacketNumberLen2,
},
nil,
)
p.data[0] ^= 0x40 // unset the QUIC bit
tracer.EXPECT().DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropHeaderParseError)
wasProcessed, err := tc.conn.handleOnePacket(p)
require.NoError(t, err)
require.False(t, wasProcessed)
})
}
func TestConnectionClientDrop0RTT(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newClientTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
p := getLongHeaderPacket(t,
tc.remoteAddr,
&wire.ExtendedHeader{
Header: wire.Header{Type: protocol.PacketType0RTT, Length: 2, Version: protocol.Version1},
PacketNumberLen: protocol.PacketNumberLen2,
},
nil,
)
tracer.EXPECT().DroppedPacket(logging.PacketType0RTT, protocol.InvalidPacketNumber, p.Size(), logging.PacketDropUnexpectedPacket)
wasProcessed, err := tc.conn.handleOnePacket(p)
require.NoError(t, err)
require.False(t, wasProcessed)
}
func TestConnectionUnpacking(t *testing.T) {
mockCtrl := gomock.NewController(t)
rph := mockackhandler.NewMockReceivedPacketHandler(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptReceivedPacketHandler(rph),
connectionOptUnpacker(unpacker),
connectionOptTracer(tr),
)
// receive a long header packet
hdr := &wire.ExtendedHeader{
Header: wire.Header{
Type: protocol.PacketTypeInitial,
DestConnectionID: tc.srcConnID,
Version: protocol.Version1,
Length: 1,
},
PacketNumber: 0x37,
PacketNumberLen: protocol.PacketNumberLen1,
}
unpackedHdr := *hdr
unpackedHdr.PacketNumber = 0x1337
packet := getLongHeaderPacket(t, tc.remoteAddr, hdr, nil)
packet.ecn = protocol.ECNCE
rcvTime := monotime.Now().Add(-10 * time.Second)
packet.rcvTime = rcvTime
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(&unpackedPacket{
encryptionLevel: protocol.EncryptionInitial,
hdr: &unpackedHdr,
data: []byte{0}, // one PADDING frame
}, nil)
gomock.InOrder(
rph.EXPECT().IsPotentiallyDuplicate(protocol.PacketNumber(0x1337), protocol.EncryptionInitial),
rph.EXPECT().ReceivedPacket(protocol.PacketNumber(0x1337), protocol.ECNCE, protocol.EncryptionInitial, rcvTime, false),
)
tracer.EXPECT().NegotiatedVersion(gomock.Any(), gomock.Any(), gomock.Any())
tracer.EXPECT().StartedConnection(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
tracer.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), logging.ECNCE, []logging.Frame{})
wasProcessed, err := tc.conn.handleOnePacket(packet)
require.NoError(t, err)
require.True(t, wasProcessed)
require.True(t, mockCtrl.Satisfied())
// receive a duplicate of this packet
packet = getLongHeaderPacket(t, tc.remoteAddr, hdr, nil)
rph.EXPECT().IsPotentiallyDuplicate(protocol.PacketNumber(0x1337), protocol.EncryptionInitial).Return(true)
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(&unpackedPacket{
encryptionLevel: protocol.EncryptionInitial,
hdr: &unpackedHdr,
data: []byte{0}, // one PADDING frame
}, nil)
tracer.EXPECT().DroppedPacket(logging.PacketTypeInitial, protocol.PacketNumber(0x1337), protocol.ByteCount(len(packet.data)), logging.PacketDropDuplicate)
wasProcessed, err = tc.conn.handleOnePacket(packet)
require.NoError(t, err)
require.False(t, wasProcessed)
require.True(t, mockCtrl.Satisfied())
// receive a short header packet
packet = getShortHeaderPacket(t, tc.remoteAddr, tc.srcConnID, 0x37, nil)
packet.ecn = protocol.ECT1
packet.rcvTime = rcvTime
gomock.InOrder(
rph.EXPECT().IsPotentiallyDuplicate(protocol.PacketNumber(0x1337), protocol.Encryption1RTT),
rph.EXPECT().ReceivedPacket(protocol.PacketNumber(0x1337), protocol.ECT1, protocol.Encryption1RTT, rcvTime, false),
)
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).Return(
protocol.PacketNumber(0x1337), protocol.PacketNumberLen2, protocol.KeyPhaseZero, []byte{0} /* PADDING */, nil,
)
tracer.EXPECT().ReceivedShortHeaderPacket(gomock.Any(), gomock.Any(), logging.ECT1, []logging.Frame{})
wasProcessed, err = tc.conn.handleOnePacket(packet)
require.NoError(t, err)
require.True(t, wasProcessed)
}
func TestConnectionUnpackCoalescedPacket(t *testing.T) {
mockCtrl := gomock.NewController(t)
rph := mockackhandler.NewMockReceivedPacketHandler(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptReceivedPacketHandler(rph),
connectionOptUnpacker(unpacker),
connectionOptTracer(tr),
)
hdr1 := &wire.ExtendedHeader{
Header: wire.Header{
Type: protocol.PacketTypeInitial,
DestConnectionID: tc.srcConnID,
Version: protocol.Version1,
Length: 1,
},
PacketNumber: 37,
PacketNumberLen: protocol.PacketNumberLen1,
}
hdr2 := &wire.ExtendedHeader{
Header: wire.Header{
Type: protocol.PacketTypeHandshake,
DestConnectionID: tc.srcConnID,
Version: protocol.Version1,
Length: 1,
},
PacketNumber: 38,
PacketNumberLen: protocol.PacketNumberLen1,
}
// add a packet with a different source connection ID
incorrectSrcConnID := protocol.ParseConnectionID([]byte{0xa, 0xb, 0xc})
hdr3 := &wire.ExtendedHeader{
Header: wire.Header{
Type: protocol.PacketTypeHandshake,
DestConnectionID: incorrectSrcConnID,
Version: protocol.Version1,
Length: 1,
},
PacketNumber: 0x42,
PacketNumberLen: protocol.PacketNumberLen1,
}
unpackedHdr1 := *hdr1
unpackedHdr1.PacketNumber = 1337
unpackedHdr2 := *hdr2
unpackedHdr2.PacketNumber = 1338
packet := getLongHeaderPacket(t, tc.remoteAddr, hdr1, nil)
packet2 := getLongHeaderPacket(t, tc.remoteAddr, hdr2, nil)
packet3 := getLongHeaderPacket(t, tc.remoteAddr, hdr3, nil)
packet.data = append(packet.data, packet2.data...)
packet.data = append(packet.data, packet3.data...)
packet.ecn = protocol.ECT1
rcvTime := monotime.Now()
packet.rcvTime = rcvTime
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(&unpackedPacket{
encryptionLevel: protocol.EncryptionInitial,
hdr: &unpackedHdr1,
data: []byte{0}, // one PADDING frame
}, nil)
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(&unpackedPacket{
encryptionLevel: protocol.EncryptionHandshake,
hdr: &unpackedHdr2,
data: []byte{1}, // one PING frame
}, nil)
gomock.InOrder(
rph.EXPECT().IsPotentiallyDuplicate(protocol.PacketNumber(1337), protocol.EncryptionInitial),
rph.EXPECT().ReceivedPacket(protocol.PacketNumber(1337), protocol.ECT1, protocol.EncryptionInitial, rcvTime, false),
rph.EXPECT().IsPotentiallyDuplicate(protocol.PacketNumber(1338), protocol.EncryptionHandshake),
rph.EXPECT().ReceivedPacket(protocol.PacketNumber(1338), protocol.ECT1, protocol.EncryptionHandshake, rcvTime, true),
)
tracer.EXPECT().NegotiatedVersion(gomock.Any(), gomock.Any(), gomock.Any())
tracer.EXPECT().StartedConnection(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
tracer.EXPECT().DroppedEncryptionLevel(protocol.EncryptionInitial)
rph.EXPECT().DropPackets(protocol.EncryptionInitial)
gomock.InOrder(
tracer.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), logging.ECT1, []logging.Frame{}),
tracer.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), logging.ECT1, []logging.Frame{&wire.PingFrame{}}),
tracer.EXPECT().DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, protocol.ByteCount(len(packet3.data)), logging.PacketDropUnknownConnectionID),
)
wasProcessed, err := tc.conn.handleOnePacket(packet)
require.NoError(t, err)
require.True(t, wasProcessed)
}
func TestConnectionUnpackFailuresFatal(t *testing.T) {
t.Run("other errors", func(t *testing.T) {
require.ErrorIs(t,
testConnectionUnpackFailureFatal(t, &qerr.TransportError{ErrorCode: qerr.ConnectionIDLimitError}),
&qerr.TransportError{ErrorCode: qerr.ConnectionIDLimitError},
)
})
t.Run("invalid reserved bits", func(t *testing.T) {
require.ErrorIs(t,
testConnectionUnpackFailureFatal(t, wire.ErrInvalidReservedBits),
&qerr.TransportError{ErrorCode: qerr.ProtocolViolation},
)
})
}
func testConnectionUnpackFailureFatal(t *testing.T, unpackErr error) error {
mockCtrl := gomock.NewController(t)
unpacker := NewMockUnpacker(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptUnpacker(unpacker),
)
tc.connRunner.EXPECT().ReplaceWithClosed(gomock.Any(), gomock.Any(), gomock.Any())
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).Return(protocol.PacketNumber(0), protocol.PacketNumberLen(0), protocol.KeyPhaseBit(0), nil, unpackErr)
tc.packer.EXPECT().PackConnectionClose(gomock.Any(), gomock.Any(), protocol.Version1).Return(&coalescedPacket{buffer: getPacketBuffer()}, nil)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any())
tc.conn.handlePacket(getShortHeaderPacket(t, tc.remoteAddr, tc.srcConnID, 0x42, nil))
select {
case err := <-errChan:
require.Error(t, err)
return err
case <-time.After(time.Second):
t.Fatal("timeout")
}
return nil
}
func TestConnectionUnpackFailureDropped(t *testing.T) {
t.Run("keys dropped", func(t *testing.T) {
testConnectionUnpackFailureDropped(t, handshake.ErrKeysDropped, logging.PacketDropKeyUnavailable)
})
t.Run("decryption failed", func(t *testing.T) {
testConnectionUnpackFailureDropped(t, handshake.ErrDecryptionFailed, logging.PacketDropPayloadDecryptError)
})
t.Run("header parse error", func(t *testing.T) {
testConnectionUnpackFailureDropped(t, &headerParseError{err: assert.AnError}, logging.PacketDropHeaderParseError)
})
}
func testConnectionUnpackFailureDropped(t *testing.T, unpackErr error, packetDropReason logging.PacketDropReason) {
mockCtrl := gomock.NewController(t)
unpacker := NewMockUnpacker(mockCtrl)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptUnpacker(unpacker),
connectionOptTracer(tr),
)
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).Return(protocol.PacketNumber(0), protocol.PacketNumberLen(0), protocol.KeyPhaseBit(0), nil, unpackErr)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
done := make(chan struct{})
tracer.EXPECT().DroppedPacket(gomock.Any(), protocol.InvalidPacketNumber, gomock.Any(), packetDropReason).Do(
func(logging.PacketType, protocol.PacketNumber, protocol.ByteCount, logging.PacketDropReason) {
close(done)
},
)
tc.conn.handlePacket(getShortHeaderPacket(t, tc.remoteAddr, tc.srcConnID, 0x42, nil))
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
// test teardown
tracer.EXPECT().ClosedConnection(gomock.Any())
tracer.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
select {
case <-errChan:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnectionMaxUnprocessedPackets(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptTracer(tr))
done := make(chan struct{})
for i := protocol.PacketNumber(0); i < protocol.MaxConnUnprocessedPackets; i++ {
// nothing here should block
tc.conn.handlePacket(receivedPacket{data: []byte("foobar")})
}
tracer.EXPECT().DroppedPacket(logging.PacketTypeNotDetermined, protocol.InvalidPacketNumber, logging.ByteCount(6), logging.PacketDropDOSPrevention).Do(func(logging.PacketType, logging.PacketNumber, logging.ByteCount, logging.PacketDropReason) {
close(done)
})
tc.conn.handlePacket(receivedPacket{data: []byte("foobar")})
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnectionRemoteClose(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
connectionOptUnpacker(unpacker),
)
ccf, err := (&wire.ConnectionCloseFrame{
ErrorCode: uint64(qerr.StreamLimitError),
ReasonPhrase: "foobar",
}).Append(nil, protocol.Version1)
require.NoError(t, err)
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).Return(protocol.PacketNumber(1), protocol.PacketNumberLen2, protocol.KeyPhaseBit(0), ccf, nil)
tracer.EXPECT().ReceivedShortHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
expectedErr := &qerr.TransportError{ErrorCode: qerr.StreamLimitError, Remote: true}
tc.connRunner.EXPECT().ReplaceWithClosed(gomock.Any(), gomock.Any(), gomock.Any())
tracerErrChan := make(chan error, 1)
tracer.EXPECT().ClosedConnection(gomock.Any()).Do(func(e error) { tracerErrChan <- e })
tracer.EXPECT().Close()
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
p := getShortHeaderPacket(t, tc.remoteAddr, tc.srcConnID, 1, []byte("encrypted"))
tc.conn.handlePacket(receivedPacket{data: p.data, buffer: p.buffer, rcvTime: monotime.Now()})
select {
case err := <-errChan:
require.ErrorIs(t, err, expectedErr)
case <-time.After(time.Second):
t.Fatal("timeout")
}
select {
case err := <-tracerErrChan:
require.ErrorIs(t, err, expectedErr)
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnectionIdleTimeoutDuringHandshake(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
const timeout = 7 * time.Second
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
&Config{HandshakeIdleTimeout: timeout},
false,
connectionOptTracer(tr),
)
tc.packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), protocol.Version1).AnyTimes()
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
gomock.InOrder(
tracer.EXPECT().ClosedConnection(&IdleTimeoutError{}),
tracer.EXPECT().Close(),
)
start := monotime.Now()
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
synctest.Wait()
select {
case err := <-errChan:
require.ErrorIs(t, err, &IdleTimeoutError{})
require.Equal(t, timeout, monotime.Since(start))
case <-time.After(timeout + time.Nanosecond):
t.Fatal("timeout")
}
})
}
func TestConnectionHandshakeIdleTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
&Config{HandshakeIdleTimeout: 7 * time.Second},
false,
connectionOptTracer(tr),
func(c *Conn) { c.creationTime = monotime.Now().Add(-20 * time.Second) },
)
tc.packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), protocol.Version1).AnyTimes()
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
gomock.InOrder(
tracer.EXPECT().ClosedConnection(&HandshakeTimeoutError{}),
tracer.EXPECT().Close(),
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
select {
case err := <-errChan:
require.ErrorIs(t, err, &HandshakeTimeoutError{})
case <-time.After(time.Second):
t.Fatal("timeout")
}
})
}
func TestConnectionTransportParameters(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
connFC := flowcontrol.NewConnectionFlowController(0, 0, nil, &utils.RTTStats{}, utils.DefaultLogger)
require.Zero(t, connFC.SendWindowSize())
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
connectionOptConnFlowController(connFC),
)
_, err := tc.conn.OpenStream()
require.ErrorIs(t, err, &StreamLimitReachedError{})
_, err = tc.conn.OpenUniStream()
require.ErrorIs(t, err, &StreamLimitReachedError{})
tracer.EXPECT().ReceivedTransportParameters(gomock.Any())
params := &wire.TransportParameters{
MaxIdleTimeout: 90 * time.Second,
InitialMaxStreamDataBidiLocal: 0x5000,
InitialMaxData: 1337,
ActiveConnectionIDLimit: 3,
// marshaling always sets it to this value
MaxUDPPayloadSize: protocol.MaxPacketBufferSize,
OriginalDestinationConnectionID: tc.destConnID,
MaxBidiStreamNum: 1,
MaxUniStreamNum: 1,
}
require.NoError(t, tc.conn.handleTransportParameters(params))
require.Equal(t, protocol.ByteCount(1337), connFC.SendWindowSize())
_, err = tc.conn.OpenStream()
require.NoError(t, err)
_, err = tc.conn.OpenUniStream()
require.NoError(t, err)
}
func TestConnectionHandleMaxStreamsFrame(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
connFC := flowcontrol.NewConnectionFlowController(0, 0, nil, &utils.RTTStats{}, utils.DefaultLogger)
tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptConnFlowController(connFC))
tc.conn.handleTransportParameters(&wire.TransportParameters{})
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
uniStreamChan := make(chan error)
go func() {
_, err := tc.conn.OpenUniStreamSync(ctx)
uniStreamChan <- err
}()
bidiStreamChan := make(chan error)
go func() {
_, err := tc.conn.OpenStreamSync(ctx)
bidiStreamChan <- err
}()
synctest.Wait()
select {
case <-uniStreamChan:
t.Fatal("uni stream should be blocked")
case <-bidiStreamChan:
t.Fatal("bidi stream should be blocked")
default:
}
// MAX_STREAMS frame for bidirectional stream
_, err := tc.conn.handleFrame(
&wire.MaxStreamsFrame{Type: protocol.StreamTypeBidi, MaxStreamNum: 10},
protocol.Encryption1RTT,
protocol.ConnectionID{},
monotime.Now(),
)
require.NoError(t, err)
synctest.Wait()
select {
case <-uniStreamChan:
t.Fatal("uni stream should be blocked")
default:
}
select {
case err := <-bidiStreamChan:
require.NoError(t, err)
default:
t.Fatal("bidi stream should be unblocked")
}
// MAX_STREAMS frame for bidirectional stream
_, err = tc.conn.handleFrame(
&wire.MaxStreamsFrame{Type: protocol.StreamTypeUni, MaxStreamNum: 10},
protocol.Encryption1RTT,
protocol.ConnectionID{},
monotime.Now(),
)
require.NoError(t, err)
synctest.Wait()
select {
case err := <-uniStreamChan:
require.NoError(t, err)
default:
t.Fatal("timeout")
}
})
}
func TestConnectionTransportParameterValidationFailureServer(t *testing.T) {
tc := newServerTestConnection(t, nil, nil, false)
err := tc.conn.handleTransportParameters(&wire.TransportParameters{
InitialSourceConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
})
assert.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.TransportParameterError})
assert.ErrorContains(t, err, "expected initial_source_connection_id to equal")
}
func TestConnectionTransportParameterValidationFailureClient(t *testing.T) {
t.Run("initial_source_connection_id", func(t *testing.T) {
tc := newClientTestConnection(t, nil, nil, false)
err := tc.conn.handleTransportParameters(&wire.TransportParameters{
InitialSourceConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
})
assert.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.TransportParameterError})
assert.ErrorContains(t, err, "expected initial_source_connection_id to equal")
})
t.Run("original_destination_connection_id", func(t *testing.T) {
tc := newClientTestConnection(t, nil, nil, false)
err := tc.conn.handleTransportParameters(&wire.TransportParameters{
InitialSourceConnectionID: tc.destConnID,
OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
})
assert.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.TransportParameterError})
assert.ErrorContains(t, err, "expected original_destination_connection_id to equal")
})
t.Run("retry_source_connection_id if no retry", func(t *testing.T) {
tc := newClientTestConnection(t, nil, nil, false)
rcid := protocol.ParseConnectionID([]byte{1, 2, 3, 4})
params := &wire.TransportParameters{
InitialSourceConnectionID: tc.destConnID,
OriginalDestinationConnectionID: tc.destConnID,
RetrySourceConnectionID: &rcid,
}
err := tc.conn.handleTransportParameters(params)
assert.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.TransportParameterError})
assert.ErrorContains(t, err, "received retry_source_connection_id, although no Retry was performed")
})
t.Run("retry_source_connection_id missing", func(t *testing.T) {
tc := newClientTestConnection(t,
nil,
nil,
false,
connectionOptRetrySrcConnID(protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef})),
)
params := &wire.TransportParameters{
InitialSourceConnectionID: tc.destConnID,
OriginalDestinationConnectionID: tc.destConnID,
}
err := tc.conn.handleTransportParameters(params)
assert.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.TransportParameterError})
assert.ErrorContains(t, err, "missing retry_source_connection_id")
})
t.Run("retry_source_connection_id incorrect", func(t *testing.T) {
tc := newClientTestConnection(t,
nil,
nil,
false,
connectionOptRetrySrcConnID(protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef})),
)
wrongCID := protocol.ParseConnectionID([]byte{1, 2, 3, 4})
params := &wire.TransportParameters{
InitialSourceConnectionID: tc.destConnID,
OriginalDestinationConnectionID: tc.destConnID,
RetrySourceConnectionID: &wrongCID,
}
err := tc.conn.handleTransportParameters(params)
assert.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.TransportParameterError})
assert.ErrorContains(t, err, "expected retry_source_connection_id to equal")
})
}
func TestConnectionHandshakeServer(t *testing.T) {
mockCtrl := gomock.NewController(t)
cs := mocks.NewMockCryptoSetup(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tc := newServerTestConnection(
t,
mockCtrl,
nil,
false,
connectionOptCryptoSetup(cs),
connectionOptUnpacker(unpacker),
)
// the state transition is driven by processing of a CRYPTO frame
hdr := &wire.ExtendedHeader{
Header: wire.Header{Type: protocol.PacketTypeHandshake, Version: protocol.Version1},
PacketNumberLen: protocol.PacketNumberLen2,
}
data, err := (&wire.CryptoFrame{Data: []byte("foobar")}).Append(nil, protocol.Version1)
require.NoError(t, err)
cs.EXPECT().DiscardInitialKeys().Times(2)
gomock.InOrder(
cs.EXPECT().StartHandshake(gomock.Any()),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent}),
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(
&unpackedPacket{hdr: hdr, encryptionLevel: protocol.EncryptionHandshake, data: data}, nil,
),
cs.EXPECT().HandleMessage([]byte("foobar"), protocol.EncryptionHandshake),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventHandshakeComplete}),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent}),
cs.EXPECT().SetHandshakeConfirmed(),
cs.EXPECT().GetSessionTicket().Return([]byte("session ticket"), nil),
)
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
p := getLongHeaderPacket(t, tc.remoteAddr, hdr, nil)
tc.conn.handlePacket(receivedPacket{data: p.data, buffer: p.buffer, rcvTime: monotime.Now()})
select {
case <-tc.conn.HandshakeComplete():
case <-tc.conn.Context().Done():
t.Fatal("connection context done")
case <-time.After(time.Second):
t.Fatal("timeout")
}
var foundSessionTicket, foundHandshakeDone, foundNewToken bool
frames, _, _ := tc.conn.framer.Append(nil, nil, protocol.MaxByteCount, monotime.Now(), protocol.Version1)
for _, frame := range frames {
switch f := frame.Frame.(type) {
case *wire.CryptoFrame:
assert.Equal(t, []byte("session ticket"), f.Data)
foundSessionTicket = true
case *wire.HandshakeDoneFrame:
foundHandshakeDone = true
case *wire.NewTokenFrame:
assert.NotEmpty(t, f.Token)
foundNewToken = true
}
}
assert.True(t, foundSessionTicket)
assert.True(t, foundHandshakeDone)
assert.True(t, foundNewToken)
// test teardown
cs.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
select {
case err := <-errChan:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnectionHandshakeClient(t *testing.T) {
t.Run("without preferred address", func(t *testing.T) {
testConnectionHandshakeClient(t, false)
})
t.Run("with preferred address", func(t *testing.T) {
testConnectionHandshakeClient(t, true)
})
}
func testConnectionHandshakeClient(t *testing.T, usePreferredAddress bool) {
mockCtrl := gomock.NewController(t)
cs := mocks.NewMockCryptoSetup(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tc := newClientTestConnection(t, mockCtrl, nil, false, connectionOptCryptoSetup(cs), connectionOptUnpacker(unpacker))
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// the state transition is driven by processing of a CRYPTO frame
hdr := &wire.ExtendedHeader{
Header: wire.Header{Type: protocol.PacketTypeHandshake, Version: protocol.Version1},
PacketNumberLen: protocol.PacketNumberLen2,
}
data, err := (&wire.CryptoFrame{Data: []byte("foobar")}).Append(nil, protocol.Version1)
require.NoError(t, err)
tp := &wire.TransportParameters{
OriginalDestinationConnectionID: tc.destConnID,
MaxIdleTimeout: time.Hour,
}
preferredAddressConnID := protocol.ParseConnectionID([]byte{10, 8, 6, 4})
preferredAddressResetToken := protocol.StatelessResetToken{16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
if usePreferredAddress {
tp.PreferredAddress = &wire.PreferredAddress{
IPv4: netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 42),
IPv6: netip.AddrPortFrom(netip.AddrFrom16([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), 13),
ConnectionID: preferredAddressConnID,
StatelessResetToken: preferredAddressResetToken,
}
}
packedFirstPacket := make(chan struct{})
gomock.InOrder(
cs.EXPECT().StartHandshake(gomock.Any()),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent}),
tc.packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), protocol.Version1).DoAndReturn(
func(b bool, bc protocol.ByteCount, t monotime.Time, v protocol.Version) (*coalescedPacket, error) {
close(packedFirstPacket)
return &coalescedPacket{buffer: getPacketBuffer(), longHdrPackets: []*longHeaderPacket{{header: hdr}}}, nil
},
),
// initial keys are dropped when the first handshake packet is sent
cs.EXPECT().DiscardInitialKeys(),
// no more data to send
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(
&unpackedPacket{hdr: hdr, encryptionLevel: protocol.EncryptionHandshake, data: data}, nil,
),
cs.EXPECT().HandleMessage([]byte("foobar"), protocol.EncryptionHandshake),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventReceivedTransportParameters, TransportParameters: tp}),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventHandshakeComplete}),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent}),
)
tc.packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), protocol.Version1).Return(nil, nil).AnyTimes()
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
select {
case <-packedFirstPacket:
case <-time.After(time.Second):
t.Fatal("timeout")
}
p := getLongHeaderPacket(t, tc.remoteAddr, hdr, nil)
tc.conn.handlePacket(receivedPacket{data: p.data, buffer: p.buffer, rcvTime: monotime.Now()})
select {
case <-tc.conn.HandshakeComplete():
case <-tc.conn.Context().Done():
t.Fatal("connection context done")
case <-time.After(time.Second):
t.Fatal("timeout")
}
require.True(t, mockCtrl.Satisfied())
// the handshake isn't confirmed until we receive a HANDSHAKE_DONE frame from the server
data, err = (&wire.HandshakeDoneFrame{}).Append(nil, protocol.Version1)
require.NoError(t, err)
done := make(chan struct{})
tc.packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), protocol.Version1).Return(nil, nil).AnyTimes()
gomock.InOrder(
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(
&unpackedPacket{hdr: hdr, encryptionLevel: protocol.Encryption1RTT, data: data}, nil,
),
cs.EXPECT().DiscardInitialKeys(),
cs.EXPECT().SetHandshakeConfirmed(),
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buf *packetBuffer, _ protocol.ByteCount, _ monotime.Time, _ protocol.Version) (shortHeaderPacket, error) {
close(done)
return shortHeaderPacket{}, errNothingToPack
},
),
)
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack).AnyTimes()
p = getLongHeaderPacket(t, tc.remoteAddr, hdr, nil)
tc.conn.handlePacket(receivedPacket{data: p.data, buffer: p.buffer, rcvTime: monotime.Now()})
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
if usePreferredAddress {
tc.connRunner.EXPECT().AddResetToken(preferredAddressResetToken, gomock.Any())
}
nextConnID := tc.conn.connIDManager.Get()
if usePreferredAddress {
require.Equal(t, preferredAddressConnID, nextConnID)
}
// test teardown
cs.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
if usePreferredAddress {
tc.connRunner.EXPECT().RemoveResetToken(preferredAddressResetToken)
}
tc.conn.destroy(nil)
select {
case err := <-errChan:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnection0RTTTransportParameters(t *testing.T) {
mockCtrl := gomock.NewController(t)
cs := mocks.NewMockCryptoSetup(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tc := newClientTestConnection(t, mockCtrl, nil, false, connectionOptCryptoSetup(cs), connectionOptUnpacker(unpacker))
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// the state transition is driven by processing of a CRYPTO frame
hdr := &wire.ExtendedHeader{
Header: wire.Header{Type: protocol.PacketTypeHandshake, Version: protocol.Version1},
PacketNumberLen: protocol.PacketNumberLen2,
}
data, err := (&wire.CryptoFrame{Data: []byte("foobar")}).Append(nil, protocol.Version1)
require.NoError(t, err)
restored := &wire.TransportParameters{
ActiveConnectionIDLimit: 3,
InitialMaxData: 0x5000,
InitialMaxStreamDataBidiLocal: 0x5000,
InitialMaxStreamDataBidiRemote: 1000,
InitialMaxStreamDataUni: 1000,
MaxBidiStreamNum: 500,
MaxUniStreamNum: 500,
}
new := *restored
new.MaxBidiStreamNum-- // the server is not allowed to reduce the limit
new.OriginalDestinationConnectionID = tc.destConnID
packedFirstPacket := make(chan struct{})
gomock.InOrder(
cs.EXPECT().StartHandshake(gomock.Any()),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventRestoredTransportParameters, TransportParameters: restored}),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent}),
tc.packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), protocol.Version1).DoAndReturn(
func(b bool, bc protocol.ByteCount, t monotime.Time, v protocol.Version) (*coalescedPacket, error) {
close(packedFirstPacket)
return &coalescedPacket{buffer: getPacketBuffer(), longHdrPackets: []*longHeaderPacket{{header: hdr}}}, nil
},
),
// initial keys are dropped when the first handshake packet is sent
cs.EXPECT().DiscardInitialKeys(),
// no more data to send
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(
&unpackedPacket{hdr: hdr, encryptionLevel: protocol.EncryptionHandshake, data: data}, nil,
),
cs.EXPECT().HandleMessage([]byte("foobar"), protocol.EncryptionHandshake),
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventReceivedTransportParameters, TransportParameters: &new}),
cs.EXPECT().ConnectionState().Return(handshake.ConnectionState{Used0RTT: true}),
// cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent}),
cs.EXPECT().Close(),
)
tc.packer.EXPECT().PackCoalescedPacket(false, gomock.Any(), gomock.Any(), protocol.Version1).Return(nil, nil).AnyTimes()
tc.packer.EXPECT().PackConnectionClose(gomock.Any(), gomock.Any(), protocol.Version1).Return(&coalescedPacket{buffer: getPacketBuffer()}, nil)
tc.connRunner.EXPECT().ReplaceWithClosed(gomock.Any(), gomock.Any(), gomock.Any())
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
select {
case <-packedFirstPacket:
case <-time.After(time.Second):
t.Fatal("timeout")
}
p := getLongHeaderPacket(t, tc.remoteAddr, hdr, nil)
tc.conn.handlePacket(receivedPacket{data: p.data, buffer: p.buffer, rcvTime: monotime.Now()})
select {
case err := <-errChan:
require.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.ProtocolViolation})
require.ErrorContains(t, err, "server sent reduced limits after accepting 0-RTT data")
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnectionReceivePrioritization(t *testing.T) {
t.Run("handshake complete", func(t *testing.T) {
events := testConnectionReceivePrioritization(t, true, 5)
require.Equal(t, []string{"unpack", "unpack", "unpack", "unpack", "unpack", "pack"}, events)
})
// before handshake completion, we trigger packing of a new packet every time we receive a packet
t.Run("handshake not complete", func(t *testing.T) {
events := testConnectionReceivePrioritization(t, false, 5)
require.Equal(t, []string{
"unpack", "pack",
"unpack", "pack",
"unpack", "pack",
"unpack", "pack",
"unpack", "pack",
}, events)
})
}
func testConnectionReceivePrioritization(t *testing.T, handshakeComplete bool, numPackets int) []string {
mockCtrl := gomock.NewController(t)
unpacker := NewMockUnpacker(mockCtrl)
opts := []testConnectionOpt{connectionOptUnpacker(unpacker)}
if handshakeComplete {
opts = append(opts, connectionOptHandshakeConfirmed())
}
tc := newServerTestConnection(t, mockCtrl, nil, false, opts...)
var events []string
var counter int
var testDone bool
done := make(chan struct{})
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).DoAndReturn(
func(rcvTime monotime.Time, data []byte) (protocol.PacketNumber, protocol.PacketNumberLen, protocol.KeyPhaseBit, []byte, error) {
counter++
if counter == numPackets {
testDone = true
}
events = append(events, "unpack")
return protocol.PacketNumber(counter), protocol.PacketNumberLen2, protocol.KeyPhaseZero, []byte{0, 1} /* PADDING, PING */, nil
},
).Times(numPackets)
switch handshakeComplete {
case false:
tc.packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(b bool, bc protocol.ByteCount, t monotime.Time, v protocol.Version) (*coalescedPacket, error) {
events = append(events, "pack")
if testDone {
close(done)
}
return nil, nil
},
).AnyTimes()
case true:
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(b *packetBuffer, bc protocol.ByteCount, t monotime.Time, v protocol.Version) (shortHeaderPacket, error) {
events = append(events, "pack")
if testDone {
close(done)
}
return shortHeaderPacket{}, errNothingToPack
},
).AnyTimes()
}
for i := range numPackets {
tc.conn.handlePacket(getShortHeaderPacket(t, tc.remoteAddr, tc.srcConnID, protocol.PacketNumber(i), []byte("foobar")))
}
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
select {
case err := <-errChan:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("timeout")
}
return events
}
func TestConnectionPacketBuffering(t *testing.T) {
mockCtrl := gomock.NewController(t)
unpacker := NewMockUnpacker(mockCtrl)
cs := mocks.NewMockCryptoSetup(mockCtrl)
tracer, tr := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptUnpacker(unpacker),
connectionOptCryptoSetup(cs),
connectionOptTracer(tracer),
)
tr.EXPECT().NegotiatedVersion(gomock.Any(), gomock.Any(), gomock.Any())
tr.EXPECT().StartedConnection(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
tr.EXPECT().DroppedEncryptionLevel(gomock.Any())
cs.EXPECT().DiscardInitialKeys()
hdr1 := wire.ExtendedHeader{
Header: wire.Header{
Type: protocol.PacketTypeHandshake,
DestConnectionID: tc.srcConnID,
SrcConnectionID: tc.destConnID,
Length: 8,
Version: protocol.Version1,
},
PacketNumberLen: protocol.PacketNumberLen1,
PacketNumber: 1,
}
hdr2 := hdr1
hdr2.PacketNumber = 2
cs.EXPECT().StartHandshake(gomock.Any())
buffered := make(chan struct{})
gomock.InOrder(
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent}),
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(nil, handshake.ErrKeysNotYetAvailable),
tr.EXPECT().BufferedPacket(logging.PacketTypeHandshake, gomock.Any()),
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(nil, handshake.ErrKeysNotYetAvailable),
tr.EXPECT().BufferedPacket(logging.PacketTypeHandshake, gomock.Any()).Do(
func(logging.PacketType, logging.ByteCount) { close(buffered) },
),
)
tc.conn.handlePacket(getLongHeaderPacket(t, tc.remoteAddr, &hdr1, []byte("packet1")))
tc.conn.handlePacket(getLongHeaderPacket(t, tc.remoteAddr, &hdr2, []byte("packet2")))
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
select {
case <-buffered:
case <-time.After(time.Second):
t.Fatal("timeout")
}
// Now send another packet.
// In reality, this packet would contain a CRYPTO frame that advances the TLS handshake
// such that new keys become available.
var packets []string
hdr3 := hdr1
hdr3.PacketNumber = 3
tc.packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
unpacked := make(chan struct{})
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventReceivedReadKeys})
cs.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent})
gomock.InOrder(
// packet 3 contains a CRYPTO frame and triggers the keys to become available
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).DoAndReturn(
func(hdr *wire.Header, data []byte) (*unpackedPacket, error) {
packets = append(packets, string(data[len(data)-7:]))
cf := &wire.CryptoFrame{Data: []byte("foobar")}
b, _ := cf.Append(nil, protocol.Version1)
return &unpackedPacket{hdr: &hdr3, encryptionLevel: protocol.EncryptionHandshake, data: b}, nil
},
),
cs.EXPECT().HandleMessage(gomock.Any(), gomock.Any()),
tr.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
// packet 1 dequeued from the buffer
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).DoAndReturn(
func(hdr *wire.Header, data []byte) (*unpackedPacket, error) {
packets = append(packets, string(data[len(data)-7:]))
return &unpackedPacket{hdr: &hdr1, encryptionLevel: protocol.EncryptionHandshake, data: []byte{0} /* PADDING */}, nil
},
),
tr.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
// packet 2 dequeued from the buffer
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).DoAndReturn(
func(hdr *wire.Header, data []byte) (*unpackedPacket, error) {
packets = append(packets, string(data[len(data)-7:]))
close(unpacked)
return &unpackedPacket{hdr: &hdr2, encryptionLevel: protocol.EncryptionHandshake, data: []byte{0} /* PADDING */}, nil
},
),
tr.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
)
tc.conn.handlePacket(getLongHeaderPacket(t, tc.remoteAddr, &hdr3, []byte("packet3")))
select {
case <-unpacked:
case <-time.After(time.Second):
t.Fatal("timeout")
}
// packet3 triggered the keys to become available
// packet1 and packet2 are processed from the buffer in order
require.Equal(t, []string{"packet3", "packet1", "packet2"}, packets)
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
cs.EXPECT().Close()
tr.EXPECT().ClosedConnection(gomock.Any())
tr.EXPECT().Close()
tc.conn.destroy(nil)
select {
case err := <-errChan:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnectionPacketPacing(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
sender := NewMockSender(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptSentPacketHandler(sph),
connectionOptSender(sender),
connectionOptHandshakeConfirmed(),
)
sender.EXPECT().Run()
const step = 50 * time.Millisecond
sph.EXPECT().GetLossDetectionTimeout().Return(monotime.Now().Add(time.Hour)).AnyTimes()
gomock.InOrder(
// 1. allow 2 packets to be sent
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny),
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny),
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendPacingLimited),
// 2. become pacing limited for 25ms
sph.EXPECT().TimeUntilSend().DoAndReturn(func() monotime.Time { return monotime.Now().Add(step) }),
// 3. send another packet
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny),
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendPacingLimited),
// 4. become pacing limited for 25ms...
sph.EXPECT().TimeUntilSend().DoAndReturn(func() monotime.Time { return monotime.Now().Add(step) }),
// ... but this time we're still pacing limited when waking up.
// In this case, we can only send an ACK.
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendPacingLimited),
// 5. stop the test by becoming pacing limited forever
sph.EXPECT().TimeUntilSend().Return(monotime.Now().Add(time.Hour)),
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()),
)
sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
for i := range 3 {
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), Version1).DoAndReturn(
func(buf *packetBuffer, _ protocol.ByteCount, _ monotime.Time, _ protocol.Version) (shortHeaderPacket, error) {
buf.Data = append(buf.Data, []byte("packet"+strconv.Itoa(i+1))...)
return shortHeaderPacket{PacketNumber: protocol.PacketNumber(i + 1)}, nil
},
)
}
tc.packer.EXPECT().PackAckOnlyPacket(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(_ protocol.ByteCount, _ monotime.Time, _ protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
buf := getPacketBuffer()
buf.Data = []byte("ack")
return shortHeaderPacket{PacketNumber: 1}, buf, nil
},
)
sender.EXPECT().WouldBlock().AnyTimes()
type sentPacket struct {
time monotime.Time
data []byte
}
sendChan := make(chan sentPacket, 10)
sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(b *packetBuffer, _ uint16, _ protocol.ECN) {
sendChan <- sentPacket{time: monotime.Now(), data: b.Data}
}).Times(4)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
var times []monotime.Time
for i := range 3 {
select {
case b := <-sendChan:
require.Equal(t, []byte("packet"+strconv.Itoa(i+1)), b.data)
times = append(times, b.time)
case <-time.After(time.Hour):
t.Fatal("should have sent a packet")
}
}
select {
case b := <-sendChan:
require.Equal(t, []byte("ack"), b.data)
times = append(times, b.time)
case <-time.After(time.Second):
t.Fatal("timeout")
}
require.Equal(t, times[0], times[1])
require.Equal(t, times[2], times[1].Add(step))
require.Equal(t, times[3], times[2].Add(step))
synctest.Wait() // make sure that no more packets are sent
require.True(t, mockCtrl.Satisfied())
// test teardown
sender.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case <-sendChan:
t.Fatal("should not have sent any more packets")
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have timed out")
}
})
}
// When the send queue blocks, we need to reset the pacing timer, otherwise the run loop might busy-loop.
// See https://github.com/quic-go/quic-go/pull/4943 for more details.
func TestConnectionPacingAndSendQueue(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
sender := NewMockSender(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptSentPacketHandler(sph),
connectionOptSender(sender),
connectionOptHandshakeConfirmed(),
)
sender.EXPECT().Run()
sendQueueAvailable := make(chan struct{})
pacingDeadline := monotime.Now().Add(-time.Millisecond)
var counter int
// allow exactly one packet to be sent, then become blocked
sender.EXPECT().WouldBlock().Return(false)
sender.EXPECT().WouldBlock().DoAndReturn(func() bool { counter++; return true }).AnyTimes()
sender.EXPECT().Available().Return(sendQueueAvailable).AnyTimes()
sph.EXPECT().GetLossDetectionTimeout().Return(monotime.Now().Add(time.Hour)).AnyTimes()
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendPacingLimited).AnyTimes()
sph.EXPECT().TimeUntilSend().Return(pacingDeadline).AnyTimes()
sph.EXPECT().ECNMode(gomock.Any()).Return(protocol.ECNNon).AnyTimes()
tc.packer.EXPECT().PackAckOnlyPacket(gomock.Any(), gomock.Any(), gomock.Any()).Return(
shortHeaderPacket{}, nil, errNothingToPack,
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
sender.EXPECT().Close()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have timed out")
}
// make sure the run loop didn't do too many iterations
require.Less(t, counter, 3)
})
}
func TestConnectionIdleTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
&Config{MaxIdleTimeout: time.Minute},
false,
connectionOptHandshakeConfirmed(),
connectionOptSentPacketHandler(sph),
connectionOptRTT(time.Millisecond),
)
// the idle timeout is set when the transport parameters are received
const idleTimeout = 500 * time.Millisecond
require.NoError(t, tc.conn.handleTransportParameters(&wire.TransportParameters{
MaxIdleTimeout: idleTimeout,
}))
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
var lastSendTime monotime.Time
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buf *packetBuffer, _ protocol.ByteCount, _ monotime.Time, _ protocol.Version) (shortHeaderPacket, error) {
buf.Data = append(buf.Data, []byte("foobar")...)
lastSendTime = monotime.Now()
return shortHeaderPacket{Frames: []ackhandler.Frame{{Frame: &wire.PingFrame{}}}, Length: 6}, nil
},
)
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack)
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any())
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
select {
case err := <-errChan:
require.ErrorIs(t, err, &IdleTimeoutError{})
require.NotZero(t, lastSendTime)
require.Equal(t, idleTimeout, monotime.Since(lastSendTime))
case <-time.After(time.Hour):
t.Fatal("should have timed out")
}
})
}
func TestConnectionKeepAlive(t *testing.T) {
t.Run("enabled", func(t *testing.T) {
testConnectionKeepAlive(t, true, true)
})
t.Run("disabled", func(t *testing.T) {
testConnectionKeepAlive(t, false, false)
})
}
func testConnectionKeepAlive(t *testing.T, enable, expectKeepAlive bool) {
synctest.Test(t, func(t *testing.T) {
var keepAlivePeriod time.Duration
if enable {
keepAlivePeriod = time.Second
}
mockCtrl := gomock.NewController(t)
unpacker := NewMockUnpacker(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
&Config{MaxIdleTimeout: time.Second, KeepAlivePeriod: keepAlivePeriod},
false,
connectionOptUnpacker(unpacker),
connectionOptHandshakeConfirmed(),
connectionOptRTT(time.Millisecond),
)
// the idle timeout is set when the transport parameters are received
const idleTimeout = 50 * time.Millisecond
require.NoError(t, tc.conn.handleTransportParameters(&wire.TransportParameters{
MaxIdleTimeout: idleTimeout,
}))
// Receive a packet. This starts the keep-alive timer.
buf := getPacketBuffer()
var err error
buf.Data, err = wire.AppendShortHeader(buf.Data, tc.srcConnID, 1, protocol.PacketNumberLen1, protocol.KeyPhaseZero)
require.NoError(t, err)
buf.Data = append(buf.Data, []byte("packet")...)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
var unpackTime, packTime monotime.Time
done := make(chan struct{})
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).DoAndReturn(
func(t monotime.Time, bytes []byte) (protocol.PacketNumber, protocol.PacketNumberLen, protocol.KeyPhaseBit, []byte, error) {
unpackTime = monotime.Now()
return protocol.PacketNumber(1), protocol.PacketNumberLen1, protocol.KeyPhaseZero, []byte{0} /* PADDING */, nil
},
)
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack)
switch expectKeepAlive {
case true:
// record the time of the keep-alive is sent
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buffer *packetBuffer, count protocol.ByteCount, t monotime.Time, version protocol.Version) (shortHeaderPacket, error) {
packTime = monotime.Now()
close(done)
return shortHeaderPacket{}, errNothingToPack
},
)
tc.conn.handlePacket(receivedPacket{data: buf.Data, buffer: buf, rcvTime: monotime.Now(), remoteAddr: tc.remoteAddr})
select {
case <-done:
// the keep-alive packet should be sent after half the idle timeout
require.Equal(t, unpackTime.Add(idleTimeout/2), packTime)
case <-time.After(idleTimeout):
t.Fatal("timeout")
}
case false: // if keep-alives are disabled, the connection will run into an idle timeout
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.handlePacket(receivedPacket{data: buf.Data, buffer: buf, rcvTime: monotime.Now(), remoteAddr: tc.remoteAddr})
}
// test teardown
if expectKeepAlive {
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
}
synctest.Wait()
select {
case err := <-errChan:
if expectKeepAlive {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, &IdleTimeoutError{})
}
case <-time.After(time.Hour):
t.Fatal("timeout")
}
})
}
func TestConnectionACKTimer(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
rph := mockackhandler.NewMockReceivedPacketHandler(mockCtrl)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
&Config{MaxIdleTimeout: time.Second},
false,
connectionOptHandshakeConfirmed(),
connectionOptReceivedPacketHandler(rph),
connectionOptSentPacketHandler(sph),
)
const alarmTimeout = 500 * time.Millisecond
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
rph.EXPECT().GetAlarmTimeout().Return(monotime.Now().Add(time.Hour))
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
var times []monotime.Time
done := make(chan struct{}, 5)
var calls []any
for i := range 2 {
calls = append(calls, tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buf *packetBuffer, _ protocol.ByteCount, _ monotime.Time, _ protocol.Version) (shortHeaderPacket, error) {
buf.Data = append(buf.Data, []byte("foobar")...)
times = append(times, monotime.Now())
return shortHeaderPacket{Frames: []ackhandler.Frame{{Frame: &wire.PingFrame{}}}, Length: 6}, nil
},
))
calls = append(calls, tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(*packetBuffer, protocol.ByteCount, monotime.Time, protocol.Version) (shortHeaderPacket, error) {
done <- struct{}{}
return shortHeaderPacket{}, errNothingToPack
},
))
if i == 0 {
calls = append(calls, rph.EXPECT().GetAlarmTimeout().Return(monotime.Now().Add(alarmTimeout)))
} else {
calls = append(calls, rph.EXPECT().GetAlarmTimeout().Return(monotime.Now().Add(time.Hour)))
}
}
gomock.InOrder(calls...)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
for range 2 {
synctest.Wait()
select {
case <-done:
case <-time.After(time.Hour):
t.Fatal("timeout")
}
}
assert.Len(t, times, 2)
require.Equal(t, times[0].Add(alarmTimeout), times[1])
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have timed out")
}
})
}
// Send a GSO batch, until we have no more data to send.
func TestConnectionGSOBatch(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
true,
connectionOptHandshakeConfirmed(),
connectionOptSentPacketHandler(sph),
)
// allow packets to be sent
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
sph.EXPECT().TimeUntilSend().AnyTimes()
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().ECNMode(gomock.Any()).Return(protocol.ECT1).AnyTimes()
maxPacketSize := tc.conn.maxPacketSize()
var expectedData []byte
for i := range 4 {
data := bytes.Repeat([]byte{byte(i)}, int(maxPacketSize))
expectedData = append(expectedData, data...)
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buffer *packetBuffer, count protocol.ByteCount, t monotime.Time, version protocol.Version) (shortHeaderPacket, error) {
buffer.Data = append(buffer.Data, data...)
return shortHeaderPacket{PacketNumber: protocol.PacketNumber(i)}, nil
},
)
}
done := make(chan struct{})
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack)
tc.sendConn.EXPECT().Write(expectedData, uint16(maxPacketSize), protocol.ECT1).DoAndReturn(
func([]byte, uint16, protocol.ECN) error { close(done); return nil },
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
select {
case <-done:
default:
t.Fatal("should have sent a packet")
}
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have timed out")
}
})
}
// Send a GSO batch, until a packet smaller than the maximum size is packed
func TestConnectionGSOBatchPacketSize(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
true,
connectionOptHandshakeConfirmed(),
connectionOptSentPacketHandler(sph),
)
// allow packets to be sent
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
sph.EXPECT().TimeUntilSend().AnyTimes()
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().ECNMode(gomock.Any()).Return(protocol.ECT1).AnyTimes()
maxPacketSize := tc.conn.maxPacketSize()
var expectedData []byte
var calls []any
for i := range 4 {
var data []byte
if i == 3 {
data = bytes.Repeat([]byte{byte(i)}, int(maxPacketSize-1))
} else {
data = bytes.Repeat([]byte{byte(i)}, int(maxPacketSize))
}
expectedData = append(expectedData, data...)
calls = append(calls, tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buffer *packetBuffer, count protocol.ByteCount, t monotime.Time, version protocol.Version) (shortHeaderPacket, error) {
buffer.Data = append(buffer.Data, data...)
return shortHeaderPacket{PacketNumber: protocol.PacketNumber(10 + i)}, nil
},
))
}
// The smaller (fourth) packet concluded this GSO batch, but the send loop will immediately start composing the next batch.
// We therefore send a "foobar", so we can check that we're actually generating two GSO batches.
calls = append(calls,
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buffer *packetBuffer, count protocol.ByteCount, t monotime.Time, version protocol.Version) (shortHeaderPacket, error) {
buffer.Data = append(buffer.Data, []byte("foobar")...)
return shortHeaderPacket{PacketNumber: protocol.PacketNumber(14)}, nil
},
),
)
calls = append(calls,
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack),
)
gomock.InOrder(calls...)
done := make(chan struct{})
gomock.InOrder(
tc.sendConn.EXPECT().Write(expectedData, uint16(maxPacketSize), protocol.ECT1),
tc.sendConn.EXPECT().Write([]byte("foobar"), uint16(maxPacketSize), protocol.ECT1).DoAndReturn(
func([]byte, uint16, protocol.ECN) error { close(done); return nil },
),
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
select {
case <-done:
default:
t.Fatal("should have sent a packet")
}
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have timed out")
}
})
}
func TestConnectionGSOBatchECN(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
true,
connectionOptHandshakeConfirmed(),
connectionOptSentPacketHandler(sph),
)
// allow packets to be sent
ecnMode := protocol.ECT1
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
sph.EXPECT().TimeUntilSend().AnyTimes()
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().ECNMode(gomock.Any()).DoAndReturn(func(bool) protocol.ECN { return ecnMode }).AnyTimes()
// 3. Send a GSO batch, until the ECN marking changes.
var expectedData []byte
var calls []any
maxPacketSize := tc.conn.maxPacketSize()
for i := range 3 {
data := bytes.Repeat([]byte{byte(i)}, int(maxPacketSize))
expectedData = append(expectedData, data...)
calls = append(calls, tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buffer *packetBuffer, count protocol.ByteCount, t monotime.Time, version protocol.Version) (shortHeaderPacket, error) {
buffer.Data = append(buffer.Data, data...)
if i == 2 {
ecnMode = protocol.ECNCE
}
return shortHeaderPacket{PacketNumber: protocol.PacketNumber(20 + i)}, nil
},
))
}
// The smaller (fourth) packet concluded this GSO batch, but the send loop will immediately start composing the next batch.
// We therefore send a "foobar", so we can check that we're actually generating two GSO batches.
calls = append(calls,
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buffer *packetBuffer, count protocol.ByteCount, t monotime.Time, version protocol.Version) (shortHeaderPacket, error) {
buffer.Data = append(buffer.Data, []byte("foobar")...)
return shortHeaderPacket{PacketNumber: protocol.PacketNumber(24)}, nil
},
),
)
calls = append(calls,
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(shortHeaderPacket{}, errNothingToPack),
)
gomock.InOrder(calls...)
done3 := make(chan struct{})
tc.sendConn.EXPECT().Write(expectedData, uint16(maxPacketSize), protocol.ECT1)
tc.sendConn.EXPECT().Write([]byte("foobar"), uint16(maxPacketSize), protocol.ECNCE).DoAndReturn(
func([]byte, uint16, protocol.ECN) error { close(done3); return nil },
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
select {
case <-done3:
default:
t.Fatal("should have sent a packet")
}
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have timed out")
}
})
}
func TestConnectionPTOProbePackets(t *testing.T) {
t.Run("Initial", func(t *testing.T) {
testConnectionPTOProbePackets(t, protocol.EncryptionInitial)
})
t.Run("Handshake", func(t *testing.T) {
testConnectionPTOProbePackets(t, protocol.EncryptionHandshake)
})
t.Run("1-RTT", func(t *testing.T) {
testConnectionPTOProbePackets(t, protocol.Encryption1RTT)
})
}
func testConnectionPTOProbePackets(t *testing.T, encLevel protocol.EncryptionLevel) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptSentPacketHandler(sph),
)
var sendMode ackhandler.SendMode
switch encLevel {
case protocol.EncryptionInitial:
sendMode = ackhandler.SendPTOInitial
case protocol.EncryptionHandshake:
sendMode = ackhandler.SendPTOHandshake
case protocol.Encryption1RTT:
sendMode = ackhandler.SendPTOAppData
}
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().TimeUntilSend().AnyTimes()
sph.EXPECT().SendMode(gomock.Any()).Return(sendMode)
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendNone)
sph.EXPECT().ECNMode(gomock.Any())
sph.EXPECT().QueueProbePacket(encLevel).Return(false)
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
tc.packer.EXPECT().PackPTOProbePacket(encLevel, gomock.Any(), true, gomock.Any(), protocol.Version1).DoAndReturn(
func(protocol.EncryptionLevel, protocol.ByteCount, bool, monotime.Time, protocol.Version) (*coalescedPacket, error) {
return &coalescedPacket{
buffer: getPacketBuffer(),
shortHdrPacket: &shortHeaderPacket{PacketNumber: 1},
}, nil
},
)
done := make(chan struct{})
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any()).Do(
func([]byte, uint16, protocol.ECN) error { close(done); return nil },
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout")
}
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have timed out")
}
})
}
func TestConnectionCongestionControl(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
false,
connectionOptHandshakeConfirmed(),
connectionOptSentPacketHandler(sph),
)
sph.EXPECT().TimeUntilSend().AnyTimes()
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().ECNMode(true).AnyTimes()
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).Times(2)
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAck).MaxTimes(1)
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(2)
// Since we're already sending out packets, we don't expect any calls to PackAckOnlyPacket
for i := range 2 {
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(buffer *packetBuffer, count protocol.ByteCount, t monotime.Time, version protocol.Version) (shortHeaderPacket, error) {
buffer.Data = append(buffer.Data, []byte("foobar")...)
return shortHeaderPacket{PacketNumber: protocol.PacketNumber(i)}, nil
},
)
}
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any())
done1 := make(chan struct{})
tc.sendConn.EXPECT().Write(gomock.Any(), gomock.Any(), gomock.Any()).Do(
func([]byte, uint16, protocol.ECN) error { close(done1); return nil },
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
select {
case <-done1:
default:
t.Fatal("should have sent a packet")
}
require.True(t, mockCtrl.Satisfied())
// Now that we're congestion limited, we can only send an ack-only packet
done2 := make(chan struct{})
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAck)
tc.packer.EXPECT().PackAckOnlyPacket(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(protocol.ByteCount, monotime.Time, protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
close(done2)
return shortHeaderPacket{}, nil, errNothingToPack
},
)
tc.conn.scheduleSending()
synctest.Wait()
select {
case <-done2:
default:
t.Fatal("should have sent an ack-only packet")
}
require.True(t, mockCtrl.Satisfied())
// If the send mode is "none", we can't even send an ack-only packet
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendNone)
tc.conn.scheduleSending()
synctest.Wait() // make sure there are no calls to the packer
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("timeout")
}
})
}
func TestConnectionSendQueue(t *testing.T) {
t.Run("with GSO", func(t *testing.T) {
testConnectionSendQueue(t, true)
})
t.Run("without GSO", func(t *testing.T) {
testConnectionSendQueue(t, false)
})
}
func testConnectionSendQueue(t *testing.T, enableGSO bool) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
sph := mockackhandler.NewMockSentPacketHandler(mockCtrl)
sender := NewMockSender(mockCtrl)
tc := newServerTestConnection(t,
mockCtrl,
nil,
enableGSO,
connectionOptSender(sender),
connectionOptHandshakeConfirmed(),
connectionOptSentPacketHandler(sph),
)
sender.EXPECT().Run().MaxTimes(1)
sender.EXPECT().WouldBlock()
sender.EXPECT().WouldBlock().Return(true).Times(2)
available := make(chan struct{})
blocked := make(chan struct{})
sender.EXPECT().Available().DoAndReturn(
func() <-chan struct{} {
close(blocked)
return available
},
)
sph.EXPECT().GetLossDetectionTimeout().AnyTimes()
sph.EXPECT().SentPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
sph.EXPECT().SendMode(gomock.Any()).Return(ackhandler.SendAny).AnyTimes()
sph.EXPECT().ECNMode(gomock.Any()).AnyTimes()
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
shortHeaderPacket{PacketNumber: protocol.PacketNumber(1)}, nil,
)
sender.EXPECT().Send(gomock.Any(), gomock.Any(), gomock.Any())
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.scheduleSending()
synctest.Wait()
select {
case <-blocked:
default:
t.Fatal("should have blocked")
}
require.True(t, mockCtrl.Satisfied())
// now make room in the send queue
sender.EXPECT().WouldBlock().AnyTimes()
unblocked := make(chan struct{})
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(*packetBuffer, protocol.ByteCount, monotime.Time, protocol.Version) (shortHeaderPacket, error) {
close(unblocked)
return shortHeaderPacket{}, errNothingToPack
},
)
available <- struct{}{}
synctest.Wait()
select {
case <-unblocked:
default:
t.Fatal("should have unblocked")
}
// test teardown
sender.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("timeout")
}
})
}
func getVersionNegotiationPacket(src, dest protocol.ConnectionID, versions []protocol.Version) receivedPacket {
b := wire.ComposeVersionNegotiation(
protocol.ArbitraryLenConnectionID(src.Bytes()),
protocol.ArbitraryLenConnectionID(dest.Bytes()),
versions,
)
return receivedPacket{
rcvTime: monotime.Now(),
data: b,
buffer: getPacketBuffer(),
}
}
func TestConnectionVersionNegotiation(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newClientTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
)
tc.packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
var tracerVersions []logging.Version
gomock.InOrder(
tracer.EXPECT().ReceivedVersionNegotiationPacket(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_, _ protocol.ArbitraryLenConnectionID, versions []logging.Version) {
tracerVersions = versions
}),
tracer.EXPECT().NegotiatedVersion(protocol.Version2, gomock.Any(), gomock.Any()),
tc.connRunner.EXPECT().Remove(gomock.Any()),
)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.handlePacket(getVersionNegotiationPacket(
tc.destConnID,
tc.srcConnID,
[]protocol.Version{1234, protocol.Version2},
))
synctest.Wait()
select {
case err := <-errChan:
var rerr *errCloseForRecreating
require.ErrorAs(t, err, &rerr)
require.Equal(t, rerr.nextVersion, protocol.Version2)
default:
t.Fatal("should have received a Version Negotiation packet")
}
require.Contains(t, tracerVersions, protocol.Version(1234))
require.Contains(t, tracerVersions, protocol.Version2)
})
}
func TestConnectionVersionNegotiationNoMatch(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newClientTestConnection(t,
mockCtrl,
&Config{Versions: []protocol.Version{protocol.Version1}},
false,
connectionOptTracer(tr),
)
tc.packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
var tracerVersions []logging.Version
tracer.EXPECT().ReceivedVersionNegotiationPacket(gomock.Any(), gomock.Any(), gomock.Any()).Do(
func(_, _ protocol.ArbitraryLenConnectionID, versions []logging.Version) { tracerVersions = versions },
)
tracer.EXPECT().ClosedConnection(gomock.Any())
tracer.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any())
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
tc.conn.handlePacket(getVersionNegotiationPacket(
tc.destConnID,
tc.srcConnID,
[]protocol.Version{protocol.Version2},
))
synctest.Wait()
select {
case err := <-errChan:
var verr *VersionNegotiationError
require.ErrorAs(t, err, &verr)
require.Contains(t, verr.Theirs, protocol.Version2)
default:
t.Fatal("should have received a Version Negotiation packet")
}
require.Contains(t, tracerVersions, protocol.Version2)
})
}
func TestConnectionVersionNegotiationInvalidPackets(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
tc := newClientTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
)
// offers the current version
tracer.EXPECT().DroppedPacket(logging.PacketTypeVersionNegotiation, gomock.Any(), gomock.Any(), logging.PacketDropUnexpectedVersion)
vnp := getVersionNegotiationPacket(
tc.destConnID,
tc.srcConnID,
[]protocol.Version{1234, protocol.Version1},
)
wasProcessed, err := tc.conn.handleOnePacket(vnp)
require.NoError(t, err)
require.False(t, wasProcessed)
require.True(t, mockCtrl.Satisfied())
// unparseable, since it's missing 2 bytes
tracer.EXPECT().DroppedPacket(logging.PacketTypeVersionNegotiation, gomock.Any(), gomock.Any(), logging.PacketDropHeaderParseError)
vnp.data = vnp.data[:len(vnp.data)-2]
wasProcessed, err = tc.conn.handleOnePacket(vnp)
require.NoError(t, err)
require.False(t, wasProcessed)
}
func getRetryPacket(t *testing.T, src, dest, origDest protocol.ConnectionID, token []byte) receivedPacket {
hdr := wire.Header{
Type: protocol.PacketTypeRetry,
SrcConnectionID: src,
DestConnectionID: dest,
Token: token,
Version: protocol.Version1,
}
b, err := (&wire.ExtendedHeader{Header: hdr}).Append(nil, protocol.Version1)
require.NoError(t, err)
tag := handshake.GetRetryIntegrityTag(b, origDest, protocol.Version1)
b = append(b, tag[:]...)
return receivedPacket{
rcvTime: monotime.Now(),
data: b,
buffer: getPacketBuffer(),
}
}
func TestConnectionRetryDrops(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tc := newClientTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
connectionOptUnpacker(unpacker),
)
newConnID := protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef})
// invalid integrity tag
tracer.EXPECT().DroppedPacket(logging.PacketTypeRetry, gomock.Any(), gomock.Any(), logging.PacketDropPayloadDecryptError)
retry := getRetryPacket(t, newConnID, tc.srcConnID, tc.destConnID, []byte("foobar"))
retry.data[len(retry.data)-1]++
wasProcessed, err := tc.conn.handleOnePacket(retry)
require.NoError(t, err)
require.False(t, wasProcessed)
require.True(t, mockCtrl.Satisfied())
// receive a retry that doesn't change the connection ID
tracer.EXPECT().DroppedPacket(logging.PacketTypeRetry, gomock.Any(), gomock.Any(), logging.PacketDropUnexpectedPacket)
retry = getRetryPacket(t, tc.destConnID, tc.srcConnID, tc.destConnID, []byte("foobar"))
wasProcessed, err = tc.conn.handleOnePacket(retry)
require.NoError(t, err)
require.False(t, wasProcessed)
}
func TestConnectionRetryAfterReceivedPacket(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tc := newClientTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
connectionOptUnpacker(unpacker),
)
// receive a regular packet
tracer.EXPECT().NegotiatedVersion(gomock.Any(), gomock.Any(), gomock.Any())
tracer.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
regular := getPacketWithPacketType(t, tc.srcConnID, protocol.PacketTypeInitial, 200)
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(
&unpackedPacket{
hdr: &wire.ExtendedHeader{Header: wire.Header{Type: protocol.PacketTypeInitial}},
encryptionLevel: protocol.EncryptionInitial,
}, nil,
)
wasProcessed, err := tc.conn.handleOnePacket(receivedPacket{
data: regular,
buffer: getPacketBuffer(),
rcvTime: monotime.Now(),
remoteAddr: tc.remoteAddr,
})
require.NoError(t, err)
require.True(t, wasProcessed)
// receive a retry
retry := getRetryPacket(t, tc.destConnID, tc.srcConnID, tc.destConnID, []byte("foobar"))
tracer.EXPECT().DroppedPacket(logging.PacketTypeRetry, gomock.Any(), gomock.Any(), logging.PacketDropUnexpectedPacket)
wasProcessed, err = tc.conn.handleOnePacket(retry)
require.NoError(t, err)
require.False(t, wasProcessed)
}
func TestConnectionConnectionIDChanges(t *testing.T) {
t.Run("with retry", func(t *testing.T) {
testConnectionConnectionIDChanges(t, true)
})
t.Run("without retry", func(t *testing.T) {
testConnectionConnectionIDChanges(t, false)
})
}
func testConnectionConnectionIDChanges(t *testing.T, sendRetry bool) {
synctest.Test(t, func(t *testing.T) {
makeInitialPacket := func(t *testing.T, hdr *wire.ExtendedHeader) []byte {
t.Helper()
data, err := hdr.Append(nil, protocol.Version1)
require.NoError(t, err)
data = append(data, make([]byte, hdr.Length-protocol.ByteCount(hdr.PacketNumberLen))...)
return data
}
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
unpacker := NewMockUnpacker(mockCtrl)
tc := newClientTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
connectionOptUnpacker(unpacker),
)
dstConnID := tc.destConnID
b := make([]byte, 3*10)
rand.Read(b)
newConnID := protocol.ParseConnectionID(b[:11])
newConnID2 := protocol.ParseConnectionID(b[11:20])
tracer.EXPECT().NegotiatedVersion(gomock.Any(), gomock.Any(), gomock.Any())
tc.packer.EXPECT().PackCoalescedPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
require.Equal(t, dstConnID, tc.conn.connIDManager.Get())
var retryConnID protocol.ConnectionID
if sendRetry {
retryConnID = protocol.ParseConnectionID(b[20:30])
hdrChan := make(chan *wire.Header)
tracer.EXPECT().ReceivedRetry(gomock.Any()).Do(func(hdr *wire.Header) { hdrChan <- hdr })
tc.packer.EXPECT().SetToken([]byte("foobar"))
tc.conn.handlePacket(getRetryPacket(t, retryConnID, tc.srcConnID, tc.destConnID, []byte("foobar")))
synctest.Wait()
select {
case hdr := <-hdrChan:
assert.Equal(t, retryConnID, hdr.SrcConnectionID)
assert.Equal(t, []byte("foobar"), hdr.Token)
require.Equal(t, retryConnID, tc.conn.connIDManager.Get())
default:
t.Fatal("should have received the retry packet")
}
}
// Send the first packet. The server changes the connection ID to newConnID.
hdr1 := wire.ExtendedHeader{
Header: wire.Header{
SrcConnectionID: newConnID,
DestConnectionID: tc.srcConnID,
Type: protocol.PacketTypeInitial,
Length: 200,
Version: protocol.Version1,
},
PacketNumber: 1,
PacketNumberLen: protocol.PacketNumberLen2,
}
hdr2 := hdr1
hdr2.SrcConnectionID = newConnID2
receivedFirst := make(chan struct{})
gomock.InOrder(
unpacker.EXPECT().UnpackLongHeader(gomock.Any(), gomock.Any()).Return(
&unpackedPacket{
hdr: &hdr1,
encryptionLevel: protocol.EncryptionInitial,
}, nil,
),
tracer.EXPECT().ReceivedLongHeaderPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(
func(*wire.ExtendedHeader, protocol.ByteCount, protocol.ECN, []logging.Frame) { close(receivedFirst) },
),
)
tc.conn.handlePacket(receivedPacket{data: makeInitialPacket(t, &hdr1), buffer: getPacketBuffer(), rcvTime: monotime.Now(), remoteAddr: tc.remoteAddr})
synctest.Wait()
select {
case <-receivedFirst:
require.Equal(t, newConnID, tc.conn.connIDManager.Get())
default:
t.Fatal("should have received the first packet")
}
// Send the second packet. We refuse to accept it, because the connection ID is changed again.
dropped := make(chan struct{})
tracer.EXPECT().DroppedPacket(logging.PacketTypeInitial, gomock.Any(), gomock.Any(), logging.PacketDropUnknownConnectionID).Do(
func(logging.PacketType, protocol.PacketNumber, protocol.ByteCount, logging.PacketDropReason) {
close(dropped)
},
)
tc.conn.handlePacket(receivedPacket{data: makeInitialPacket(t, &hdr2), buffer: getPacketBuffer(), rcvTime: monotime.Now(), remoteAddr: tc.remoteAddr})
synctest.Wait()
select {
case <-dropped:
// the connection ID should not have changed
require.Equal(t, newConnID, tc.conn.connIDManager.Get())
default:
t.Fatal("should have dropped the packet")
}
// test teardown
tracer.EXPECT().ClosedConnection(gomock.Any())
tracer.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any())
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have shut down")
}
})
}
// When the connection is closed before sending the first packet,
// we don't send a CONNECTION_CLOSE.
// This can happen if there's something wrong the tls.Config, and
// crypto/tls refuses to start the handshake.
func TestConnectionEarlyClose(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
cryptoSetup := mocks.NewMockCryptoSetup(mockCtrl)
tc := newClientTestConnection(t,
mockCtrl,
nil,
false,
connectionOptTracer(tr),
connectionOptCryptoSetup(cryptoSetup),
)
tc.conn.sentFirstPacket = false
tracer.EXPECT().ClosedConnection(gomock.Any())
tracer.EXPECT().Close()
cryptoSetup.EXPECT().StartHandshake(gomock.Any()).Do(func(context.Context) error {
tc.conn.closeLocal(errors.New("early error"))
return nil
})
cryptoSetup.EXPECT().NextEvent().Return(handshake.Event{Kind: handshake.EventNoEvent})
cryptoSetup.EXPECT().Close()
tc.connRunner.EXPECT().Remove(gomock.Any())
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
synctest.Wait()
select {
case err := <-errChan:
require.Error(t, err)
require.ErrorContains(t, err, "early error")
default:
t.Fatal("should have shut down")
}
})
}
func TestConnectionPathValidation(t *testing.T) {
t.Run("NAT rebinding", func(t *testing.T) {
testConnectionPathValidation(t, true)
})
t.Run("intentional migration", func(t *testing.T) {
testConnectionPathValidation(t, false)
})
}
func testConnectionPathValidation(t *testing.T, isNATRebinding bool) {
synctest.Test(t, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
unpacker := NewMockUnpacker(mockCtrl)
tc := newServerTestConnection(
t,
mockCtrl,
nil,
false,
connectionOptUnpacker(unpacker),
connectionOptHandshakeConfirmed(),
connectionOptRTT(time.Second),
)
require.NoError(t, tc.conn.handleTransportParameters(&wire.TransportParameters{MaxUDPPayloadSize: 1456}))
newRemoteAddr := &net.UDPAddr{IP: net.IPv4(192, 168, 1, 1), Port: 1234}
require.NotEqual(t, tc.remoteAddr, newRemoteAddr)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
probeSent := make(chan struct{})
var pathChallenge *wire.PathChallengeFrame
payload := []byte{0} // PADDING frame
if isNATRebinding {
payload = []byte{1} // PING frame
}
gomock.InOrder(
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).Return(
protocol.PacketNumber(10), protocol.PacketNumberLen2, protocol.KeyPhaseZero, payload, nil,
),
tc.packer.EXPECT().PackPathProbePacket(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(_ protocol.ConnectionID, frames []ackhandler.Frame, _ protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
pathChallenge = frames[0].Frame.(*wire.PathChallengeFrame)
return shortHeaderPacket{IsPathProbePacket: true}, getPacketBuffer(), nil
},
),
tc.sendConn.EXPECT().WriteTo(gomock.Any(), newRemoteAddr).DoAndReturn(
func([]byte, net.Addr) error { close(probeSent); return nil },
),
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
shortHeaderPacket{}, errNothingToPack,
),
)
tc.conn.handlePacket(receivedPacket{
data: make([]byte, 10),
buffer: getPacketBuffer(),
remoteAddr: newRemoteAddr,
rcvTime: monotime.Now(),
})
synctest.Wait()
select {
case <-probeSent:
case <-time.After(time.Second):
t.Fatal("timeout")
}
// Receive a packed containing a PATH_RESPONSE frame.
// Only if the first packet received on the path was a probing packet
// (i.e. we're dealing with a NAT rebinding), this makes us switch to the new path.
migrated := make(chan struct{})
data, err := (&wire.PathResponseFrame{Data: pathChallenge.Data}).Append(nil, protocol.Version1)
require.NoError(t, err)
calls := []any{
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).Return(
protocol.PacketNumber(11), protocol.PacketNumberLen2, protocol.KeyPhaseZero, data, nil,
),
}
if isNATRebinding {
calls = append(calls,
tc.sendConn.EXPECT().ChangeRemoteAddr(newRemoteAddr, gomock.Any()).Do(
func(net.Addr, packetInfo) { close(migrated) },
),
)
}
calls = append(calls,
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
shortHeaderPacket{}, errNothingToPack,
).MaxTimes(1),
)
gomock.InOrder(calls...)
require.Equal(t, tc.remoteAddr, tc.conn.RemoteAddr())
// the PATH_RESPONSE can be sent on the old path, if the client is just probing the new path
addr := tc.remoteAddr
if isNATRebinding {
addr = newRemoteAddr
}
tc.conn.handlePacket(receivedPacket{
data: make([]byte, 100),
buffer: getPacketBuffer(),
remoteAddr: addr,
rcvTime: monotime.Now(),
})
synctest.Wait()
if !isNATRebinding {
// If the first packet was a probing packet, we only switch to the new path when we
// receive a non-probing packet on that path.
select {
case <-migrated:
t.Fatal("didn't expect a migration yet")
default:
}
payload := []byte{1} // PING frame
payload, err = (&wire.PathResponseFrame{Data: pathChallenge.Data}).Append(payload, protocol.Version1)
require.NoError(t, err)
gomock.InOrder(
unpacker.EXPECT().UnpackShortHeader(gomock.Any(), gomock.Any()).Return(
protocol.PacketNumber(12), protocol.PacketNumberLen2, protocol.KeyPhaseZero, payload, nil,
),
tc.sendConn.EXPECT().ChangeRemoteAddr(newRemoteAddr, gomock.Any()).Do(
func(net.Addr, packetInfo) { close(migrated) },
),
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
shortHeaderPacket{}, errNothingToPack,
).MaxTimes(1),
)
tc.conn.handlePacket(receivedPacket{
data: make([]byte, 100),
buffer: getPacketBuffer(),
remoteAddr: newRemoteAddr,
rcvTime: monotime.Now(),
})
}
synctest.Wait()
select {
case <-migrated:
default:
t.Fatal("should have migrated")
}
// test teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.conn.destroy(nil)
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("should have shut down")
}
})
}
func TestConnectionMigrationServer(t *testing.T) {
tc := newServerTestConnection(t, nil, nil, false)
_, err := tc.conn.AddPath(&Transport{})
require.Error(t, err)
require.ErrorContains(t, err, "server cannot initiate connection migration")
}
func TestConnectionMigration(t *testing.T) {
t.Run("disabled", func(t *testing.T) {
testConnectionMigration(t, false)
})
t.Run("enabled", func(t *testing.T) {
testConnectionMigration(t, true)
})
}
func testConnectionMigration(t *testing.T, enabled bool) {
tc := newClientTestConnection(t, nil, nil, false, connectionOptHandshakeConfirmed())
require.NoError(t, tc.conn.handleTransportParameters(&wire.TransportParameters{
InitialSourceConnectionID: tc.destConnID,
OriginalDestinationConnectionID: tc.destConnID,
DisableActiveMigration: !enabled,
}))
tr := &Transport{
Conn: newUDPConnLocalhost(t),
StatelessResetKey: &StatelessResetKey{},
}
defer tr.Close()
path, err := tc.conn.AddPath(tr)
if !enabled {
require.Error(t, err)
require.ErrorContains(t, err, "server disabled connection migration")
return
}
require.NoError(t, err)
require.NotNil(t, path)
tc.packer.EXPECT().AppendPacket(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
shortHeaderPacket{}, errNothingToPack,
).AnyTimes()
packedProbe := make(chan struct{})
tc.packer.EXPECT().PackPathProbePacket(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(protocol.ConnectionID, []ackhandler.Frame, protocol.Version) (shortHeaderPacket, *packetBuffer, error) {
defer close(packedProbe)
return shortHeaderPacket{IsPathProbePacket: true}, getPacketBuffer(), nil
},
).AnyTimes()
tc.connRunner.EXPECT().AddResetToken(gomock.Any(), gomock.Any())
// add a new connection ID, so the path can be probed
_, err = tc.conn.handleFrame(&wire.NewConnectionIDFrame{
SequenceNumber: 1,
ConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
}, protocol.EncryptionInitial, tc.destConnID, monotime.Now())
require.NoError(t, err)
errChan := make(chan error, 1)
go func() { errChan <- tc.conn.run() }()
// Adding the path initialized the transport.
// We can test this by triggering a stateless reset.
conn := newUDPConnLocalhost(t)
_, err = conn.WriteTo(append([]byte{0x40}, make([]byte, 100)...), tr.Conn.LocalAddr())
require.NoError(t, err)
conn.SetReadDeadline(time.Now().Add(time.Second))
_, _, err = conn.ReadFrom(make([]byte, 100))
require.NoError(t, err)
go func() { path.Probe(context.Background()) }()
select {
case <-packedProbe:
case <-time.After(time.Second):
t.Fatal("timeout")
}
// teardown
tc.connRunner.EXPECT().Remove(gomock.Any()).AnyTimes()
tc.connRunner.EXPECT().RemoveResetToken(gomock.Any()).MaxTimes(1)
tc.conn.destroy(nil)
select {
case <-errChan:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestConnectionDatagrams(t *testing.T) {
t.Run("disabled", func(t *testing.T) {
testConnectionDatagrams(t, false)
})
t.Run("enabled", func(t *testing.T) {
testConnectionDatagrams(t, true)
})
}
func testConnectionDatagrams(t *testing.T, enabled bool) {
tc := newServerTestConnection(t, nil, &Config{EnableDatagrams: enabled}, false)
data, err := (&wire.DatagramFrame{Data: []byte("foo"), DataLenPresent: true}).Append(nil, protocol.Version1)
require.NoError(t, err)
data, err = (&wire.DatagramFrame{Data: []byte("bar")}).Append(data, protocol.Version1)
require.NoError(t, err)
_, _, _, err = tc.conn.handleFrames(data, protocol.ConnectionID{}, protocol.Encryption1RTT, nil, monotime.Now())
if !enabled {
require.ErrorIs(t, err, &qerr.TransportError{ErrorCode: qerr.FrameEncodingError, FrameType: uint64(wire.FrameTypeDatagramWithLength)})
return
}
require.NoError(t, err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
d, err := tc.conn.ReceiveDatagram(ctx)
require.NoError(t, err)
require.Equal(t, []byte("foo"), d)
d, err = tc.conn.ReceiveDatagram(ctx)
require.NoError(t, err)
require.Equal(t, []byte("bar"), d)
}
golang-github-lucas-clemente-quic-go-0.55.0/crypto_stream.go 0000664 0000000 0000000 00000015000 15074232574 0024020 0 ustar 00root root 0000000 0000000 package quic
import (
"errors"
"fmt"
"io"
"os"
"slices"
"strconv"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/wire"
)
const disableClientHelloScramblingEnv = "QUIC_GO_DISABLE_CLIENTHELLO_SCRAMBLING"
// The baseCryptoStream is used by the cryptoStream and the initialCryptoStream.
// This allows us to implement different logic for PopCryptoFrame for the two streams.
type baseCryptoStream struct {
queue frameSorter
highestOffset protocol.ByteCount
finished bool
writeOffset protocol.ByteCount
writeBuf []byte
}
func newCryptoStream() *cryptoStream {
return &cryptoStream{baseCryptoStream{queue: *newFrameSorter()}}
}
func (s *baseCryptoStream) HandleCryptoFrame(f *wire.CryptoFrame) error {
highestOffset := f.Offset + protocol.ByteCount(len(f.Data))
if maxOffset := highestOffset; maxOffset > protocol.MaxCryptoStreamOffset {
return &qerr.TransportError{
ErrorCode: qerr.CryptoBufferExceeded,
ErrorMessage: fmt.Sprintf("received invalid offset %d on crypto stream, maximum allowed %d", maxOffset, protocol.MaxCryptoStreamOffset),
}
}
if s.finished {
if highestOffset > s.highestOffset {
// reject crypto data received after this stream was already finished
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "received crypto data after change of encryption level",
}
}
// ignore data with a smaller offset than the highest received
// could e.g. be a retransmission
return nil
}
s.highestOffset = max(s.highestOffset, highestOffset)
return s.queue.Push(f.Data, f.Offset, nil)
}
// GetCryptoData retrieves data that was received in CRYPTO frames
func (s *baseCryptoStream) GetCryptoData() []byte {
_, data, _ := s.queue.Pop()
return data
}
func (s *baseCryptoStream) Finish() error {
if s.queue.HasMoreData() {
return &qerr.TransportError{
ErrorCode: qerr.ProtocolViolation,
ErrorMessage: "encryption level changed, but crypto stream has more data to read",
}
}
s.finished = true
return nil
}
// Writes writes data that should be sent out in CRYPTO frames
func (s *baseCryptoStream) Write(p []byte) (int, error) {
s.writeBuf = append(s.writeBuf, p...)
return len(p), nil
}
func (s *baseCryptoStream) HasData() bool {
return len(s.writeBuf) > 0
}
func (s *baseCryptoStream) PopCryptoFrame(maxLen protocol.ByteCount) *wire.CryptoFrame {
f := &wire.CryptoFrame{Offset: s.writeOffset}
n := min(f.MaxDataLen(maxLen), protocol.ByteCount(len(s.writeBuf)))
if n <= 0 {
return nil
}
f.Data = s.writeBuf[:n]
s.writeBuf = s.writeBuf[n:]
s.writeOffset += n
return f
}
type cryptoStream struct {
baseCryptoStream
}
type clientHelloCut struct {
start protocol.ByteCount
end protocol.ByteCount
}
type initialCryptoStream struct {
baseCryptoStream
scramble bool
end protocol.ByteCount
cuts [2]clientHelloCut
}
func newInitialCryptoStream(isClient bool) *initialCryptoStream {
var scramble bool
if isClient {
disabled, err := strconv.ParseBool(os.Getenv(disableClientHelloScramblingEnv))
scramble = err != nil || !disabled
}
s := &initialCryptoStream{
baseCryptoStream: baseCryptoStream{queue: *newFrameSorter()},
scramble: scramble,
}
for i := range len(s.cuts) {
s.cuts[i].start = protocol.InvalidByteCount
s.cuts[i].end = protocol.InvalidByteCount
}
return s
}
func (s *initialCryptoStream) HasData() bool {
// The ClientHello might be written in multiple parts.
// In order to correctly split the ClientHello, we need the entire ClientHello has been queued.
if s.scramble && s.writeOffset == 0 && s.cuts[0].start == protocol.InvalidByteCount {
return false
}
return s.baseCryptoStream.HasData()
}
func (s *initialCryptoStream) Write(p []byte) (int, error) {
s.writeBuf = append(s.writeBuf, p...)
if !s.scramble {
return len(p), nil
}
if s.cuts[0].start == protocol.InvalidByteCount {
sniPos, sniLen, echPos, err := findSNIAndECH(s.writeBuf)
if errors.Is(err, io.ErrUnexpectedEOF) {
return len(p), nil
}
if err != nil {
return len(p), err
}
if sniPos == -1 && echPos == -1 {
// Neither SNI nor ECH found.
// There's nothing to scramble.
s.scramble = false
return len(p), nil
}
s.end = protocol.ByteCount(len(s.writeBuf))
s.cuts[0].start = protocol.ByteCount(sniPos + sniLen/2) // right in the middle
s.cuts[0].end = protocol.ByteCount(sniPos + sniLen)
if echPos > 0 {
// ECH extension found, cut the ECH extension type value (a uint16) in half
start := protocol.ByteCount(echPos + 1)
s.cuts[1].start = start
// cut somewhere (16 bytes), most likely in the ECH extension value
s.cuts[1].end = min(start+16, s.end)
}
slices.SortFunc(s.cuts[:], func(a, b clientHelloCut) int {
if a.start == protocol.InvalidByteCount {
return 1
}
if a.start > b.start {
return 1
}
return -1
})
}
return len(p), nil
}
func (s *initialCryptoStream) PopCryptoFrame(maxLen protocol.ByteCount) *wire.CryptoFrame {
if !s.scramble {
return s.baseCryptoStream.PopCryptoFrame(maxLen)
}
// send out the skipped parts
if s.writeOffset == s.end {
var foundCuts bool
var f *wire.CryptoFrame
for i, c := range s.cuts {
if c.start == protocol.InvalidByteCount {
continue
}
foundCuts = true
if f != nil {
break
}
f = &wire.CryptoFrame{Offset: c.start}
n := min(f.MaxDataLen(maxLen), c.end-c.start)
if n <= 0 {
return nil
}
f.Data = s.writeBuf[c.start : c.start+n]
s.cuts[i].start += n
if s.cuts[i].start == c.end {
s.cuts[i].start = protocol.InvalidByteCount
s.cuts[i].end = protocol.InvalidByteCount
foundCuts = false
}
}
if !foundCuts {
// no more cuts found, we're done sending out everything up until s.end
s.writeBuf = s.writeBuf[s.end:]
s.end = protocol.InvalidByteCount
s.scramble = false
}
return f
}
nextCut := clientHelloCut{start: protocol.InvalidByteCount, end: protocol.InvalidByteCount}
for _, c := range s.cuts {
if c.start == protocol.InvalidByteCount {
continue
}
if c.start > s.writeOffset {
nextCut = c
break
}
}
f := &wire.CryptoFrame{Offset: s.writeOffset}
maxOffset := nextCut.start
if maxOffset == protocol.InvalidByteCount {
maxOffset = s.end
}
n := min(f.MaxDataLen(maxLen), maxOffset-s.writeOffset)
if n <= 0 {
return nil
}
f.Data = s.writeBuf[s.writeOffset : s.writeOffset+n]
// Don't reslice the writeBuf yet.
// This is done once all parts have been sent out.
s.writeOffset += n
if s.writeOffset == nextCut.start {
s.writeOffset = nextCut.end
}
return f
}
golang-github-lucas-clemente-quic-go-0.55.0/crypto_stream_go124_test.go 0000664 0000000 0000000 00000005236 15074232574 0026005 0 ustar 00root root 0000000 0000000 //go:build go1.24
package quic
import (
"fmt"
mrand "math/rand/v2"
"slices"
"strings"
"testing"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/wire"
"github.com/stretchr/testify/require"
)
func randomDomainName(length int) string {
const alphabet = "abcdefghijklmnopqrstuvwxyz"
b := make([]byte, length)
for i := range b {
if i > 0 && i < length-1 && mrand.IntN(5) == 0 && b[i-1] != '.' {
b[i] = '.'
} else {
b[i] = alphabet[mrand.IntN(len(alphabet))]
}
}
return string(b)
}
func TestInitialCryptoStreamClientRandomizedSizes(t *testing.T) {
skipIfDisableScramblingEnvSet(t)
for i := range 100 {
t.Run(fmt.Sprintf("run %d", i), func(t *testing.T) {
var serverName string
if mrand.Int()%4 > 0 {
serverName = randomDomainName(6 + mrand.IntN(20))
}
var clientHello []byte
if serverName == "" || !strings.Contains(serverName, ".") || mrand.Int()%2 == 0 {
t.Logf("using a ClientHello without ECH, hostname: %q", serverName)
clientHello = getClientHello(t, serverName)
} else {
t.Logf("using a ClientHello with ECH, hostname: %q", serverName)
clientHello = getClientHelloWithECH(t, serverName)
}
testInitialCryptoStreamClientRandomizedSizes(t, clientHello, serverName)
})
}
}
func testInitialCryptoStreamClientRandomizedSizes(t *testing.T, clientHello []byte, expectedServerName string) {
str := newInitialCryptoStream(true)
b := slices.Clone(clientHello)
for len(b) > 0 {
n := min(len(b), mrand.IntN(2*len(b)))
_, err := str.Write(b[:n])
require.NoError(t, err)
b = b[n:]
}
require.True(t, str.HasData())
_, err := str.Write([]byte("foobar"))
require.NoError(t, err)
segments := make(map[protocol.ByteCount][]byte)
var frames []*wire.CryptoFrame
for str.HasData() {
// fmt.Println("popping a frame")
var maxSize protocol.ByteCount
if mrand.Int()%4 == 0 {
maxSize = protocol.ByteCount(mrand.IntN(512) + 1)
} else {
maxSize = protocol.ByteCount(mrand.IntN(32) + 1)
}
f := str.PopCryptoFrame(maxSize)
if f == nil {
continue
}
frames = append(frames, f)
require.LessOrEqual(t, f.Length(protocol.Version1), maxSize)
}
t.Logf("received %d frames", len(frames))
for _, f := range frames {
t.Logf("offset %d: %d bytes", f.Offset, len(f.Data))
if expectedServerName != "" {
require.NotContainsf(t, string(f.Data), expectedServerName, "frame at offset %d contains the server name", f.Offset)
}
segments[f.Offset] = f.Data
}
reassembled := reassembleCryptoData(t, segments)
require.Equal(t, append(clientHello, []byte("foobar")...), reassembled)
if expectedServerName != "" {
require.Contains(t, string(reassembled), expectedServerName)
}
}
golang-github-lucas-clemente-quic-go-0.55.0/crypto_stream_manager.go 0000664 0000000 0000000 00000004250 15074232574 0025517 0 ustar 00root root 0000000 0000000 package quic
import (
"fmt"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/wire"
)
type cryptoStreamManager struct {
initialStream *initialCryptoStream
handshakeStream *cryptoStream
oneRTTStream *cryptoStream
}
func newCryptoStreamManager(
initialStream *initialCryptoStream,
handshakeStream *cryptoStream,
oneRTTStream *cryptoStream,
) *cryptoStreamManager {
return &cryptoStreamManager{
initialStream: initialStream,
handshakeStream: handshakeStream,
oneRTTStream: oneRTTStream,
}
}
func (m *cryptoStreamManager) HandleCryptoFrame(frame *wire.CryptoFrame, encLevel protocol.EncryptionLevel) error {
//nolint:exhaustive // CRYPTO frames cannot be sent in 0-RTT packets.
switch encLevel {
case protocol.EncryptionInitial:
return m.initialStream.HandleCryptoFrame(frame)
case protocol.EncryptionHandshake:
return m.handshakeStream.HandleCryptoFrame(frame)
case protocol.Encryption1RTT:
return m.oneRTTStream.HandleCryptoFrame(frame)
default:
return fmt.Errorf("received CRYPTO frame with unexpected encryption level: %s", encLevel)
}
}
func (m *cryptoStreamManager) GetCryptoData(encLevel protocol.EncryptionLevel) []byte {
//nolint:exhaustive // CRYPTO frames cannot be sent in 0-RTT packets.
switch encLevel {
case protocol.EncryptionInitial:
return m.initialStream.GetCryptoData()
case protocol.EncryptionHandshake:
return m.handshakeStream.GetCryptoData()
case protocol.Encryption1RTT:
return m.oneRTTStream.GetCryptoData()
default:
panic(fmt.Sprintf("received CRYPTO frame with unexpected encryption level: %s", encLevel))
}
}
func (m *cryptoStreamManager) GetPostHandshakeData(maxSize protocol.ByteCount) *wire.CryptoFrame {
if !m.oneRTTStream.HasData() {
return nil
}
return m.oneRTTStream.PopCryptoFrame(maxSize)
}
func (m *cryptoStreamManager) Drop(encLevel protocol.EncryptionLevel) error {
//nolint:exhaustive // 1-RTT keys should never get dropped.
switch encLevel {
case protocol.EncryptionInitial:
return m.initialStream.Finish()
case protocol.EncryptionHandshake:
return m.handshakeStream.Finish()
default:
panic(fmt.Sprintf("dropped unexpected encryption level: %s", encLevel))
}
}
golang-github-lucas-clemente-quic-go-0.55.0/crypto_stream_manager_test.go 0000664 0000000 0000000 00000005466 15074232574 0026570 0 ustar 00root root 0000000 0000000 package quic
import (
"testing"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/wire"
"github.com/stretchr/testify/require"
)
func TestCryptoStreamManager(t *testing.T) {
t.Run("Initial", func(t *testing.T) {
testCryptoStreamManager(t, protocol.EncryptionInitial)
})
t.Run("Handshake", func(t *testing.T) {
testCryptoStreamManager(t, protocol.EncryptionHandshake)
})
t.Run("1-RTT", func(t *testing.T) {
testCryptoStreamManager(t, protocol.Encryption1RTT)
})
}
func testCryptoStreamManager(t *testing.T, encLevel protocol.EncryptionLevel) {
initialStream := newInitialCryptoStream(true)
handshakeStream := newCryptoStream()
oneRTTStream := newCryptoStream()
csm := newCryptoStreamManager(initialStream, handshakeStream, oneRTTStream)
require.NoError(t, csm.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foo")}, encLevel))
require.NoError(t, csm.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("bar"), Offset: 3}, encLevel))
var data []byte
for {
b := csm.GetCryptoData(encLevel)
if len(b) == 0 {
break
}
data = append(data, b...)
}
require.Equal(t, []byte("foobar"), data)
}
func TestCryptoStreamManagerInvalidEncryptionLevel(t *testing.T) {
csm := newCryptoStreamManager(nil, nil, nil)
require.ErrorContains(t,
csm.HandleCryptoFrame(&wire.CryptoFrame{}, protocol.Encryption0RTT),
"received CRYPTO frame with unexpected encryption level",
)
}
func TestCryptoStreamManagerDropEncryptionLevel(t *testing.T) {
t.Run("Initial", func(t *testing.T) {
testCryptoStreamManagerDropEncryptionLevel(t, protocol.EncryptionInitial)
})
t.Run("Handshake", func(t *testing.T) {
testCryptoStreamManagerDropEncryptionLevel(t, protocol.EncryptionHandshake)
})
}
func testCryptoStreamManagerDropEncryptionLevel(t *testing.T, encLevel protocol.EncryptionLevel) {
initialStream := newInitialCryptoStream(true)
handshakeStream := newCryptoStream()
oneRTTStream := newCryptoStream()
csm := newCryptoStreamManager(initialStream, handshakeStream, oneRTTStream)
require.NoError(t, csm.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foo")}, encLevel))
require.ErrorContains(t, csm.Drop(encLevel), "encryption level changed, but crypto stream has more data to read")
require.Equal(t, []byte("foo"), csm.GetCryptoData(encLevel))
require.NoError(t, csm.Drop(encLevel))
}
func TestCryptoStreamManagerPostHandshake(t *testing.T) {
initialStream := newInitialCryptoStream(true)
handshakeStream := newCryptoStream()
oneRTTStream := newCryptoStream()
csm := newCryptoStreamManager(initialStream, handshakeStream, oneRTTStream)
_, err := oneRTTStream.Write([]byte("foo"))
require.NoError(t, err)
_, err = oneRTTStream.Write([]byte("bar"))
require.NoError(t, err)
require.Equal(t,
&wire.CryptoFrame{Data: []byte("foobar")},
csm.GetPostHandshakeData(protocol.ByteCount(10)),
)
}
golang-github-lucas-clemente-quic-go-0.55.0/crypto_stream_test.go 0000664 0000000 0000000 00000014146 15074232574 0025071 0 ustar 00root root 0000000 0000000 package quic
import (
"os"
"strconv"
"testing"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/qerr"
"github.com/quic-go/quic-go/internal/wire"
"github.com/stretchr/testify/require"
)
func TestCryptoStreamDataAssembly(t *testing.T) {
str := newCryptoStream()
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("bar"), Offset: 3}))
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foo")}))
// receive a retransmission
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("bar"), Offset: 3}))
var data []byte
for {
b := str.GetCryptoData()
if b == nil {
break
}
data = append(data, b...)
}
require.Equal(t, []byte("foobar"), data)
}
func TestCryptoStreamMaxOffset(t *testing.T) {
str := newCryptoStream()
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{
Offset: protocol.MaxCryptoStreamOffset - 5,
Data: []byte("foo"),
}))
require.ErrorIs(t,
str.HandleCryptoFrame(&wire.CryptoFrame{
Offset: protocol.MaxCryptoStreamOffset - 2,
Data: []byte("bar"),
}),
&qerr.TransportError{ErrorCode: qerr.CryptoBufferExceeded},
)
}
func TestCryptoStreamFinishWithQueuedData(t *testing.T) {
t.Run("with data at current offset", func(t *testing.T) {
str := newCryptoStream()
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foo")}))
require.Equal(t, []byte("foo"), str.GetCryptoData())
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("bar"), Offset: 3}))
require.ErrorIs(t, str.Finish(), &qerr.TransportError{ErrorCode: qerr.ProtocolViolation})
})
t.Run("with data at a higher offset", func(t *testing.T) {
str := newCryptoStream()
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foobar"), Offset: 20}))
require.ErrorIs(t, str.Finish(), &qerr.TransportError{ErrorCode: qerr.ProtocolViolation})
})
}
func TestCryptoStreamReceiveDataAfterFinish(t *testing.T) {
str := newCryptoStream()
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foobar")}))
require.Equal(t, []byte("foobar"), str.GetCryptoData())
require.NoError(t, str.Finish())
// receiving a retransmission is ok
require.NoError(t, str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("bar"), Offset: 3}))
// but receiving new data is not
require.ErrorIs(t,
str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("baz"), Offset: 4}),
&qerr.TransportError{ErrorCode: qerr.ProtocolViolation},
)
}
func expectedCryptoFrameLen(offset protocol.ByteCount) protocol.ByteCount {
f := &wire.CryptoFrame{Offset: offset}
return f.Length(protocol.Version1)
}
func TestCryptoStreamWrite(t *testing.T) {
str := newCryptoStream()
require.False(t, str.HasData())
_, err := str.Write([]byte("foo"))
require.NoError(t, err)
require.True(t, str.HasData())
_, err = str.Write([]byte("bar"))
require.NoError(t, err)
_, err = str.Write([]byte("baz"))
require.NoError(t, err)
require.True(t, str.HasData())
for i := range expectedCryptoFrameLen(0) {
require.Nil(t, str.PopCryptoFrame(i))
}
f := str.PopCryptoFrame(expectedCryptoFrameLen(0) + 1)
require.Equal(t, &wire.CryptoFrame{Data: []byte("f")}, f)
require.True(t, str.HasData())
f = str.PopCryptoFrame(expectedCryptoFrameLen(1) + 3)
// the three write calls were coalesced into a single frame
require.Equal(t, &wire.CryptoFrame{Offset: 1, Data: []byte("oob")}, f)
f = str.PopCryptoFrame(protocol.MaxByteCount)
require.Equal(t, &wire.CryptoFrame{Offset: 4, Data: []byte("arbaz")}, f)
require.False(t, str.HasData())
}
func TestInitialCryptoStreamServer(t *testing.T) {
str := newInitialCryptoStream(false)
_, err := str.Write([]byte("foobar"))
require.NoError(t, err)
f := str.PopCryptoFrame(expectedCryptoFrameLen(0) + 3)
require.Equal(t, &wire.CryptoFrame{Offset: 0, Data: []byte("foo")}, f)
require.True(t, str.HasData())
// append another CRYPTO frame to the existing slice
f = str.PopCryptoFrame(expectedCryptoFrameLen(3) + 3)
require.Equal(t, &wire.CryptoFrame{Offset: 3, Data: []byte("bar")}, f)
require.False(t, str.HasData())
}
func reassembleCryptoData(t *testing.T, segments map[protocol.ByteCount][]byte) []byte {
t.Helper()
var reassembled []byte
var offset protocol.ByteCount
for len(segments) > 0 {
b, ok := segments[offset]
if !ok {
break
}
reassembled = append(reassembled, b...)
delete(segments, offset)
offset = protocol.ByteCount(len(reassembled))
}
require.Empty(t, segments)
return reassembled
}
func skipIfDisableScramblingEnvSet(t *testing.T) {
t.Helper()
disabled, err := strconv.ParseBool(os.Getenv(disableClientHelloScramblingEnv))
if err == nil && disabled {
t.Skip("ClientHello scrambling disabled via " + disableClientHelloScramblingEnv)
}
}
func TestInitialCryptoStreamClientStatic(t *testing.T) {
skipIfDisableScramblingEnvSet(t)
str := newInitialCryptoStream(true)
clientHello := getClientHello(t, "quic-go.net")
_, err := str.Write(clientHello)
require.NoError(t, err)
require.True(t, str.HasData())
_, err = str.Write([]byte("foobar"))
require.NoError(t, err)
segments := make(map[protocol.ByteCount][]byte)
f1 := str.PopCryptoFrame(protocol.MaxByteCount)
require.NotNil(t, f1)
segments[f1.Offset] = f1.Data
require.True(t, str.HasData())
f2 := str.PopCryptoFrame(protocol.MaxByteCount)
require.NotNil(t, f2)
require.NotContains(t, segments, f2.Offset)
segments[f2.Offset] = f2.Data
require.True(t, str.HasData())
require.NotEqual(t, f2.Offset, protocol.ByteCount(len(f1.Data)))
f3 := str.PopCryptoFrame(protocol.MaxByteCount)
require.NotNil(t, f2)
require.NotContains(t, segments, f3.Offset)
segments[f3.Offset] = f3.Data
require.True(t, str.HasData())
require.NotEqual(t, f3.Offset, protocol.ByteCount(len(f2.Data)))
f4 := str.PopCryptoFrame(protocol.MaxByteCount)
require.NotNil(t, f4)
require.NotContains(t, segments, f4.Offset)
segments[f4.Offset] = f4.Data
require.Equal(t, []byte("foobar"), f4.Data)
require.False(t, str.HasData())
require.NotEqual(t, f4.Offset, protocol.ByteCount(len(f3.Data)))
reassembled := reassembleCryptoData(t, segments)
require.Equal(t, append(clientHello, []byte("foobar")...), reassembled)
}
golang-github-lucas-clemente-quic-go-0.55.0/datagram_queue.go 0000664 0000000 0000000 00000005640 15074232574 0024122 0 ustar 00root root 0000000 0000000 package quic
import (
"context"
"sync"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/internal/utils/ringbuffer"
"github.com/quic-go/quic-go/internal/wire"
)
const (
maxDatagramSendQueueLen = 32
maxDatagramRcvQueueLen = 128
)
type datagramQueue struct {
sendMx sync.Mutex
sendQueue ringbuffer.RingBuffer[*wire.DatagramFrame]
sent chan struct{} // used to notify Add that a datagram was dequeued
rcvMx sync.Mutex
rcvQueue [][]byte
rcvd chan struct{} // used to notify Receive that a new datagram was received
closeErr error
closed chan struct{}
hasData func()
logger utils.Logger
}
func newDatagramQueue(hasData func(), logger utils.Logger) *datagramQueue {
return &datagramQueue{
hasData: hasData,
rcvd: make(chan struct{}, 1),
sent: make(chan struct{}, 1),
closed: make(chan struct{}),
logger: logger,
}
}
// Add queues a new DATAGRAM frame for sending.
// Up to 32 DATAGRAM frames will be queued.
// Once that limit is reached, Add blocks until the queue size has reduced.
func (h *datagramQueue) Add(f *wire.DatagramFrame) error {
h.sendMx.Lock()
for {
if h.sendQueue.Len() < maxDatagramSendQueueLen {
h.sendQueue.PushBack(f)
h.sendMx.Unlock()
h.hasData()
return nil
}
select {
case <-h.sent: // drain the queue so we don't loop immediately
default:
}
h.sendMx.Unlock()
select {
case <-h.closed:
return h.closeErr
case <-h.sent:
}
h.sendMx.Lock()
}
}
// Peek gets the next DATAGRAM frame for sending.
// If actually sent out, Pop needs to be called before the next call to Peek.
func (h *datagramQueue) Peek() *wire.DatagramFrame {
h.sendMx.Lock()
defer h.sendMx.Unlock()
if h.sendQueue.Empty() {
return nil
}
return h.sendQueue.PeekFront()
}
func (h *datagramQueue) Pop() {
h.sendMx.Lock()
defer h.sendMx.Unlock()
_ = h.sendQueue.PopFront()
select {
case h.sent <- struct{}{}:
default:
}
}
// HandleDatagramFrame handles a received DATAGRAM frame.
func (h *datagramQueue) HandleDatagramFrame(f *wire.DatagramFrame) {
data := make([]byte, len(f.Data))
copy(data, f.Data)
var queued bool
h.rcvMx.Lock()
if len(h.rcvQueue) < maxDatagramRcvQueueLen {
h.rcvQueue = append(h.rcvQueue, data)
queued = true
select {
case h.rcvd <- struct{}{}:
default:
}
}
h.rcvMx.Unlock()
if !queued && h.logger.Debug() {
h.logger.Debugf("Discarding received DATAGRAM frame (%d bytes payload)", len(f.Data))
}
}
// Receive gets a received DATAGRAM frame.
func (h *datagramQueue) Receive(ctx context.Context) ([]byte, error) {
for {
h.rcvMx.Lock()
if len(h.rcvQueue) > 0 {
data := h.rcvQueue[0]
h.rcvQueue = h.rcvQueue[1:]
h.rcvMx.Unlock()
return data, nil
}
h.rcvMx.Unlock()
select {
case <-h.rcvd:
continue
case <-h.closed:
return nil, h.closeErr
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
func (h *datagramQueue) CloseWithError(e error) {
h.closeErr = e
close(h.closed)
}
golang-github-lucas-clemente-quic-go-0.55.0/datagram_queue_test.go 0000664 0000000 0000000 00000010713 15074232574 0025156 0 ustar 00root root 0000000 0000000 package quic
import (
"context"
"testing"
"github.com/quic-go/quic-go/internal/synctest"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/internal/wire"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDatagramQueuePeekAndPop(t *testing.T) {
var queued []struct{}
queue := newDatagramQueue(func() { queued = append(queued, struct{}{}) }, utils.DefaultLogger)
require.Nil(t, queue.Peek())
require.Empty(t, queued)
require.NoError(t, queue.Add(&wire.DatagramFrame{Data: []byte("foo")}))
require.Len(t, queued, 1)
require.Equal(t, &wire.DatagramFrame{Data: []byte("foo")}, queue.Peek())
// calling peek again returns the same datagram
require.Equal(t, &wire.DatagramFrame{Data: []byte("foo")}, queue.Peek())
queue.Pop()
require.Nil(t, queue.Peek())
}
func TestDatagramQueueSendQueueLength(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
queue := newDatagramQueue(func() {}, utils.DefaultLogger)
for range maxDatagramSendQueueLen {
require.NoError(t, queue.Add(&wire.DatagramFrame{Data: []byte{0}}))
}
errChan := make(chan error, 1)
go func() { errChan <- queue.Add(&wire.DatagramFrame{Data: []byte("foobar")}) }()
synctest.Wait()
select {
case <-errChan:
t.Fatal("expected to not receive error")
default:
}
// peeking doesn't remove the datagram from the queue...
require.NotNil(t, queue.Peek())
synctest.Wait()
select {
case <-errChan:
t.Fatal("expected to not receive error")
default:
}
// ...but popping does
queue.Pop()
synctest.Wait()
select {
case err := <-errChan:
require.NoError(t, err)
default:
t.Fatal("timeout")
}
// pop all the remaining datagrams
for range maxDatagramSendQueueLen - 1 {
queue.Pop()
}
f := queue.Peek()
require.NotNil(t, f)
require.Equal(t, &wire.DatagramFrame{Data: []byte("foobar")}, f)
})
}
func TestDatagramQueueReceive(t *testing.T) {
queue := newDatagramQueue(func() {}, utils.DefaultLogger)
// receive frames that were received earlier
queue.HandleDatagramFrame(&wire.DatagramFrame{Data: []byte("foo")})
queue.HandleDatagramFrame(&wire.DatagramFrame{Data: []byte("bar")})
data, err := queue.Receive(context.Background())
require.NoError(t, err)
require.Equal(t, []byte("foo"), data)
data, err = queue.Receive(context.Background())
require.NoError(t, err)
require.Equal(t, []byte("bar"), data)
}
func TestDatagramQueueReceiveBlocking(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
queue := newDatagramQueue(func() {}, utils.DefaultLogger)
// block until a new frame is received
type result struct {
data []byte
err error
}
resultChan := make(chan result, 1)
go func() {
data, err := queue.Receive(context.Background())
resultChan <- result{data, err}
}()
synctest.Wait()
select {
case <-resultChan:
t.Fatal("expected to not receive result")
default:
}
queue.HandleDatagramFrame(&wire.DatagramFrame{Data: []byte("foobar")})
synctest.Wait()
select {
case result := <-resultChan:
require.NoError(t, result.err)
require.Equal(t, []byte("foobar"), result.data)
default:
t.Fatal("should have received a datagram frame")
}
// unblock when the context is canceled
ctx, cancel := context.WithCancel(context.Background())
errChan := make(chan error, 1)
go func() {
_, err := queue.Receive(ctx)
errChan <- err
}()
synctest.Wait()
select {
case <-errChan:
t.Fatal("expected to not receive error")
default:
}
cancel()
synctest.Wait()
select {
case err := <-errChan:
require.ErrorIs(t, err, context.Canceled)
default:
t.Fatal("should have received a context canceled error")
}
})
}
func TestDatagramQueueClose(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
queue := newDatagramQueue(func() {}, utils.DefaultLogger)
for range maxDatagramSendQueueLen {
require.NoError(t, queue.Add(&wire.DatagramFrame{Data: []byte{0}}))
}
errChan1 := make(chan error, 1)
go func() { errChan1 <- queue.Add(&wire.DatagramFrame{Data: []byte("foobar")}) }()
errChan2 := make(chan error, 1)
go func() {
_, err := queue.Receive(context.Background())
errChan2 <- err
}()
queue.CloseWithError(assert.AnError)
synctest.Wait()
select {
case err := <-errChan1:
require.ErrorIs(t, err, assert.AnError)
default:
t.Fatal("should have received an error")
}
select {
case err := <-errChan2:
require.ErrorIs(t, err, assert.AnError)
default:
t.Fatal("should have received an error")
}
})
}
golang-github-lucas-clemente-quic-go-0.55.0/docs/ 0000775 0000000 0000000 00000000000 15074232574 0021532 5 ustar 00root root 0000000 0000000 golang-github-lucas-clemente-quic-go-0.55.0/docs/quic.png 0000664 0000000 0000000 00000041623 15074232574 0023207 0 ustar 00root root 0000000 0000000 ‰PNG
IHDR ^ ø =ûè\ CZIDATxìÖÌØPFáÙ¶mÛ¶mÛ¶mÛ¶Íh¶mÛ6¾ÙûmïIž¨nïmëE)¥”RJ)¥”RJ9.¿È…šè‚q‚æ(…¸pfJ)¥”RÊJc>Ãpí?§| ºa"Ö`?Öc&ú¡‚â?J)¥”R¥pæ¯ÐþñßGi0·aŽðkQ>áÌT@DG2dAaTB´GO´A=”GdD"Do¸eJ)¥TBl†ý(xìx·DYKY¿™eéÚÏÒ·éb‰«Ô²¨Ùs›þ~ZP‘ADÄE*¤C|„G ¸uá0æ§PHyG\”B,Ã9¼…¹À+Æ,´A>D€k¤”RJåÃ#Øg~C„´ÔMÚXù5›Þ±+¿UkßiË9p”…O•îÇo×[4„kéQ}°ûq7ñÌ ßÕÛ8ÝXŽî(ð
çÖ ` 0\„?«Yº°ãgZ©EkðÔ–£ßpKÕ¸•…IšÂ¼xõú«sÝ„(ø”J†®Ø‰g0wt«QááÔ”RJ©zx
ƒyóéË’Õj`5vŸ0~¬œ$gÿ‘æ7xˆ¿UmáÔ¼"!c2¶â6Ì<ÄfŒ@uDpä9„@ÌüE¬ø¼•ŽºU6î³ô;›Ÿ ïØ;è(’î‹7– $DÐww‚»/îîn«ÖqY÷Å}qwwwww[ÇÞ¿ÞœjþõõŽôÔtÍDÞïœûI¦3ЗªW÷ç>Ÿ¶L$cªÉoÆ
&ˆ%zÍÍßp>œê
‚ ‚è%¾K²FB›5ÛÑH«ë¾Ó±T9ã;j˜æš¦®|vçÄ2äïØüv½aj18w>h