فهرست مقاله
وقتی ابزار مهندسی با نامِ یک خدمت یا دستگاه کار میکند، بخشی از مسیر میتواند به DNS وابسته باشد؛ اما آزمونِ همهٔ آن مسیر، با آزمونِ چند بایتِ قالبِ نام یکی نیست. این درس، جزء کوچکی را جدا میکند: نامی که بخشی از آن با اشارهگر به بایتهای قبلی نوشته شده است. بایتها ساختگیاند، هیچ درخواست شبکهای ارسال نمیشود و نتیجه، گواهِ سلامتِ حلکنندهٔ نام یا امنیتِ یک محصول نیست.
۱. برچسب بایتی، نقطهٔ نمایشی و ریشه

طبق بخشهای ساختار نام در RFC 1035، برچسب معمولی با یک بایتِ طول آغاز میشود و برچسبِ ریشه طول صفر دارد. نامِ نمایشی با نقطه، نمایشِ آسانخوانی است؛ در این قالب، نقطه جداکنندهٔ بایتیِ ذخیرهشده نیست. برای amp و example، ابتدا طول سه، سه بایتِ اول، طول هفت، هفت بایتِ دوم و سرانجام صفر داریم؛ در مجموع سیزده بایت.
محدودیتِ برچسب معمولی ۶۳ بایت است. محدودیتِ نامِ بازشده ۲۵۵ بایت، شامل بایتهای طول و صفرِ ریشه است؛ تنها جمعِ حروف را نباید شمرد. چهار برچسبِ ۶۳بایتی، با طولها و ریشه، ۲۵۷ بایت میشوند و رد میشوند. سه برچسبِ ۶۳بایتی و یکی ۶۱بایتی، دقیقاً به ۲۵۵ میرسند. این مرزها در کد با انتظار مستقل آزموده شدهاند.
خروجیِ تابع، چند bytes است، نه یک رشتهٔ متنیِ آمادهٔ نمایش. محتوای برچسب میتواند هشتبیتی باشد؛ نمونهٔ ما حتی صفر و FF داخلِ دادهٔ یک برچسب دارد. صفرِ طول، ریشه است؛ صفرِ درونِ داده، جداکننده نیست. بزرگیِ حروف هم حفظ میشود. این انتخاب دربارهٔ نگهداری بایتهاست؛ قواعد مقایسهٔ نام، نامِ میزبان، IDNA و تبدیلِ نمایشی را پیادهسازی نمیکنیم.
۲. مبدأ اشارهگر و دو طول متفاوت

در بخش ۴٫۱٫۴ همان RFC، دو بیتِ بالای بایتِ نخستِ اشارهگر، یک هستند و چهارده بیتِ باقی، فاصله از آغازِ سربرگِ پیام را میدهند. فاصله از ابتدای نامِ جاری نیست. برای روشنماندنِ حساب، دوازده صفرِ ساختگی پیش از نامِ نخست میگذاریم؛ این صفرها یک سربرگِ کاملِ معتبر را ثابت نمیکنند و فیلدهای سربرگ خوانده نمیشوند.
نامِ اول از جایگاه دوازده آغاز میشود و amp و example را دارد. ابتدای برچسبِ example در جایگاه شانزده است. نامِ دوم از جایگاه ۲۵، طول چهار و node را مینویسد، سپس c0 10 به جایگاه شانزده اشاره میکند. در محلِ نامِ دوم، پنج بایتِ پیشوند و دو بایتِ اشارهگر مصرف شدهاند؛ پاسخِ محلی هفت است. نامِ بازشده node و example، چهارده بایتِ قالبِ بازشده دارد.
اگر فیلدِ بعدی بلافاصله پس از همین نام باشد، از جایگاه ۳۲ ادامه میدهیم؛ افزودنِ چهارده به ۲۵ اشتباه است. در نمونه، جایگاه ۳۲ خودش یک اشارهگرِ دوبایتی به نامِ دوم دارد؛ آن نیز چهارده بایت باز میشود، اما فقط دو بایت در محلِ اصلی مصرف میکند. تابع هر دو اندازه را جدا برمیگرداند. پردازشِ فیلد بعدی و حدودِ همان فیلد، مسئولیتِ رمزگشای پیامِ کامل خواهد بود.
۳. قرارداد محدود و کد کامل

