پرش به مطلب اصلی

TcpListener

TcpListener یک adapter سرور TCP است. این node یک آدرس و پورت محلی TCP را bind می‌کند، اتصال‌های ورودی client‌ها را می‌پذیرد، برای هر socket پذیرفته‌شده یک line در WaterWall می‌سازد و آن line را در جهت upstream به node بعدی chain می‌فرستد.

جایگاه معمول:

client -> TcpListener -> ... -> TcpConnector -> remote service

این node معمولاً ابتدای یک chain stream قرار می‌گیرد.

چه کاری انجام می‌دهد؟​

  • روی یک پورت TCP، چند پورت مشخص یا یک بازه‌ی پیوسته از پورت‌ها گوش می‌دهد.
  • اتصال client‌های TCP ورودی را می‌پذیرد.
  • برای هر socket پذیرفته‌شده یک line در WaterWall می‌سازد.
  • payload رسیده از client را در جهت upstream به node بعدی می‌فرستد.
  • payload رسیده از chain در جهت downstream را روی socket مربوط به client می‌نویسد.
  • هنگام پذیرش اتصال، filter‌های اختیاری مانند whitelist، ‏blacklist و balance group‌ها را اعمال می‌کند.

TcpListener یک node از نوع chain Head است. اتصال‌ها را client‌های خارجی TCP ایجاد می‌کنند، نه تونل قبلی در WaterWall.

نمونه‌ی config​

{
"name": "inbound-listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": [
443,
80,
2083
],
"nodelay": true,
"large-send-buffer": true,
"large-recv-buffer": true,
"interface": "eth0",
"fwmark": 10,
"balance-group": "public-443",
"balance-interval": 30000,
"initial-idle-timeout-ms": 60000,
"active-idle-timeout-ms": 600000,
"multiport-backend": "socket",
"whitelist": [
"192.168.1.0/24",
"2001:db8::/64"
],
"blacklist": [
"192.168.1.50/32"
]
},
"next": "next-node-name"
}

فیلدهای الزامی​

فیلدهای سطح اول:

فیلدنوعتوضیح
namestringنامی که کاربر برای node انتخاب می‌کند و باید در فایل config یکتا باشد.
typestringباید دقیقاً "TcpListener" باشد.
nextstringنام node‌ای که line‌های پذیرفته‌شده‌ی TCP را دریافت می‌کند.

فیلدهای الزامی در settings:

فیلدنوعتوضیح
addressstringآدرس محلی برای bind، مانند "0.0.0.0"، ‏"::" یا یک IP محلی مشخص.
port یا port-rangenumber، ‏array یا range arrayتعریف پورت listen؛ وجود دقیقاً یکی از این دو فیلد الزامی است.

settings باید یک object غیرخالی باشد.

تنظیم پورت​

TcpListener سه حالت برای پورت دارد.

یک پورت​

{
"settings": {
"address": "0.0.0.0",
"port": 443
}
}

فهرست صریح پورت‌ها​

{
"settings": {
"address": "0.0.0.0",
"port": [
443,
80,
2083
]
}
}

در این حالت فقط روی پورت‌های نوشته‌شده گوش داده می‌شود؛ این array به‌معنای بازه‌ی 80 تا 2083 نیست.

بازه‌ی پیوسته‌ی پورت‌ها​

{
"settings": {
"address": "0.0.0.0",
"port-range": [
40000,
40100
]
}
}

port-range باید array‌ای شامل دقیقاً دو پورت integer مثبت به‌شکل [min, max] باشد. مقدار کمینه باید کوچک‌تر یا مساوی مقدار بیشینه باشد.

هشدار

از نمونه‌های قدیمی بازه با قالب string، مانند "40000-40100"، استفاده نکنید. parser فعلی برای port-range یک array دومقداری انتظار دارد.

قواعد:

قاعدهرفتار
استفاده‌ی هم‌زمان از port و port-rangeراه‌اندازی متوقف می‌شود.
مقدار number برای portروی یک پورت گوش می‌دهد.
مقدار array برای portروی پورت‌های صریح موجود در array گوش می‌دهد.
مقدار array برای port-rangeروی همه‌ی پورت‌ها از مقدار کمینه تا بیشینه گوش می‌دهد.
پورت نامعتبر، صفر، منفی یا بیشتر از 65535راه‌اندازی متوقف می‌شود.

تنظیمات اختیاری​

