فهرست مقاله
۱. نشانیِ مبدأ را هویتِ تماسگیرنده ندانیم

در طراحیِ ورودیِ یک سامانهٔ ارتباطی، ممکن است بخواهیم قواعدِ شبکهٔ مبدأ را پیش از استقرار بازبینی کنیم. اگر یک نشانی هم در شبکهٔ بزرگ و هم در استثنای کوچک قرار بگیرد، کدام قاعده انتخاب میشود؟ این درس یک سیاستِ تألیفیِ آفلاین میسازد: پیشوندِ بلندتر برنده است و نبودِ تطبیق به deny میرسد. هیچ بسته، تماس یا تنظیمِ واقعی تغییر نمیکند.
این قرارداد، پیادهسازیِ SBC، پشتهٔ SIP یا دیوارهٔ آتش نیست. رشتهٔ مبدأ از قبل به تابع داده شده است؛ برنامه آن را از سوکت استخراج نمیکند و دربارهٔ منشأِ قابلِ اعتمادش شاهدی ندارد. انتخابِ allow برای آن رشته، هویتِ تماسگیرنده، تعلقِ واقعیِ بسته به آن مبدأ، عبور از NAT یا مجوزِ اجرای یک اقدام را اثبات نمیکند.
در خواندنِ پاسخِ STUN، مسئله استخراجِ نشانی از بایتهای پیام بود؛ اینجا نشانی آماده است و عضویت در مجموعههای عددی بررسی میشود. فیلترِ مجوزِ اسناد نیز به ویژگیهای فرضشدهٔ درخواستکننده تکیه دارد. در هر دو زمینه، محاسبهٔ یک تصمیم با اثباتِ معتبر بودنِ ورودی تفاوت دارد.
نمونهٔ ما از بلوکهای مستندسازیِ RFC 5737 استفاده میکند؛ انتخابِ زیرشبکهها، نشانیهای نمونه و اقدامها تألیفی است. این نشانیها مقصدِ یک آزمایشِ اتصال نیستند. بهجای لاگِ واقعی یا دستگاهِ بیرونی، فهرستی از قواعد و چند نشانی داریم تا رفتارِ همپوشانی، پیشفرض و مرزِ نمایش را دقیق و قابلِ تکرار ببینیم.
۲. شبکهٔ CIDR و دامنهٔ عضویت را دقیق کنیم

IPv4 یک عددِ سیودوبیتی است که معمولاً به شکلِ چهار بخشِ دهدهیِ صفر تا ۲۵۵ نوشته میشود. در CIDR، عددِ پس از ممیزِ مورب میگوید چند بیتِ پرارزش، پیشوندِ شبکهاند. بخشِ ۳٫۱ از RFC 4632 این نمایش را توضیح میدهد؛ هرچه پیشوند بلندتر باشد، تعدادِ نشانیهای بلوک کمتر میشود.
برای /24، تعدادِ اعضا ۲۵۶ است؛ /31 دو عضو و /32 یک عضو دارد. /0 تمامِ فضای عددیِ IPv4 را پوشش میدهد. اینها شمارِ اعضای یک مجموعهاند، نه تعدادِ میزبانهای قابلِ استفاده یا تماسهای همزمان. برنامه شبکه و broadcast را از عضویت حذف نمیکند؛ در این درس تابعِ hosts را بهجای عملگرِ عضویت به کار نمیبریم.
از IPv4Address و IPv4Network در پایتون برای خواندنِ نشانی و شبکه استفاده میکنیم، اما رابطِ ما محدودتر است. فقط str واقعی با نویسههای ASCII و نمایشِ معیار پذیرفته میشود. نشانیِ بدونِ پیشوند برای شبکه، ماسکِ نقطهای، صفرِ ابتداییِ اضافی، فاصله، IPv6 و شبکهٔ دارای بیتِ میزبان رد میشوند؛ تبدیلِ ضمنیِ رشتهٔ نامرتب به شبکه نداریم.
مثلاً 192.0.2.1/24 در این قرارداد شبکهٔ معتبرِ ورودی نیست، چون بیتِ میزبان دارد. انتخابِ strict در سازندهٔ کتابخانه همین خطا را آشکار میکند؛ ما علاوه بر آن، برابریِ متن با نمایشِ معیار را میخواهیم. دلیلِ محدودیت، داشتنِ یک شکلِ صریح برای بازبینی است. این تصمیم را قابلیت یا اجبارِ عمومیِ همهٔ ورودیهای کتابخانه نمینامیم.
۳. کد کاملِ انتخابِ پیشوندِ بلندتر

