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:
@@ -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 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",
|
||||
)
|
||||
Reference in New Issue
Block a user