
به اشتراک بگذارید

به اشتراک بگذارید
بستن IP خارجی روی سرور لینوکس یعنی فایروال فقط به رنجهای ثبتشده ایران اجازه ورود بدهد و بقیه دنیا را drop کند. این کار در یک وضعیت باریک منطقی است: سروری که کاربرش داخل کشور است و زیر اسکن گسترده قرار دارد. برای وبسایت عمومی تقریباً همیشه اشتباه است، چون خزنده گوگل و تمدید گواهی SSL هم از بیرون میآیند.
💡 دو پیشنیاز، پیش از اولین دستور
فیلتر کشوری یعنی فایروال، آدرس منبع هر بسته را با فهرست رنجهای ثبتشده یک کشور میسنجد؛ یک دفتر ثبت اداری، نه اندازهگیری موقعیت جغرافیایی.
✅ یک راه ورود مستقل از شبکه داشته باشید: کنسول سرور یا حالت Rescue پنل. اگر سرور شما هیچکدام را ندارد، این آموزش را اجرا نکنید.
✅ بازگشت زماندار را قبل از اولین قانون drop بگذارید.
این راهنما اول خط قرمزها را روشن میکند تا بدانید چه چیزی را از دست میدهید؛ بعد دستورها را با ترتیبی میآورد که نشست SSH شما را زنده نگه میدارد. 👇
📌 آنچه در این مقاله میخوانید
◀️ در چه شرایطی منطقی است و چه چیزی را میشکند؟
◀️ ساخت فهرست رنجهای ایران از داده رسمی RIPE
◀️ پیادهسازی با ipset و iptables بدون قفلشدن
◀️ همان سیاست با nftables و firewalld

پاسخ صادقانه باریک است. این سیاست فقط برای سروری جواب میدهد که مخاطبش داخل کشور است و سرویسش عمومی نیست: پنل مدیریت داخلی، سرور دیتابیس، یا ماشینی که امروز زیر موج اسکن است. برای وبسایت عمومی، پاسخ منفی است.
ترافیک ورودی و خروجی را جدا در نظر بگیرید. اگر فقط زنجیره INPUT را فیلتر کنید و قانون conntrack سر جایش باشد، سرور همچنان خودش به بیرون وصل میشود. چیزی که از بین میرود، اتصالهای آغازشده از بیرون است.
| سرویس | جهت | با drop ورودی | با drop خروجی |
|---|---|---|---|
| خزنده گوگل | ورودی | گوگل صفحه را دریافت نمیکند | بیاثر |
| اعتبارسنجی HTTP-01 گواهی SSL | ورودی | تمدید خودکار شکست میخورد | صدور قطع میشود |
| کالبک درگاه پرداخت خارجی | ورودی | تأیید تراکنش نمیرسد | درخواست اولیه میشکند |
| مخزن بستهها و میرورها | خروجی | سالم میماند، چون پاسخ ESTABLISHED است | دستور apt میشکند |
| دسترسی خودتان از بیرون ایران | ورودی | همان لحظه SSH قطع میشود | بیاثر |
⚠️ فهرستهای کشوری تقریبیاند و کهنه میشوند
رنج IP به یک کشور «تخصیص» داده میشود، ولی تضمینی نیست که دستگاه همان لحظه همانجا باشد. مثل پلاک خودرو است: پلاک اصفهان روی ماشینی که در تبریز پارک شده هم اصفهان میماند. پس هر فهرستی از روز دوم کمی غلط است.
🔶 نکته درباره IP مستقیم
روی سرور مجازی ایرانی وبداده هر سرور IP مستقیم دارد که از اینترنت بینالملل هم در دسترس است، و کنسول VNC و حالت Rescue در پنل هست. فیلتر کشوری همین دسترسی را خودتان میبندید.