قواعد یک tuple واقعی، شاملِ حداکثر شصتوچهار زوجِ tuple هستند. هر زوج، CIDR و رشتهٔ دقیقِ allow یا deny دارد. پیشوندِ تکراری حتی با اقدامِ یکسان رد میشود؛ در نتیجه برای یک شبکه دو تصمیمِ همرتبه نداریم. فهرستِ تهی مجاز است و هر نشانیِ معتبرِ آن به تصمیمِ پیشفرضِ deny میرسد.
قواعدِ نمونه شاملِ deny برای /0، allow برای 192.0.2.0/24، deny برای نیمهٔ 192.0.2.128/25 و allow برای تکنشانیِ 192.0.2.200/32 است. قاعدهٔ دیگری هم 198.51.100.0/31 را allow میکند. نشانیِ 192.0.2.7 از /24 اجازهٔ قراردادی میگیرد، 192.0.2.140 به /25 میرسد و 192.0.2.200 با استثنای /32 انتخاب میشود.
بخشِ ۵٫۱ از RFC 4632، ترجیحِ پیشوندِ مشخصتر را در زمینهٔ هدایتِ بسته توضیح میدهد؛ این منبع استانداردِ اقدامهای سیاستِ ما نیست. اینجا خودمان انتخابِ پیشوندِ بلندتر، معنیِ دو اقدام و پیشفرض را تعریف کردهایم. آن را به رفتارِ تمامِ ACLها تعمیم نمیدهیم؛ یک سامانهٔ دیگر ممکن است قواعدِ مرتبِ نخستینتطبیق یا ترکیبِ شرطهای بیشتری داشته باشد.
کد، تمامِ قواعدِ محدود را بررسی و بلندترینِ منطبق را انتخاب میکند؛ ترتیبِ فهرست جایِ طولِ پیشوند را نمیگیرد. شیءِ سیاست فقط رشتهها و عددهای تغییرناپذیر را نگه میدارد. آن را با build_policy بسازید؛ ساختِ دستیِ Policy با فیلدهای ناسازگار پشتیبانی نمیشود. خروجی، قاعدهٔ انتخابشده و تصمیم را ثبت میکند تا نبودِ تطبیق با denyِ صریح قابلِ تشخیص باشد.
"""Own offline IPv4 source-prefix action selection; not a router or firewall."""
from dataclasses import dataclass
from ipaddress import IPv4Address,IPv4Network
from itertools import product
import json
import platform
def address(text):
if type(text) is not str:
raise TypeError('source must be an actual string')
if not text.isascii() or not 7 <= len(text) <= 15:
raise ValueError('canonical ASCII dotted-decimal IPv4 required')
value=IPv4Address(text)
if str(value) != text:
raise ValueError('noncanonical source spelling')
return value
def network(text):
if type(text) is not str:
raise TypeError('CIDR must be an actual string')
if not text.isascii() or not 9 <= len(text) <= 18 or text.count('/') != 1:
raise ValueError('canonical ASCII IPv4/prefix required')
value=IPv4Network(text,strict=True)
if str(value) != text:
raise ValueError('canonical prefix-length spelling required')
return value
@dataclass(frozen=True,slots=True)
class Policy:
# Only primitive immutable fields are stored, not mutable network objects.
rules: tuple
def build_policy(rules):
if type(rules) is not tuple:
raise TypeError('rules must be an actual tuple')
if len(rules) > 64:
raise ValueError('own maximum64 rules exceeded')
seen=set();stored=[]
for rule in rules:
if type(rule) is not tuple or len(rule) != 2:
raise TypeError('each rule must be an actual pair tuple')
cidr,action=rule
net=network(cidr)
if type(action) is not str:
raise TypeError('action must be an actual string')
if action not in ('allow','deny'):
raise ValueError('own action must be allow or deny')
if cidr in seen:
raise ValueError('duplicate canonical prefix, including same-action duplicates')
seen.add(cidr);stored.append((cidr,action,net.prefixlen))
return Policy(tuple(stored))
def select(policy,source):
"""Use build_policy only; malformed manual Policy construction unsupported."""
if type(policy) is not Policy:
raise TypeError('factory-built exact Policy required')
source_address=address(source);best=None
for rule in policy.rules:
cidr,action,prefix=rule
if source_address in IPv4Network(cidr,strict=True) and (best is None or prefix > best[2]):
best=rule
return dict(source=source,matched_cidr=best[0] if best else None,
prefix_length=best[2] if best else None,decision=best[1] if best else 'deny')
def interval_reference(rules,source):
"""Test only: manual octets and half-open intervals, no ipaddress use."""
def integer(text):
value=0
for octet in text.split('.'):
value=value*256+int(octet)
return value
candidates=[];number=integer(source)
for cidr,action in rules:
base,prefix_text=cidr.split('/');prefix=int(prefix_text)
low=integer(base);span=1 << (32-prefix)
if low <= number < low+span:
candidates.append((span,cidr,prefix,action))
best=min(candidates) if candidates else None
return dict(source=source,matched_cidr=best[1] if best else None,
prefix_length=best[2] if best else None,decision=best[3] if best else 'deny')
def rejected(exception,function,*args):
try:
function(*args)
except exception:
return True
return False
def main():
rules=(('0.0.0.0/0','deny'),('192.0.2.0/24','allow'),
('192.0.2.128/25','deny'),('192.0.2.200/32','allow'),('198.51.100.0/31','allow'))
policy=build_policy(rules)
sources=('192.0.2.7','192.0.2.140','192.0.2.200','198.51.100.0',
'198.51.100.1','198.51.100.2','203.0.113.9')
outputs=[select(policy,s)for s in sources];checks={}
checks['hand_nested_override_decisions'] = [x['decision']for x in outputs] == ['allow','deny','allow','allow','allow','deny','deny']
checks['no_match_default_differs_from_explicit_default_rule'] = (
select(build_policy(()),'203.0.113.9')['matched_cidr'] is None
and select(policy,'203.0.113.9')['matched_cidr'] == '0.0.0.0/0')
all_addresses=build_policy((('0.0.0.0/0','allow'),))
checks['zero_prefix_contains_entire_numeric_address_space'] = all(
select(all_addresses,s)['decision'] == 'allow'for s in ('0.0.0.0','255.255.255.255'))
pair=build_policy((('198.51.100.0/31','allow'),))
checks['slash31_numeric_membership_includes_both_addresses'] = (
[select(pair,s)['decision']for s in ('198.51.100.0','198.51.100.1','198.51.100.2')] == ['allow','allow','deny'])
single=build_policy((('192.0.2.200/32','allow'),))
checks['slash32_matches_exactly_one_address'] = (
[select(single,s)['decision']for s in ('192.0.2.199','192.0.2.200','192.0.2.201')] == ['deny','allow','deny'])
checks['rule_order_does_not_replace_longest_prefix'] = (
[select(build_policy(tuple(reversed(rules))),s)for s in sources] == outputs)
ordinary=build_policy((('192.0.2.0/24','allow'),))
checks['network_and_broadcast_numbers_are_set_members'] = all(
select(ordinary,s)['decision'] == 'allow'for s in ('192.0.2.0','192.0.2.255'))
checks['duplicate_prefix_rejected_even_with_same_action'] = all(
rejected(ValueError,build_policy,(('192.0.2.0/24','allow'),('192.0.2.0/24',action)))for action in ('allow','deny'))
checks['noncanonical_network_and_host_bits_rejected'] = all(
rejected(ValueError,build_policy,((cidr,'allow'),))for cidr in
('192.0.2.1/24','192.0.2.0/255.255.255.0','192.0.2.0/024','192.0.2.0','192.0.2.0/33','192.0.2.0/-1'))
checks['noncanonical_source_and_mixed_family_rejected'] = all(
rejected(ValueError,select,policy,s)for s in ('192.000.2.1',' 192.0.2.1','192.0.2.1 ','192.0.2.256','::1','::ffff:192.0.2.1'))
checks['exact_rule_action_policy_and_source_types'] = all([
rejected(TypeError,build_policy,list(rules)),rejected(TypeError,build_policy,(['192.0.2.0/24','allow'],)),
rejected(TypeError,build_policy,(('192.0.2.0/24',True),)),rejected(TypeError,select,policy,0),
rejected(TypeError,select,(),sources[0]),rejected(ValueError,build_policy,(('192.0.2.0/24','ALLOW'),))])
capped=tuple(('203.0.113.'+str(i)+'/32','allow')for i in range(64))
checks['empty_and_inclusive64_rule_cap'] = (
build_policy(()).rules == () and len(build_policy(capped).rules) == 64
and select(build_policy(capped),'203.0.113.63')['decision'] == 'allow'
and select(build_policy(capped),'203.0.113.64')['decision'] == 'deny'
and rejected(ValueError,build_policy,capped+(('203.0.113.64/32','allow'),)))
checks['ascii_spelling_required'] = all([
rejected(ValueError,select,policy,'\u06f1\u06f9\u06f2.0.2.1'),
rejected(ValueError,build_policy,(('192.0.2.0/\u06f2\u06f4','allow'),))])
cases,passed=0,True
for included in product((False,True),repeat=len(rules)):
subset=tuple(rule for rule,yes in zip(rules,included)if yes);candidate=build_policy(subset)
addresses=tuple('192.0.2.'+str(i)for i in range(256))+tuple('198.51.100.'+str(i)for i in range(4))
for source in addresses:
passed &= select(candidate,source) == interval_reference(subset,source)
cases+=1
checks['independent_manual_octet_interval_oracle'] = bool(passed and cases == 8320)
assert len(checks) == 14 and all(type(v) is bool and v for v in checks.values()),checks
print(json.dumps(dict(python=platform.python_version(),checks=checks,rules=rules,example=outputs,
independent_oracle_cases_in_one_group=cases,
scope='Own offline source-prefix action policy only; not SIP/SBC, caller authentication, firewall deployment or network reachability.'),indent=2))
if __name__ == '__main__':
main()
۴. همپوشانی را با مرجعِ بازهٔ صحیح بسنجیم