ورودی باید bytes دقیق، بین یک و ۵۱۲ بایت باشد؛ ۵۱۲ سقفِ انتخابیِ این تمرین است و حدِ همگانیِ DNS امروزی معرفی نمیشود. start باید int دقیق، غیرمنفی و داخلِ بافر باشد. bool، اعشاری، NaN و بینهایت جایِ آفستِ صحیح پذیرفته نیستند. واحدِ تمامِ فاصلهها و طولهای این برنامه، بایت است؛ نامِ تابع هیچ اندازهگیریِ زمان یا سرعتی انجام نمیدهد.
"""Bounded DNS-name wire primitive on synthetic bytes; not a DNS resolver."""
import json
import platform
MAX_MESSAGE_BYTES = 512 # Own exercise profile, not a universal modern DNS limit.
def decode_name(message, start):
if type(message) is not bytes or not 1 <= len(message) <= MAX_MESSAGE_BYTES:
raise ValueError("message_profile")
if type(start) is not int or not 0 <= start < len(message):
raise ValueError("start_offset")
position = start
consumed = 0
jumped = False
seen = set()
labels = []
expanded_wire_bytes = 1 # Account for the eventual root length octet.
while True:
if position >= len(message):
raise ValueError("missing_terminator")
if position in seen:
raise ValueError("pointer_loop")
seen.add(position)
first = message[position]
kind = first & 0xC0
if kind == 0xC0:
if position + 1 >= len(message):
raise ValueError("truncated_pointer")
target = ((first & 0x3F) << 8) | message[position + 1]
if target >= len(message):
raise ValueError("pointer_outside_message")
if target >= position:
raise ValueError("pointer_not_prior")
if not jumped:
consumed += 2
jumped = True
position = target
continue
if kind != 0:
raise ValueError("unsupported_label_kind")
length = first
if length == 0:
if not jumped:
consumed += 1
return tuple(labels), consumed, expanded_wire_bytes
end = position + 1 + length
if end > len(message):
raise ValueError("truncated_label")
expanded_wire_bytes += 1 + length
if expanded_wire_bytes > 255:
raise ValueError("expanded_name_too_long")
labels.append(message[position + 1:end])
if not jumped:
consumed += 1 + length
position = end
def error_code(message, start=0):
try:
decode_name(message, start)
except ValueError as error:
return str(error)
return None
def plain_wire(labels):
# Fixture writer, not a name decoder or output oracle.
return b"".join(bytes((len(label),)) + label for label in labels) + b"\0"
def run_example():
checks = {}
def check(name, condition):
checks[name] = bool(condition)
if not checks[name]:
raise AssertionError(name)
message = b"\0" * 12 + b"\x03amp\x07example\0" + b"\x04node\xc0\x10" + b"\xc0\x19"
first = decode_name(message, 12)
compressed = decode_name(message, 25)
chained = decode_name(message, 32)
check("hand_plain_name_labels_and_wire_length", first == ((b"amp", b"example"), 13, 13))
check("root_label_empty_name_consumes_one", decode_name(b"\0", 0) == ((), 1, 1))
check("prefix_pointer_uses_message_origin_and_local_consumed", compressed == ((b"node", b"example"), 7, 14))
pointer_only = b"\x03amp\x07example\0\xc0\x00"
check("pointer_only_consumes_two_not_expanded_length", decode_name(pointer_only, 13) == ((b"amp", b"example"), 2, 13))
check("bounded_nested_pointer_expansion", chained == ((b"node", b"example"), 2, 14))
high_offset = b"\0" * 300 + b"\x01z\0\xc1\x2c"
check("fourteen_bit_target_not_only_second_octet", decode_name(high_offset, 303) == ((b"z",), 2, 3))
check("binary_label_octets_and_case_preserved", decode_name(b"\x02\0\xff\x03AMP\0", 0) == ((b"\0\xff", b"AMP"), 8, 8))
check("maximum_ordinary_label_63_octets", decode_name(b"\x3f" + b"x" * 63 + b"\0", 0) == ((b"x" * 63,), 65, 65))
max_labels = (b"a" * 63, b"b" * 63, b"c" * 63, b"d" * 61)
check("expanded_wire_limit_255_includes_root", decode_name(plain_wire(max_labels), 0) == (max_labels, 255, 255))
too_long = (b"a" * 63,) * 4
check("expanded_257_octets_rejected", error_code(plain_wire(too_long)) == "expanded_name_too_long")
check("truncated_label_and_missing_root_rejected", error_code(b"\x03ab") == "truncated_label" and error_code(b"\x01a") == "missing_terminator")
check("truncated_or_out_of_bounds_pointer_rejected", error_code(b"\xc0") == "truncated_pointer" and error_code(b"\xff\xff") == "pointer_outside_message")
check("self_and_forward_pointers_rejected", error_code(b"\xc0\x00") == "pointer_not_prior" and error_code(b"\xc0\x02\0") == "pointer_not_prior")
check("backward_pointer_can_still_loop_through_labels", error_code(b"\x01a\xc0\x00") == "pointer_loop")
check("reserved_top_bit_patterns_rejected", error_code(b"\x40\0") == "unsupported_label_kind" and error_code(b"\x80\0") == "unsupported_label_kind")
invalid_offsets = (True, -1, 1, 0.0, float("nan"), float("inf"))
check("bounded_message_type_and_offset_contract", all(error_code(b"\0", v) == "start_offset" for v in invalid_offsets) and all(error_code(v) == "message_profile" for v in (b"", b"\0" * 513, bytearray(b"\0"), "name", None)))
cases = 0
for padding in (0, 12, 30):
for suffix in ((b"lab",), (b"grid", b"example"), (b"sensor", b"local")):
for prefix in (b"a", b"amp", b"node"):
initial = b"\0" * padding + plain_wire(suffix)
start = len(initial)
pointer = bytes((0xC0 | (padding >> 8), padding & 0xFF))
wire = initial + bytes((len(prefix),)) + prefix + pointer
answer = decode_name(wire, start)
expected_labels = (prefix,) + suffix
expected_expanded = sum(1 + len(x) for x in expected_labels) + 1
if answer != (expected_labels, 1 + len(prefix) + 2, expected_expanded):
raise AssertionError("independent declared-label fixture")
cases += 1
check("declared_label_reference_27_fixtures_one_group", cases == 27)
return {"python_version": platform.python_version(),
"scope": "bounded synthetic name-decoding primitive; exact bytes, whole buffer offsets, prior-only pointers and repeated-position rejection; no header/RR/field-boundary validation, resolver, real DNS traffic, IDNA or security assurance",
"max_message_bytes_own_profile": MAX_MESSAGE_BYTES,
"hand_message_hex": message.hex(),
"hand_plain": {"start": 12, "label_hex": [x.hex() for x in first[0]], "consumed_bytes": first[1], "expanded_wire_bytes": first[2]},
"hand_compressed": {"start": 25, "labels_ascii_for_this_fixture_only": [x.decode("ascii") for x in compressed[0]], "pointer_target_offset": 16, "consumed_bytes": compressed[1], "expanded_wire_bytes": compressed[2]},
"hand_nested": {"start": 32, "consumed_bytes": chained[1], "expanded_wire_bytes": chained[2]},
"independent_declared_label_fixtures_one_group": cases,
"limitations": "A target that happens to parse is not proven to be an actual prior name boundary. The caller would need full DNS-message context; this primitive does not enforce RFC9267's complete recommendations or claim production validation.",
"checks": checks}
if __name__ == "__main__":
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
پیش از خواندنِ دو بایتِ اشارهگر، کاملبودنِ آن کنترل میشود. مقصد باید داخلِ بافر و پیش از جایگاهِ همان اشارهگر باشد. الگوهای رزروشدهٔ بیتِ بالا نیز رد میشوند. برچسبِ ناقص، پایانِ بدونِ ریشه و نامِ بازشدهٔ بیشازحد، خطای صریح دارند؛ تابع برای ساختنِ جواب، بایتِ گمشده اضافه یا برچسبِ طولانی را قطع نمیکند. خطا، نتیجهٔ جزئیِ موفق برنمیگرداند.
حرکت به عقب، بهتنهایی پایان را تضمین نمیکند: بایتِ طول یک، حرف a و اشارهگر به ابتدای همان برچسب، مسیرِ تکراری میسازند. مجموعهٔ جایگاههای دیدهشده این بازدید را رد میکند. اشارهگرِ تودرتوی محدود در قراردادِ ما مجاز است، ولی تعدادِ جایگاههای قابل بازدید با بافرِ محدود مهار میشود. این سیاست را با اجرای همهٔ توصیههای اعتبارسنجیِ DNS یکی نمیگیریم.
۴. مرجع مستقل، خروجی و حدودِ نتیجه