تنظیمنوعمقدار پیش‌فرضتوضیح
nodelaybooleanfalseگزینه‌ی TCP_NODELAY را روی socket‌های پذیرفته‌شده فعال می‌کند.
large-send-bufferboolean یا integer مثبتfalse؛ اگر Mux در chain باشد و این گزینه حذف شده باشد، trueمقدار SO_SNDBUF را روی socket‌های پذیرفته‌شده تنظیم می‌کند.
large-recv-bufferboolean یا integer مثبتfalse؛ اگر Mux در chain باشد و این گزینه حذف شده باشد، trueمقدار SO_RCVBUF را روی socket‌های پذیرفته‌شده تنظیم می‌کند.
interfacestringتنظیم نشدهlistener را به یک interface محلی شبکه محدود می‌کند.
fwmarkintegerتنظیم نشدهدر صورت پشتیبانی، یک socket mark به سبک Linux اعمال می‌کند.
balance-groupstringتنظیم نشدهlistener‌های هم‌پورت را برای توزیع client‌ها در یک گروه قرار می‌دهد.
balance-intervalinteger، بر حسب میلی‌ثانیهمقدار پیش‌فرض socket managerمدت حفظ انتخاب قبلی برای client‌های تکراری در balance group.
initial-idle-timeout-msinteger مثبت، بر حسب میلی‌ثانیه60000timeout پیش از مشاهده‌ی فعالیت payload در سمت listener.
active-idle-timeout-msinteger مثبت، بر حسب میلی‌ثانیه600000timeout پس از مشاهده‌ی فعالیت در سمت listener.
multiport-backendstringمقدار پیش‌فرض runtime برای port-rangebackend مربوط به بازه‌ی پیوسته‌ی پورت‌ها: "iptables" یا "socket".
whitelistarray‌ای از string‌هاتنظیم نشدهIP‌ها یا بازه‌های CIDR مجاز برای client.
blacklistarray‌ای از string‌هاتنظیم نشدهIP‌ها یا بازه‌های CIDR مسدود برای client.

گزینه‌های socket buffer​

large-send-buffer و large-recv-buffer مقادیر زیر را می‌پذیرند:

مقدارمفهوم
trueاز اندازه‌ی پیش‌فرض WaterWall برای socket buffer بزرگ استفاده می‌کند که در حال حاضر 16777216 byte است.
falseاین socket buffer را صریحاً تنظیم نمی‌کند و مقدار پیش‌فرض Kernel به کار می‌رود.
integer مثبتهمان تعداد byte را مستقیماً درخواست می‌کند.

اگر chain نهایی شامل MuxClient یا MuxServer باشد، گزینه‌های حذف‌شده‌ی buffer اندازه‌ی پیش‌فرض 16 MiB را صریحاً درخواست می‌کنند، مستقل از core misc.tcp-tune و موفقیت اجرای sysctl. در نبود Mux در chain، گزینه‌های حذف‌شده به kernel واگذار می‌شوند. مقادیر صریح، از جمله false، در هر دو حالت اولویت دارند.

‏interface و fwmark​

interface، ‏listener را به یک دستگاه محلی شبکه محدود می‌کند.

در Linux، ‏WaterWall از SO_BINDTODEVICE استفاده می‌کند. در platform‌هایی که bind به device را پشتیبانی نمی‌کنند، WaterWall در صورت امکان روی آدرس IPv4 همان interface bind می‌شود.

روی socket متصل به device، انتخاب فقط میان listenerهای همان interface و listenerهای بدون محدودیت interface انجام می‌شود. listener مربوط به interface دیگر، حتی با اولویت بالاتر ACL یا انتخاب ذخیره‌شده‌ی balance، واجد شرایط نیست. listenerهای بدون محدودیت همچنان بر اساس اولویت معمول آدرس و ACL و ترتیب ثبت رقابت می‌کنند؛ محدودیت interface اولویت تازه‌ای ایجاد نمی‌کند. socket بدون محدودیت، interface ورودی مشخصی ندارد و نمی‌تواند listener محدود به یک interface را انتخاب کند. در platformهای دیگر، انتخاب بر اساس آدرس مؤثر bind همان interface انجام می‌شود. انتخاب پایدار balance علاوه بر IP کلاینت، آدرس محلی و پورت مقصد منطقی، بر اساس protocol و محدوده‌ی مؤثر interface نیز جدا می‌شود.

fwmark در platform‌های پشتیبانی‌شده از SO_MARK استفاده می‌کند. این قابلیت به platform وابسته است و در Windows در دسترس نیست.

نمونه‌های رایج​

