Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
323 lines
8.0 KiB
Python
323 lines
8.0 KiB
Python
from __future__ import annotations
|
||
|
||
import base64
|
||
import binascii
|
||
import socket
|
||
import struct
|
||
import typing
|
||
|
||
if typing.TYPE_CHECKING:
|
||
|
||
class HttpsRecord(typing.TypedDict):
|
||
priority: int
|
||
target: str
|
||
alpn: list[str]
|
||
ipv4hint: list[str]
|
||
ipv6hint: list[str]
|
||
echconfig: list[str]
|
||
|
||
|
||
def inet4_ntoa(address: bytes) -> str:
|
||
"""
|
||
Convert an IPv4 address from bytes to str.
|
||
"""
|
||
if len(address) != 4:
|
||
raise ValueError(
|
||
f"IPv4 addresses are 4 bytes long, got {len(address)} byte(s) instead"
|
||
)
|
||
|
||
return "%u.%u.%u.%u" % (address[0], address[1], address[2], address[3])
|
||
|
||
|
||
def inet6_ntoa(address: bytes) -> str:
|
||
"""
|
||
Convert an IPv6 address from bytes to str.
|
||
"""
|
||
if len(address) != 16:
|
||
raise ValueError(
|
||
f"IPv6 addresses are 16 bytes long, got {len(address)} byte(s) instead"
|
||
)
|
||
|
||
hex = binascii.hexlify(address)
|
||
chunks = []
|
||
|
||
i = 0
|
||
length = len(hex)
|
||
|
||
while i < length:
|
||
chunk = hex[i : i + 4].decode().lstrip("0") or "0"
|
||
chunks.append(chunk)
|
||
i += 4
|
||
|
||
# Compress the longest subsequence of 0-value chunks to ::
|
||
best_start = 0
|
||
best_len = 0
|
||
start = -1
|
||
last_was_zero = False
|
||
|
||
for i in range(8):
|
||
if chunks[i] != "0":
|
||
if last_was_zero:
|
||
end = i
|
||
current_len = end - start
|
||
if current_len > best_len:
|
||
best_start = start
|
||
best_len = current_len
|
||
last_was_zero = False
|
||
elif not last_was_zero:
|
||
start = i
|
||
last_was_zero = True
|
||
if last_was_zero:
|
||
end = 8
|
||
current_len = end - start
|
||
if current_len > best_len:
|
||
best_start = start
|
||
best_len = current_len
|
||
if best_len > 1:
|
||
if best_start == 0 and (best_len == 6 or best_len == 5 and chunks[5] == "ffff"):
|
||
# We have an embedded IPv4 address
|
||
if best_len == 6:
|
||
prefix = "::"
|
||
else:
|
||
prefix = "::ffff:"
|
||
thex = prefix + inet4_ntoa(address[12:])
|
||
else:
|
||
thex = (
|
||
":".join(chunks[:best_start])
|
||
+ "::"
|
||
+ ":".join(chunks[best_start + best_len :])
|
||
)
|
||
else:
|
||
thex = ":".join(chunks)
|
||
|
||
return thex
|
||
|
||
|
||
def packet_fragment(payload: bytes, *identifiers: bytes) -> tuple[bytes, ...]:
|
||
results = []
|
||
|
||
offset = 0
|
||
|
||
start_packet_idx = []
|
||
lead_identifier = None
|
||
|
||
for identifier in identifiers:
|
||
idx = payload[:12].find(identifier)
|
||
|
||
if idx == -1:
|
||
continue
|
||
|
||
if idx != 0:
|
||
offset = idx
|
||
|
||
start_packet_idx.append(idx - offset)
|
||
|
||
lead_identifier = identifier
|
||
break
|
||
|
||
for identifier in identifiers:
|
||
if identifier == lead_identifier:
|
||
continue
|
||
|
||
if offset == 0:
|
||
idx = payload.find(b"\x02" + identifier)
|
||
else:
|
||
idx = payload.find(identifier)
|
||
|
||
if idx == -1:
|
||
continue
|
||
|
||
start_packet_idx.append(idx - offset)
|
||
|
||
if not start_packet_idx:
|
||
raise ValueError(
|
||
"no identifiable dns message emerged from given payload. "
|
||
"this should not happen at all. networking issue?"
|
||
)
|
||
|
||
if len(start_packet_idx) == 1:
|
||
return (payload,)
|
||
|
||
start_packet_idx = sorted(start_packet_idx)
|
||
|
||
previous_idx = None
|
||
|
||
for idx in start_packet_idx:
|
||
if previous_idx is None:
|
||
previous_idx = idx
|
||
continue
|
||
results.append(payload[previous_idx:idx])
|
||
previous_idx = idx
|
||
|
||
results.append(payload[previous_idx:])
|
||
|
||
return tuple(results)
|
||
|
||
|
||
def is_ipv4(addr: str) -> bool:
|
||
try:
|
||
socket.inet_aton(addr)
|
||
return True
|
||
except OSError:
|
||
return False
|
||
|
||
|
||
def is_ipv6(addr: str) -> bool:
|
||
try:
|
||
socket.inet_pton(socket.AF_INET6, addr)
|
||
return True
|
||
except OSError:
|
||
return False
|
||
|
||
|
||
def validate_length_of(hostname: str) -> None:
|
||
"""RFC 1035 impose a limit on a domain name length. We verify it there."""
|
||
if len(hostname.strip(".")) > 253:
|
||
raise UnicodeError("hostname to resolve exceed 253 characters")
|
||
elif any([len(_) > 63 for _ in hostname.split(".")]):
|
||
raise UnicodeError("at least one label to resolve exceed 63 characters")
|
||
|
||
|
||
def rfc1035_should_read(payload: bytes) -> bool:
|
||
if not payload:
|
||
return False
|
||
if len(payload) <= 2:
|
||
return True
|
||
|
||
cursor = payload
|
||
|
||
while True:
|
||
expected_size: int = struct.unpack("!H", cursor[:2])[0]
|
||
|
||
if len(cursor[2:]) == expected_size:
|
||
return False
|
||
elif len(cursor[2:]) < expected_size:
|
||
return True
|
||
|
||
cursor = cursor[2 + expected_size :]
|
||
|
||
|
||
def rfc1035_unpack(payload: bytes) -> tuple[bytes, ...]:
|
||
cursor = payload
|
||
packets = []
|
||
|
||
while cursor:
|
||
expected_size: int = struct.unpack("!H", cursor[:2])[0]
|
||
|
||
packets.append(cursor[2 : 2 + expected_size])
|
||
cursor = cursor[2 + expected_size :]
|
||
|
||
return tuple(packets)
|
||
|
||
|
||
def rfc1035_pack(message: bytes) -> bytes:
|
||
return struct.pack("!H", len(message)) + message
|
||
|
||
|
||
def read_name(data: bytes, offset: int) -> tuple[str, int]:
|
||
"""
|
||
Read a DNS‐encoded name (with compression pointers) from data[offset:].
|
||
Returns (name, new_offset).
|
||
"""
|
||
labels = []
|
||
while True:
|
||
length = data[offset]
|
||
# compression pointer?
|
||
if length & 0xC0 == 0xC0:
|
||
pointer = struct.unpack_from("!H", data, offset)[0] & 0x3FFF
|
||
subname, _ = read_name(data, pointer)
|
||
labels.append(subname)
|
||
offset += 2
|
||
break
|
||
if length == 0:
|
||
offset += 1
|
||
break
|
||
offset += 1
|
||
labels.append(data[offset : offset + length].decode())
|
||
offset += length
|
||
return ".".join(labels), offset
|
||
|
||
|
||
def parse_echconfigs(buf: bytes) -> list[str]:
|
||
"""
|
||
buf is the raw bytes of the ECHConfig vector:
|
||
- 2-byte total length, then for each:
|
||
- 2-byte cfg length + that many bytes of cfg
|
||
We return a list of Base64 strings (one per config).
|
||
"""
|
||
if len(buf) < 2:
|
||
return []
|
||
off = 2
|
||
total = struct.unpack_from("!H", buf, 0)[0]
|
||
end = 2 + total
|
||
out = []
|
||
while off + 2 <= end:
|
||
cfg_len = struct.unpack_from("!H", buf, off)[0]
|
||
off += 2
|
||
cfg = buf[off : off + cfg_len]
|
||
off += cfg_len
|
||
out.append(base64.b64encode(cfg).decode())
|
||
return out
|
||
|
||
|
||
def parse_https_rdata(rdata: bytes) -> HttpsRecord:
|
||
"""
|
||
Parse the RDATA of an SVCB/HTTPS record.
|
||
Returns a dict with keys: priority, target, alpn, ipv4hint, ipv6hint, echconfig.
|
||
"""
|
||
off = 0
|
||
priority = struct.unpack_from("!H", rdata, off)[0]
|
||
off += 2
|
||
|
||
target, off = read_name(rdata, off)
|
||
|
||
# pull out all the key/value params
|
||
params = {}
|
||
while off + 4 <= len(rdata):
|
||
key, length = struct.unpack_from("!HH", rdata, off)
|
||
off += 4
|
||
params[key] = rdata[off : off + length]
|
||
off += length
|
||
|
||
# decode ALPN (key=1), IPv4 (4), IPv6 (6), ECHConfig (5)
|
||
def parse_alpn(buf: bytes) -> list[str]:
|
||
out = []
|
||
i: int = 0
|
||
while i < len(buf):
|
||
ln = buf[i]
|
||
out.append(buf[i + 1 : i + 1 + ln].decode())
|
||
i += 1 + ln
|
||
return out
|
||
|
||
alpn: list[str] = parse_alpn(params.get(1, b""))
|
||
ipv4 = [
|
||
inet4_ntoa(params[4][i : i + 4]) for i in range(0, len(params.get(4, b"")), 4)
|
||
]
|
||
ipv6 = [
|
||
inet6_ntoa(params[6][i : i + 16]) for i in range(0, len(params.get(6, b"")), 16)
|
||
]
|
||
echconfs = parse_echconfigs(params.get(5, b""))
|
||
|
||
return {
|
||
"priority": priority,
|
||
"target": target or ".", # empty name → root
|
||
"alpn": alpn,
|
||
"ipv4hint": ipv4,
|
||
"ipv6hint": ipv6,
|
||
"echconfig": echconfs,
|
||
}
|
||
|
||
|
||
__all__ = (
|
||
"inet4_ntoa",
|
||
"inet6_ntoa",
|
||
"packet_fragment",
|
||
"is_ipv4",
|
||
"is_ipv6",
|
||
"validate_length_of",
|
||
"rfc1035_pack",
|
||
"rfc1035_unpack",
|
||
"rfc1035_should_read",
|
||
"parse_https_rdata",
|
||
)
|