Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Created March 5, 2026 03:37
Show Gist options
  • Select an option

  • Save masakielastic/da25e5e77c912f5785df24bb7438dc4d to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/da25e5e77c912f5785df24bb7438dc4d to your computer and use it in GitHub Desktop.
QUIC Initial Packet Parser (PHP)

QUIC Initial Packet Parser (PHP)

この Gist は、QUIC Initial パケットの Long Header 部分だけを教育目的でパースする最小実装です。

  • 実装: QuicInitialPacketParser.php
  • 実行例: example.php

example.php がやっていること

example.php は次の手順で動作します。

  1. 16進文字列を用意する
  2. hex2bin() で UDP データグラムのバイナリに変換する
  3. QuicInitialPacketParserparse() を実行する
  4. 結果の連想配列を print_r() で表示する

対象のサンプル入力:

$hex = 'c0000000010811223344556677880899aabbccddeeff00000501deadbeef';

実行方法

php example.php

実行結果の意味

出力例:

Array
(
    [type] => initial
    [header_form] => 1
    [fixed_bit] => 1
    [long_packet_type] => 0
    [version] => 1
    [dcid_length] => 8
    [dcid] => 1122334455667788
    [scid_length] => 8
    [scid] => 99aabbccddeeff00
    [token_length] => 0
    [token] =>
    [length] => 5
    [packet_number] => 1
    [payload_length] => 4
    [remaining_payload_length] => 4
)

各フィールドの意味:

  • type: パケット種別。Initial と判定されたことを示します。
  • header_form: QUIC Header Form ビット。1 は Long Header。
  • fixed_bit: QUIC Fixed Bit。1 である必要があります。
  • long_packet_type: Long Header の種別。0 は Initial。
  • version: QUIC Version(このサンプルでは 1)。
  • dcid_length: Destination Connection ID の長さ(バイト)。
  • dcid: Destination Connection ID(16進文字列)。
  • scid_length: Source Connection ID の長さ(バイト)。
  • scid: Source Connection ID(16進文字列)。
  • token_length: Token の長さ(QUIC varint で読み取り)。
  • token: Token 本体(16進文字列)。このサンプルは長さ 0。
  • length: QUIC Length フィールドの値。Packet Number 長 + payload 長を含みます。
  • packet_number: Packet Number(この実装ではヘッダ下位2bitで長さを決定して読み取り)。
  • payload_length: length - packet_number_length で計算した宣言上の payload 長。
  • remaining_payload_length: 実際にヘッダ読取後にバッファに残っているバイト数。

このサンプルでは payload_lengthremaining_payload_length がどちらも 4 で一致し、 Length フィールドの内容と実データの整合が取れていることを確認できます。

注意

この実装は教育用の最小パーサーです。以下は未実装です。

  • Header Protection の解除
  • AEAD 復号
  • TLS CRYPTO frame の解析
  • パケット結合(coalescing)対応
declare(strict_types=1);
require __DIR__ . '/QuicInitialPacketParser.php';
$hex = 'c0000000010811223344556677880899aabbccddeeff00000501deadbeef';
$packet = hex2bin($hex);
if ($packet === false) {
throw new RuntimeException('Invalid hex string.');
}
$parser = new QuicInitialPacketParser();
$result = $parser->parse($packet);
print_r($result);
<?php
declare(strict_types=1);
final class QuicInitialPacketParser
{
private string $buffer = '';
private int $offset = 0;
public function parse(string $packet): array
{
$this->buffer = $packet;
$this->offset = 0;
$firstByte = $this->readUint8();
$headerForm = ($firstByte >> 7) & 0x01;
$fixedBit = ($firstByte >> 6) & 0x01;
$longPacketType = ($firstByte >> 4) & 0x03;
$packetNumberLength = ($firstByte & 0x03) + 1;
if ($headerForm !== 1) {
throw new InvalidArgumentException('Not a QUIC long header packet (Header Form must be 1).');
}
if ($fixedBit !== 1) {
throw new InvalidArgumentException('Invalid QUIC packet (Fixed Bit must be 1).');
}
if ($longPacketType !== 0) {
throw new InvalidArgumentException('Long header packet is not Initial (Long Packet Type must be 0).');
}
$version = $this->readUint32();
$dcidLength = $this->readUint8();
if ($dcidLength > 20) {
throw new InvalidArgumentException('Destination Connection ID length exceeds 20 bytes.');
}
$dcid = $this->readBytes($dcidLength);
$scidLength = $this->readUint8();
if ($scidLength > 20) {
throw new InvalidArgumentException('Source Connection ID length exceeds 20 bytes.');
}
$scid = $this->readBytes($scidLength);
$tokenLength = $this->readVarInt();
$token = $this->readBytes($tokenLength);
// RFC 9000: Length includes Packet Number field + payload.
$length = $this->readVarInt();
if ($length < $packetNumberLength) {
throw new InvalidArgumentException('Invalid Length field: smaller than Packet Number length.');
}
$packetNumberRaw = $this->readBytes($packetNumberLength);
$packetNumber = $this->bytesToInt($packetNumberRaw);
$remainingPayloadLength = strlen($this->buffer) - $this->offset;
$declaredPayloadLength = $length - $packetNumberLength;
return [
'type' => 'initial',
'header_form' => $headerForm,
'fixed_bit' => $fixedBit,
'long_packet_type' => $longPacketType,
'version' => $version,
'dcid_length' => $dcidLength,
'dcid' => bin2hex($dcid),
'scid_length' => $scidLength,
'scid' => bin2hex($scid),
'token_length' => $tokenLength,
'token' => bin2hex($token),
'length' => $length,
'packet_number' => $packetNumber,
'payload_length' => $declaredPayloadLength,
'remaining_payload_length' => $remainingPayloadLength,
];
}
private function readUint8(): int
{
$this->ensureAvailable(1);
return ord($this->buffer[$this->offset++]);
}
private function readUint32(): int
{
$bytes = $this->readBytes(4);
$parts = unpack('Nvalue', $bytes);
return (int) $parts['value'];
}
private function readBytes(int $n): string
{
if ($n < 0) {
throw new InvalidArgumentException('Cannot read a negative number of bytes.');
}
$this->ensureAvailable($n);
$chunk = substr($this->buffer, $this->offset, $n);
$this->offset += $n;
return $chunk;
}
private function readVarInt(): int
{
$first = $this->readUint8();
$prefix = $first >> 6;
$length = 1 << $prefix;
$value = $first & 0x3f;
for ($i = 1; $i < $length; $i++) {
$value = ($value << 8) | $this->readUint8();
}
return $value;
}
private function bytesToInt(string $bytes): int
{
$value = 0;
$len = strlen($bytes);
for ($i = 0; $i < $len; $i++) {
$value = ($value << 8) | ord($bytes[$i]);
}
return $value;
}
private function ensureAvailable(int $needed): void
{
$remaining = strlen($this->buffer) - $this->offset;
if ($remaining < $needed) {
throw new InvalidArgumentException(
sprintf('Packet too short: needed %d byte(s), but only %d remain at offset %d.', $needed, $remaining, $this->offset)
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment