وقتی یک پاسخِ شبکه نشانی و درگاهی را گزارش می‌کند، پیش از تفسیرِ آن باید بدانیم این دو مقدار از کدام بایت‌ها آمده‌اند و به کدام درخواست مربوط‌اند. این درس یک پاسخِ ساختگیِ STUN را کاملاً آفلاین می‌خواند. تمرکز بر سرآیند، مرزِ ویژگی‌ها و بازکردنِ نشانیِ XOR است؛ هیچ سرور، تماس، سوکت یا NAT واقعی آزمایش نمی‌شود.

۱. نشانیِ مشاهده‌شده را از عبورِ موفقِ تماس جدا کنیم

نشانیِ مشاهده‌شده با مسیرِ برقرارشده فرق دارد
تصویر مفهومی: نشانیِ مشاهده‌شده با مسیرِ برقرارشده فرق دارد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در مثالِ ما، برنامه از قبل یک پیامِ کامل و شناسهٔ تراکنشِ موردِ انتظار را دارد. خروجی شامل نشانیِ IPv4، شمارهٔ درگاه و فهرستِ ویژگی‌های همان پیام است. این خروجی، صرفاً خوانشِ یک قالبِ بایتی است. نشانیِ گزارش‌شده را نشانیِ محلیِ کارتِ شبکه، نشانیِ معتبرِ یک تماس‌گیرنده یا گواهِ برقرارشدنِ مسیرِ صوت نمی‌نامیم.

RFC 8489، قراردادِ STUN و ویژگیِ XOR-MAPPED-ADDRESS، این قالب را در چارچوبِ ابزارهای کار با NAT توضیح می‌دهد. در این آموزش، فقط بخشی از رمزگشاییِ یک پاسخِ موفقِ Binding را می‌سازیم. STUN به‌تنهایی راه‌حلِ کاملِ عبور از NAT نیست و خواندنِ یک مقدارِ آن نیز آزمایشِ اتصال محسوب نمی‌شود.

نشانیِ تألیفیِ مشاهده‌شده را 198.51.100.77 و درگاه را ۴۵۰۰۰ انتخاب کرده‌ایم. شناسهٔ تراکنش نیز دوازده بایتِ ثابتِ آموزشی است. این ثابت، نمونهٔ تولیدِ شناسهٔ امن برای یک مشتریِ واقعی نیست؛ برنامه اصلاً درخواست نمی‌فرستد. درگاه یک عددِ شانزده‌بیتی است، نه زمان، تأخیر یا نرخِ نمونه‌برداری.

درسِ زمان‌سنجِ INVITE در SIP فرض می‌کرد پاسخ‌ها از قبل به درخواستِ درست مربوط‌اند. اینجا یک شاهدِ بایتیِ ارتباط با تراکنش را کنترل می‌کنیم، اما زمان‌بندی و ارسالِ مجدد نداریم. همچنین این شناسه جایِ احرازِ اصالت، بررسیِ منبعِ شبکه یا قواعدِ کاملِ یک پشتهٔ ارتباطی را نمی‌گیرد.

۲. سرآیند، طول و شناسهٔ تراکنش را کنترل کنیم

طولِ بسته و هویتِ تراکنش
تصویر مفهومی: طولِ بسته و هویتِ تراکنش؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

سرآیندِ موردِ استفاده بیست بایت است. نوعِ پیام باید دقیقاً پاسخِ موفقِ Binding و magic cookie برابرِ 2112a442 باشد. طولِ اعلام‌شده فقط بدنهٔ پس از سرآیند را می‌شمارد. پیامِ اصلیِ ما سی‌ودو بایت بدنه و پنجاه‌ودو بایت طولِ کامل دارد؛ بنابراین نوشتنِ پنجاه‌ودو در فیلدِ طول، قرارداد را خراب می‌کند.

