چالش اتصال رابط شبکه در محیط‌های چند اتصالی VPN

پشته‌های شبکه نوین در هسته لینوکس معمولاً در پیکربندی‌های چند اتصالی (multi-homed) فعالیت می‌کنند که در آن‌ها کارت‌های شبکه فیزیکی (eth0 و wlan0)، مودم‌های داده سلولی (wwan0) و آداپتورهای تونل مجازی (wg0 و tun0) به طور هم‌زمان فعال هستند. در حالت استاندارد، سیستم‌عامل برای تعیین رابط خروجی بسته‌ها (egress) بر پایگاه اطلاعات هدایت بسته FIB (Forwarding Information Base) و قواعد مسیریابی مبتنی بر سیاست (ip rule) تکیه می‌کند. هنگامی که یک برنامه توابع connect() یا sendto() را فراخوانی می‌کند، زیرسیستم مسیریابی بر اساس آدرس مقصد، آدرس مبدأ، نشانه‌های بسته (fwmark) و مقادیر اولویت به جستجوی مسیر می‌پردازد.

با این وجود، اتکای محض به جداول مسیریابی عمومی در محیط‌های عملیاتی VPN آسیب‌پذیری‌های امنیتی جدی به همراه دارد. چنانچه اتصال تونل مجازی به طور موقت قطع شود، کارخواه DHCP گیت‌وی پیش‌فرض جدیدی تنظیم کند، یا جدول مسیریابی در حین جابه‌جایی پیوندها پاک شود، بسته‌های اطلاعاتی برنامه‌ها ممکن است به صورت متن ساده و رمزنگاری‌نشده از طریق رابط فیزیکی ارسال شوند. این پدیده که نشت مسیریابی (routing leak) نام دارد، داده‌های کاربر و درخواست‌های DNS را در اختیار ارائه‌دهندگان اینترنت و شبکه‌های محلی قرار می‌دهد.

علاوه بر این، برنامه‌های کارخواه با چالش بازگشت چرخه‌ای مسیرها (routing loop) روبه‌رو هستند: سرویس تونل باید بسته‌های UDP رمزنگاری‌شده را از طریق کارت شبکه فیزیکی به اینترنت ارسال کند، در حالی که سایر برنامه‌های سیستم باید ترافیک خود را دقیقاً به درون رابط مجازی تونل هدایت نمایند. در صورت بروز اختلال در سیاست‌های مسیریابی، بسته‌های کپسوله‌شده به اشتباه مجدداً وارد رابط مجازی شده و با سرریز بافرها، اتصال را قطع می‌کنند.

هسته لینوکس این مسئله را در لایه انتزاعی سوکت‌ها از طریق گزینه‌های SO_BINDTODEVICE و SO_BINDTOIFINDEX به شکل بنیادین حل کرده است. با قفل کردن مستقیم سوکت به شناسه یک رابط مشخص، تصمیم‌گیری پیرامون مسیر خروجی در سطح سوکت تثبیت شده و بر قوانین عمومی جدول‌های مسیریابی اولویت می‌یابد.

+-------------------------------------------------------------------------+
|                               برنامه کاربردی                             |
+-------------------------------------------------------------------------+
                                     |
               setsockopt(..., SOL_SOCKET, SO_BINDTODEVICE, "wg0", ...)
                                     |
+------------------------------------+------------------------------------+
| لایه سوکت هسته لینوکس (net/core/sock.c)                                 |
| -> تنظیم مقدار sk->sk_bound_dev_if = ifindex("wg0")                     |
+-------------------------------------------------------------------------+
                                     |
         +---------------------------+---------------------------+
         |                                                       |
         v                                                       v
