درگاه‌های سازمانی پرظرفیت VPN، پراکسی‌های خروجی و متمرکزکننده‌های ترجمه آدرس شبکه مبدأ (SNAT) به‌طور مداوم ده‌ها هزار تونل ارتباطی همزمان را مدیریت می‌کنند. هنگامی که درخواست‌های کاربران از طریق این گره‌ها به سمت رابط‌های برنامه‌نویسی ابری، میکروسرویس‌ها و سرورهای وب عمومی هدایت می‌شود، درگاه ارتباطی باید اتصالات خروجی TCP را به نمایندگی از دستگاه‌های کلاینت برقرار نماید.

در محیط‌های با ترافیک فشرده، مهندسان زیرساخت معمولاً با یک اختلال غیرمنتظره و شدید مواجه می‌شوند: فراخوانی‌های سیستمی خروجی connect() به طور ناگهانی با خطای EADDRNOTAVAIL (کد خطای ۹۹: «Cannot assign requested address») شکست می‌خورند، در حالی که سرور هنوز ده‌ها گیگابایت حافظه رم آزاد و ظرفیت پردازشی دست‌نخورده در اختیار دارد.

ریشه ساختاری این مشکل در معماری قدیمی سوکت‌های برکلی نهفته است: تخصیص زودهنگام پورت موقت مبدأ (Ephemeral Port) قبل از مشخص شدن مقصد واقعی ارتباط. برای حل بنیادین این چالش، هسته لینوکس گزینه سوکت IP_BIND_ADDRESS_NO_PORT را ارائه کرد. با به تعویق انداختن انتخاب پورت تا زمان برقراری قطعی ارتباط، درگاه‌های امروزی از کمبود پورت جلوگیری کرده و ظرفیت اتصالات همزمان را روی یک نشانی IP چندین برابر می‌نمایند.

مقایسه چرخه حیات سوکت‌های سنتی با IP_BIND_ADDRESS_NO_PORT:

سوکت‌های خروجی سنتی (تخصیص زودهنگام پورت):
ایجاد سوکت ──> bind(Source_IP, port = 0)
                     │
                     ▼ (هسته بلافاصله یک پورت موقت را قفل می‌کند، مثلاً ۴۲۱۰۰)
               قفل کردن الگوی کلی ۴ بخشی: [Source_IP : 42100 : * : *]
                     │
                     ▼
               connect(Dest_IP_A : 443)
               نتیجه: پورت ۴۲۱۰۰ به طور سرتاسری برای این Source_IP رزرو شد.
               (اتمام ظرفیت کل پورت‌ها پس از حدود ۲۸,۲۳۲ اتصال!)

سوکت‌های بهینه‌شده با IP_BIND_ADDRESS_NO_PORT:
ایجاد سوکت ──> setsockopt(IP_BIND_ADDRESS_NO_PORT) ──> bind(Source_IP, port = 0)
                                                            │
                                                            ▼
                                               فقط Source_IP ثبت می‌شود.
                                               تخصیص پورت به تعویق می‌افتد!
                                                            │
                                                            ▼
                                               connect(Dest_IP_A : 443)
                                               هسته ترکیب کامل ۴ بخشی را بررسی می‌کند:
                                               [Source_IP : 42100 -> Dest_IP_A : 443]
                                               پورت ۴۲۱۰۰ همچنان برای Dest_IP_B قابل استفاده است!

مکانیسم تخلیه و کمبود پورت‌های موقت

بر اساس استاندارد پایه پروتکل TCP (سند RFC 793)، هر اتصال فعال با یک ترکیب ۴ بخشی (4-tuple) در هسته به طور یکتا شناسایی می‌شود:

$$\text{Session} = (\text{IP مبدأ}, \text{پورت مبدأ}, \text{IP مقصد}, \text{پورت مقصد})$$

تا زمانی که حداقل یکی از این چهار مؤلفه متفاوت باشد، پشته شبکه هسته لینوکس بسته‌ها را تفکیک کرده و بدون تداخل، میلیون‌ها جریان همزمان را مدیریت می‌کند.

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

// روش سنتی اتصال سوکت به رابط خروجی مشخص
struct sockaddr_in src_addr;
src_addr.sin_family = AF_INET;
src_addr.sin_addr.s_addr = inet_addr("198.51.100.10");
src_addr.sin_port = 0; // درخواست پورت موقت از هسته لینوکس

bind(sockfd, (struct sockaddr *)&src_addr, sizeof(src_addr));
connect(sockfd, (struct sockaddr *)&dst_addr, sizeof(dst_addr));

نقص ساختاری در فراخوانی کلاسیک bind()

هنگامی که تابع bind() با مقدار port = 0 اجرا می‌شود، هسته لینوکس فوراً جدول پورت‌های موقت محلی را جستجو می‌کند (این بازه توسط پارامتر net.ipv4.ip_local_port_range تعیین می‌شود و به طور پیش‌فرض بین 32768 60999 است که تنها ۲۸,۲۳۲ پورت در اختیار می‌گذارد).