RFC 9267، سندی اطلاعرسان از مسیرِ مستقل است و استانداردِ مسیرِ اصلی اینترنت نیست. بخشهای اشارهگر و طول آن برای فهمِ خطاهای متداول خوانده شدهاند. نمونهٔ حاضر توصیههای کاملِ آن را اجرا نمیکند: مقصدی که تصادفاً قابلخواندن است، الزاماً مرزِ واقعیِ نامِ قبلی نیست. بدونِ بررسیِ زمینهٔ سربرگ و رکورد، تابع چنین چیزی را اثبات نمیکند.
{
"python_version": "3.14.5",
"scope": "bounded synthetic name-decoding primitive; exact bytes, whole buffer offsets, prior-only pointers and repeated-position rejection; no header/RR/field-boundary validation, resolver, real DNS traffic, IDNA or security assurance",
"max_message_bytes_own_profile": 512,
"hand_message_hex": "00000000000000000000000003616d70076578616d706c6500046e6f6465c010c019",
"hand_plain": {
"start": 12,
"label_hex": [
"616d70",
"6578616d706c65"
],
"consumed_bytes": 13,
"expanded_wire_bytes": 13
},
"hand_compressed": {
"start": 25,
"labels_ascii_for_this_fixture_only": [
"node",
"example"
],
"pointer_target_offset": 16,
"consumed_bytes": 7,
"expanded_wire_bytes": 14
},
"hand_nested": {
"start": 32,
"consumed_bytes": 2,
"expanded_wire_bytes": 14
},
"independent_declared_label_fixtures_one_group": 27,
"limitations": "A target that happens to parse is not proven to be an actual prior name boundary. The caller would need full DNS-message context; this primitive does not enforce RFC9267's complete recommendations or claim production validation.",
"checks": {
"hand_plain_name_labels_and_wire_length": true,
"root_label_empty_name_consumes_one": true,
"prefix_pointer_uses_message_origin_and_local_consumed": true,
"pointer_only_consumes_two_not_expanded_length": true,
"bounded_nested_pointer_expansion": true,
"fourteen_bit_target_not_only_second_octet": true,
"binary_label_octets_and_case_preserved": true,
"maximum_ordinary_label_63_octets": true,
"expanded_wire_limit_255_includes_root": true,
"expanded_257_octets_rejected": true,
"truncated_label_and_missing_root_rejected": true,
"truncated_or_out_of_bounds_pointer_rejected": true,
"self_and_forward_pointers_rejected": true,
"backward_pointer_can_still_loop_through_labels": true,
"reserved_top_bit_patterns_rejected": true,
"bounded_message_type_and_offset_contract": true,
"declared_label_reference_27_fixtures_one_group": true
}
}
هفده گروهِ بررسی با Python 3.14.5 اجرا شدهاند. پاسخِ دستیِ سه نام، مصرفِ سیزده، هفت و دو بایت و طولِ بازشدهٔ سیزده، چهارده و چهارده است. برای آزمونِ چهاردهبیتیِ آفست، مقصدِ ۳۰۰ نیز جدا داریم؛ خواندنِ فقط بایتِ دومِ اشارهگر، آن شاهد را خراب میکند. ورودیِ تهی، نوعِ نامناسب، اشارهگرِ بیرونقاب، مقصدِ جلو و حلقه نیز بررسی میشوند.
بیستوهفت fixture از برچسبهای ازپیشمعلوم و فاصلههای متفاوت ساخته میشوند. انتظارِ برچسبها از همان فهرستِ اعلامشده میآید و تابعِ رمزگشا در مرجع فراخوانی نمیشود. این بیستوهفت مورد فقط یک گروهاند؛ اجرای دوباره هم تعدادِ آزمونِ تازه نمیسازد. جوابِ دستی و شاهدهای مرزی کنارِ این گروه لازماند تا خطای مشترکِ تولیدکننده و مصرفکننده پنهان نماند.
مرزبندی پیامِ حسگر مسئولِ جداکردنِ پیام از جریان بود؛ این درس داخلِ یک بافرِ موجود، نام را میخواند. قراردادِ Base64 نیز نمایشِ متنیِ بایت را میسنجد. هیچکدام، جایِ بررسیِ کاملِ پاسخ DNS، نوعِ رکورد، اصالتِ پاسخ یا سیاستِ نامگذاری را نمیگیرند. منابع در ۲۵ مهر ۱۴۰۵ خوانده شدهاند و مثال، خبرِ تازه معرفی نمیشود.
۵. چهار پرسش با پاسخِ کامل

