feat(protocol): carry complete encoded media frames
This commit is contained in:
@@ -87,6 +87,7 @@ def check_text_boundaries() -> None:
|
||||
ROOT / "openapi/control-v1.yaml",
|
||||
ROOT / "proto/versevdi/control/v1/control.proto",
|
||||
ROOT / "frames/datagram-v1.md",
|
||||
ROOT / "frames/datagram-v2.md",
|
||||
ROOT / "frames/registry.json",
|
||||
ROOT / "registries/features.json",
|
||||
ROOT / "registries/datagrams.json",
|
||||
|
||||
@@ -14,11 +14,6 @@ import (
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
datagramHeaderBytes = 21
|
||||
maximumFrameBytes = 65536
|
||||
)
|
||||
|
||||
func main() {
|
||||
entries, err := os.ReadDir("fixtures/conformance")
|
||||
if err != nil {
|
||||
@@ -320,16 +315,24 @@ func classifyDatagram(encoded string) string {
|
||||
if err != nil {
|
||||
return "invalid:hex"
|
||||
}
|
||||
if len(raw) < datagramHeaderBytes {
|
||||
if len(raw) < 3 {
|
||||
return "invalid:truncated"
|
||||
}
|
||||
if string(raw[:2]) != "VD" {
|
||||
return "invalid:magic"
|
||||
}
|
||||
if raw[2] != 1 {
|
||||
if raw[2] != 1 && raw[2] != 2 {
|
||||
return "invalid:unsupported_version"
|
||||
}
|
||||
headerBytes := 21
|
||||
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||
if raw[2] == 2 {
|
||||
headerBytes = 23
|
||||
limits = map[byte]int{10: 1177, 11: 1177}
|
||||
}
|
||||
if len(raw) < headerBytes {
|
||||
return "invalid:truncated"
|
||||
}
|
||||
limit, ok := limits[raw[3]]
|
||||
if !ok {
|
||||
return "invalid:unknown_channel"
|
||||
@@ -337,17 +340,27 @@ func classifyDatagram(encoded string) string {
|
||||
if raw[4] != 0 {
|
||||
return "invalid:flags"
|
||||
}
|
||||
if raw[18] == 0 || raw[17] >= raw[18] {
|
||||
fragmentIndex, fragmentCount := int(raw[17]), int(raw[18])
|
||||
payloadOffset := 19
|
||||
if raw[2] == 2 {
|
||||
fragmentIndex = int(raw[17])<<8 | int(raw[18])
|
||||
fragmentCount = int(raw[19])<<8 | int(raw[20])
|
||||
payloadOffset = 21
|
||||
if fragmentCount > 891 {
|
||||
return "invalid:fragment_limit"
|
||||
}
|
||||
}
|
||||
if fragmentCount == 0 || fragmentIndex >= fragmentCount {
|
||||
return "invalid:fragment"
|
||||
}
|
||||
payloadLength := int(raw[19])<<8 | int(raw[20])
|
||||
payloadLength := int(raw[payloadOffset])<<8 | int(raw[payloadOffset+1])
|
||||
if payloadLength > limit {
|
||||
return "invalid:payload_limit"
|
||||
}
|
||||
if len(raw) != datagramHeaderBytes+payloadLength {
|
||||
if len(raw) != headerBytes+payloadLength {
|
||||
return "invalid:length_mismatch"
|
||||
}
|
||||
if len(raw) > maximumFrameBytes {
|
||||
if raw[2] == 1 && len(raw) > 65536 || raw[2] == 2 && len(raw) > 1200 {
|
||||
return "invalid:frame_limit"
|
||||
}
|
||||
return "valid"
|
||||
|
||||
+25
-11
@@ -189,36 +189,50 @@ fn classify_datagram(encoded: &str) -> &'static str {
|
||||
Some(raw) => raw,
|
||||
None => return "invalid:hex",
|
||||
};
|
||||
if raw.len() < 21 {
|
||||
if raw.len() < 3 {
|
||||
return "invalid:truncated";
|
||||
}
|
||||
if raw[0..2] != *b"VD" {
|
||||
return "invalid:magic";
|
||||
}
|
||||
if raw[2] != 1 {
|
||||
if raw[2] != 1 && raw[2] != 2 {
|
||||
return "invalid:unsupported_version";
|
||||
}
|
||||
let limit = match raw[3] {
|
||||
1 => 1024,
|
||||
2 => 2048,
|
||||
3 => 65515,
|
||||
10 | 11 | 12 => 1179,
|
||||
let header_bytes = if raw[2] == 1 { 21 } else { 23 };
|
||||
if raw.len() < header_bytes {
|
||||
return "invalid:truncated";
|
||||
}
|
||||
let limit = match (raw[2], raw[3]) {
|
||||
(1, 1) => 1024,
|
||||
(1, 2) => 2048,
|
||||
(1, 3) => 65515,
|
||||
(1, 10 | 11 | 12) => 1179,
|
||||
(2, 10 | 11) => 1177,
|
||||
_ => return "invalid:unknown_channel",
|
||||
};
|
||||
if raw[4] != 0 {
|
||||
return "invalid:flags";
|
||||
}
|
||||
if raw[18] == 0 || raw[17] >= raw[18] {
|
||||
let (fragment_index, fragment_count, payload_offset) = if raw[2] == 1 {
|
||||
(raw[17] as usize, raw[18] as usize, 19)
|
||||
} else {
|
||||
let count = ((raw[19] as usize) << 8) | raw[20] as usize;
|
||||
if count > 891 {
|
||||
return "invalid:fragment_limit";
|
||||
}
|
||||
((((raw[17] as usize) << 8) | raw[18] as usize), count, 21)
|
||||
};
|
||||
if fragment_count == 0 || fragment_index >= fragment_count {
|
||||
return "invalid:fragment";
|
||||
}
|
||||
let payload_length = ((raw[19] as usize) << 8) | raw[20] as usize;
|
||||
let payload_length = ((raw[payload_offset] as usize) << 8) | raw[payload_offset + 1] as usize;
|
||||
if payload_length > limit {
|
||||
return "invalid:payload_limit";
|
||||
}
|
||||
if raw.len() != 21 + payload_length {
|
||||
if raw.len() != header_bytes + payload_length {
|
||||
return "invalid:length_mismatch";
|
||||
}
|
||||
if raw.len() > 65536 {
|
||||
if raw[2] == 1 && raw.len() > 65536 || raw[2] == 2 && raw.len() > 1200 {
|
||||
return "invalid:frame_limit";
|
||||
}
|
||||
"valid"
|
||||
|
||||
@@ -129,23 +129,30 @@ func validFECStatus(_ body: [UInt8]) -> Bool {
|
||||
|
||||
func classifyDatagram(_ encoded: String) -> String {
|
||||
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
||||
guard raw.count >= 21 else { return "invalid:truncated" }
|
||||
guard raw.count >= 3 else { return "invalid:truncated" }
|
||||
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
||||
guard raw[2] == 1 else { return "invalid:unsupported_version" }
|
||||
guard raw[2] == 1 || raw[2] == 2 else { return "invalid:unsupported_version" }
|
||||
let headerBytes = raw[2] == 1 ? 21 : 23
|
||||
guard raw.count >= headerBytes else { return "invalid:truncated" }
|
||||
let limit: Int
|
||||
switch raw[3] {
|
||||
case 1: limit = 1024
|
||||
case 2: limit = 2048
|
||||
case 3: limit = 65515
|
||||
case 10, 11, 12: limit = 1179
|
||||
switch (raw[2], raw[3]) {
|
||||
case (1, 1): limit = 1024
|
||||
case (1, 2): limit = 2048
|
||||
case (1, 3): limit = 65515
|
||||
case (1, 10), (1, 11), (1, 12): limit = 1179
|
||||
case (2, 10), (2, 11): limit = 1177
|
||||
default: return "invalid:unknown_channel"
|
||||
}
|
||||
guard raw[4] == 0 else { return "invalid:flags" }
|
||||
guard raw[18] > 0 && raw[17] < raw[18] else { return "invalid:fragment" }
|
||||
let payloadLength = Int(raw[19]) * 256 + Int(raw[20])
|
||||
let fragmentIndex = raw[2] == 1 ? Int(raw[17]) : Int(raw[17]) * 256 + Int(raw[18])
|
||||
let fragmentCount = raw[2] == 1 ? Int(raw[18]) : Int(raw[19]) * 256 + Int(raw[20])
|
||||
if raw[2] == 2 && fragmentCount > 891 { return "invalid:fragment_limit" }
|
||||
guard fragmentCount > 0 && fragmentIndex < fragmentCount else { return "invalid:fragment" }
|
||||
let payloadOffset = raw[2] == 1 ? 19 : 21
|
||||
let payloadLength = Int(raw[payloadOffset]) * 256 + Int(raw[payloadOffset + 1])
|
||||
guard payloadLength <= limit else { return "invalid:payload_limit" }
|
||||
guard raw.count == 21 + payloadLength else { return "invalid:length_mismatch" }
|
||||
guard raw.count <= 65536 else { return "invalid:frame_limit" }
|
||||
guard raw.count == headerBytes + payloadLength else { return "invalid:length_mismatch" }
|
||||
guard raw[2] == 1 ? raw.count <= 65536 : raw.count <= 1200 else { return "invalid:frame_limit" }
|
||||
return "valid"
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ TEXT_PATHS = (
|
||||
"proto/versevdi/control/v1/control.proto",
|
||||
"proto/versevdi/tunnel/v1/tunnel.proto",
|
||||
"frames/datagram-v1.md",
|
||||
"frames/datagram-v2.md",
|
||||
"frames/registry.json",
|
||||
"registries/features.json",
|
||||
"registries/datagrams.json",
|
||||
|
||||
@@ -35,6 +35,17 @@ let capability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
)
|
||||
_ = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
)
|
||||
do {
|
||||
_ = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v3", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
)
|
||||
fatalError("unregistered framing was accepted")
|
||||
} catch { }
|
||||
let request = try TunnelAdmissionRequest(
|
||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
||||
@@ -162,6 +173,14 @@ fn main() {
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
).unwrap();
|
||||
assert!(CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v2".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
).is_ok());
|
||||
assert!(CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v3".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
).is_err());
|
||||
let request = TunnelAdmissionRequest::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||
|
||||
+2
-1
@@ -31,9 +31,10 @@ def main() -> int:
|
||||
value = json.loads((ROOT / registry).read_text(encoding="utf-8"))
|
||||
entries = value.get("features", value.get("datagrams"))
|
||||
assert entries and len({entry["id"] for entry in entries}) == len(entries)
|
||||
maximum_bound = 1_048_576 if registry.endswith("datagrams.json") else 65_536
|
||||
for entry in entries:
|
||||
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
|
||||
assert isinstance(maximum, int) and 1 <= maximum <= 65536
|
||||
assert isinstance(maximum, int) and 1 <= maximum <= maximum_bound
|
||||
|
||||
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
|
||||
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
|
||||
|
||||
+33
-22
@@ -7,47 +7,58 @@ import binascii
|
||||
import pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
HEADER_BYTES = 21
|
||||
MAX_FRAME_BYTES = 65536
|
||||
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||
V1_CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||
V2_CHANNEL_LIMITS = {10: 1177, 11: 1177}
|
||||
|
||||
|
||||
def classify(raw: bytes) -> str:
|
||||
if len(raw) < HEADER_BYTES:
|
||||
if len(raw) < 3:
|
||||
return "invalid:truncated"
|
||||
if raw[:2] != b"VD":
|
||||
return "invalid:magic"
|
||||
if raw[2] != 1:
|
||||
if raw[2] not in (1, 2):
|
||||
return "invalid:unsupported_version"
|
||||
if raw[3] not in CHANNEL_LIMITS:
|
||||
header_bytes = 21 if raw[2] == 1 else 23
|
||||
if len(raw) < header_bytes:
|
||||
return "invalid:truncated"
|
||||
limits = V1_CHANNEL_LIMITS if raw[2] == 1 else V2_CHANNEL_LIMITS
|
||||
if raw[3] not in limits:
|
||||
return "invalid:unknown_channel"
|
||||
if raw[4] != 0:
|
||||
return "invalid:flags"
|
||||
fragment_index, fragment_count = raw[17], raw[18]
|
||||
if raw[2] == 1:
|
||||
fragment_index, fragment_count = raw[17], raw[18]
|
||||
payload_length = int.from_bytes(raw[19:21], "big")
|
||||
else:
|
||||
fragment_index = int.from_bytes(raw[17:19], "big")
|
||||
fragment_count = int.from_bytes(raw[19:21], "big")
|
||||
payload_length = int.from_bytes(raw[21:23], "big")
|
||||
if fragment_count > 891:
|
||||
return "invalid:fragment_limit"
|
||||
if fragment_count == 0 or fragment_index >= fragment_count:
|
||||
return "invalid:fragment"
|
||||
payload_length = int.from_bytes(raw[19:21], "big")
|
||||
if payload_length > CHANNEL_LIMITS[raw[3]]:
|
||||
if payload_length > limits[raw[3]]:
|
||||
return "invalid:payload_limit"
|
||||
if len(raw) != HEADER_BYTES + payload_length:
|
||||
if len(raw) != header_bytes + payload_length:
|
||||
return "invalid:length_mismatch"
|
||||
if len(raw) > MAX_FRAME_BYTES:
|
||||
if raw[2] == 1 and len(raw) > 65536 or raw[2] == 2 and len(raw) > 1200:
|
||||
return "invalid:frame_limit"
|
||||
return "valid"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lines = (ROOT / "fixtures/conformance/datagram-v1.tsv").read_text(encoding="utf-8").splitlines()
|
||||
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
||||
for line in lines[1:]:
|
||||
identifier, version, kind, input_value, expected = line.split("\t")
|
||||
assert kind == "datagram" and version == "1"
|
||||
encoded = input_value.removeprefix("hex=")
|
||||
try:
|
||||
actual = classify(binascii.unhexlify(encoded))
|
||||
except binascii.Error:
|
||||
actual = "invalid:hex"
|
||||
assert actual == expected, f"{identifier}: {actual} != {expected}"
|
||||
for fixture in ("datagram-v1.tsv", "datagram-v2.tsv"):
|
||||
lines = (ROOT / "fixtures/conformance" / fixture).read_text(encoding="utf-8").splitlines()
|
||||
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
||||
for line in lines[1:]:
|
||||
identifier, version, kind, input_value, expected = line.split("\t")
|
||||
assert kind == "datagram" and version in ("1", "2")
|
||||
encoded = input_value.removeprefix("hex=")
|
||||
try:
|
||||
actual = classify(binascii.unhexlify(encoded))
|
||||
except binascii.Error:
|
||||
actual = "invalid:hex"
|
||||
assert actual == expected, f"{fixture}:{identifier}: {actual} != {expected}"
|
||||
print("Datagram frame validation passed")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user