از آنجا که فراخوانی connect() هنوز انجام نشده است، هسته اطلاعاتی درباره نشانی IP و پورت مقصد ندارد. برای جلوگیری از هرگونه تداخل آدرس محلی، هسته مجبور است پورت اختصاص‌یافته را برای تمامی مقصدهای احتمالی این IP مبدأ قفل کند:

$$(\text{IP مبدأ}, \text{پورت اختصاص‌یافته}, *, *)$$

این ساختار باعث می‌شود زمانی که درگاه به ۲۸,۲۳۲ اتصال فعال همزمان از آن IP مبدأ می‌رسد، مخزن پورت‌ها به طور کامل خالی شود. پس از آن، هرگونه تلاش جدید برای فراخوانی bind() با خطای EADDRNOTAVAIL متوقف می‌شود، حتی اگر اتصالات جدید به سرورهای کاملاً مجزا در اینترنت ارسال شوند.

عملکرد گزینه IP_BIND_ADDRESS_NO_PORT در هسته لینوکس

برای حل بنیادین این مسئله، هسته لینوکس نسخه ۴.۲ گزینه سوکت IP_BIND_ADDRESS_NO_PORT را معرفی کرد (توسعه‌یافته توسط اریک دومازه)، و معادل آن یعنی IPV6_BIND_ADDRESS_NO_PORT نیز برای پروتکل IPv6 اضافه شد:

#include <netinet/in.h>

int val = 1;
setsockopt(sockfd, IPPROTO_IP, IP_BIND_ADDRESS_NO_PORT, &val, sizeof(val));

منطق تعویق تخصیص پورت در net/ipv4/af_inet.c

فعال‌سازی این گزینه پیش از فراخوانی bind() رفتار درونی هسته را دگرگون می‌کند:

  1. ثبت آدرس بدون رزرو پورت: در تابع هسته inet_bind()، معتبر بودن آدرس مبدأ بررسی شده و فیلدهای inet->inet_rcv_saddr و inet->inet_saddr مقداردهی می‌شوند. سپس پرچم داخلی تنظیم می‌گردد:
    inet_sk(sk)->bind_address_no_port = 1;
    
    فراخوانی inet_bind() بدون اجرای تابع تخصیص پورت inet_csk_get_port() به پایان می‌رسد و مقدار پورت مبدأ همچنان 0 باقی می‌ماند.
  2. ارزیابی در مرحله connect: هنگامی که برنامه تابع connect() را صدا می‌زند، کنترل به tcp_v4_connect() منتقل می‌شود. در این لحظه هسته به تمامی مشخصات شامل IP مبدأ، IP مقصد و پورت مقصد دسترسی دارد.
  3. جستجوی بهینه در جدول درهم‌سازی ehash: هسته تابع inet_hash_connect() را فراخوانی می‌کند تا جدول اتصالات فعال (tcp_hashinfo.ehash) را بررسی کند. الگوریتم دیگر به دنبال آزاد بودن سرتاسری پورت نیست، بلکه فقط اطمینان حاصل می‌کند که ترکیب کامل ۴ بخشی یکتا باشد.
مقایسه ظرفیت همزمانی به ازای هر نشانی IP خروجی:

سوکت‌های کلاسیک (رزرو پورت در مرحله bind):
حداکثر کل اتصالات خروجی = بازه پورت‌های موقت (حدود ۲۸,۲۳۲ تا ۶۴,۰۰۰ اتصال)

سوکت‌ها با فعال بودن IP_BIND_ADDRESS_NO_PORT:
حداکثر کل اتصالات خروجی = (بازه پورت‌های موقت) × (تعداد مقصدهای یکتا)
مثال: ۶۴,۰۰۰ پورت × ۵۰۰ سرویس بیرونی = ۳۲,۰۰۰,۰۰۰ اتصال همزمان!

پیاده‌سازی کاربردی در محیط‌های مختلف

زیرساخت‌های نوین مقیاس‌پذیر این قابلیت را در زبان‌های گوناگون به کار می‌گیرند.

۱. پیاده‌سازی در زبان C

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>