منبع درست، فایل آماری خود مرکز RIPE است؛ همان جایی که تخصیصها ثبت میشوند. این فایل روزانه تازه میشود و هر رکورد را با کد کشور، نوع آدرس و وضعیت نشان میدهد. فایل delegated-extended مرکز RIPE کلید نمیخواهد.
curl -fsSL -o /tmp/delegated-ripencc-extended-latest \
https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest
head -n 1 /tmp/delegated-ripencc-extended-latest
فیلدهای هر سطر با کاراکتر | جدا میشوند: رجیستری، کد کشور، نوع آدرس، آدرس شروع، مقدار، تاریخ و وضعیت. برای بستن IP خارجی همین فهرست ورودی فایروال است. فقط رکوردهای allocated و assigned را بردارید.
🔸 تلهای که اسکریپتهای رایج در آن میافتند
در رکوردهای IPv4، ستون پنجم تعداد آدرس است، نه طول پیشوند؛ فرمت رسمی RIR هم میگوید این عدد لازم نیست یک رنج CIDR را نشان بدهد. بیشتر آموزشها آن را با تقسیم ساده به prefix تبدیل میکنند و فرض میکنند توانی از دو است. در فایل ۲۳ اکتبر ۲۰۲۶ سه رکورد ایرانی چنین نبودند.
اسکریپت زیر هر رکورد را به بلوکهای همتراز میشکند و بلوکهای مجاور را ادغام میکند. فقط کتابخانه استاندارد پایتون ۳.۵ یا بالاتر لازم است.
#!/usr/bin/env python3
# cc-cidr.py <delegated-file> <CC>
import ipaddress, sys
src, cc = sys.argv[1], sys.argv[2]
nets = []
with open(src, encoding="utf-8", errors="replace") as fh:
for line in fh:
f = line.rstrip("\n").split("|")
if len(f) < 7 or f[1] != cc or f[2] != "ipv4":
continue
if f[6] not in ("allocated", "assigned"):
continue
start, remain, off = int(ipaddress.IPv4Address(f[3])), int(f[4]), 0
while remain > 0:
size = 1
while size * 2 <= remain and (start + off) % (size * 2) == 0:
size *= 2
nets.append(ipaddress.IPv4Network((start + off, 33 - size.bit_length())))
off += size
remain -= size
for net in ipaddress.collapse_addresses(nets):
print(net)
install -d -m 0755 /etc/ipset
python3 /usr/local/sbin/cc-cidr.py /tmp/delegated-ripencc-extended-latest IR \
> /etc/ipset/iran-v4.cidr
wc -l < /etc/ipset/iran-v4.cidr
اجرای این اسکریپت روی فایل ۲۳ اکتبر ۲۰۲۶ نتیجهای قابل بازتولید داد: ۱۹۶۹ رکورد IPv4 با کد کشور IR که بعد از ادغام به ۱۷۴۷ بلوک CIDR و ۱۰٬۸۴۰٬۰۶۴ آدرس رسیدند. این اسکریپت فقط IPv4 میسازد.