عددهای چندبایتی با ترتیبِ شبکه خوانده می‌شوند. تابع، طولِ واقعیِ رشته را دقیقاً با بیست به‌اضافهٔ طولِ اعلام‌شده برابر می‌خواهد و طولِ بدنه باید مضربی از چهار باشد. اضافه‌بودنِ حتی یک بایت، مانند کمبودِ یک بایت رد می‌شود. ورودی یک پیامِ ازپیش‌جداشده است؛ دریافتِ جریانی و جداسازیِ چند پیامِ پشت‌سرهم پیاده نشده‌اند.

شناسهٔ داخلِ سرآیند باید بایت‌به‌بایت با شناسهٔ موردِ انتظار یکسان باشد. درست‌بودنِ طولِ دوازده‌بایتی به‌تنهایی کافی نیست. این بررسی، پاسخِ متعلق به شناسه‌ای دیگر را از نمونهٔ ما جدا می‌کند؛ با این حال، ساختنِ بایت‌های مطابق برای یک دادهٔ جعلی خارج از توانِ این مقایسه نیست. تابع هیچ برچسبِ «قابلِ اعتماد» به نتیجه نمی‌افزاید.

محدودهٔ آموزشیِ طولِ کامل را بیست تا ۴۰۹۶ بایت می‌گیریم. ورودیِ پیام و شناسه باید دقیقاً bytes باشند؛ bytearray و رشته تبدیلِ ضمنی نمی‌شوند. این سقف و این انتخابِ نوع، سیاستِ همین فایل‌اند و حدِ عمومیِ STUN نیستند. پیامِ خطا، روشِ دیگر، نسخهٔ قدیمی یا خانوادهٔ IPv6 در این رابط پشتیبانی نمی‌شود.

۳. نشانی و درگاهِ XOR را در IPv4 باز کنیم

دو بخشِ مستقلِ نشانی و درگاه
تصویر مفهومی: دو بخشِ مستقلِ نشانی و درگاه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مقدارِ ویژگیِ موردِ نظر هشت بایت است: یک بایتِ رزروشده، خانواده، درگاهِ پوشیده و نشانیِ پوشیده. درگاه با شانزده بیتِ بالاییِ cookie و نشانیِ IPv4 با تمامِ cookie، XOR می‌شود. چون XOR با یک مقدار، با تکرارِ همان عمل برمی‌گردد، برای بازکردن نیز همان ماسک‌ها را به کار می‌بریم. خانوادهٔ پذیرفته‌شده در این درس فقط یک است.

مقدارِ بایتیِ نمونه 00018edae721c00f است. بخشِ درگاه، 8eda، پس از XOR با 2112 به afc8 یا ۴۵۰۰۰ برمی‌گردد. بخشِ نشانی نیز پس از XOR به چهار بایتِ متناظر با نشانیِ انتخابی می‌رسد. این محاسبه، رمزنگاری یا پنهان‌سازیِ دارای کلیدِ محرمانه نیست؛ ماسک در خودِ قرارداد معلوم است.

بایتِ رزروشدهٔ ابتدای مقدار در دریافت نادیده گرفته می‌شود. در مقابل، اندازه و خانواده را کنترل می‌کنیم. اگر ویژگی غایب باشد یا چند بار دیده شود، رابطِ محدودِ ما خطا می‌دهد. ردِ تکرار، سیاستِ سخت‌گیرانهٔ این درس است؛ آن را قاعدهٔ عمومیِ همهٔ گیرنده‌های STUN نمی‌نامیم. ترتیبِ سایر ویژگی‌ها نیز هنگامِ خواندن از بین نمی‌رود.

کدِ کامل با Python 3.14.5 و فقط کتابخانهٔ استاندارد اجرا شده است. سازنده‌های کوچکِ داخلِ فایل فقط بایت‌های آزمون را می‌سازند. خروجیِ Decoded، اطلاعاتِ قالب و ویژگی‌های خام را نگه می‌دارد؛ نه یک نشستِ فعال است و نه نتیجهٔ اعتبارسنجیِ امنیتی. توابعِ کمکیِ تولیدِ آزمون نیز فرستندهٔ پروتکلی نیستند.

"""Offline narrow STUN IPv4 response inspection, not a client or trust check."""
from dataclasses import dataclass
import ipaddress
import json
import platform
import struct

COOKIE = 0x2112a442
MAX_WIRE = 4096  # Own bounded teaching profile, not a protocol limit.


@dataclass(frozen=True)
class Attribute:
    kind: int
    value: bytes
    padding: bytes


@dataclass(frozen=True)
class Decoded:
    transaction_id: bytes
    address: str
    port: int
    attributes: tuple[Attribute, ...]


def exact_bytes(value):
    if type(value) is not bytes:
        raise TypeError('actual bytes required')
    return value


def decode_binding_ipv4(wire, expected_transaction_id):
    exact_bytes(wire); exact_bytes(expected_transaction_id)
    if len(expected_transaction_id) != 12:
        raise ValueError('expected transaction ID must have 12 bytes')
    if not 20 <= len(wire) <= MAX_WIRE:
        raise ValueError('wire length outside teaching profile')
    kind, length, cookie = struct.unpack('!HHI', wire[:8])
    if kind != 0x0101 or cookie != COOKIE:
        raise ValueError('only modern Binding success responses are inspected')
    if length % 4 or len(wire) != 20 + length:
        raise ValueError('exact aligned body length required')
    if wire[8:20] != expected_transaction_id:
        raise ValueError('transaction ID mismatch')
    attributes = []
    offset = 20
    while offset < len(wire):
        if offset + 4 > len(wire):
            raise ValueError('truncated attribute header')
        attr_type, count = struct.unpack('!HH', wire[offset:offset+4])
        start, stop = offset + 4, offset + 4 + count
        following = stop + (-count % 4)
        if following > len(wire):
            raise ValueError('truncated attribute value or padding')
        attributes.append(Attribute(attr_type, wire[start:stop], wire[stop:following]))
        offset = following
    mapped = [a.value for a in attributes if a.kind == 0x0020]
    # This narrow profile rejects duplicates; RFC receivers can ignore repeats.
    if len(mapped) != 1:
        raise ValueError('exactly one XOR-MAPPED-ADDRESS required by this profile')
    value = mapped[0]
    if len(value) != 8 or value[1] != 1:
        raise ValueError('only 8-byte IPv4 XOR-MAPPED-ADDRESS supported')
    # Reserved first value byte is ignored, as required for receivers.
    port = int.from_bytes(value[2:4], 'big') ^ (COOKIE >> 16)
    address = ipaddress.IPv4Address(int.from_bytes(value[4:8], 'big') ^ COOKIE)
    return Decoded(wire[8:20], str(address), port, tuple(attributes))


def bytewise_oracle(value):
    """Independent byte-by-byte XOR; no integer address XOR or decoder call."""
    decoded_port = bytes(a ^ b for a, b in zip(value[2:4], b'\x21\x12'))
    decoded_address = bytes(a ^ b for a, b in zip(value[4:8], b'\x21\x12\xa4\x42'))
    return str(ipaddress.IPv4Address(decoded_address)), int.from_bytes(decoded_port, 'big')


def attribute(kind, value, pad_byte=0):
    return struct.pack('!HH', kind, len(value)) + value + bytes([pad_byte]) * (-len(value) % 4)


def message(body, transaction_id):
    return struct.pack('!HHI', 0x0101, len(body), COOKIE) + transaction_id + body


def encode_value(address, port):
    return b'\0\1' + (port ^ (COOKIE >> 16)).to_bytes(2, 'big') + (int(ipaddress.IPv4Address(address)) ^ COOKIE).to_bytes(4, 'big')


def rejects(function, *args):
    try:
        function(*args)
    except (ValueError, TypeError):
        return True
    return False


def run_example():
    txid = bytes.fromhex('00112233445566778899aabb')
    value = encode_value('198.51.100.77', 45000)
    body = attribute(0x8022, b'lab') + attribute(0x0020, value) + attribute(0x8055, b'abcde', 0xaa)
    wire = message(body, txid)
    result = decode_binding_ipv4(wire, txid)
    # External known wire vector, explicitly from RFC5769 section2.2.
    rfc_wire = bytes.fromhex(
        '0101003c2112a442b7e7a701bc34d686fa87dfae'
        '8022000b7465737420766563746f7220'
        '002000080001a147e112a643'
        '000800142b91f599fd9e90c38c7489f92af9ba53f06be7d7'
        '80280004c07d4c96')
    rfc_result = decode_binding_ipv4(rfc_wire, bytes.fromhex('b7e7a701bc34d686fa87dfae'))
    checks = {}
    checks['own_literal_header_and_mapping'] = (len(wire) == 52 and wire[:8].hex() == '010100202112a442'
            and value.hex() == '00018edae721c00f' and (result.address, result.port) == ('198.51.100.77', 45000))
    checks['rfc5769_known_response_not_authentication'] = (len(rfc_wire) == 80
            and (rfc_result.address, rfc_result.port) == ('192.0.2.1', 32853)
            and len(rfc_result.attributes) == 4)
    checks['opaque_values_and_padding_preserved'] = (result.attributes[0] == Attribute(0x8022, b'lab', b'\0')
            and result.attributes[2] == Attribute(0x8055, b'abcde', b'\xaa\xaa\xaa'))
    checks['reserved_value_byte_ignored'] = decode_binding_ipv4(message(attribute(0x0020, b'\xff'+value[1:]), txid), txid).port == 45000
    checks['exact_transaction_binding'] = rejects(decode_binding_ipv4, wire, bytes(12))
    checks['wrong_type_and_cookie_rejected'] = (rejects(decode_binding_ipv4, b'\0\1'+wire[2:], txid)
            and rejects(decode_binding_ipv4, wire[:4]+bytes(4)+wire[8:], txid))
    checks['declared_length_alignment_and_exact_frame'] = all(rejects(decode_binding_ipv4, bad, txid)
            for bad in (wire+b'\0', wire[:-1], wire[:2]+b'\0\x1f'+wire[4:]))
    checks['attribute_length_overrun_rejected'] = rejects(decode_binding_ipv4, message(b'\0\x20\0\x0c'+value, txid), txid)
    checks['missing_and_duplicate_mapping_rejected'] = (rejects(decode_binding_ipv4, message(attribute(0x8022,b'ab'),txid),txid)
            and rejects(decode_binding_ipv4,message(attribute(0x0020,value)*2,txid),txid))
    checks['unsupported_family_and_value_length_rejected'] = (rejects(decode_binding_ipv4,message(attribute(0x0020,b'\0\2'+bytes(18)),txid),txid)
            and rejects(decode_binding_ipv4,message(attribute(0x0020,value[:-1]),txid),txid))
    checks['zero_length_opaque_attribute_advances'] = decode_binding_ipv4(message(attribute(0x8055,b'')+attribute(0x0020,value),txid),txid).attributes[0] == Attribute(0x8055,b'',b'')
    port_cases = 0; oracle_ok = True
    for port in range(65536):
        candidate = encode_value('203.0.113.9', port)
        decoded = decode_binding_ipv4(message(attribute(0x0020,candidate),txid),txid)
        oracle_ok = oracle_ok and (decoded.address,decoded.port) == bytewise_oracle(candidate)
        port_cases += 1
    for byte in range(256):
        candidate = encode_value(f'192.0.2.{byte}', (byte*257)%65536)
        decoded = decode_binding_ipv4(message(attribute(0x0020,candidate),txid),txid)
        oracle_ok = oracle_ok and (decoded.address,decoded.port) == bytewise_oracle(candidate)
    checks['bytewise_oracle_all_ports_and_256_addresses_ONE_group'] = oracle_ok and port_cases == 65536
    checks['input_types_and_bounded_wire_profile'] = all([
        rejects(decode_binding_ipv4,bytearray(wire),txid),rejects(decode_binding_ipv4,wire,bytearray(txid)),
        rejects(decode_binding_ipv4,wire,b'short'),rejects(decode_binding_ipv4,b'',txid),
        rejects(decode_binding_ipv4,message(attribute(0x8055,bytes(4072))+attribute(0x0020,value),txid),txid)])
    # Corrupt an opaque HMAC value: format/mapping still decode. No trust check.
    tampered = bytearray(rfc_wire);tampered[60] ^= 1
    checks['integrity_attribute_is_opaque_not_verified'] = decode_binding_ipv4(bytes(tampered),rfc_wire[8:20]).address == '192.0.2.1'
    assert all(type(x) is bool and x for x in checks.values()), checks
    return dict(python=platform.python_version(),dependencies='standard library only',
        scope='offline bounded IPv4 format/mapping inspection only; no STUN transaction or integrity verification',
        wire_length_bytes=len(wire),body_length_bytes=len(body),transaction_id_hex=txid.hex(),
        xor_value_hex=value.hex(),decoded_address=result.address,decoded_port=result.port,
        attributes=[dict(kind_hex=f'{a.kind:04x}',value_hex=a.value.hex(),padding_hex=a.padding.hex())for a in result.attributes],
        rfc5769_decoded_address=rfc_result.address,rfc5769_decoded_port=rfc_result.port,
        independent_port_cases_inside_ONE_group=port_cases,independent_address_cases_inside_same_group=256,
        authentication_performed=False,checks=checks)


if __name__ == '__main__':
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

۴. مرزِ TLV و padding را با مرجعِ مستقل بسنجیم

مرزِ مقدار و فضایِ تکمیل
تصویر مفهومی: مرزِ مقدار و فضایِ تکمیل؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

هر ویژگی، نوع، طول و مقدار دارد. طولِ آن فقط بایت‌های مقدار را می‌شمارد؛ فضایِ تکمیل برای رسیدن به مرزِ چهاربایتی در آن نیست. در نمونه، مقدارِ سه‌بایتیِ lab یک بایتِ تکمیل دارد و مقدارِ پنج‌بایتی، سه بایت. تکمیلِ نمونهٔ دوم عمداً aa است تا نادیده‌گرفتنِ آن در دریافت سنجیده شود؛ فرستندهٔ مطابقِ RFC 8489 باید تکمیلِ صفر بسازد.

ویژگی‌هایی که این رمزگشا معنا نمی‌کند، با نوع، مقدار و padding خام باقی می‌مانند. این نگهداری با تصمیمِ یک مشتریِ کامل دربارهٔ ویژگیِ ناشناختهٔ اجباری یکسان نیست. برنامه MESSAGE-INTEGRITY و FINGERPRINT را بررسی نمی‌کند؛ حتی تغییرِ عمدیِ یک بایتِ مقدارِ HMAC در بردارِ آزمون، مانعِ خواندنِ نشانی نمی‌شود. پس خروجی برای اعلامِ پذیرشِ امنیتی کافی نیست.

{
  "python": "3.14.5",
  "dependencies": "standard library only",
  "scope": "offline bounded IPv4 format/mapping inspection only; no STUN transaction or integrity verification",
  "wire_length_bytes": 52,
  "body_length_bytes": 32,
  "transaction_id_hex": "00112233445566778899aabb",
  "xor_value_hex": "00018edae721c00f",
  "decoded_address": "198.51.100.77",
  "decoded_port": 45000,
  "attributes": [
    {
      "kind_hex": "8022",
      "value_hex": "6c6162",
      "padding_hex": "00"
    },
    {
      "kind_hex": "0020",
      "value_hex": "00018edae721c00f",
      "padding_hex": ""
    },
    {
      "kind_hex": "8055",
      "value_hex": "6162636465",
      "padding_hex": "aaaaaa"
    }
  ],
  "rfc5769_decoded_address": "192.0.2.1",
  "rfc5769_decoded_port": 32853,
  "independent_port_cases_inside_ONE_group": 65536,
  "independent_address_cases_inside_same_group": 256,
  "authentication_performed": false,
  "checks": {
    "own_literal_header_and_mapping": true,
    "rfc5769_known_response_not_authentication": true,
    "opaque_values_and_padding_preserved": true,
    "reserved_value_byte_ignored": true,
    "exact_transaction_binding": true,
    "wrong_type_and_cookie_rejected": true,
    "declared_length_alignment_and_exact_frame": true,
    "attribute_length_overrun_rejected": true,
    "missing_and_duplicate_mapping_rejected": true,
    "unsupported_family_and_value_length_rejected": true,
    "zero_length_opaque_attribute_advances": true,
    "bytewise_oracle_all_ports_and_256_addresses_ONE_group": true,
    "input_types_and_bounded_wire_profile": true,
    "integrity_attribute_is_opaque_not_verified": true
  }
}