int create_optimized_egress_socket(const char *src_ip, const char *dst_ip, int dst_port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) return -1;

    // فعال‌سازی تعویق در تخصیص پورت
    int optval = 1;
    if (setsockopt(fd, IPPROTO_IP, IP_BIND_ADDRESS_NO_PORT, &optval, sizeof(optval)) < 0) {
        perror("خطای setsockopt IP_BIND_ADDRESS_NO_PORT");
        close(fd);
        return -1;
    }

    // اتصال دقیق به IP خروجی مشخص
    struct sockaddr_in local_addr = {0};
    local_addr.sin_family = AF_INET;
    local_addr.sin_addr.s_addr = inet_addr(src_ip);
    local_addr.sin_port = 0; // به تعویق انداختن انتخاب پورت

    if (bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
        perror("خطای bind");
        close(fd);
        return -1;
    }

    // برقراری اتصال به مقصد (پورت مبدأ در این مرحله تعیین می‌شود)
    struct sockaddr_in remote_addr = {0};
    remote_addr.sin_family = AF_INET;
    remote_addr.sin_addr.s_addr = inet_addr(dst_ip);
    remote_addr.sin_port = htons(dst_port);

    if (connect(fd, (struct sockaddr *)&remote_addr, sizeof(remote_addr)) < 0) {
        perror("خطای connect");
        close(fd);
        return -1;
    }

    return fd;
}

۲. پیاده‌سازی در Go با قلاب Control در net.Dialer

در زبان Go می‌توان از قلاب Control موجود در net.Dialer برای پیکربندی سوکت پیش از برقراری دست‌تکانی استفاده کرد:

package main

import (
	"context"
	"net"
	"syscall"
	"golang.org/x/sys/unix"
)

func createOptimizedDialer(egressIP string) *net.Dialer {
	return &net.Dialer{
		LocalAddr: &net.TCPAddr{
			IP:   net.ParseIP(egressIP),
			Port: 0,
		},
		Control: func(network, address string, c syscall.RawConn) error {
			var controlErr error
			err := c.Control(func(fd uintptr) {
				// تنظیم گزینه IP_BIND_ADDRESS_NO_PORT در لینوکس
				controlErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BIND_ADDRESS_NO_PORT, 1)
			})
			if err != nil {
				return err
			}
			return controlErr
		},
	}
}

۳. افزایش بازه پورت‌های سیستم در لینوکس

همگام با استفاده از گزینه سوکت، مدیران سیستم باید بازه پورت‌های موقت را در فایل /etc/sysctl.d/99-networking.conf به بالاترین حد ممکن برسانند:

# گسترش دامنه پورت‌های موقت تا حداکثر مقدار مجاز
net.ipv4.ip_local_port_range = 1024 65535

# استفاده مجدد ایمن از سوکت‌های حالت TIME_WAIT برای ترافیک خروجی (RFC 6191)
net.ipv4.tcp_tw_reuse = 2

اعمال فوری تغییرات در هسته سیستم:

sysctl -w net.ipv4.ip_local_port_range = 1024 65535
sysctl -w net.ipv4.tcp_tw_reuse = 2

پایش و عیب‌یابی اشباع پورت‌ها

مهندسان شبکه می‌توانند وضعیت اشباع پورت‌های موقت را با ابزارهای بومی لینوکس به طور دقیق زیر نظر بگیرند:

# مشاهده آمار کلی تخصیص سوکت‌ها در هسته
cat /proc/net/sockstat

# شمارش اتصالات خروجی به تفکیک پورت‌های موقت
ss -tan 'sport >= :1024 and sport <= :65535' | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -nr | head -n 10

# رصد خطاهای ناموفق در برقراری اتصال
netstat -s | grep -i "failed"

اگر اتصالات با خطای connect() شکست می‌خورند در حالی که حافظه رم سیستم به مقدار کافی وجود دارد، علت اصلی عدم فعال‌سازی تخصیص معوق پورت‌ها است.

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

معماری شبکه سرویس VPNRice و هسته اختصاصی RiceGuard به گونه‌ای طراحی شده است که پایداری ارتباط میلیون‌ها اتصال را در سرورهای بین‌المللی تضمین نماید:

  1. تخصیص معوق پیش‌فرض: کلیه سرویس‌های خروجی و گره‌های Anycast در RiceGuard ملزم به فعال‌سازی IP_BIND_ADDRESS_NO_PORT و IPV6_BIND_ADDRESS_NO_PORT هستند. یک سرور به تنهایی می‌تواند بیش از ۵۰۰,۰۰۰ اتصال خروجی را بدون کمبود پورت مدیریت کند.
  2. تصادفی‌سازی طبق استاندارد RFC 6056: تعویق در تعیین پورت با الگوریتم‌های تصادفی‌سازی استاندارد ترکیب شده و خطرات جعل بسته یا مسمومیت حافظه کش DNS را از بین می‌برد.
  3. حذف قفل‌های رقابتی در هسته (Lock Contention): موکول کردن بررسی پورت به مرحله connect()، فشار قفل‌های سراسری روی جدول درهم‌سازی را حذف کرده و عملکرد پردازنده‌های چند هسته‌ای را به صورت خطی ارتقا می‌دهد.

تسلط بر تنظیمات IP_BIND_ADDRESS_NO_PORT یک مهارت کلیدی برای مهندسان زیرساخت به شمار می‌رود تا نهایت بهره‌وری را از سخت‌افزارهای شبکه به دست آورند.