برای بستن IP خارجی، ابزار ipset یک ساختار hash در هسته میسازد و iptables با یک قانون به کل مجموعه نگاه میکند. در hash:net به ازای هر طول پیشوندِ موجود یک جستوجو انجام میشود؛ این فهرست ۱۳ طول پیشوند دارد، یعنی ۱۳ جستوجو بهجای پیمایش ۱۷۴۷ قانون.
قوانین این بخش با سوئیچ -A به انتهای زنجیره اضافه میشوند، پس نتیجه به محتوای فعلی زنجیره بستگی دارد. اول زنجیره INPUT را ببینید و آدرسی که با آن وصلاید را بخوانید. یک نشست SSH دوم هم باز کنید و تا آخر نبندید.
echo "$SSH_CLIENT" | awk '{print $1}' # آدرسی که همین حالا با آن وصلاید
iptables -S INPUT # قوانین فعلی زنجیره
iptables -S INPUT | wc -l
⚠️ اگر زنجیره INPUT از قبل قانون دارد
اگر خروجی بالا چیزی بیش از -P INPUT ACCEPT داشت، یعنی ufw، CSF، docker یا پنل میزبانی پیش از شما قانون گذاشتهاند. دستورهای -A این بخش را اجرا نکنید: با یک DROP قبلی، قوانین ACCEPT بعدی بیاثر میمانند و قفل میشوید؛ با یک ACCEPT عمومی قبلی، قانون drop بیاثر است. قوانین را با همان ابزار موجود بنویسید، یا از -I با شماره سطر استفاده کنید.
طبق صفحه man رسمی ipset، نوع hash:net شبکههایی با طول پیشوند مختلف را نگه میدارد و maxelem سقف تعداد عضو است؛ پیشفرضش ۶۵۵۳۶ است. فرمان restore یک نشست کامل را یکجا میخواند، پس برای هزاران عضو از تکرار add بهتر است.
apt install -y ipset # در راکی و آلما: dnf install -y ipset
# maxelem دو برابر پیشفرض، فقط برای جای رشد؛ ۱۷۴۷ عضو با پیشفرض هم جا میشد.
{ echo "create iran_v4 hash:net family inet hashsize 4096 maxelem 131072"
sed 's/^/add iran_v4 /' /etc/ipset/iran-v4.cidr; } | ipset restore -exist
ipset -t list iran_v4
این گام را قبل از بخش بعدی اجرا کنید، نه بعد از آن. یک کار تأخیری میسازیم که ۱۰ دقیقه بعد قانون drop را حذف کند. اگر دسترسی سالم ماند تایمر را متوقف میکنید؛ اگر قطع شدید، سرور خودش قانون را برمیدارد. فرمان systemd-run کنار سرویس یک تایمر گذرا هم میسازد؛ اگر فهرست تایمرها خالی برگشت، شبکه ایمنی ساخته نشده و نباید جلو بروید.
systemd-run --on-active=10min --unit=fw-rollback --collect \
/bin/sh -c 'iptables -D INPUT -m set ! --match-set iran_v4 src -j DROP'
systemctl list-timers 'fw-rollback*' # باید یک سطر با زمان باقیمانده نشان بدهد
# فقط بعد از اینکه از نشست دوم دسترسی را تست کردید:
# systemctl stop fw-rollback.timer
قانون drop را هرگز اول ننویسید. ترتیب زیر را از بالا به پایین بخوانید: لوپبک، اتصالهای برقرار، اجازه صریح SSH از IP مدیریتی خودتان و تنها در انتها drop. ماژول set با نشانه src آدرس منبع را میسنجد و علامت ! شرط را برعکس میکند.
ADMIN_IP=203.0.113.10 # آدرس نمونه RFC 5737؛ آدرس خودتان را بگذارید
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -s "$ADMIN_IP" -j ACCEPT
iptables -A INPUT -m set ! --match-set iran_v4 src -j DROP
iptables -L INPUT -n --line-numbers
⚠️ قوانین بالا هیچ اثری روی IPv6 ندارند
دستور iptables فقط جدول IPv4 را عوض میکند و ip6tables دستنخورده میماند. اگر سرور آدرس IPv6 دارد، کل دنیا از همان مسیر باز است و این کار بیاثر میشود. چون فهرست کشوری IPv6 نساختهایم، حالت ایمن بستن اتصال ورودی IPv6 است: ACCEPT برای -i lo و ESTABLISHED,RELATED و -p ipv6-icmp، و در آخر ip6tables -A INPUT -j DROP.
قوانین iptables و محتوای ipset هر دو در حافظهاند و با ریبوت پاک میشوند. در دبیان و اوبونتو بسته iptables-persistent قوانین را و ipset-persistent مجموعهها را ذخیره میکند؛ دومی در مخزن universe است. افزونه ۱۰-ipset پیش از ۱۵-ip4tables اجرا میشود، چون قانونی که به مجموعه ناموجود ارجاع بدهد بار نمیشود.
DEBIAN_FRONTEND=noninteractive apt install -y iptables-persistent ipset-persistent
netfilter-persistent save # rules.v4 و /etc/iptables/ipsets
# بعد از ریبوت، پایداری را تأیید کنید:
ipset -t list iran_v4 && iptables -S INPUT
فرمان ذخیره را فقط بعد از انقضای تایمر و تأیید دسترسی بزنید. اگر پیش از آن ذخیره کنید، قانون drop در rules.v4 مینشیند و تایمر بعداً تنها نسخه حافظه را حذف میکند. ناپایدار ماندن قوانین حالت ایمن است: بازگشت دسترسی بعد از ریبوت یعنی ذخیره نشده. ترتیب زنجیرهها را در راهنمای پیکربندی پیشرفته iptables ببینید.