+-----------------------------+             +-----------------------------+
|      جستجوی مسیر در FIB      |             |       قطعی پیوند شبکه       |
| (موتور FIB جستجو را فقط به  |             | (رابط تونل wg0 خاموش است)   |
|  رابط خروجی wg0 محدود می‌کند|             |                             |
+-----------------------------+             +-----------------------------+
         |                                                       |
         v                                                       v
ارسال امن بسته از طریق تونل wg0              هسته خطای ENETUNREACH برمی‌گرداند
(عدم نشت داده‌های رمزنگاری‌نشده)             (مسدودسازی قطعی خروج از eth0)

سازوکار درونی هسته: تفاوت SO_BINDTODEVICE و SO_BINDTOIFINDEX

در سورس‌کد هسته لینوکس در فایل net/core/sock.c، اطلاعات اتصال رابط مستقیماً در ساختار اصلی سوکت نگهداری می‌شود:

struct sock {
    ...
    int sk_bound_dev_if; /* شناسه رابط متصل‌شده در صورت بزرگ‌تر بودن از 0 */
    ...
};

هنگامی که برنامه‌ای تابع setsockopt را با گزینه SO_BINDTODEVICE فراخوانی می‌کند، رشته‌ای متنی حاوی نام رابط (مانند "wg0" یا "eth0") را ارسال می‌نماید. تابع داخلی هسته sock_setbindtodevice() مراحل زیر را طی می‌کند:

  1. قفل انحصاری سوکت (lock_sock) را فعال می‌کند.
  2. اگر رشته خالی باشد، مقدار sk->sk_bound_dev_if = 0 را بازنشانی کرده و اتصال سوکت را لغو می‌نماید.
  3. در صورت ارائه نام رابط، تابع dev_get_by_name(net, devname) را جهت یافتن رابط در فضای نام شبکه مربوطه فراخوانی می‌کند.
  4. پس از یافتن دستگاه، ایندکس عددی ۳۲ بیتی آن (dev->ifindex) درون sk->sk_bound_dev_if ذخیره می‌شود.
  5. شمارنده‌های ارجاع به دستگاه شبکه به دقت مدیریت می‌شوند تا اشاره‌گرهای معلق در حافظه باقی نمانند.

از نسخه ۵.۰ هسته لینوکس به بعد، گزینه SO_BINDTOIFINDEX در اختیار توسعه‌دهندگان قرار گرفت. به جای ارسال رشته متنی که نیازمند کپی در حافظه و جستجوی نام با dev_get_by_name() است، برنامه مستقیماً مقدار صحیح ifindex را به هسته تحویل می‌دهد:

int ifindex = if_nametoindex("wg0");
if (ifindex > 0) {
    setsockopt(sockfd, SOL_SOCKET, SO_BINDTOIFINDEX, &ifindex, sizeof(ifindex));
}

این بهینه‌سازی بار پردازشی ناشی از پردازش رشته‌ها را در گیت‌وی‌ها و پروکسی‌های پرسرعت که هزاران سوکت را به صورت هم‌زمان ایجاد و متصل می‌کنند، کاملاً از میان برمی‌دارد.

محدودیت‌های جستجوی FIB و الزام خروج از رابط مشخص

زمانی که بسته‌ای روی یک سوکت متصل‌شده تولید می‌شود، پشته شبکه ساختار جریان داده (struct flowi4 برای IPv4 یا struct flowi6 برای IPv6) را شکل می‌دهد. مقدار sk->sk_bound_dev_if مستقیماً درون فیلد flowi4_oif کپی می‌شود:

struct flowi4 fl4 = {
    .flowi4_oif = sk->sk_bound_dev_if,
    .daddr = dst_ip,
    .saddr = src_ip,
    ...
};

در فرایند جستجوی جدول مسیرها (fib_lookup() در net/ipv4/fib_frontend.c)، موتور مسیریابی گزینه‌های انتخابی را دقیقاً به مواردی محدود می‌سازد که با flowi4_oif تطابق دارند:

  • حتی اگر مسیری با نشانی پیش‌فرض 0.0.0.0/0 روی eth0 با اولویت بالا در دسترس باشد، موتور مسیریابی آن را رد می‌کند زیرا flowi4_oif به سمت wg0 تنظیم شده است.
  • هسته تنها مسیرهایی را می‌پذیرد که رابط خروجی آن‌ها مشخصاً wg0 باشد.
  • اگر رابط به صورت مدیریتی غیرفعال شود (ip link set wg0 down)، جستجو بلافاصله با شکست روبه‌رو شده و هسته خطای -ENETUNREACH را به برنامه بازمی‌گرداند.

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

جلوگیری از حلقه‌های بازگشتی تونل بدون وابستگی به Fwmark

شیوه سنتی مهار حلقه‌های ترافیک تونل از مسیریابی مبتنی بر سیاست استفاده می‌کند:

  1. تعریف قاعده ip rule برای بررسی نشانه fwmark 51820.
  2. هدایت ترافیک فاقد نشانه به جدول مسیریابی اختصاصی VPN.
  3. نشانه گذاری بسته‌های خارجی UDP توسط سرویس دهنده از طریق SO_MARK = 51820.

هرچند این راهکار کارآمد است، اما به ثبات و عدم تداخل قوانین در iptables و nftables وابسته است. با بازنشانی یا تغییر در اسکریپت‌های فایروال محلی، این نشانه‌ها حذف شده و سامانه دچار اختلال چرخه‌ای می‌شود.

مسیریابی سنتی با سیاست‌ها (آسیب‌پذیر در برابر تغییر فایروال):
بسته -> نشانه Netfilter -> بررسی ip rule -> جدول VPN یا جدول فیزیکی

تثبیت در لایه سوکت (تضمین شده توسط هسته):
سرویس تونل خارجی -> SO_BINDTOIFINDEX(eth0) -> ارسال مستقیم به صف eth0
سوکت برنامه‌ها     -> SO_BINDTOIFINDEX(wg0)  -> ارسال مستقیم به تونل wg0

با به‌کارگیری SO_BINDTODEVICE یا SO_BINDTOIFINDEX، عملکرد به شکل قطعی تثبیت می‌شود:

  • برنامه تونل شناسه رابط فیزیکی متصل به اینترنت (eth0_idx) را به دست می‌آورد.
  • با فراخوانی setsockopt(tunnel_fd, SOL_SOCKET, SO_BINDTOIFINDEX, &eth0_idx, sizeof(eth0_idx)) سوکت خود را به آن قفل می‌کند.
  • تمام بسته‌های رمزنگاری‌شده منحصراً از خروجی eth0 ارسال می‌شوند و مسیر پیش‌فرض روی wg0 بر آن‌ها بی‌اثر خواهد بود.

قابلیت قطع اضطراری اتصال (Killswitch) در سطح سیستم‌عامل

سازوکارهای نرم‌افزاری قطع اتصال (Killswitch) در فضای کاربری با تأخیر در واکنش روبه‌رو هستند. هنگام قطع ناگهانی لینک، پردازه‌های ناظر ممکن است برای دریافت رخداد از طریق netlink و اعمال قوانین فایروال به ده‌ها میلی‌ثانیه زمان نیاز داشته باشند. در این فاصله زمانی، نشست‌های باز TCP می‌توانند ترافیک را به شکل رمزنگاری‌نشده به رابط فیزیکی بفرستند.

در مقابل، اتصال سوکت به صورت آنی و هم‌گام درون هسته عمل می‌کند:

شرایط رخداد قابلیت قطع نرم‌افزاری کاربر اتصال سوکت به رابط (SO_BINDTODEVICE)
قطعی تونل مجازی نشت اطلاعات در زمان پردازش ناظر خطای ENETUNREACH در همان فراخوانی سیستمی
پاک شدن جدول مسیر هدایت بسته به مسیر فیزیکی پیش‌فرض حفظ الزام خروج از رابط مشخص توسط هسته
تغییرات ناخواسته DHCP انحراف بسته‌ها به سمت مسیر جدید رد رابط جدید توسط فیلد flowi_oif
کاهش دسترسی پردازه ناتوانی برنامه از تغییر فایروال تداوم اعتبار اتصال سوکت پس از سلب دسترسی

از آنجا که این پیوند به توصیف‌گر فایل سوکت متصل است، ریسه‌های زیرمجموعه و اتصالات پایدار در تمام چرخه حیات خود از این محافظت بهره‌مند خواهند بود.

پیاده‌سازی کاربردی در زبان C و تعامل با VRF

۱. تابع ایجاد سوکت مقید در زبان C

کد زیر نحوه قفل کردن ایمن یک سوکت به رابط تونل را نشان می‌دهد:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>

int create_pinned_socket(const char *interface_name) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("خطا در ایجاد سوکت");
        return -1;
    }

    /* استخراج شناسه عددی رابط شبکه */
    unsigned int ifindex = if_nametoindex(interface_name);
    if (ifindex == 0) {
        fprintf(stderr, "رابط %s یافت نشد: %s\n",
                interface_name, strerror(errno));
        close(fd);
        return -1;
    }

    /* روش ترجیحی: SO_BINDTOIFINDEX در لینوکس 5.0 به بعد */
    if (setsockopt(fd, SOL_SOCKET, SO_BINDTOIFINDEX,
                   &ifindex, sizeof(ifindex)) < 0) {
        /* راهکار جایگزین برای هسته‌های قدیمی‌تر */
        if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE,
                       interface_name, strlen(interface_name) + 1) < 0) {
            perror("خطا در setsockopt SO_BINDTODEVICE");
            close(fd);
            return -1;
        }
    }

    return fd;
}