پرسش اول: در نامِ دوم، اشارهگرِ c0 10 از کجا اندازه میگیرد؟ پاسخ: از آغازِ بافرِ نمایندهٔ پیام؛ مقصد شانزده است. افزودنِ شانزده به جایگاهِ ۲۵ نامِ جاری، قراردادِ آفست را عوض میکند و جوابِ درستِ example را نمیدهد.
پرسش دوم: پس از نامِ دوم، چند بایت جلو برویم؟ پاسخ: هفت بایت؛ پنج برای برچسبِ node و دو برای اشارهگر. چهارده، اندازهٔ قالبِ نامِ بازشده است و فاصلهٔ فیلدِ بعدی در محلِ اصلی نیست.
پرسش سوم: آیا اشارهگرِ رو به عقب میتواند حلقه بسازد؟ پاسخ: بله؛ برچسبِ a سپس اشارهگر به ابتدای همان برچسب، دوباره همان جایگاه را میخواند. کنترلِ مقصدِ قبلی کافی نیست؛ نگهبانِ جایگاهِ تکراری نیز لازم است.
پرسش چهارم: موفقیتِ تابع، اعتبارِ پاسخِ واقعیِ DNS را ثابت میکند؟ پاسخ: خیر؛ تنها قراردادِ محدودِ نام را آزمودهایم. مرزِ واقعیِ برچسبِ مقصد، رکوردها، سربرگ، زمینهٔ درخواست و سیاستِ اعتبارِ پاسخ، در این برنامه پیادهسازی نشدهاند.
منابع و مطالعهٔ بیشتر
بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

