fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099

Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-19 13:53:55 +09:00
parent dc08d29509
commit c2257d3a86
2709 changed files with 619549 additions and 10 deletions

View File

@@ -0,0 +1,322 @@
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 DNSencoded 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",
)