۲. تکامل دسترسی‌ها در هسته‌های نوین لینوکس

در نسخه‌های پیشین، فراخوانی SO_BINDTODEVICE به دسترسی مدیر سیستم (root) یا برخورداری از مجوز CAP_NET_RAW نیاز داشت. در سرویس‌های ایزوله و کانتینرها، اعطای این مجوز موجب افزایش آسیب‌پذیری‌ها می‌شد.

از لینوکس ۵.۷ به این سو، این شرایط تسهیل گردیده است:

  • پردازه‌های عادی در صورتی که سوکت متعلق به فضای کاربری دارای فضای نام شبکه مربوطه باشد، می‌توانند این عملیات را اجرا کنند (ns_capable(net->user_ns, CAP_NET_RAW)).
  • برنامه‌ها بدون مجوزهای خاص قادرند سوکت‌های خود را به رابط‌های موجود در فضای نام شبکه فعلی مقید نمایند.
  • سرویس‌ها می‌توانند سوکت را در بدو اجرا به wg0 متصل کنند و سپس با setuid() دسترسی خود را ایمن سازند، در حالی که اتصال سوکت معتبر باقی می‌ماند.

۳. هماهنگی با دامنه‌های مسیریابی مجازی VRF

در بستر ابزارهای مجازی‌سازی شبکه لینوکس VRF (l3mdev)، دستگاه‌های اصلی VRF مانند رابط‌های شبکه عمل می‌کنند. اعمال SO_BINDTODEVICE بر روی یک رابط اصلی VRF (مانند vrf-secure) باعث می‌شود جستجوها در جدول FIB اختصاصی آن دامنه انجام شوند.

