From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 3032D1FF0C1 for ; Wed, 26 Aug 2026 11:29:39 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id BBAAD21590; Wed, 26 Aug 2026 11:29:18 +0200 (CEST) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=lMkbsrRzFoqez3iwZTEcXSvWR7k5lGyQSAl9H/UPCuu7vn+rGw7SYeV2G5kpTMq1J8rA7eVB7cISICx6PuEHoAs3Rm60XlPy1ZoedxrHME5P/oxGUMdhR0J4UTuRpRfoM1/aXKUsrj15hrQX/49lyvsvxg3fVIN8F6zIXa623D/eRIhpDfERb76wu7K0Vo/bapQ1FDwNjJLpnfJliEF8AHXZa0wHnOVls2kmNfR5Qxs7Mv7m7h1t+X0kRPV6prH3VYmyKSfauBYBNFKoSHuY+eGkxsivm7UvGcst3+Z0U1vsSVdS3t1N98erredEyBmF3ATf1cf2JLwMJyZ8xD/1IQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=llB8sTQVeLX/n/JAwuQ6XNXciRS/A6k87n5TA6m17AY=; b=PmWT2QWcfGfwQdHk4zqHRDWTpNP8GsrNkQR2ztcpAmRok7XT8LmuxrpfV76EFB+iO/P38HcKPZcWFyaWDiqSd++CPvdC802hD/aALT7c4Kt6us3cU+iieibWNraPlJwy8dM4Xe8ckdTU/aB590V6LX4paFXBQwG0Je6KIeMBoPDNtbsdRIMj4LYV0c9UBGMfwIL1u5JE5NCbbjupr6P4adMhhysjK0Jlmihy1JTN1ivqereldqWP9Zi9i0RXmn1KaRoiCTkR5883WgJRF62mH0nsALwT8ve/1+VjzmOcWoQuSZwMyH3fzvvm7Nfk0RDxjpPVsBqhdgKRCoWWsYdZ8A== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass smtp.mailfrom=groupe-cyllene.com; dmarc=pass action=none header.from=groupe-cyllene.com; dkim=pass header.d=groupe-cyllene.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=groupecyllene.onmicrosoft.com; s=selector2-groupecyllene-onmicrosoft-com; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=llB8sTQVeLX/n/JAwuQ6XNXciRS/A6k87n5TA6m17AY=; b=v4J8MMwcmShJZMXMUTDpGNdtPAbqEVSpc9hwPnwjWYtZqB83eVweJjfeFErKtYEWwtnR3ygMZhpms/OtHxtCyFb5Y/eqrxvPoL4xUGVDKu9votS9txROhD+xY1DhGBdWhgmiMnf9s763og9zbtws7pqX5HhnZD4GEleVLFN06aQ= Authentication-Results: dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=groupe-cyllene.com; From: Alexandre Derumier To: pve-devel@lists.proxmox.com Subject: [RFC pve-kyberproxy 09/13] Add pve-kyberproxy Date: Tue, 25 Aug 2026 13:34:34 +0200 Message-ID: <20260825113442.947620-10-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260825113442.947620-1-alexandre.derumier@groupe-cyllene.com> References: <20260825113442.947620-1-alexandre.derumier@groupe-cyllene.com> Content-Transfer-Encoding: 8bit Content-Type: text/plain X-ClientProxiedBy: PR3P192CA0021.EURP192.PROD.OUTLOOK.COM (2603:10a6:102:56::26) To PR0P264MB3691.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:14b::12) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: PR0P264MB3691:EE_|PATP264MB7783:EE_ X-MS-Office365-Filtering-Correlation-Id: ca26b179-b903-48c2-ac5a-08df029ce6a3 X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|52116014|376014|1800799024|23010399003|366016|13003099007|38350700014|6133799003|3023799007|56012099006|10067099003|5023799004|18002099003|22082099003; X-Microsoft-Antispam-Message-Info: MwH28Cmls2CFHUKEASqTcHM2faZ8tViCjYRKZmACsqr7Zid+mIEslFYCX5hdyTilGMQQGOovjg4M/wfNyV/zaIzYMtdWFyzaui2NGakF1fepAtLaB8bZq/x8b/dKfssUHH3LLzCeKrz0jOHDOrXPBMdp7poX92EAVdwEv2561gHYiS9OJV1QTy6Q83O7BWvup0NPyQjVzCgQa+kQiZHQOSTezBJ9PQDrk7v9G3gk+LQPpcwM7GWgnJvhxDerRpxPNyhROh5+O52SKYdoI6GxRZV0tbimegyka8PoTsga1wkYkRcUnE/tsnVfgNB/O992W1ZCs7GVz/Do4G/eLNfiS7pjLCzN4YJHGgnypSk7Em6IxN7+wPmxXrO1mtcf+WKh0bySedXJR5a46724JS7hzYq2s1XZTNbn48nOIa60Nbh61HMbpMPM+agzAAB8TXataOxKm44ij/EsN8n+g2Zhe1vwyfVJ1GlCTAJizdwNccSr+yi5pNsHVW3dCLgEtYpC/Cr2+h75F+J+sT77mwPalrp2Fd8u+7PRI+bGDCmr/zJC3SuGRxJhcTKzeFfcYispkaJYjU2eS/h0s3Pt2jtyTslF8llheDShISi74zfUvCnQp9BOZytg9y0hVTVyFoyNZ1anDuj0MJrXOXvsjgyejksSBVyWux68FJT8FoZcPa/+Wino13nlA8DgFilib+rV+B5ndPGdaoELFyToICXmstWpgs50TJyxVnCHioYjaL0= X-Forefront-Antispam-Report: CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:PR0P264MB3691.FRAP264.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(13230040)(52116014)(376014)(1800799024)(23010399003)(366016)(13003099007)(38350700014)(6133799003)(3023799007)(56012099006)(10067099003)(5023799004)(18002099003)(22082099003);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?us-ascii?Q?B21cF3qWdv3Eam7GzxJeJOQj+JaNB4paTrbtYkvOhLcGrAfNnjWz3JufOyNx?= =?us-ascii?Q?qWZu8w//WNIbvPZ074nIZgvitajqMDxV4Q5emHD9OyvskbTZ0Fvb0HmKiLtE?= =?us-ascii?Q?4e/uRjwYDPWtMKhLwO+kzQw5dbug4iF4o7QjwS62+WGF0ruGAN61uZu8NQm2?= =?us-ascii?Q?Wg2EJJIO/tWAVQJrLs4Jp1wMNT84JHqvrb7b1ncAv0MOYO/9zNXC8YLhxp9+?= =?us-ascii?Q?ATolLjG7eicBhwlonpy53sp/yJt1lzr/e11YUar3Qq9q9jMfhu36Mf5pp8YI?= =?us-ascii?Q?CbMrrAtUQMp2lQa7TEuDNRyDk+zjXhoX2TVvTdfQkPP5eTnBW7hmIMh9a8QR?= =?us-ascii?Q?2edDf5lxEN7VXU55KIuljpM84nf5P4JuggKqz/4w+RekhakOY11sJeOIJ0g3?= =?us-ascii?Q?LUs83bcYsZCgDZJJqRuIK0mt+Rb+NsCUwQ5+4LevdM3kA4QZxogkOq+9F+wI?= =?us-ascii?Q?TK7aLMiNa0fcODcr/ZGuMRRDyJy3vcot3ms3ot5BSd1+KGn1bHQSBkL3i8PD?= =?us-ascii?Q?gDD2ia3sY03I42Om3zKNdMe7WccFf6sPuTy3Wq5DY9AgtPRL64SrwLaAEaFu?= =?us-ascii?Q?bKyWCJksm+c5chFyz3e/GGAs8MKdW/0/NFvWWK3uRxccJL+WoY0NaqwMJ5aB?= =?us-ascii?Q?m+W630ylA1L5OFDVEY1s/XX3z2JJmtVUGTOdLKIvNCoYE42J5B5wmYEw9GvD?= =?us-ascii?Q?bk6H13FU+b8/ZcZqrNSAsrvvC0sGZC8HR5ChVV59Jw2RCkdj8r2GY7YJ03y2?= =?us-ascii?Q?Hg6oQzKPfcvVvsGwR5stXFVgkx2+pGh34oouLYvD8ukhK46RFdN0lyvOJLf/?= =?us-ascii?Q?c1GiW3kDVYPfmUnc9vN1KkBBsdc9BYxhy+lXYYTYQbzuqs0YRVNRCzKJibtz?= =?us-ascii?Q?cmYA0nYVtkyOZQDmdBV0UGhuje0o37Rl8fITIWewczOmBjwrww40H+WIp9UT?= =?us-ascii?Q?bXX/ow2nj74hGQmD/vQitb62F5AVDUQfG/UUCfksRndRAN1DhJfxGZTFewLm?= =?us-ascii?Q?p1HiZ7OJUsVa83PzNouZDFY3TxWlWAMQ2wtorBHshLYVg1zPHO+v6omE/RFL?= =?us-ascii?Q?X3Qr3qwIkChT8fRki8brCMFfdk4597E1fPS5+pUQV8+p215wEYFvc1mJK/eK?= =?us-ascii?Q?Pz4zI6E3ku7pXx2fpLiw45oo/BX1MPAjnTcSG2IVqrbWvYBed5mWkSP1/P8N?= =?us-ascii?Q?BMcpbmEp2l7ahUEruiLrRms4a7Sh6kpyAwOxllDC1/fxTz8oCNW/YgGQcQEO?= =?us-ascii?Q?Notu5zzwg/aKR+1EMFqdG80MzWXCZ9nxMOfP7mlyCkkhgetB5ShQPTlYxCbz?= =?us-ascii?Q?nibz/8b2tht/zvTuZ1jDBi4f2Bo5NrwDO2z/fv3WImybAJZ+zsFngDfeReon?= =?us-ascii?Q?rQFWQ9/xOYvZf9ezqNu67Fj8yBf7ezVqmbAUZDFc9SCLRQXDsAkAC0Fw7jw0?= =?us-ascii?Q?WbIJZEVlickMGmsx1U+BqLvwFPDDRW031J6uB9O8HmdnFwXNW4cDQdap8SCR?= =?us-ascii?Q?pSwBWiuZ6/cIhCY4Br9wwVvGO/ALbVVLx+aObnVspeA/b4TuDwPIpoAgZkUi?= =?us-ascii?Q?S44TkVzVaK14gwSq3paGJwfD7ijkdEga4iwXF16xW/6DflMLgoHmj1M0tMt8?= =?us-ascii?Q?s8SLrbmbnSAfJw14g4hrgwlQk2muyfcq5+HL+Z8rBxlq7N53sRkK/+XzbnC0?= =?us-ascii?Q?OfQ/TAsQ2qiLwIZxlLxf00cO6KUxH272OnEcl+9Qo1w50XnSCBKSrTqlAXKd?= =?us-ascii?Q?VV4YsdXcIWzGOgi82GzqmFhsc4tmXYt4myc8NgCV5NydoUP+1CXp?= X-OriginatorOrg: groupe-cyllene.com X-MS-Exchange-CrossTenant-Network-Message-Id: ca26b179-b903-48c2-ac5a-08df029ce6a3 X-MS-Exchange-CrossTenant-AuthSource: PR0P264MB3691.FRAP264.PROD.OUTLOOK.COM X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Aug 2026 11:35:01.9386 (UTC) X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted X-MS-Exchange-CrossTenant-Id: ee11ccf7-112c-4284-848b-f229745e715b X-MS-Exchange-CrossTenant-MailboxType: HOSTED X-MS-Exchange-CrossTenant-UserPrincipalName: sPY4TB1zotpmdbySSCp9yAA5Cb7xZ3m2g/EsUB/0W8dx6l5uXQQgyx0k7BQ+ixJd2qrn+bJKHiUAoQOMX3cmQ2DE1FQLNt5xd5nvDyIb1MVVSXBUpZLQWNWZwJ9A4Qqa X-MS-Exchange-Transport-CrossTenantHeadersStamped: PATP264MB7783 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.534 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DMARC_PASS -0.1 DMARC pass policy KAM_ASCII_DIVIDERS 0.8 Email that uses ascii formatting dividers and possible spam tricks KAM_LOTSOFHASH 0.25 Emails with lots of hash-like gibberish POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_PASS -0.001 SPF: HELO matches SPF record SPF_PASS -0.001 SPF: sender matches SPF record X-MailFrom: Alexandre.DERUMIER@groupe-cyllene.com X-Mailman-Rule-Hits: max-size X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; news-moderation; no-subject; digests; suspicious-header Message-ID-Hash: DTNRCNMFYPV6HNGCXE4YG5YFN3MVGHFH X-Message-ID-Hash: DTNRCNMFYPV6HNGCXE4YG5YFN3MVGHFH X-Mailman-Approved-At: Wed, 26 Aug 2026 11:28:46 +0200 X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Control plane: forwards the console's control plane from pveproxy to the VM's controller. Dataplane : terminates WebTransport on one UDP port, routing sessions by token; browser | +-------------------------+-------------------------+ | | control plane data plane HTTPS :8006 WebTransport (QUIC) /api2/json/nodes//qemu//kyber/ UDP :63100, per node | routed by the token v start_mux handed out +---------------------+ | | pveproxy | | | checks VM.Console | | +---------------------+ | | | | unix /run/pvekyberproxy.sock | v v +---------------------------------------------------------------+ | pvekyberproxy | | start_mux is answered with this daemon's port and its | | certificate hash, which is what sends the client here | +---------------------------------------------------------------+ | | | unix | QUIC | /run/qemu-server/.kyber.sock | 127.0.0.1:63000 v v +---------------------------------------------------------------+ | kycontroller (pve-kyber) | +---------------------------------------------------------------+ | spawns v kyavserver / kynputserver | spawn v kyber-qemu-server ---> kymux tcp 127.0.0.1:9091/0 video, /1 audio | | D-Bus unix /run/qemu-server/.dbusdisplay, org.qemu on v the private bus qemu-server starts for that VM QEMU -display dbus,addr=unix:path=... Signed-off-by: Alexandre Derumier --- .gitignore | 6 + Cargo.lock | 1870 +++++++++++++++++++ Cargo.toml | 53 + Makefile | 74 + debian/changelog | 5 + debian/control | 24 + debian/copyright | 20 + debian/install | 1 + debian/pve-kyberproxy.pvekyberproxy.service | 32 + debian/pvekyberproxy.default | 12 + debian/rules | 43 + debian/source/format | 1 + src/control.rs | 506 +++++ src/main.rs | 139 ++ src/relay.rs | 234 +++ src/webtransport.rs | 225 +++ 16 files changed, 3245 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/install create mode 100644 debian/pve-kyberproxy.pvekyberproxy.service create mode 100644 debian/pvekyberproxy.default create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 src/control.rs create mode 100644 src/main.rs create mode 100644 src/relay.rs create mode 100644 src/webtransport.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1d47f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target/ +/vendor/ +/pve-kyberproxy-[0-9]*/ +*.deb +*.buildinfo +*.changes diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b1ad2be --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1870 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "httlib-huffman" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9fcbcc408c5526c3ab80d534e5c86e7967c1fb7aa0a8c76abd1edc27deb877" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "octets" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866cb5af6f3aa3c1b44c3c2d79d22165fbb1b102e1b3fb499864bfe34736ec4b" + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pve-kyberproxy" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "http-body-util", + "hyper", + "hyper-util", + "libc", + "log", + "rcgen", + "serde", + "serde_json", + "tokio", + "tokio-rustls", + "wtransport", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rcgen" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wtransport" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036" +dependencies = [ + "bytes", + "pem", + "quinn", + "rcgen", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha2", + "socket2", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "wtransport-proto", + "x509-parser", +] + +[[package]] +name = "wtransport-proto" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad9059572c7dbd6901ccef37f3b7321678cd708dcf58a64b1921dbeab7bfede" +dependencies = [ + "httlib-huffman", + "octets", + "thiserror", + "url", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0f3807a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "pve-kyberproxy" +version = "0.1.0" +edition = "2021" +description = "QUIC front door for per-VM Kyber consoles on a Proxmox node" +license = "AGPL-3.0-or-later" + +[[bin]] +name = "pvekyberproxy" +path = "src/main.rs" + +[features] +# The unauthenticated TCP control plane, for working on this without a Proxmox +# around it. On by default so a local checkout builds what the run script +# expects; a package for a node is built with --no-default-features, and then +# the flag does not exist rather than merely being unused. Its dependencies - +# a TLS stack and a certificate generator - go with it. +default = ["dev-tcp", "webtransport"] +dev-tcp = ["dep:rcgen", "dep:tokio-rustls"] +# The single-port data plane. Off leaves the per-session UDP relay in place. +webtransport = ["dep:wtransport"] + +[dependencies] +# Nothing from Kyber. The relay moves datagrams and the control plane moves +# JSON, so neither half needs the protocol crates - which is the clearest sign +# that not terminating was the right call. + +anyhow = "1" +clap = { version = "4", features = ["derive", "env"] } +env_logger = "0.11" +http-body-util = "0.1" +hyper = { version = "1", features = ["server", "client", "http1"] } +hyper-util = { version = "0.1", features = ["tokio"] } +# getgrnam, to hand the control socket to pveproxy's group by name. Already in +# the tree as a transitive dependency; named here because we call it. +libc = "0.2" +log = "0.4" +# Self-signed TLS for --insecure-listen-tcp only. The client builds every +# control-plane URL as https:// with no way to say otherwise, so a plain +# listener there is not a lesser version of the real thing - it is one no +# client can talk to at all. ring rather than the default aws-lc-rs: it is a +# throwaway certificate on loopback, and ring needs no C toolchain to build. +rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem", "ring"], optional = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal", "sync", "time"] } +# A WebTransport server and client on quinn. Terminating is the price of one +# UDP port for the node: nothing in an encrypted datagram says which VM it is +# for, so the only way to share a port is to read the session's path, and the +# only way to read it is to decrypt. Default features are self-signed + ring, +# so no C toolchain and no CA. +wtransport = { version = "0.7", optional = true } +tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"], optional = true } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d86d080 --- /dev/null +++ b/Makefile @@ -0,0 +1,74 @@ +include /usr/share/dpkg/architecture.mk +include /usr/share/dpkg/pkg-info.mk + +PACKAGE=pve-kyberproxy +ARCH:=$(DEB_HOST_ARCH) + +DEB=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION)_$(ARCH).deb +DEB_DBG=$(PACKAGE)-dbgsym_$(DEB_VERSION_UPSTREAM_REVISION)_$(ARCH).deb + +DSC=$(PACKAGE)_$(DEB_VERSION).dsc +BUILDDIR=$(PACKAGE)-$(DEB_VERSION_UPSTREAM) + +all: $(DEB) + +# Dependencies are vendored rather than taken from librust-*-dev: the hyper 1.x +# stack this needs is not packaged in Debian, and a console proxy is not a good +# reason to ask for it to be. Same arrangement proxmox-biome uses. +.PHONY: vendor +vendor: + rm -rf vendor + cargo vendor --locked vendor + +.PHONY: builddir +builddir: + rm -rf $(BUILDDIR) + $(MAKE) $(BUILDDIR) + +$(BUILDDIR): vendor + rm -rf $@ $@.tmp + mkdir $@.tmp + cp -a src/ Cargo.toml Cargo.lock $@.tmp/ + cp -a debian/ $@.tmp/debian + cp -a vendor/ $@.tmp/vendor + mkdir -p $@.tmp/.cargo + printf '[source.crates-io]\nreplace-with = "vendored-sources"\n\n[source.vendored-sources]\ndirectory = "vendor"\n' \ + > $@.tmp/.cargo/config.toml + mv $@.tmp $@ + +.PHONY: deb +deb: $(DEB) +$(DEB) $(DEB_DBG) &: $(BUILDDIR) + cd $(BUILDDIR); dpkg-buildpackage -b -us -uc + lintian $(DEB) + +# A source package, for sbuild and for review: Proxmox builds every package +# this way, so it has to work even when the binary path is what gets used. +.PHONY: dsc +dsc: + rm -rf $(BUILDDIR) $(DSC) + $(MAKE) $(DSC) + lintian $(DSC) + +$(DSC): $(BUILDDIR) + cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d + +sbuild: $(DSC) + sbuild $< + +.PHONY: dinstall +dinstall: deb + dpkg -i $(DEB) + +.PHONY: upload +upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION) +upload: $(DEB) $(DEB_DBG) + tar cf - $(DEB) $(DEB_DBG) | ssh repoman@repo.proxmox.com -- upload --product pve --dist $(UPLOAD_DIST) --arch $(ARCH) + +.PHONY: clean +clean: + rm -rf *~ debian/*~ *.deb *.changes *.buildinfo $(PACKAGE)-[0-9]*/ vendor/ + cargo clean + +.PHONY: distclean +distclean: clean diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..30572a2 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +pve-kyberproxy (0.1.0) trixie; urgency=medium + + * Initial release: QUIC front door for per-VM Kyber consoles. + + -- Proxmox Support Team Tue, 18 Aug 2026 15:00:00 +0200 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..77404be --- /dev/null +++ b/debian/control @@ -0,0 +1,24 @@ +Source: pve-kyberproxy +Section: admin +Priority: optional +Maintainer: Proxmox Support Team +Uploaders: Alexandre Derumier +Build-Depends: cargo, + debhelper-compat (= 13), +Standards-Version: 4.7.0.0 + +Package: pve-kyberproxy +Architecture: any +Depends: ${misc:Depends}, + ${shlibs:Depends}, +Description: front door for Proxmox VE Kyber consoles + One daemon per node, in front of the per-VM Kyber console controllers. + . + It forwards the console's control plane, which arrives from pveproxy on a + unix socket, to the controller belonging to the VM named in the path, and + rewrites the one response that tells a client where its data plane lives. + . + The data plane itself is relayed rather than terminated: the daemon moves + UDP datagrams between the client and the controller, holding no key and + reassembling no stream, so the client's QUIC connection runs end to end and + stays one congestion-controlled path. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..8b5623f --- /dev/null +++ b/debian/copyright @@ -0,0 +1,20 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: pve-kyberproxy + +Files: * +Copyright: 2026 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..9c75aa2 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +target/release/pvekyberproxy usr/bin/ diff --git a/debian/pve-kyberproxy.pvekyberproxy.service b/debian/pve-kyberproxy.pvekyberproxy.service new file mode 100644 index 0000000..d9b1e86 --- /dev/null +++ b/debian/pve-kyberproxy.pvekyberproxy.service @@ -0,0 +1,32 @@ +[Unit] +Description=PVE Kyber Console Proxy +# One per node. It routes by path and by port, so nothing restarts it when a +# VM does. +Documentation=file:///usr/share/doc/pve-kyberproxy/kyber-proxy.md +After=network.target + +[Service] +Type=simple +# pveproxy has already authenticated the caller, so this socket's permissions +# are what stand between an unprivileged process and every console on the node. +# --insecure-listen-tcp must never appear here. +# Node-local rather than datacenter.cfg: what decides it is the node's +# firewall. Same arrangement as /etc/default/pveproxy. +EnvironmentFile=-/etc/default/pvekyberproxy + +ExecStart=/usr/bin/pvekyberproxy \ + --listen-socket /run/pvekyberproxy.sock \ + --run-dir /var/run/qemu-server \ + --socket-group www-data \ + $DATAPLANE_ARGS + +# pveproxy runs as www-data; the controllers' sockets are root's. +User=root +Group=root +RuntimeDirectory=pvekyberproxy + +Restart=on-failure +RestartSec=1 + +[Install] +WantedBy=multi-user.target diff --git a/debian/pvekyberproxy.default b/debian/pvekyberproxy.default new file mode 100644 index 0000000..2907be7 --- /dev/null +++ b/debian/pvekyberproxy.default @@ -0,0 +1,12 @@ +# Configuration for pvekyberproxy, the Kyber console's front door. +# +# DATAPLANE_ARGS chooses how console video reaches a browser. Uncomment one. +# Changing it restarts the daemon, which drops every open console. + +# One UDP port for the node: WebTransport is terminated here and each session +# routed by the token in its path. Needs the client from pve-kyber-web. +DATAPLANE_ARGS="--webtransport-port 63100" + +# Or relay datagrams unread, at one UDP port per open console - nothing in an +# encrypted datagram says which VM it is for, so the port is the routing key. +#DATAPLANE_ARGS="--relay-ports 63100-63150" diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..8deefff --- /dev/null +++ b/debian/rules @@ -0,0 +1,43 @@ +#!/usr/bin/make -f + +include /usr/share/dpkg/architecture.mk +include /usr/share/dpkg/pkg-info.mk + +#export DH_VERBOSE=1 + +%: + dh $@ + +override_dh_update_autotools_config: + +# The vendored crates carry Cargo.toml.orig, which cargo checksums and dh_clean +# would otherwise delete as build residue - taking the build with it. +override_dh_clean: + dh_clean -X Cargo.toml.orig + +# --no-default-features drops the dev-tcp feature, and with it +# --insecure-listen-tcp: an unauthenticated control plane has no business +# existing in a packaged binary, rather than merely being left out of the +# unit file. +override_dh_auto_build: + cargo build --release --no-default-features --features webtransport + +override_dh_auto_test: + cargo test --release --no-default-features --features webtransport + +override_dh_auto_clean: + cargo clean + +# The unit is pvekyberproxy.service, not pve-kyberproxy.service - PVE's +# services do not carry the dash its package name does (pveproxy, spiceproxy, +# pvedaemon). debhelper only finds a unit named after the package on its own, +# so the name has to be spelled out or the unit is silently left out of the +# .deb entirely. +override_dh_installsystemd: + dh_installsystemd --name=pvekyberproxy + +# A conffile, so a node's choice of data plane survives an upgrade. +override_dh_install: + dh_install + install -D -m 0644 debian/pvekyberproxy.default \ + debian/pve-kyberproxy/etc/default/pvekyberproxy diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/src/control.rs b/src/control.rs new file mode 100644 index 0000000..cd457fa --- /dev/null +++ b/src/control.rs @@ -0,0 +1,506 @@ +// The control plane, forwarded to the VM's controller. +// +// pveproxy hands this everything under /nodes/{node}/qemu/{vmid}/kyber/ over a +// unix socket, having already authenticated the caller and checked its ACL; +// nothing here re-checks that. +// +// Requests pass through untouched except start_mux, which would otherwise name +// a controller no browser can reach. /ws is a websocket rather than a +// request/response, so its upgrade is spliced instead of forwarded. + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; +use http_body_util::{BodyExt, Full}; +use hyper::body::{Bytes, Incoming}; +use hyper::header::{CONNECTION, UPGRADE}; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Request, Response, StatusCode}; +use hyper_util::rt::TokioIo; +use log::{debug, info, warn}; +use tokio::net::{UnixListener, UnixStream}; +#[cfg(feature = "dev-tcp")] +use tokio::net::TcpListener; +#[cfg(feature = "dev-tcp")] +use tokio_rustls::TlsAcceptor; + +use crate::relay; + +pub struct Control { + /// Where the per-VM controller sockets live. + pub run_dir: PathBuf, + /// UDP ports the relay may use, when a firewall needs them knowable. + pub relay_ports: Option, + /// The single-port data plane, when it is serving instead of the relay. + #[cfg(feature = "webtransport")] + pub gateway: Option>, +} + +impl Control { + fn controller_socket(&self, vmid: u32) -> PathBuf { + self.run_dir.join(format!("{vmid}.kyber.sock")) + } +} + +/// Split a proxied path into the VM it names and the controller path under it. +/// A path that does not match is a bug on our side, so it is refused. +fn split_path(path: &str) -> Option<(u32, String)> { + let rest = path.strip_prefix("/api2/json").unwrap_or(path); + let rest = rest.strip_prefix("/nodes/")?; + let (_node, rest) = rest.split_once('/')?; + let rest = rest.strip_prefix("qemu/")?; + let (vmid, rest) = rest.split_once('/')?; + let vmid: u32 = vmid.parse().ok()?; + let rest = rest.strip_prefix("kyber")?; + + if rest.is_empty() { + return Some((vmid, "/".to_string())); + } + if !rest.starts_with('/') { + return None; + } + + Some((vmid, rest.to_string())) +} + +/// Give the socket to one group and take it from everyone else. chown before +/// chmod, or it would be world-connectable for as long as the chown took. +fn restrict_socket(socket: &Path, group: &str) -> Result<()> { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + use std::os::unix::fs::PermissionsExt; + + let name = CString::new(group).context("group name")?; + // getgrnam rather than /etc/group: the answer can come from anywhere NSS + // is configured to ask, and on a node that is not always a file. + let entry = unsafe { libc::getgrnam(name.as_ptr()) }; + if entry.is_null() { + return Err(anyhow!("no group named '{group}' on this system")); + } + let gid = unsafe { (*entry).gr_gid }; + + let path = CString::new(socket.as_os_str().as_bytes()).context("socket path")?; + // -1 leaves the owner alone; only the group is being changed. + if unsafe { libc::chown(path.as_ptr(), libc::uid_t::MAX, gid) } != 0 { + return Err(std::io::Error::last_os_error()).context("chown"); + } + + std::fs::set_permissions(socket, std::fs::Permissions::from_mode(0o660)).context("chmod")?; + + Ok(()) +} + +pub async fn serve(control: Arc, socket: &Path, group: Option<&str>) -> Result<()> { + // A socket left by an unclean exit would make every start fail. + match std::fs::remove_file(socket) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err).context("removing a stale control socket"), + } + + let listener = UnixListener::bind(socket) + .with_context(|| format!("binding {}", socket.display()))?; + + // The whole of this socket's access control, and not optional: at the + // umask's default pveproxy cannot connect to it at all. + if let Some(group) = group { + restrict_socket(socket, group) + .with_context(|| format!("handing {} to group {group}", socket.display()))?; + } + + info!("control plane on {}", socket.display()); + + loop { + let (stream, _) = listener.accept().await.context("accepting")?; + let control = control.clone(); + + tokio::spawn(async move { + let service = service_fn(move |req| handle(control.clone(), req)); + // /ws is a websocket; without this the 101 is written and the + // connection dropped underneath it. + if let Err(err) = http1::Builder::new() + .serve_connection(TokioIo::new(stream), service) + .with_upgrades() + .await + { + debug!("control connection ended: {err}"); + } + }); + } +} + +/// The same service on a TCP address, for working without a Proxmox around it. +/// What it skips is authentication, not TLS: the client builds every +/// control-plane URL as https:// with no way to say otherwise. +#[cfg(feature = "dev-tcp")] +pub async fn serve_tcp( + control: Arc, + addr: std::net::SocketAddr, + tls: TlsAcceptor, +) -> Result<()> { + let listener = TcpListener::bind(addr) + .await + .with_context(|| format!("binding {addr}"))?; + info!("control plane on {addr}, unauthenticated"); + + loop { + let (stream, peer) = listener.accept().await.context("accepting")?; + let control = control.clone(); + let tls = tls.clone(); + + tokio::spawn(async move { + // Per connection: a failed handshake must not take the listener + // down, and one fails whenever a certificate is not yet accepted. + let stream = match tls.accept(stream).await { + Ok(stream) => stream, + Err(err) => { + debug!("TLS handshake with {peer} failed: {err}"); + return; + } + }; + + let service = service_fn(move |req| handle(control.clone(), req)); + if let Err(err) = http1::Builder::new() + .serve_connection(TokioIo::new(stream), service) + .with_upgrades() + .await + { + debug!("control connection ended: {err}"); + } + }); + } +} + +/// A throwaway certificate for the address above, generated rather than stored: +/// a key on disk that nothing needs is only something to leak. +#[cfg(feature = "dev-tcp")] +pub fn self_signed_tls(addr: std::net::SocketAddr) -> Result { + use tokio_rustls::rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer}; + use tokio_rustls::rustls::ServerConfig; + + let names = vec!["localhost".to_string(), addr.ip().to_string()]; + let certified = rcgen::generate_simple_self_signed(names) + .context("generating a certificate for the TCP control plane")?; + + let cert = certified.cert.der().clone(); + let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(certified.signing_key.serialize_der())); + + // Passed in rather than installed process-wide, which would be a global + // fact set by a development flag. + let provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()); + let mut config = ServerConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .context("selecting TLS versions")? + .with_no_client_auth() + .with_single_cert(vec![cert], key) + .context("building the TLS configuration")?; + + // hyper serves HTTP/1.1; say so or a browser may negotiate h2. + config.alpn_protocols = vec![b"http/1.1".to_vec()]; + + Ok(TlsAcceptor::from(Arc::new(config))) +} + +fn bad_request(msg: &str) -> Response> { + let mut response = Response::new(Full::new(Bytes::from(msg.to_string()))); + *response.status_mut() = StatusCode::BAD_REQUEST; + response +} + +fn bad_gateway(msg: String) -> Response> { + let mut response = Response::new(Full::new(Bytes::from(msg))); + *response.status_mut() = StatusCode::BAD_GATEWAY; + response +} + +/// Whether this request asks to stop speaking HTTP. Connection is a list and +/// its token is case-insensitive, so neither header compares whole. +fn wants_upgrade(req: &Request) -> bool { + let connection = req + .headers() + .get(CONNECTION) + .and_then(|value| value.to_str().ok()) + .map(|value| { + value + .split(',') + .any(|token| token.trim().eq_ignore_ascii_case("upgrade")) + }) + .unwrap_or(false); + + connection && req.headers().contains_key(UPGRADE) +} + +async fn handle( + control: Arc, + req: Request, +) -> Result>, std::convert::Infallible> { + let path = req.uri().path().to_string(); + + let Some((vmid, rest)) = split_path(&path) else { + warn!("control plane asked for a path it does not serve: {path}"); + return Ok(bad_request("not a Kyber console path\n")); + }; + + match forward(&control, vmid, &rest, req).await { + Ok(response) => Ok(response), + Err(err) => { + warn!("VM {vmid}: {rest} failed: {err:#}"); + Ok(bad_gateway(format!("{err:#}\n"))) + } + } +} + +async fn forward( + control: &Control, + vmid: u32, + rest: &str, + mut req: Request, +) -> Result>> { + let socket = control.controller_socket(vmid); + let stream = UnixStream::connect(&socket) + .await + .with_context(|| format!("no controller for VM {vmid} at {}", socket.display()))?; + + let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream)) + .await + .context("HTTP handshake with the controller")?; + + // Claimed before the request is taken apart. + let upgrading = wants_upgrade(&req); + let client_upgrade = upgrading.then(|| hyper::upgrade::on(&mut req)); + + if upgrading { + tokio::spawn(async move { + if let Err(err) = conn.with_upgrades().await { + debug!("controller connection ended: {err}"); + } + }); + } else { + tokio::spawn(async move { + if let Err(err) = conn.await { + debug!("controller connection ended: {err}"); + } + }); + } + + // Rebuild against the controller's path, query string included. + let target = match req.uri().query() { + Some(query) => format!("{rest}?{query}"), + None => rest.to_string(), + }; + + let (parts, body) = req.into_parts(); + let body = body + .collect() + .await + .context("reading the request body")? + .to_bytes(); + + let mut upstream = Request::builder() + .method(parts.method.clone()) + .uri(target) + .body(Full::new(body)) + .context("building the upstream request")?; + + // Everything the client sent, minus Host. + for (name, value) in parts.headers.iter() { + if name == hyper::header::HOST { + continue; + } + upstream.headers_mut().insert(name, value.clone()); + } + upstream + .headers_mut() + .insert(hyper::header::HOST, "localhost".parse().unwrap()); + + let mut response = sender + .send_request(upstream) + .await + .context("forwarding to the controller")?; + + // Once the controller agrees, this connection stops being HTTP: splice the + // two halves and copy until one stops. + if response.status() == StatusCode::SWITCHING_PROTOCOLS { + let Some(client_upgrade) = client_upgrade else { + return Err(anyhow!( + "the controller upgraded a request that did not ask to be upgraded" + )); + }; + let upstream_upgrade = hyper::upgrade::on(&mut response); + + tokio::spawn(async move { + let (client, controller) = match tokio::try_join!(client_upgrade, upstream_upgrade) { + Ok(pair) => pair, + Err(err) => { + debug!("VM {vmid}: websocket upgrade never completed: {err}"); + return; + } + }; + + let mut client = TokioIo::new(client); + let mut controller = TokioIo::new(controller); + match tokio::io::copy_bidirectional(&mut client, &mut controller).await { + Ok((to_controller, to_client)) => debug!( + "VM {vmid}: websocket closed after {to_controller} up, {to_client} down" + ), + Err(err) => debug!("VM {vmid}: websocket ended: {err}"), + } + }); + + let (parts, _) = response.into_parts(); + let mut out = Response::new(Full::new(Bytes::new())); + *out.status_mut() = parts.status; + // Every header: Sec-WebSocket-Accept is computed from a key we never + // saw, so the controller's own answer is what the client needs. + for (name, value) in parts.headers.iter() { + out.headers_mut().insert(name, value.clone()); + } + return Ok(out); + } + + let (parts, body) = response.into_parts(); + let body = body + .collect() + .await + .context("reading the controller's response")? + .to_bytes(); + + let body = if rest == "/kymux/start_mux" && parts.status.is_success() { + rewrite_start_mux( + vmid, + body, + control.relay_ports.as_ref(), + #[cfg(feature = "webtransport")] + control.gateway.as_ref(), + ) + .await? + } else { + body + }; + + let mut out = Response::new(Full::new(body)); + *out.status_mut() = parts.status; + for (name, value) in parts.headers.iter() { + // Length changes under the rewrite; hyper sets it again. + if name == hyper::header::CONTENT_LENGTH { + continue; + } + out.headers_mut().insert(name, value.clone()); + } + + Ok(out) +} + +/// Point the client at this daemon rather than at the controller. Relaying +/// replaces only the port, since TLS still runs to the controller; terminating +/// replaces the certificate hash too, because the server is then this daemon. +/// The controller reports its certificate hash as plain hex. +#[cfg(feature = "webtransport")] +fn parse_hash(text: &str) -> Option<[u8; 32]> { + let text = text.trim(); + if text.len() != 64 { + return None; + } + let mut out = [0u8; 32]; + for (i, byte) in out.iter_mut().enumerate() { + *byte = u8::from_str_radix(text.get(i * 2..i * 2 + 2)?, 16).ok()?; + } + Some(out) +} + +async fn rewrite_start_mux( + vmid: u32, + body: Bytes, + ports: Option<&relay::PortRange>, + #[cfg(feature = "webtransport")] gateway: Option<&std::sync::Arc>, +) -> Result { + let mut json: serde_json::Value = + serde_json::from_slice(&body).context("start_mux answered with something that is not JSON")?; + + let object = json + .as_object_mut() + .ok_or_else(|| anyhow!("start_mux answered with a JSON value that is not an object"))?; + + let upstream_port = object + .get("port") + .and_then(|port| port.as_u64()) + .ok_or_else(|| anyhow!("start_mux answered without a port"))?; + let upstream_port = + u16::try_from(upstream_port).context("start_mux answered with a port that is not one")?; + + let upstream = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), upstream_port); + + #[cfg(feature = "webtransport")] + if let Some(gateway) = gateway { + // One port for the node: the client is told this daemon's port and + // certificate, and routes itself by the token it already has. + let token = object + .get("token") + .and_then(|token| token.as_str()) + .ok_or_else(|| anyhow!("start_mux answered without a token"))? + .to_string(); + + let cert_hash = object + .get("certificate_hash") + .and_then(|hash| hash.get("hash")) + .and_then(|hash| hash.as_str()) + .and_then(parse_hash); + + gateway + .register( + token, + crate::webtransport::Upstream { + vmid, + addr: upstream, + cert_hash, + }, + ) + .await; + + object.insert("port".to_string(), serde_json::Value::from(gateway.port)); + if let Some(hash) = object + .get_mut("certificate_hash") + .and_then(|hash| hash.get_mut("hash")) + { + *hash = serde_json::Value::from(gateway.certificate_hash()); + } + + return Ok(Bytes::from(serde_json::to_vec(&json)?)); + } + + let session = relay::start(vmid, upstream, ports) + .await + .context("opening a relay for this session")?; + + object.insert("port".to_string(), serde_json::Value::from(session.port)); + + Ok(Bytes::from(serde_json::to_vec(&json)?)) +} + +#[cfg(test)] +mod tests { + use super::split_path; + + #[test] + fn splits_a_console_path() { + assert_eq!( + split_path("/api2/json/nodes/pve1/qemu/100/kyber/capabilities"), + Some((100, "/capabilities".to_string())) + ); + assert_eq!( + split_path("/nodes/pve1/qemu/100/kyber/kymux/start_mux"), + Some((100, "/kymux/start_mux".to_string())) + ); + } + + #[test] + fn refuses_anything_else() { + assert_eq!(split_path("/nodes/pve1/qemu/100/vncwebsocket"), None); + assert_eq!(split_path("/nodes/pve1/qemu/abc/kyber/x"), None); + // A prefix that only looks like ours. + assert_eq!(split_path("/nodes/pve1/qemu/100/kyberproxy"), None); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0f8638c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,139 @@ +// pvekyberproxy - the front door for Kyber consoles on a Proxmox node. +// +// One per node. The control plane arrives from pveproxy on a unix socket, the +// data plane over QUIC; both are forwarded to the VM's controller, which +// listens only on this machine. +// +// See docs/kyber-proxy.md for why it is shaped this way. + +mod control; +mod relay; +#[cfg(feature = "webtransport")] +mod webtransport; + +#[cfg(feature = "dev-tcp")] +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use log::info; +#[cfg(feature = "dev-tcp")] +use log::warn; + +#[derive(Debug, Parser)] +#[command(version, about = "QUIC front door for Kyber consoles")] +struct Cli { + /// Unix socket pveproxy forwards the control plane to. + #[arg(long, default_value = "/run/pvekyberproxy.sock")] + listen_socket: PathBuf, + + /// Where the per-VM controller sockets live. + #[arg(long, default_value = "/run/qemu-server")] + run_dir: PathBuf, + + /// Serve the data plane on one UDP port, by terminating WebTransport. + /// The alternative, --relay-ports, reads nothing but costs a port per + /// console. + #[cfg(feature = "webtransport")] + #[arg(long, value_name = "PORT", conflicts_with = "relay_ports")] + webtransport_port: Option, + + /// UDP ports the relay may use, as LOW-HIGH. One per open console, chosen + /// by the kernel when unset - which no firewall rule can express. + #[arg(long, value_name = "LOW-HIGH")] + relay_ports: Option, + + /// Group given access to the control socket. pveproxy runs as www-data and + /// cannot connect to the mode bind leaves. + #[arg(long, value_name = "GROUP")] + socket_group: Option, + + /// Also serve the control plane on a TCP address, with no authentication. + /// + /// For working without a Proxmox around it: anyone who can reach this can + /// open any console on the node. It still serves HTTPS, because the client + /// has no way to ask for anything else. + #[cfg(feature = "dev-tcp")] + #[arg(long, value_name = "ADDR")] + insecure_listen_tcp: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + let cli = Cli::parse(); + + if let Some(range) = &cli.relay_ports { + info!("data-plane relay confined to UDP {range}"); + } + + #[cfg(feature = "webtransport")] + let (gateway, gateway_config) = match cli.webtransport_port { + Some(port) => { + let (gateway, config) = webtransport::Gateway::new(port)?; + (Some(gateway), Some(config)) + } + None => (None, None), + }; + #[cfg(not(feature = "webtransport"))] + let gateway: Option = None; + + let control = Arc::new(control::Control { + run_dir: cli.run_dir, + relay_ports: cli.relay_ports, + #[cfg(feature = "webtransport")] + gateway: gateway.clone(), + }); + + #[cfg(feature = "webtransport")] + let data_plane = gateway + .clone() + .zip(gateway_config) + .map(|(gateway, config)| tokio::spawn(webtransport::serve(gateway, config))); + + #[cfg(not(feature = "dev-tcp"))] + let tcp: Option>> = None; + + #[cfg(feature = "dev-tcp")] + let tcp = match cli.insecure_listen_tcp { + None => None, + Some(addr) => { + warn!("serving the control plane on {addr} with NO authentication"); + // Before the listener, so a bad certificate stops the daemon here. + let tls = control::self_signed_tls(addr)?; + Some(tokio::spawn({ + let control = control.clone(); + async move { control::serve_tcp(control, addr, tls).await } + })) + } + }; + + tokio::select! { + result = async { + #[cfg(feature = "webtransport")] + match data_plane { + Some(task) => task.await.unwrap_or_else(|err| Err(anyhow!("{err}"))), + None => std::future::pending().await, + } + #[cfg(not(feature = "webtransport"))] + std::future::pending::>().await + } => result.context("data plane"), + result = control::serve(control.clone(), &cli.listen_socket, cli.socket_group.as_deref()) => { + result.context("control plane") + } + result = async { + match tcp { + Some(task) => task.await.unwrap_or_else(|err| Err(anyhow!("{err}"))), + // Nothing to wait for; leave the decision to the others. + None => std::future::pending().await, + } + } => result.context("TCP control plane"), + _ = tokio::signal::ctrl_c() => { + info!("stopping"); + Ok(()) + } + } +} diff --git a/src/relay.rs b/src/relay.rs new file mode 100644 index 0000000..286c9b1 --- /dev/null +++ b/src/relay.rs @@ -0,0 +1,234 @@ +// The data plane, relayed rather than terminated: datagrams in, datagrams out, +// no key held and no stream reassembled. +// +// Nothing in an encrypted QUIC packet says which VM it is for, so each session +// gets a port of its own and the port is the routing key. See webtransport.rs +// for the alternative, which spends a session's privacy for one port. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use log::{debug, info, warn}; +use tokio::net::UdpSocket; +use tokio::sync::Mutex; + +/// A console silent this long has gone; the client keeps alive well inside it. +const IDLE_TIMEOUT: Duration = Duration::from_secs(120); + +/// The UDP maximum, not the expected MTU: `recvfrom` truncates an oversized +/// datagram silently, and a QUIC packet with its tail cut off is discarded by +/// the peer with nothing logged anywhere. +const MAX_DATAGRAM: usize = 65535; + +pub struct Session { + /// The port the client was told to use. + pub port: u16, +} + +/// Open a relay from a fresh port to a VM's controller. +/// +/// Dual-stack: the browser picks the address family from DNS, so an IPv4-only +/// relay answers some clients and silently fails others. `range` exists for +/// firewalls, which cannot express an ephemeral port. +pub async fn start(vmid: u32, upstream: SocketAddr, range: Option<&PortRange>) -> Result { + let socket = bind_relay(range).await?; + let port = socket.local_addr().context("reading the relay port")?.port(); + + info!("VM {vmid}: relaying UDP {port} to {upstream}"); + + tokio::spawn(async move { + if let Err(err) = run(vmid, socket, upstream).await { + debug!("VM {vmid}: relay on {port} ended: {err:#}"); + } + info!("VM {vmid}: relay on {port} closed"); + }); + + Ok(Session { port }) +} + +/// A closed range of UDP ports the relay may use. +#[derive(Debug, Clone, Copy)] +pub struct PortRange { + pub low: u16, + pub high: u16, +} + +impl std::str::FromStr for PortRange { + type Err = anyhow::Error; + + fn from_str(text: &str) -> Result { + let (low, high) = text + .split_once('-') + .ok_or_else(|| anyhow::anyhow!("expected LOW-HIGH, got '{text}'"))?; + let low: u16 = low.trim().parse().context("low port")?; + let high: u16 = high.trim().parse().context("high port")?; + if low > high { + return Err(anyhow::anyhow!("port range {low}-{high} is backwards")); + } + Ok(Self { low, high }) + } +} + +impl std::fmt::Display for PortRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}", self.low, self.high) + } +} + +/// Bind the client-facing socket, dual-stack where the host allows it. +async fn bind_relay(range: Option<&PortRange>) -> Result { + let mut last: Option = None; + + match range { + None => { + // The kernel picks; try IPv6 first so the socket is dual-stack. + match UdpSocket::bind(("::", 0)).await { + Ok(socket) => return Ok(socket), + Err(err) => last = Some(err), + } + match UdpSocket::bind(("0.0.0.0", 0)).await { + Ok(socket) => { + warn!("relay bound IPv4-only: {}", last.as_ref().unwrap()); + return Ok(socket); + } + Err(err) => last = Some(err), + } + } + Some(range) => { + for port in range.low..=range.high { + match UdpSocket::bind(("::", port)).await { + Ok(socket) => return Ok(socket), + Err(err) => last = Some(err), + } + match UdpSocket::bind(("0.0.0.0", port)).await { + Ok(socket) => { + warn!("relay bound IPv4-only on {port}"); + return Ok(socket); + } + Err(err) => last = Some(err), + } + } + return Err(anyhow::anyhow!( + "no free port in {range}: {}", + last.map(|e| e.to_string()).unwrap_or_default() + )); + } + } + + Err(last.unwrap()).context("binding a relay port") +} + +async fn run(vmid: u32, client_side: UdpSocket, upstream: SocketAddr) -> Result<()> { + // A second socket, so replies are told apart by where they arrived. + // Matched to the controller's family; a v4-mapped socket cannot send to a + // v4 address it did not bind for. + let bind_addr = if upstream.is_ipv4() { "127.0.0.1" } else { "::1" }; + let upstream_side = UdpSocket::bind((bind_addr, 0)) + .await + .context("binding the controller side")?; + upstream_side + .connect(upstream) + .await + .context("pointing the controller side at the controller")?; + + let client_side = Arc::new(client_side); + let upstream_side = Arc::new(upstream_side); + + // Pinned at the first datagram. Following whoever spoke last would let + // anyone who can reach the port take the stream, and nothing here can + // authenticate a migration: path validation runs inside the encryption. + // A client that really does move reconnects instead. + let client_addr: Arc>> = Arc::new(Mutex::new(None)); + let last = Arc::new(Mutex::new(Instant::now())); + + let to_upstream = { + let client_side = client_side.clone(); + let upstream_side = upstream_side.clone(); + let client_addr = client_addr.clone(); + let last = last.clone(); + + async move { + let mut buf = vec![0u8; MAX_DATAGRAM]; + let mut turned_away: u64 = 0; + loop { + let (read, from) = client_side.recv_from(&mut buf).await?; + + { + let mut pinned = client_addr.lock().await; + match *pinned { + None => { + info!("VM {vmid}: relay bound to {from}"); + *pinned = Some(from); + } + Some(client) if client != from => { + // Not forwarded: the controller should not spend + // cycles decrypting a stranger's datagram. + turned_away += 1; + if turned_away.is_power_of_two() { + warn!( + "VM {vmid}: {turned_away} datagram(s) from {from} \ + ignored, this relay belongs to {client}" + ); + } + continue; + } + Some(_) => {} + } + } + + *last.lock().await = Instant::now(); + upstream_side.send(&buf[..read]).await?; + } + #[allow(unreachable_code)] + Ok::<(), std::io::Error>(()) + } + }; + + let to_client = { + let client_side = client_side.clone(); + let upstream_side = upstream_side.clone(); + let client_addr = client_addr.clone(); + let last = last.clone(); + + async move { + let mut buf = vec![0u8; MAX_DATAGRAM]; + loop { + let read = upstream_side.recv(&mut buf).await?; + *last.lock().await = Instant::now(); + let Some(addr) = *client_addr.lock().await else { + // The controller spoke first, which it cannot do: it has + // nothing to say until a client has said something. + continue; + }; + client_side.send_to(&buf[..read], addr).await?; + } + #[allow(unreachable_code)] + Ok::<(), std::io::Error>(()) + } + }; + + // No session state, so silence is the only end-of-console signal. + tokio::select! { + result = to_upstream => result.context("client side")?, + result = to_client => result.context("controller side")?, + _ = idle(last.clone()) => { + debug!("VM {vmid}: relay idle"); + } + } + + Ok(()) +} + +/// Resolves once nothing has crossed either way for IDLE_TIMEOUT. A timestamp +/// rather than a peek at the socket, which two tasks already hold. +async fn idle(last: Arc>) { + loop { + let elapsed = last.lock().await.elapsed(); + if elapsed >= IDLE_TIMEOUT { + return; + } + tokio::time::sleep(IDLE_TIMEOUT - elapsed).await; + } +} diff --git a/src/webtransport.rs b/src/webtransport.rs new file mode 100644 index 0000000..a284362 --- /dev/null +++ b/src/webtransport.rs @@ -0,0 +1,225 @@ +// The data plane on one UDP port, by terminating WebTransport instead of +// relaying datagrams. +// +// Sharing a port means reading the session's path, which means decrypting. +// That splits one congestion-controlled path into two, which matters in front +// of a distant backend and not in front of loopback. +// +// Sessions are routed by the token the controller already minted: the proxy +// sees it when it rewrites start_mux, and the client sends it in the path. + +use std::collections::HashMap; +use std::time::Duration; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; +use log::{debug, info, warn}; +use tokio::sync::Mutex; +use wtransport::endpoint::IncomingSession; +use wtransport::tls::{Sha256Digest, Sha256DigestFmt}; +use wtransport::{ClientConfig, Endpoint, Identity, ServerConfig}; + +/// Keep-alive defaults to off in quinn, and a console encodes almost nothing +/// while its screen is still - so without this a connection carrying a static +/// desktop idles out and the console drops for no visible reason. +const KEEP_ALIVE: Duration = Duration::from_secs(5); + +/// Long enough to ride out a lost keep-alive or two, short enough that a dead +/// peer is noticed. +const IDLE_TIMEOUT: Duration = Duration::from_secs(30); + +/// Where a session's traffic is going, learned from the controller's own +/// answer to start_mux. +#[derive(Clone)] +pub struct Upstream { + pub vmid: u32, + pub addr: SocketAddr, + /// The controller's certificate hash, as it reported it. + pub cert_hash: Option<[u8; 32]>, +} + +pub struct Gateway { + /// The one port every console arrives on. + pub port: u16, + cert_hash: String, + sessions: Mutex>, +} + +impl Gateway { + /// A certificate of this daemon's own, and its hash for start_mux. The + /// client pins by hash, so no CA is involved. + pub fn new(port: u16) -> Result<(Arc, ServerConfig)> { + let identity = + Identity::self_signed(["localhost", "127.0.0.1", "::1"]).context("certificate")?; + + // The client wants plain hex; wtransport formats it dotted. + let cert_hash = identity + .certificate_chain() + .as_slice() + .first() + .ok_or_else(|| anyhow!("a certificate with no certificate in it"))? + .hash() + .fmt(Sha256DigestFmt::DottedHex) + .replace(':', ""); + + let config = ServerConfig::builder() + .with_bind_default(port) + .with_identity(identity) + .keep_alive_interval(Some(KEEP_ALIVE)) + .max_idle_timeout(Some(IDLE_TIMEOUT)) + .context("idle timeout")? + .build(); + + Ok(( + Arc::new(Self { + port, + cert_hash, + sessions: Mutex::new(HashMap::new()), + }), + config, + )) + } + + pub fn certificate_hash(&self) -> &str { + &self.cert_hash + } + + /// Remember where a token leads, so its session can be routed. + pub async fn register(&self, token: String, upstream: Upstream) { + let mut sessions = self.sessions.lock().await; + info!( + "VM {}: session registered for {} on the shared port", + upstream.vmid, upstream.addr + ); + sessions.insert(token, upstream); + } + + async fn lookup(&self, token: &str) -> Option { + self.sessions.lock().await.get(token).cloned() + } +} + +pub async fn serve(gateway: Arc, config: ServerConfig) -> Result<()> { + let endpoint = Endpoint::server(config).context("binding the WebTransport endpoint")?; + info!( + "data plane on UDP {} (WebTransport), certificate {}", + gateway.port, + &gateway.cert_hash[..16] + ); + + loop { + let incoming = endpoint.accept().await; + let gateway = gateway.clone(); + tokio::spawn(async move { + if let Err(err) = accept(gateway, incoming).await { + info!("session ended: {err:#}"); + } + }); + } +} + +async fn accept(gateway: Arc, incoming: IncomingSession) -> Result<()> { + let request = incoming.await.context("awaiting the session request")?; + + // A token this daemon did not issue is refused: start_mux registers every + // console before the client is told where to go. + let token = request.path().trim_start_matches('/').to_string(); + let Some(upstream) = gateway.lookup(&token).await else { + warn!("a session arrived with a token this proxy did not issue"); + request.not_found().await; + return Ok(()); + }; + + let vmid = upstream.vmid; + let client = request.accept().await.context("accepting the session")?; + + let controller = connect_upstream(&upstream) + .await + .with_context(|| format!("VM {vmid}: connecting to the controller"))?; + + info!("VM {vmid}: session open"); + let result = pipe(client, controller).await; + info!("VM {vmid}: session closed"); + result +} + +async fn connect_upstream(upstream: &Upstream) -> Result { + // Pinned by the hash the controller reported - the one the client would + // have pinned directly. No hash means nothing to pin, so refuse. + let hash = upstream + .cert_hash + .ok_or_else(|| anyhow!("the controller reported no certificate to pin"))?; + // Loopback, not the wildcard with_bind_default picks: exactly one port + // should be reachable from the network. + let config = ClientConfig::builder() + .with_bind_address(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)) + .with_server_certificate_hashes([Sha256Digest::new(hash)]) + .keep_alive_interval(Some(KEEP_ALIVE)) + .max_idle_timeout(Some(IDLE_TIMEOUT)) + .context("idle timeout")? + .build(); + + let endpoint = Endpoint::client(config).context("client endpoint")?; + let url = format!("https://{}/", upstream.addr); + endpoint.connect(url).await.context("connect").map_err(Into::into) +} + +/// Copy streams and datagrams both ways without reading them: kyproto stays a +/// conversation between the client and the controller. +async fn pipe(client: wtransport::Connection, controller: wtransport::Connection) -> Result<()> { + let client = Arc::new(client); + let controller = Arc::new(controller); + + let datagrams_up = forward_datagrams(client.clone(), controller.clone()); + let datagrams_down = forward_datagrams(controller.clone(), client.clone()); + let bidi_up = forward_bidi(client.clone(), controller.clone()); + let bidi_down = forward_bidi(controller.clone(), client.clone()); + let uni_up = forward_uni(client.clone(), controller.clone()); + let uni_down = forward_uni(controller.clone(), client.clone()); + + // Either side finishing ends the session. + tokio::select! { + result = datagrams_up => result.context("datagrams to the controller"), + result = datagrams_down => result.context("datagrams to the client"), + result = bidi_up => result.context("streams to the controller"), + result = bidi_down => result.context("streams to the client"), + result = uni_up => result.context("one-way streams to the controller"), + result = uni_down => result.context("one-way streams to the client"), + } +} + +async fn forward_datagrams(from: Arc, to: Arc) -> Result<()> { + loop { + let datagram = from.receive_datagram().await?; + to.send_datagram(datagram.payload())?; + } +} + +async fn forward_bidi(from: Arc, to: Arc) -> Result<()> { + loop { + let (mut from_send, mut from_recv) = from.accept_bi().await?; + let (mut to_send, mut to_recv) = to.open_bi().await?.await?; + + tokio::spawn(async move { + let up = tokio::io::copy(&mut from_recv, &mut to_send); + let down = tokio::io::copy(&mut to_recv, &mut from_send); + if let Err(err) = tokio::try_join!(up, down) { + debug!("stream ended: {err}"); + } + }); + } +} + +async fn forward_uni(from: Arc, to: Arc) -> Result<()> { + loop { + let mut from_recv = from.accept_uni().await?; + let mut to_send = to.open_uni().await?.await?; + + tokio::spawn(async move { + if let Err(err) = tokio::io::copy(&mut from_recv, &mut to_send).await { + debug!("one-way stream ended: {err}"); + } + }); + } +} -- 2.55.0