مرجعِ مستقل از ipaddress برای تعیینِ عضویت استفاده نمیکند. چهار بخشِ نشانی را با ضربِ پیاپی در ۲۵۶ به عدد تبدیل میکند. برای پیشوندِ p، اندازهٔ بلوک دو به توانِ سیودو منهای p است. بازه از عددِ شبکه شروع میشود و انتهایش شامل نمیشود؛ نشانیِ برابرِ انتها دیگر عضوِ آن بلوک نیست.
از میانِ بازههای شاملِ نشانی، مرجع کوچکترین بازه را انتخاب میکند؛ این همان انتخابِ پیشوندِ بلندتر برای قواعدِ معتبرِ ورودی است. مرجع، پارسرِ مستقلِ همهٔ رشتههای نامعتبر نیست؛ دادههای معتبر را برای بررسیِ محاسبهٔ عضویت و انتخاب میگیرد. آزمونهای منفیِ جدا، خطاهای نوع، نگارش، بیتِ میزبان، اقدام و شمارِ قواعد را کنترل میکنند.
تمامِ ۳۲ زیرمجموعهٔ پنج قاعدهٔ نمونه را با ۲۶۰ نشانی مقایسه کردیم: ۸۳۲۰ مقایسه، همگی در یک گروهِ مرجع. گروههای مکمل، /0، /31، /32، استثنای تودرتو، جابهجاییِ ترتیب، شبکه و broadcast، تکرارِ قاعده و سقفِ ورودی را میسنجند. تعدادِ کلِ گروههای نامدار چهارده است؛ این بررسیِ محدود، پیمایشِ تمامِ فضای سیودوبیتی یا همهٔ سیاستهای ممکن نیست.
اجرای واقعی با Python 3.14.5 چهارده نتیجهٔ درست و stderr تهی ثبت کرد. صفحهٔ مستندات، نسخهٔ 3.14.8 را نشان میداد؛ RFC 4632 مربوط به اوتِ ۲۰۰۶ و RFC 5737 مربوط به ژانویهٔ ۲۰۱۰ است. دسترسی در ۱۸ اکتبرِ ۲۰۲۶ ثبت شد. این تاریخها از اجرای کد جدا هستند؛ هیچ DNS، سوکت، سنجشِ سرعت یا تغییرِ تنظیماتِ شبکه انجام نشده است.
{
"python": "3.14.5",
"checks": {
"hand_nested_override_decisions": true,
"no_match_default_differs_from_explicit_default_rule": true,
"zero_prefix_contains_entire_numeric_address_space": true,
"slash31_numeric_membership_includes_both_addresses": true,
"slash32_matches_exactly_one_address": true,
"rule_order_does_not_replace_longest_prefix": true,
"network_and_broadcast_numbers_are_set_members": true,
"duplicate_prefix_rejected_even_with_same_action": true,
"noncanonical_network_and_host_bits_rejected": true,
"noncanonical_source_and_mixed_family_rejected": true,
"exact_rule_action_policy_and_source_types": true,
"empty_and_inclusive64_rule_cap": true,
"ascii_spelling_required": true,
"independent_manual_octet_interval_oracle": true
},
"rules": [
[
"0.0.0.0/0",
"deny"
],
[
"192.0.2.0/24",
"allow"
],
[
"192.0.2.128/25",
"deny"
],
[
"192.0.2.200/32",
"allow"
],
[
"198.51.100.0/31",
"allow"
]
],
"example": [
{
"source": "192.0.2.7",
"matched_cidr": "192.0.2.0/24",
"prefix_length": 24,
"decision": "allow"
},
{
"source": "192.0.2.140",
"matched_cidr": "192.0.2.128/25",
"prefix_length": 25,
"decision": "deny"
},
{
"source": "192.0.2.200",
"matched_cidr": "192.0.2.200/32",
"prefix_length": 32,
"decision": "allow"
},
{
"source": "198.51.100.0",
"matched_cidr": "198.51.100.0/31",
"prefix_length": 31,
"decision": "allow"
},
{
"source": "198.51.100.1",
"matched_cidr": "198.51.100.0/31",
"prefix_length": 31,
"decision": "allow"
},
{
"source": "198.51.100.2",
"matched_cidr": "0.0.0.0/0",
"prefix_length": 0,
"decision": "deny"
},
{
"source": "203.0.113.9",
"matched_cidr": "0.0.0.0/0",
"prefix_length": 0,
"decision": "deny"
}
],
"independent_oracle_cases_in_one_group": 8320,
"scope": "Own offline source-prefix action policy only; not SIP/SBC, caller authentication, firewall deployment or network reachability."
}
۵. چهار پرسش دربارهٔ پیشفرض و مرزِ کنترل