اگر سوکت به رابط فرعی متصل شود (مانند wg0 زیرمجموعه vrf-secure)، قید دوگانه‌ای شکل می‌گیرد: خروج منحصراً از درگاه wg0 صورت گرفته و در چارچوب ایزوله دامنه VRF مربوطه محصور می‌گردد.

معماری اتصال دولایه در پروتکل RiceGuard اختصاصی VPNRice

سامانه VPNRice تثبیت درگاه‌های سوکت را به عنوان ستون فقرات پروتکل ارتباطی RiceGuard به کار گرفته است. پلتفرم با یکپارچه‌سازی وضعیت سوکت‌ها، ساختاری دولایه ایجاد می‌کند:

[ برنامه‌های کاربردی و سامانه تحلیل نام DNS ]
       |
       v
  لایه سوکت: قفل مستقیم بر روی آداپتور مجازی RiceGuard (wg0 / rice0)
  نتیجه: حذف کامل نشت پرس‌وجوهای DNS و جلوگیری از دستکاری محلی
       |
[ موتور انتقال داده‌های رمزنگاری‌شده RiceGuard ]
       |
       v
  سوکت انتقال: قفل بر روی ifindex کارت شبکه فیزیکی فعال WAN
  نتیجه: ارسال مستقیم بسته‌های رمزشده با مهار قطعی حلقه‌های تکراری
  1. مهار قطعی جریان‌های کارخواه: کلاینت VPNRice سوکت‌های وضوح نام DNS و داده‌های برنامه‌ها را به شناسه آداپتور مجازی تونل متصل می‌کند. بدین ترتیب حتی در زمان جابه‌جایی میان شبکه‌های وای‌فای، داده‌های فاقد رمزنگاری هرگز راهی به رابط‌های فیزیکی نخواهند یافت.
  2. تطبیق بلادرنگ با تغییرات پیوند فیزیکی: با تغییر اتصال فیزیکی به اینترنت، لایه انتقال داده‌های پروتکل RiceGuard شناسه دستگاه جدید را استخراج کرده و سوکت انتقال را با SO_BINDTOIFINDEX به شکل اتمیک به‌روز می‌سازد، بدون اینکه ارتباط جاری کاربر قطع گردد.
  3. پایداری در برابر شبکه‌های عمومی ناامن: تثبیت ساختار در سطح سوکت مانع از تأثیرگذاری سرورهای جعلی DHCP در شبکه‌های وای‌فای عمومی بر مسیر داده‌ها می‌شود.

بهره‌گیری از توانمندی‌های هسته لینوکس، پایداری مسیرهای ارتباطی را ارتقا بخشیده و انتقالی سریع با امنیت پایدار برای کاربران فراهم می‌سازد.