روی توزیعهای تازه، nftables جایگزین پیشفرض است و خودش named set دارد؛ روی راکی و آلما هم firewalld یک لایه بالاتر میدهد. هر دو همان سیاست را برای بستن IP خارجی پیاده میکنند، ولی فوراً و دائمی اثر میگذارند، پس بازگشت زماندار واجبتر است.
طبق ویکی رسمی nftables، یک named set با نوع ipv4_addr و پرچم interval پیشوند CIDR نگه میدارد و با @ صدا زده میشود. فایل زیر قوانین را در جدول جداگانه wd_geo میگذارد؛ دو سطر اول آن را اعلام و بعد حذف میکنند تا اجرای دوباره، جدول تکراری نسازد.
nft list ruleset > /root/nft-backup.conf # پشتیبان، برای بازگشت
ADMIN_IP=203.0.113.10 # آدرس نمونه RFC 5737
cat > /etc/nftables-geo.conf <<EOF
table inet wd_geo
delete table inet wd_geo
table inet wd_geo {
set iran_v4 {
type ipv4_addr
flags interval
elements = { $(paste -sd, /etc/ipset/iran-v4.cidr) }
}
chain input {
type filter hook input priority filter; policy accept;
iif lo accept
ct state established,related accept
tcp dport 22 ip saddr $ADMIN_IP accept
icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert } accept
meta nfproto ipv6 drop
ip saddr != @iran_v4 drop
}
}
EOF
nft -c -f /etc/nftables-geo.conf # فقط بررسی نحوی
سوئیچ -c فایل را فقط بررسی میکند، پس با خطای نحوی فایروال فعلی دستنخورده میماند. دقت کنید فرمان enable –now روی سرویسِ از قبل فعال کاری نمیکند و فایل جدید هرگز بار نمیشود؛ پس بار کردن را صریح انجام بدهید و نتیجه را تأیید کنید.
systemd-run --on-active=10min --unit=fw-rollback --collect \
/bin/sh -c 'nft delete table inet wd_geo'
systemctl list-timers 'fw-rollback*'
nft -f /etc/nftables-geo.conf # بار کردن صریح
nft list set inet wd_geo iran_v4 | head # تأیید
echo 'include "/etc/nftables-geo.conf"' >> /etc/nftables.conf # پایداری
systemctl enable nftables
⚠️ از فرمان flush ruleset در این فایل استفاده نکنید
نمونههای رایج nftables با سطر flush ruleset شروع میشوند. اگر آن سطر در فایلی باشد که هر بوت بار میشود، کل ruleset پاک میشود، از جمله قوانین docker و fail2ban. به همین دلیل جدول جداگانه ساختهایم؛ اول nft list ruleset را ببینید.
در صفحه man ابزار firewall-cmd آمده که میتوانید ipset دائمی بسازید، ورودیها را از فایل بخوانید و همان مجموعه را منبع یک منطقه کنید. منطق برعکس iptables است: منطقه public را فقط به منبع ایرانی میدهید و منطقه پیشفرض را drop میکنید.
firewall-cmd --permanent --new-ipset=iran_v4 --type=hash:net \
--option=family=inet --option=maxelem=131072
firewall-cmd --permanent --ipset=iran_v4 \
--add-entries-from-file=/etc/ipset/iran-v4.cidr
firewall-cmd --permanent --zone=public --add-source=203.0.113.10/32
firewall-cmd --permanent --zone=public --add-source=ipset:iran_v4
firewall-cmd --permanent --zone=public --add-service=http --add-service=https
firewall-cmd --reload
firewall-cmd --get-active-zones
systemd-run --on-active=10min --unit=fw-rollback --collect \
/usr/bin/firewall-cmd --set-default-zone=public
firewall-cmd --set-default-zone=drop # آخرین قدم
⚠️ دو شرطی که مسیر firewalld را بیاثر یا ویرانگر میکند
اول، طبق همان صفحه man، فرمان set-default-zone فقط روی اتصالها و اینترفیسهایی اثر دارد که منطقهشان انتخاب نشده است. اگر کانکشن NetworkManager مقدار connection.zone صریح داشته باشد، این فرمان کاری نمیکند و خیال میکنید فیلتر فعال است؛ خروجی get-active-zones این را نشان میدهد. دوم، منطقه public پیشفرض فقط ssh را باز دارد.
فهرست را نمیشود وسط کار flush کرد، چون در آن فاصله قانون drop همه را میاندازد. راه درست، مجموعه موقت و جابهجا کردن نامها با فرمان swap است؛ هر دو مجموعه باید موجود و از نوع سازگار باشند. اسکریپت زیر را هفتگی با cron اجرا کنید.
#!/bin/sh
# /usr/local/sbin/cc-update.sh
set -eu
F=$(mktemp); N=$(mktemp)
curl -fsSL -o "$F" https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest
python3 /usr/local/sbin/cc-cidr.py "$F" IR > "$N"
[ "$(wc -l < "$N")" -ge 1000 ] || { rm -f "$F" "$N"; exit 1; }
# باقیمانده اجرای ناتمام قبلی را پاک کن، وگرنه add روی محتوای کهنه جمع میشود
ipset destroy iran_v4_new 2>/dev/null || true
{ echo "create iran_v4_new hash:net family inet maxelem 131072"
sed 's/^/add iran_v4_new /' "$N"; } | ipset restore
ipset swap iran_v4_new iran_v4 && ipset destroy iran_v4_new
install -m 0644 "$N" /etc/ipset/iran-v4.cidr
command -v netfilter-persistent >/dev/null && netfilter-persistent save || true
برای مستثنا کردن خزنده گوگل، مستندات تأیید خزندههای گوگل فهرست رنجها را در فایل common-crawlers.json میدهد. مقدارهای ipv4Prefix را در مجموعه دومی بریزید و قانون -m set --match-set allow_v4 src -j ACCEPT را پیش از drop بگذارید. برای فیلتر کشوری در سطح وبسرور، نصب GeoIP در لینوکس مسیر دیگری است.
سوالهایی که پیش از اجرا بیشتر پرسیده میشود:
اگر سرویس وب عمومی روی همان سرور باشد، خزنده گوگل از رنجهای خودش وصل میشود و با drop شدن اتصال، گوگل صفحه را دریافت نمیکند. راه حل، گذاشتن رنجهای اعلامشده گوگل در یک مجموعه مجاز است، پیش از قانون drop.
در روش HTTP-01 بله، چون طبق RFC 8555 مرجع صدور باید خودش به پورت ۸۰ دامنه وصل شود. روش DNS-01 اعتبارسنجی را روی رکورد DNS انجام میدهد و اتصال ورودی نمیخواهد. پیش از بستن پورت، روش تمدید را عوض کنید.
آدرس ثابت خودتان را پیش از قانون drop بهعنوان اجازه صریح بنویسید، یا به مجموعه مجاز اضافه کنید. اگر IP شما ثابت نیست، این سیاست مناسب شما نیست.
فایل آماری RIPE روزانه تازه میشود؛ یک بهروزرسانی هفتگی کافی است. فهرست دستنخورده بعد از چند ماه هم رنجهای تازه داخلی را از دست میدهد و هم رنجهایی را نگه میدارد که دیگر داخلی نیستند.
اگر بازگشت زماندار را قبل از قانون drop تنظیم کرده باشید، سرور خودش بعد از ۱۰ دقیقه قانون را حذف میکند. اگر نکرده باشید، تنها راه کنسول یا حالت Rescue پنل ارائهدهنده است؛ به همین دلیل یکی از این دو پیشنیاز شروع کار است.
نه بهطور کامل. قانون فایروال بعد از رسیدن بسته اجرا میشود، پس پهنای باند ورودی هنوز مصرف میشود. برای حجمهای بالا فیلتر باید در لبه شبکه انجام شود.
ترافیک بازدیدکننده از IP لبههای CDN میرسد که عمدتاً خارجیاند و با قانون drop بسته میشوند؛ یعنی سایت برای همه از کار میافتد. رنجهای همان سرویس را در مجموعه مجاز بگذارید.
جمعبندی؛ بستن IP خارجی یک ابزار باریک است، نه سیاست پیشفرض
فهرست رنجهای کشوری را میشود از داده رسمی RIPE ساخت و با ipset یا معادلهایش در nftables و firewalld اعمال کرد. ولی ترتیب کار مهمتر از خود قانون است: راه ورود مستقل از شبکه، آگاهی از وضعیت فعلی زنجیره، بازگشت زماندارِ تأییدشده، و تنها در پایان قانون drop. سه تله هم هر سه مسیر را بیاثر یا ویرانگر میکند: باز ماندن IPv6، پاک کردن ruleset موجود با flush، و منطقه پیشفرضی که در firewalld روی اینترفیس دارای منطقه صریح اثر ندارد. مهمتر از همه، همان سوال عنوان است: این سیاست برای سرویس داخلی و ماشینی که زیر فشار اسکن است جواب میدهد، ولی برای وبسایت عمومی هزینهاش بیشتر از فایدهاش است. در بیشتر مواردی که با «حمله از خارج» شروع میشود، محدود کردن پورت مدیریتی به چند آدرس و نرخگیری روی اتصالهای جدید همان نتیجه را با ریسک کمتری میدهند.
🚀 سرور را روی شبکه داخلی بالا بیاورید
اگر مخاطب سرویس شما داخل کشور است، نقطه شروع درست، سروری در دیتاسنتر داخلی با IP مستقیم خودتان است. تحویل بعد از پرداخت خودکار انجام میشود.
اگر سوالی داشتید، در بخش نظرات با ما در ارتباط باشید.