listener محلی برای آزمایش​

{
"name": "local-entry",
"type": "TcpListener",
"settings": {
"address": "127.0.0.1",
"port": 8080
},
"next": "outbound"
}

این listener فقط client‌های همان دستگاه را می‌پذیرد.

listener عمومی روی VPS​

{
"name": "public-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443,
"nodelay": true,
"initial-idle-timeout-ms": 60000,
"active-idle-timeout-ms": 600000
},
"next": "tls-server"
}

این listener، ‏client‌های IPv4 را روی همه‌ی interface‌های محلی می‌پذیرد. پورت انتخاب‌شده باید هم در firewall خود VPS و هم در firewall شرکت ارائه‌دهنده مجاز باشد.

listener چندپورتی​

{
"name": "multiport-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": [
443,
8443,
2083
],
"nodelay": true
},
"next": "router"
}

برای array صریح port، به‌ازای هر پورت یک socket جداگانه در حالت listen ساخته می‌شود.

listener روی بازه‌ی پورت​

{
"name": "range-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port-range": [
40000,
40100
],
"multiport-backend": "socket"
},
"next": "router"
}

برای port-range، ‏runtime می‌تواند با backend نوع socket به‌ازای هر پورت یک socket در حالت listen بسازد، یا با backend نوع iptables از یک socket اصلی و rule‌های Redirect استفاده کند. هرگاه به backend مشخصی نیاز دارید، multiport-backend را صریحاً تنظیم کنید.

مسیر پذیرش اتصال​

وقتی یک client مبتنی بر TCP متصل می‌شود، TcpListener مراحل زیر را انجام می‌دهد:

  1. socket پذیرفته‌شده را به Event Loop یک worker متصل می‌کند؛
  2. یک line تازه در WaterWall می‌سازد؛
  3. line state مربوط به listener را برای آن اتصال ذخیره می‌کند؛
  4. اطلاعات routing را از اتصال پذیرفته‌شده پر می‌کند؛
  5. رویداد init را در upstream به node بعدی می‌فرستد؛
  6. خواندن از socket مربوط به client را آغاز می‌کند.

source context مربوط به line از اتصال پذیرفته‌شده پر می‌شود. IP سمت مقابل در src_ctx ثبت می‌شود؛ پورت محلی پذیرنده‌ی اتصال در src_ctx.port و local_listener_port قرار می‌گیرد؛ و پورت واقعی مبدأ TCP مربوط به client به‌صورت جداگانه در peer_source_port نگه داشته می‌شود.

جریان داده​

client socket -> TcpListener -> next node
client socket <- TcpListener <- next node

رفتار هر جهت:

جهترفتار
از client به chainداده‌ی خوانده‌شده از socket به payload در upstream برای next تبدیل می‌شود.
از chain به clientpayload در downstream روی socket پذیرفته‌شده نوشته می‌شود.

node بعدی باید انتظار ترافیک client‌های پذیرفته‌شده را داشته باشد و پاسخ‌ها را در جهت downstream برگرداند.

Backpressure و صف نوشتن​

TcpListener از پردازش در برابر socket مربوط به clientی که کند است یا موقتاً امکان نوشتن ندارد محافظت می‌کند.

رفتار:

  • اگر عملیات Write بلافاصله کامل نشود، buffer‌های خروجی در صف قرار می‌گیرند.
  • وقتی حجم یا هزینهٔ ظرفیت صف به 1 KiB برسد، node بعدی Pause می‌شود.
  • وقتی socket دوباره قابل‌نوشتن باشد، buffer‌های صف Flush و node بعدی Resume می‌شود.
  • اگر حجم یا هزینهٔ ظرفیت صف از 16 MiB عبور کند، اتصال بسته می‌شود.

پیش از ارسال Pause، buffer، حسابداری صف و وضعیت اعلان ثبت می‌شوند تا callback تو‌در‌تو ترتیب FIFO را تغییر ندهد. حجم منطقی و هزینهٔ ظرفیت buffer (شامل sbuf، padding و alignment) هرکدام سقف مستقل 16 MiB دارند؛ مقدار برابر سقف پذیرفته می‌شود. با رسیدن هرکدام به 1 KiB، Pause ارسال می‌شود. buffer خالی نیز هزینهٔ ظرفیت دارد. بودجهٔ TCP صف adapter و buffer فعال WIO را با هم حساب می‌کند. نوشتن جزئی تا زمان آزاد شدن buffer، هزینهٔ ظرفیت آن را کم نمی‌کند. عبور از سقف، همان اتصال را از طریق مالک line می‌بندد.

