diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 8fdd88a86..2bdafda2e 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -130,7 +130,7 @@ jobs: verbose: false vey-proxy-test: name: vey-proxy test - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: Checkout sources uses: actions/checkout@v6 @@ -147,7 +147,7 @@ jobs: - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install capnproto libjemalloc-dev libc-ares-dev libssl-dev liblua5.4-dev + sudo apt-get install capnproto libjemalloc-dev libc-ares-dev libssl-dev libpython3-dev liblua5.4-dev - name: Install CA Certificates run: | sudo apt-get install ca-certificates @@ -189,6 +189,7 @@ jobs: resolvectl query httpbin.local vey-proxy.local - name: run unit test run: | + cargo clean ./scripts/coverage/vey-proxy.sh - name: Restore /etc/hosts # delete this after systemd-resolved 261 run: | @@ -205,7 +206,7 @@ jobs: verbose: false vey-bench-test: name: vey-bench test - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: Checkout sources uses: actions/checkout@v6 @@ -222,7 +223,7 @@ jobs: - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install capnproto libmimalloc-dev libc-ares-dev libssl-dev liblua5.4-dev + sudo apt-get install capnproto libmimalloc-dev libc-ares-dev libssl-dev libpython3-dev liblua5.4-dev - name: Install CA Certificates run: | sudo apt-get install ca-certificates @@ -251,6 +252,7 @@ jobs: resolvectl query httpbin.local vey-proxy.local - name: run unit test run: | + cargo clean ./scripts/coverage/vey-bench.sh - name: Restore /etc/hosts # delete this after systemd-resolved 261 run: | diff --git a/Cargo.lock b/Cargo.lock index 179e22a9c..30b3ed73f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -981,9 +981,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", diff --git a/lib/vey-hickory-client/src/io/h3.rs b/lib/vey-hickory-client/src/io/h3.rs index 81129c76b..9117b48c7 100644 --- a/lib/vey-hickory-client/src/io/h3.rs +++ b/lib/vey-hickory-client/src/io/h3.rs @@ -39,7 +39,10 @@ pub async fn connect( .map_err(|_| anyhow!("quic connect to {} timed out", connect_info.server))??; let h3_connection = h3_quinn::Connection::new(connection); - let (driver, send_request) = h3::client::new(h3_connection) + let mut client_builder = h3::client::builder(); + client_builder.send_grease(false); + let (driver, send_request) = client_builder + .build(h3_connection) .await .map_err(|e| anyhow!("h3 connect to {} failed: {e}", connect_info.server))?; diff --git a/scripts/coverage/vey-bench/nginx.conf b/scripts/coverage/vey-bench/nginx.conf index 4f7c4397a..19d184812 100644 --- a/scripts/coverage/vey-bench/nginx.conf +++ b/scripts/coverage/vey-bench/nginx.conf @@ -14,11 +14,14 @@ http { listen 2080; listen [::]:2080; - listen 2443 ssl http2; - listen [::]:2443 ssl http2; + listen 2443 ssl; + listen [::]:2443 ssl; + + listen 2443 quic reuseport; + listen [::]:2443 quic reuseport; # requires nginx 1.25.1 - # http2 on; + http2 on; ssl_certificate httpbin.local.pem; ssl_certificate_key httpbin.local-key.pem; diff --git a/scripts/coverage/vey-bench/run.sh b/scripts/coverage/vey-bench/run.sh index 81f92f105..73d80cdc4 100644 --- a/scripts/coverage/vey-bench/run.sh +++ b/scripts/coverage/vey-bench/run.sh @@ -1,13 +1,13 @@ # generate resource files "${RUN_DIR}"/mkcert.sh -# start vey-proxy -"${PROJECT_DIR}"/target/debug/vey-proxy -c "${RUN_DIR}"/vey-proxy.yaml -G "${TEST_NAME}" & -PROXY_PID=$! - # start docker compose services (httpbin, influxdb, graphite, nginx) docker compose -f "${PROJECT_DIR}"/scripts/coverage/vey-bench/docker-compose.yml up -d +sleep 2 +# start vey-proxy +"${PROJECT_DIR}"/target/debug/vey-proxy -c "${RUN_DIR}"/vey-proxy.yaml -G "${TEST_NAME}" & +PROXY_PID=$! # start vey-statsd [ -n "${INFLUX_TOKEN}" ] || INFLUX_TOKEN=$(curl -X POST http://127.0.0.1:8181/api/v3/configure/token/admin | jq ".token" -r) @@ -34,6 +34,7 @@ set -x . "${RUN_DIR}"/target_dns.sh . "${RUN_DIR}"/target_h1.sh . "${RUN_DIR}"/target_h2.sh +. "${RUN_DIR}"/target_h3.sh . "${RUN_DIR}"/target_keyless_openssl.sh . "${RUN_DIR}"/target_openssl.sh . "${RUN_DIR}"/target_rustls.sh diff --git a/scripts/coverage/vey-bench/target_h3.sh b/scripts/coverage/vey-bench/target_h3.sh new file mode 100644 index 000000000..402361ede --- /dev/null +++ b/scripts/coverage/vey-bench/target_h3.sh @@ -0,0 +1,23 @@ + +for port in 2443 8443 +do + +# GET + +URL=https://httpbin.local:${port}/get + +vey_bench h3 "${URL}" --ok-status 200 --tls-ca-cert "${TEST_CA_CERT_FILE}" + +vey_bench h3 "${URL}" -H "Accept: application/json" --ok-status 200 --tls-ca-cert "${TEST_CA_CERT_FILE}" + +vey_bench h3 "${URL}" -x socks5h://t1:toor@vey-proxy.local:1080 --ok-status 200 --tls-ca-cert "${TEST_CA_CERT_FILE}" + +# POST + +URL=https://httpbin.local:${port}/post + +vey_bench h3 "${URL}" --method POST --payload 31323334 --ok-status 200 --tls-ca-cert "${TEST_CA_CERT_FILE}" +vey_bench h3 "${URL}" --method POST --payload 31323334 --binary --ok-status 200 --tls-ca-cert "${TEST_CA_CERT_FILE}" +vey_bench h3 "${URL}" --method POST --payload name=foo -H "Content-Type: application/x-www-form-urlencoded" --ok-status 200 --tls-ca-cert "${TEST_CA_CERT_FILE}" + +done diff --git a/scripts/coverage/vey-bench/vey-proxy.yaml b/scripts/coverage/vey-bench/vey-proxy.yaml index b097d9957..61ad269eb 100644 --- a/scripts/coverage/vey-bench/vey-proxy.yaml +++ b/scripts/coverage/vey-bench/vey-proxy.yaml @@ -64,3 +64,8 @@ server: listen: 127.0.0.1:1080 escaper: default user-group: default + - name: quic + type: udp_stream + listen: 127.0.0.1:8443 + escaper: default + upstream: 127.0.0.1:2443 diff --git a/scripts/coverage/vey-proxy/0003_base_udp_stream/main.yaml b/scripts/coverage/vey-proxy/0003_base_udp_stream/main.yaml new file mode 100644 index 000000000..d8c9635a4 --- /dev/null +++ b/scripts/coverage/vey-proxy/0003_base_udp_stream/main.yaml @@ -0,0 +1,30 @@ +--- + +log: journal + +stat: + target: + udp: 127.0.0.1:8125 + +resolver: + - name: default + type: c-ares + +escaper: + - name: default + type: direct_fixed + resolver: default + egress_net_filter: + default: allow + allow: 127.0.0.0/8 + +server: + - name: quic + type: udp_stream + listen: 127.0.0.1:8443 + escaper: default + upstream: 127.0.0.1:2443 + flush_task_log_on_created: true + flush_task_log_on_connected: true + task_log_flush_interval: 1s + udp_sock_speed_limit: 800K diff --git a/scripts/coverage/vey-proxy/0003_base_udp_stream/testcases.sh b/scripts/coverage/vey-proxy/0003_base_udp_stream/testcases.sh new file mode 100644 index 000000000..7a73c1186 --- /dev/null +++ b/scripts/coverage/vey-proxy/0003_base_udp_stream/testcases.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ "${CURL_TEST_H3}" = "yes" ] +then + python3 "${PROJECT_DIR}/vey-proxy/ci/python3+curl/test_httpbin_h3.py" -T https://httpbin.local:8443 --ca-cert "${TEST_CA_CERT_FILE}" +fi diff --git a/scripts/coverage/vey-proxy/0006_chain_http_proxy/testcases.sh b/scripts/coverage/vey-proxy/0006_chain_http_proxy/testcases.sh index b55220bc9..920d1fb14 100644 --- a/scripts/coverage/vey-proxy/0006_chain_http_proxy/testcases.sh +++ b/scripts/coverage/vey-proxy/0006_chain_http_proxy/testcases.sh @@ -35,7 +35,10 @@ test_http_proxy_h2() { date - python3 "${PROJECT_DIR}/vey-proxy/ci/python3+curl/test_httpbin_h2.py" -x ${HTTP_PROXY} -T https://httpbin.local:2443 --ca-cert "${TEST_CA_CERT_FILE}" + if [ "${CURL_TEST_H2}" = "yes" ] + then + python3 "${PROJECT_DIR}/vey-proxy/ci/python3+curl/test_httpbin_h2.py" -x ${HTTP_PROXY} -T https://httpbin.local:2443 --ca-cert "${TEST_CA_CERT_FILE}" + fi } @@ -73,7 +76,10 @@ test_https_proxy_h2() { date - python3 "${PROJECT_DIR}/vey-proxy/ci/python3+curl/test_httpbin_h2.py" -x ${HTTPS_PROXY} -T https://httpbin.local:2443 --proxy-ca-cert "${TEST_CA_CERT_FILE}" --ca-cert "${TEST_CA_CERT_FILE}" + if [ "${CURL_TEST_H2}" = "yes" ] + then + python3 "${PROJECT_DIR}/vey-proxy/ci/python3+curl/test_httpbin_h2.py" -x ${HTTPS_PROXY} -T https://httpbin.local:2443 --proxy-ca-cert "${TEST_CA_CERT_FILE}" --ca-cert "${TEST_CA_CERT_FILE}" + fi } diff --git a/scripts/coverage/vey-proxy/nginx.conf b/scripts/coverage/vey-proxy/nginx.conf index ed1d5795a..866537e4d 100644 --- a/scripts/coverage/vey-proxy/nginx.conf +++ b/scripts/coverage/vey-proxy/nginx.conf @@ -14,11 +14,14 @@ http { listen 2080; listen [::]:2080; - listen 2443 ssl http2; - listen [::]:2443 ssl http2; + listen 2443 ssl; + listen [::]:2443 ssl; + + listen 2443 quic reuseport; + listen [::]:2443 quic reuseport; # requires nginx 1.25.1 - # http2 on; + http2 on; ssl_certificate httpbin.local.pem; ssl_certificate_key httpbin.local-key.pem; diff --git a/scripts/coverage/vey-proxy/run.sh b/scripts/coverage/vey-proxy/run.sh index 7dae047cf..8aaef387a 100644 --- a/scripts/coverage/vey-proxy/run.sh +++ b/scripts/coverage/vey-proxy/run.sh @@ -4,7 +4,24 @@ # start docker compose services (nginx, httpbin, ftp-server, influxdb, graphite) [ -d /tmp/vsftpd ] || mkdir -p /tmp/vsftpd docker compose -f "${PROJECT_DIR}"/scripts/coverage/vey-proxy/docker-compose.yml up -d +sleep 2 +CURL_TEST_H2=no +CURL_TEST_H3=no + +for feature in $(curl --version | awk -F':' '$1 == "Features" { print $2 }') +do + case "${feature}" in + "HTTP2") + CURL_TEST_H2=yes + ;; + "HTTP3") + CURL_TEST_H3=yes + ;; + *) + ;; + esac +done # start vey-dcgen "${PROJECT_DIR}"/target/debug/vey-dcgen -c "${RUN_DIR}"/vey-dcgen.yaml -G port2999 & diff --git a/vey-bench/src/module/http/connection/h3.rs b/vey-bench/src/module/http/connection/h3.rs index 125934257..5ec24269c 100644 --- a/vey-bench/src/module/http/connection/h3.rs +++ b/vey-bench/src/module/http/connection/h3.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use anyhow::{Context, anyhow}; use bytes::Bytes; -use clap::{Arg, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command}; use h3::client::SendRequest; use h3_quinn::{OpenStreams, VarInt}; use quinn::crypto::rustls::QuicClientConfig; @@ -28,6 +28,7 @@ use crate::module::rustls::{AppendRustlsArgs, RustlsTlsClientArgs}; use crate::module::socket::{AppendSocketArgs, SocketArgs}; const HTTP_ARG_PROXY: &str = "proxy"; +const HTTP_ARG_GREASE: &str = "grease"; pub(crate) trait AppendH3ConnectArgs { fn append_h3_connect_args(self) -> Self; @@ -35,6 +36,7 @@ pub(crate) trait AppendH3ConnectArgs { pub(crate) struct H3ConnectArgs { socks_proxy: Option, + enable_grease: bool, socket: SocketArgs, target_tls: RustlsTlsClientArgs, @@ -59,6 +61,7 @@ impl H3ConnectArgs { H3ConnectArgs { socks_proxy: None, + enable_grease: false, socket: SocketArgs::default(), target_tls: tls, proxy_peer_addrs: None, @@ -204,6 +207,7 @@ impl H3ConnectArgs { let quic_conn = self.new_quic_connection(target, stats, proc_args).await?; let mut client_builder = h3::client::builder(); + client_builder.send_grease(self.enable_grease); // TODO add more client config let (mut driver, send_request) = client_builder .build(quic_conn) @@ -226,6 +230,10 @@ impl H3ConnectArgs { self.socks_proxy = Some(proxy); } + if args.get_flag(HTTP_ARG_GREASE) { + self.enable_grease = true; + } + self.socket .parse_args(args) .context("invalid socket config")?; @@ -244,8 +252,13 @@ impl AppendH3ConnectArgs for Command { .short('x') .help("Use a proxy") .long(HTTP_ARG_PROXY) - .num_args(1) - .value_name("PROXY URL"), + .num_args(1), + ) + .arg( + Arg::new(HTTP_ARG_GREASE) + .help("Enable HTTP/3 GREASE") + .long(HTTP_ARG_GREASE) + .action(ArgAction::SetTrue), ) .append_socket_args() .append_rustls_args() diff --git a/vey-proxy/ci/python3+curl/test_httpbin_h3.py b/vey-proxy/ci/python3+curl/test_httpbin_h3.py new file mode 100644 index 000000000..c26f93db3 --- /dev/null +++ b/vey-proxy/ci/python3+curl/test_httpbin_h3.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 + +import argparse +import sys +import unittest +import base64 +from io import BytesIO +from urllib.parse import urlencode + +import pycurl + +target_site = 'https://httpbin.org' +target_ca_cert = None +target_proxy = None +proxy_ca_cert = None +local_resolve = None + +ACCEPT_JSON = 'Accept: application/json' +ACCEPT_HTML = 'Accept: text/html' + + +class TestHttpBin(unittest.TestCase): + def setUp(self): + self.buffer = BytesIO() + + self.c = pycurl.Curl() + self.c.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_3ONLY) + self.c.setopt(pycurl.WRITEFUNCTION, self.buffer.write) + self.c.setopt(pycurl.HTTPHEADER, [ACCEPT_JSON]) + if target_ca_cert is not None: + self.c.setopt(pycurl.CAINFO, target_ca_cert) + if target_proxy is not None: + self.c.setopt(pycurl.PROXY, target_proxy) + if proxy_ca_cert is not None: + self.c.setopt(pycurl.PROXY_CAINFO, proxy_ca_cert) + if local_resolve is not None: + self.c.setopt(pycurl.RESOLVE, [local_resolve]) + + def tearDown(self): + self.c.close() + + def set_url_and_request_target(self, path: str): + self.c.setopt(pycurl.URL, f"{target_site}{path}") + + def test_simple_get(self): + self.set_url_and_request_target('/get') + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + def test_get_delay(self): + self.set_url_and_request_target('/delay/1') + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + def test_get_chunked_small(self): + self.set_url_and_request_target('/stream/1') + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + self.set_url_and_request_target('/stream/4') + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + def test_get_chunked_large(self): + self.set_url_and_request_target('/stream/100') + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + def test_basic_auth_get(self): + self.set_url_and_request_target('/basic-auth/name/pass') + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 401) + + auth_header = "Authorization: Basic {}".format(base64.standard_b64encode(b'name:pass').decode('utf-8')) + self.c.setopt(pycurl.HTTPHEADER, [ACCEPT_JSON, auth_header]) + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + auth_header = "Authorization: Basic {}".format(base64.standard_b64encode(b'name:pas').decode('utf-8')) + self.c.setopt(pycurl.HTTPHEADER, [ACCEPT_JSON, auth_header]) + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 401) + + def test_base64_decode(self): + self.set_url_and_request_target('/base64/SFRUUEJJTiBpcyBhd2Vzb21l') + self.c.setopt(pycurl.HTTPHEADER, [ACCEPT_HTML]) + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + self.assertEqual(self.buffer.getvalue(), b"HTTPBIN is awesome") + + def test_post_small(self): + data = "Content to post" + + self.set_url_and_request_target('/post') + self.c.setopt(pycurl.POSTFIELDS, data) + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + def test_post_large(self): + post_data = {'data': "Content to post" * 1024 * 100} + post_fields = urlencode(post_data) + + # curl won't send Expect with HTTP3.0 + self.set_url_and_request_target('/post') + self.c.setopt(pycurl.POSTFIELDS, post_fields) + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + + def test_put_file(self): + self.set_url_and_request_target('/put') + self.c.setopt(pycurl.UPLOAD, 1) + file = open(__file__) + self.c.setopt(pycurl.READDATA, file) + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + file.close() + + def test_put_file_delay(self): + self.set_url_and_request_target('/delay/1') + self.c.setopt(pycurl.UPLOAD, 1) + file = open(__file__) + self.c.setopt(pycurl.READDATA, file) + self.c.perform() + self.assertEqual(self.c.getinfo(pycurl.RESPONSE_CODE), 200) + file.close() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--proxy', '-x', nargs='?', help='Proxy URL') + parser.add_argument('--site', '-T', nargs='?', help='Target Site', default=target_site) + parser.add_argument('--ca-cert', nargs='?', help='CA Cert') + parser.add_argument('--proxy-ca-cert', nargs='?', help='Proxy CA Cert') + parser.add_argument('--resolve', nargs='?', help='Local Resolve Record for curl') + + (args, left_args) = parser.parse_known_args() + + if args.ca_cert is not None: + target_ca_cert = args.ca_cert + if args.proxy is not None: + target_proxy = args.proxy + if args.proxy_ca_cert is not None: + proxy_ca_cert = args.proxy_ca_cert + if args.resolve is not None: + local_resolve = args.resolve + target_site = args.site + + left_args.insert(0, sys.argv[0]) + + unittest.main(argv=left_args)