مرجعِ مستقل، XOR را بایت‌به‌بایت با ماسک‌های صریح انجام می‌دهد؛ تابعِ اصلی نشانی را به عددِ صحیح تبدیل می‌کند. همهٔ ۶۵۵۳۶ درگاه و ۲۵۶ نشانی در یک گروهِ مقایسه قرار دارند. این تعداد، تعدادِ گروه‌ها را زیاد نمی‌کند. اندازه‌های خراب، شناسهٔ متفاوت، خانوادهٔ خارج از دامنه، ویژگیِ تهی و فضایِ تکمیل نیز شاهدهای جدا دارند.

بردارِ بیرونیِ بخشِ ۲٫۲ در RFC 5769 نیز مستقل از دادهٔ تألیفی خوانده شده و نشانیِ 192.0.2.1 با درگاهِ ۳۲۸۵۳ را می‌دهد. این استفاده از بردار، فقط قالب و نشانی را می‌سنجد؛ صحتِ HMAC یا CRC آن را محاسبه نکرده‌ایم. چهارده گروهِ نام‌دار موفق شده‌اند؛ زمان یا کیفیتِ شبکه اندازه‌گیری نشده است.

در خواندنِ سرآیندِ RTP نیز مرزِ داده مهم بود، اما معنای padding و فیلدِ طول متفاوت بود. انتقالِ یک قاعدهٔ مرزی میانِ دو پروتکل، فقط به دلیلِ شباهتِ نام‌ها درست نیست. اینجا حرکتِ هر ویژگی از طولِ خودش و تکمیلِ چهاربایتی محاسبه می‌شود و پیش از برش، داخلِ پیام بودنِ انتها بررسی می‌گردد.

۵. چهار پرسش دربارهٔ زمینه، دادهٔ ناشناخته و اعتماد

دادهٔ ناشناخته و مرزِ اعتماد
تصویر مفهومی: دادهٔ ناشناخته و مرزِ اعتماد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: چرا فیلدِ طول سی‌ودو است؟ پاسخ: بیست بایتِ سرآیند در آن شمرده نمی‌شود. طولِ کاملِ نمونه پنجاه‌ودو است؛ فضایِ تکمیلِ ویژگی‌ها جزوِ بدنه است، ولی جزوِ طولِ مقدارِ همان ویژگی نیست. این دو سطحِ طول را جدا نگه می‌داریم.

پرسش دوم: شناسهٔ درست، فرستنده را ثابت می‌کند؟ پاسخ: خیر؛ فقط بایت‌های پاسخ را با زمینهٔ موردِ انتظار مقایسه کرده‌ایم. بررسیِ اعتبارِ پیام، منبع و سازوکارهای امنیتی انجام نشده است. نتیجهٔ خواندن نباید مستقیماً به مجوزِ استفاده از نشانی در یک سامانهٔ واقعی تبدیل شود.

پرسش سوم: می‌توان همین XOR را برای IPv6 به کار برد؟ پاسخ: این تابع آن را رد می‌کند. طولِ نشانی و ماسکِ لازم متفاوت‌اند و شناسهٔ تراکنش در ماسکِ IPv6 نقش دارد. افزودنِ آن خانواده به تغییرِ قرارداد، کد و آزمونِ مستقل نیاز دارد؛ برابر بودنِ نامِ ویژگی کافی نیست.

پرسش چهارم: خروجیِ درست یعنی تماس از NAT عبور می‌کند؟ پاسخ: خیر؛ هیچ تبادلِ شبکه یا آزمونِ مسیر انجام نشده است. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ RFC 8489 مربوط به فوریهٔ ۲۰۲۰ و بردارِ RFC 5769 مربوط به آوریلِ ۲۰۱۰ است. این آموزش، خبرِ تازه یا گواهِ کارکردِ محصول نیست.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.