این سازوکار اجازه نمی‌دهد یک client کند باعث رشد نامحدود حافظه شود.

رفتار idle timeout​

هر اتصال پذیرفته‌شده در یک جدول idle محلی worker ردیابی می‌شود.

مرحلهtimeout پیش‌فرض
اتصال تازه‌پذیرفته‌شده، پیش از فعالیت payload در سمت listener60000 ms
اتصال فعال، پس از مشاهده‌ی فعالیت در سمت listener600000 ms

اگر timeout اتصال تمام شود، socket و line بسته می‌شوند.

مقادیر مرحله‌ی اولیه و فعال با گزینه‌های زیر تنظیم می‌شوند:

{
"settings": {
"initial-idle-timeout-ms": 60000,
"active-idle-timeout-ms": 600000
}
}

مدت اولیه از زمان انتظار اولیه‌ی پیش‌فرض NGINX پیروی می‌کند. مدت مرحله‌ی فعال یک محدودیت ده‌دقیقه‌ای برای پاک‌سازی اتصال‌های بی‌فعالیت است که با TcpConnector هماهنگ است تا اتصال fallback بتواند از زمان بسته‌شدن کوتاه‌تر مقصد پیروی کند. بسته‌شدن مقصد بدون انتظار تا پایان مهلت idle در chain منتقل می‌شود؛ البته داده‌های خروجی صف‌شده ابتدا تخلیه می‌شوند. ده دقیقه سقف NGINX نیست. یک timeout کوتاه‌تر که صریحاً برای listener تنظیم شده باشد همچنان می‌تواند اتصال fallback را زودتر از مقصد ببندد؛ اگر مقصد بی‌فعالیتی طولانی‌تری را مجاز بداند، محدودیت idle هر دو adapter باید آن را پوشش دهد.

این Timer‌ها فقط بی‌فعالیتی TCP را اندازه می‌گیرند؛ فعالیت، مهلت را تمدید می‌کند و مرحله‌های درخواست HTTP را تشخیص نمی‌دهند. TlsServer مهلت مستقل handshake خود را با پیش‌فرض 60s اعمال می‌کند و هنگام ورود به fallback آن را غیرفعال می‌کند. این دو گزینه مهلت‌های header و Body در HTTP، timeout نوشتن پاسخ و رفتار Lingering Close در NGINX را پیاده‌سازی نمی‌کنند.

در chain‌هایی مانند TcpListener -> TlsServer، فعالیت سمت listener با کامل‌شدن TLS handshake یکسان نیست. clientی که فقط بخشی از یک TLS record را می‌فرستد ممکن است وارد timeout مرحله‌ی فعال شود، در حالی که TlsServer هنوز منتظر بایت‌های دیگر handshake است. هنگام تنظیم fallback مقاوم در برابر Probe در TLS، این timeout‌ها را با زمان‌بندی سرویس عمومی‌ای که می‌خواهید شبیه آن باشید هماهنگ نگه دارید.

balance group‌ها​

balance-group این listener را در کنار listener‌های سازگار دیگر روی همان پورت در یک گروه قرار می‌دهد.

وقتی چند listener، ‏balance-group و پورت یکسانی دارند، socket manager با Hash کردن IP مربوط به client یکی از آن‌ها را برای client تازه انتخاب و این انتخاب را به‌خاطر می‌سپارد. در طول balance-interval، اتصال‌های بعدی همان IP همچنان به همان listener فرستاده می‌شوند.

نمونه:

{
"settings": {
"address": "0.0.0.0",
"port": 443,
"balance-group": "public-443",
"balance-interval": 30000
}
}

Whitelist و Blacklist​

whitelist و blacklist، ‏array‌هایی از IP یا CIDR در قالب string می‌پذیرند و هر دو IPv4 و IPv6 پشتیبانی می‌شوند.

نمونه:

{
"settings": {
"whitelist": [
"10.0.0.0/8",
"192.168.1.20/32",
"2001:db8::/64"
],
"blacklist": [
"10.0.0.13/32"
]
}
}

رفتار:

configنتیجه
هیچ فهرستی وجود ندارداین listener روی IP مربوط به client filter اعمال نمی‌کند.
whitelist وجود داردفقط client‌هایی که IP آن‌ها با فهرست منطبق است پذیرفته می‌شوند.
blacklist وجود داردclient‌هایی که IP آن‌ها با فهرست منطبق است رد می‌شوند.
هر دو وجود دارندclient باید، در صورت وجود Whitelist، با آن منطبق باشد و نباید با Blacklist تطابق داشته باشد.

اگر چند listener روی یک پورت ثبت شده باشند، ممکن است listener دیگری همان اتصال را بگیرد؛ به شرط آنکه filter آن منطبق باشد و filter این listener منطبق نباشد.

نکات مربوط به Multiport backend​

در array صریح port، ‏WaterWall هر عضو را یک پورت مستقل در نظر می‌گیرد و به‌ازای هر پورت یک socket در حالت listen می‌سازد.

برای port-range، ‏WaterWall می‌تواند از backend‌های زیر استفاده کند:

backendتوضیح
"socket"به‌ازای هر پورت یک socket در حالت listen می‌سازد؛ رفتارش ساده و قابل‌درک است.
"iptables"از rule‌های Redirect استفاده می‌کند و می‌تواند برای بازه‌های بزرگ مناسب باشد، اما به محیط Host و قواعد firewall وابسته است.

برای بازه‌های کوچک معمولاً debug کردن socket آسان‌تر است. در بازه‌های بزرگ، اگر استقرار شما کنترل rule‌های سیستم را در اختیار دارد، iptables می‌تواند تعداد socket‌ها را کاهش دهد.

اولویت filter‌ها و مسیرهای پیش‌فرض​

وقتی چند node از نوع TcpListener یک پورت محلی را پوشش می‌دهند، socket manager همه را هم‌ارز در نظر نمی‌گیرد و ابتدا filter‌های دقیق‌تر را بررسی می‌کند. listener دارای whitelist یا blacklist از listener مشابهی که filter مربوط به IP ندارد اولویت بیشتری دارد.

این رفتار امکان ساخت الگوی مسیر پیش‌فرض را فراهم می‌کند:

same public port
├─ matching filtered listener -> special path
└─ unfiltered listener -> default path

نمونه:

{
"nodes": [
{
"name": "trusted-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443,
"whitelist": [
"203.0.113.10/32",
"2001:db8:100::/48"
]
},
"next": "trusted-path"
},
{
"name": "default-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443
},
"next": "default-path"
}
]
}

در این ساختار، trusted-entry مسئول client‌هایی است که با trusted-entry.whitelist تطابق دارند. client‌های دیگر به default-entry می‌رسند، زیرا listener بدون filter نقش مسیر پیش‌فرض آن پورت را دارد.

می‌توانید همین الگو را با blacklist به کار ببرید تا یک بازه را از مسیری خارج کنید و، در صورت تطابق filter‌های listener دیگر، اتصال را به آن بسپارید.

filter‌های TcpListener فقط IP/CIDR را می‌پذیرند. نام‌های GeoIP، دسته‌بندی دامنه، SNI، ‏HTTP Host و protocol sniffing پشتیبانی نمی‌شوند. برای مسیریابی بر اساس کشور، مانند geoip:ir، ابتدا اتصال را بپذیرید و سپس از node نوع Router استفاده کنید.

اگر balance-group فعال باشد، listener‌های منطبق از طریق منطق balance group انتخاب می‌شوند و مانند یک شاخه‌ی First-match ساده رفتار نمی‌کنند. برای مسیریابی قطعی بر اساس IP، الگوی اولویت/مسیر پیش‌فرض را با listener‌های filterشده‌ی معمولی به کار ببرید.

نکات و محدودیت‌ها​

  • TcpListener برای نقطه‌ی ورود ترافیک طراحی شده است.
  • parser برای port یک number یا array صریحی از شماره‌ی پورت‌ها انتظار دارد.
  • parser برای port-range یک range array دومقداری انتظار دارد.
  • multiport-backend فقط برای listener‌های دارای port-range پیوسته اثر دارد.
  • fwmark و bindشدن به device به platform وابسته‌اند.
  • listener متصل به 127.0.0.1 فقط client‌های محلی را می‌پذیرد.
  • listener متصل به 0.0.0.0، با رعایت firewall سیستم‌عامل و شرکت ارائه‌دهنده، روی همه‌ی interface‌های IPv4 اتصال می‌پذیرد.

متادیتای node​

متادیتای برگرفته از کد منبع:

ویژگیمقدار
node flagskNodeFlagChainHead | kNodeFlagSupportsSplice
can_have_prevfalse
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayerNone
layer_group_next_nodekNodeLayer4
required_padding_left0 بایت