پرسش اول: اگر قاعدهٔ denyِ /25 پیش از allowِ /24 یا پس از آن نوشته شود، تصمیمِ مربوط به 192.0.2.140 چه میشود؟ طولِ متنِ قاعده ملاک است؟
پاسخ: در هر دو ترتیب، /25 انتخاب میشود و تصمیم deny است؛ تعدادِ بیتهای پیشوند ملاک است، نه جایِ سطر یا تعدادِ نویسهها. رفتارِ نخستینتطبیق، قراردادِ دیگری است و نباید نتیجهٔ آن را انتظارِ همین تابع قرار داد.
پرسش دوم: چرا هر دو عددِ 198.51.100.0 و 198.51.100.1 در قاعدهٔ /31 منطبق میشوند؟ آیا از این نتیجه میتوان وضعیتِ واقعیِ میزبانها را فهمید؟
پاسخ: /31 یک بیتِ آزاد دارد و مجموعهٔ عددیِ آن دو عضو دارد. ما عضویت در آن مجموعه را بررسی میکنیم؛ میزبانِ فعال، قابلیتِ استفاده در یک شبکهٔ مشخص یا وضعیتِ تماس را کشف نمیکنیم. فهرستِ hosts مسئله و قراردادِ جداگانهای دارد.
پرسش سوم: چرا قاعدهٔ تکراری با اقدامِ یکسان هم رد میشود؟ آیا بازگرداندنِ deny برای فهرستِ تهی با denyِ صریحِ /0 کاملاً یک گزارش دارد؟
پاسخ: ردِ تکرار، ابهامِ پیکربندی و همرتبگی را از قرارداد حذف میکند. در فهرستِ تهی، قاعدهٔ منطبق None است؛ در denyِ /0، خودِ همان پیشوند ثبت میشود. تصمیمِ نهایی یکسان است، اما منشأِ تصمیم برای بازبینی تفاوت دارد.
پرسش چهارم: اگر تمامِ آزمونها درست باشند و یک رشته به allow برسد، آیا ورودیِ تماس واقعاً احرازِ هویت و محافظت شده است؟ چه بخشی هنوز آزموده نشده؟
پاسخ: خیر؛ اعتبارِ منشأِ نشانی، دریافتِ واقعیِ بسته، NAT، جعلِ مبدأ، هویت و محلِ اعمالِ تصمیم خارجِ برنامهاند. این کد فقط سیاستِ عددیِ تعریفشده را بازبینی میکند؛ اجازهٔ قراردادیِ یک نمونه، گواهِ امنیت یا اجرایِ کنترل در یک سامانهٔ واقعی نیست.
منابع و مطالعهٔ بیشتر
- Python3.14؛ IPv4Address، IPv4Network و عضویتِ نشانی
- RFC 4632؛ نمایشِ CIDR و پیشوندِ مشخصتر
- RFC 5737؛ بلوکهای نشانی برای مستندسازی
بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.


