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

HeaderClient

HeaderClient در ابتدای نخستین payloadِ upstream هر line عادی در لایه ۴، یک header یک‌باره قرار می‌دهد.

این node معمولاً یکی از دو کاربرد زیر را دارد:

  • فرستادن metadata مسیریابی WaterWall به یک HeaderServer جفت‌شده
  • افزودن header پروتکل HAProxy PROXY پیش از رسیدن ترافیک به گیرنده‌ای که PROXY protocol را می‌شناسد

HeaderClient socket باز نمی‌کند و line نمی‌سازد. تنها نخستین payloadِ upstream را تغییر می‌دهد و پیش از تحویل آن، برای همیشه روی آن line از مسیر callbackها کنار می‌رود. حالت‌های protocol از Init تا تحویل نخستین payload یا Finish، یک snapshot کوچک برای هر line نگه می‌دارند؛ حالت‌های port و PROXY state اختصاصی line ندارند. ماسک nodeهای فعال line، رفتار یک‌باره را تضمین می‌کند. callbackهای بعدی مستقیماً به node فعال مجاور می‌رسند. Pause/Resume در تمام مراحل راه‌اندازی شفاف و تکرارپذیر هستند.

عملکرد​

  • برای انتخاب فرمت header یک‌باره به settings.data نیاز دارد.
  • در حالت‌های protocol، پیش از فرستادن Init در upstream به next، پروتکل انتخاب‌شده را بررسی و ذخیره می‌کند. حالت‌های دیگر Init را بی‌درنگ عبور می‌دهند.
  • فقط روی اولین upstream payload، header تنظیم‌شده را prepend می‌کند.
  • از header دوبایتی port و header تک‌بایتی پروتکل TCP/UDP در WaterWall پشتیبانی می‌کند.
  • از header‌های IPv4-only مربوط به HAProxy PROXY protocol v1 و v2 با یک frontend IPv4 تنظیم‌شده پشتیبانی می‌کند.
  • همه payload‌های بعدی upstream را بدون تغییر عبور می‌دهد.
  • payload و callback‌های Est، Pause و Resume را در downstream بدون تغییر عبور می‌دهد.
  • snapshot پروتکل را پیش از bypass یا عبور جهت‌دار Finish صفر می‌کند؛ حالت‌های دیگر از callbackهای پیش‌فرض Init و Finish استفاده می‌کنند.

جایگاه رایج​

header داخلی WaterWall برای port:

TcpListener -> HeaderClient -> ... -> HeaderServer -> TcpConnector

header پروتکل PROXY برای گیرنده راه دور سازگار با PROXY:

sender side:   TcpListener -> HeaderClient -> TcpConnector
receiver side: HeaderServer/HAProxy/nginx/backend

در حالت پروتکل PROXY، peer واقعی بعدی باید انتظار HAProxy PROXY protocol داشته باشد. این peer می‌تواند HeaderServer با settings.override = "proxy-protocol->source-fields" یا یک backend دیگر سازگار با PROXY باشد. این حالت همچنین به settings.frontend-ipv4 نیاز دارد؛ این مقدار باید IPv4 مربوط به frontend‌ای باشد که کلاینت‌ها به آن وصل می‌شوند، نه آدرس backend.

مثال‌های کاربردی​

حفظ port listener پذیرفته‌شده در یک relay​

این کاربرد کلاسیک HeaderClient در کنار HeaderServer است. یک instance سمت کلاینت روی چند port عمومی گوش می‌دهد، اما همه ترافیک را به یک port میانی روی instance دیگری از WaterWall می‌فرستد. سمت relay، port اصلی اتصال را در dest_context->port بازیابی می‌کند و سپس به service محلی متصل می‌شود.

سمت کلاینت:

[
{
"name": "public-listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": [80, 443, 8443]
},
"next": "header-client"
},
{
"name": "header-client",
"type": "HeaderClient",
"settings": {
"data": "src_context->port"
},
"next": "relay-connector"
},
{
"name": "relay-connector",
"type": "TcpConnector",
"settings": {
"address": "198.51.100.20",
"port": 9000
}
}
]

سمت relay:

[
{
"name": "relay-listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 9000
},
"next": "header-server"
},
{
"name": "header-server",
"type": "HeaderServer",
"settings": {
"override": "dest_context->port"
},
"next": "local-service"
},
{
"name": "local-service",
"type": "TcpConnector",
"settings": {
"address": "127.0.0.1",
"port": "dest_context->port"
}
}
]

با این چیدمان، اگر سمت کلاینت اتصال را روی port عمومی 443 بپذیرد، اتصال از طریق 198.51.100.20:9000 عبور می‌کند و سپس سمت relay به 127.0.0.1:443 وصل می‌شود.

فرستادن تنها هویت TCP/UDP​

اگر گیرنده از قبل آدرس و port مقصد را می‌داند، می‌توانید تنها پروتکل مقصد مورد نظر را بفرستید:

{
"name": "protocol-header",
"type": "HeaderClient",
"settings": {
"data": "dest_context->protocol"
},
"next": "relay-connector"
}

در سمت مقابل از HeaderServer.settings.override = "dest_context->protocol" استفاده کنید. اگر هدف تشخیص transport مبدأ پذیرفته‌شده است، مقدار "src_context->protocol" را انتخاب کنید. context انتخاب‌شده باید هنگام upstream Init در HeaderClient دقیقاً TCP یا UDP را مشخص کرده باشد؛ از context دیگر به‌عنوان fallback استفاده نمی‌شود.

این header آدرس یا port ندارد. برای حمل UDP روی TCP همچنان به framing جداگانهٔ datagram، مانند جفت UdpOverTcp، نیاز دارید.

اجبار همه ترافیک relay شده به یک backend port​

اگر سمت relay منتظر header دوبایتی WaterWall است، اما مقدار باید ثابت باشد و از contextِ line کپی نشود، برای settings.data یک عدد بنویسید.

{
"name": "header-client",
"type": "HeaderClient",
"settings": {
"data": 8443
},
"next": "relay-connector"
}

در سمت مقابل، HeaderServer را طوری تنظیم کنید که این header را بخواند و در dest_context->port بنویسد؛ سپس TcpConnector از "dest_context->port" استفاده کند. در نتیجه هر line این کلاینت، port شماره 8443 را در backend انتخاب می‌کند.

ارسال PROXY protocol v2 به backend محلی​

اگر peer واقعی بعدی HAProxy، nginx، Envoy یا service دیگری است که پیش از payload اصلی کلاینت انتظار PROXY protocol دارد، از "proxy-protocol" استفاده کنید.

[
{
"name": "public-listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443
},
"next": "proxy-header"
},
{
"name": "proxy-header",
"type": "HeaderClient",
"settings": {
"data": "proxy-protocol",
"frontend-ipv4": "203.0.113.10"
},
"next": "backend"
},
{
"name": "backend",
"type": "TcpConnector",
"settings": {
"address": "192.0.2.50",
"port": 443
}
}
]

"proxy-protocol" نام مستعار "proxy-protocol-v2" است. PROXY protocol v2 فشرده و binary است و اگر backend از آن پشتیبانی کند، گزینه پیش‌فرض بهتری است. frontend-ipv4 را روی IPv4‌ای بگذارید که کلاینت برای رسیدن به frontendِ WaterWall استفاده می‌کند؛ نه 0.0.0.0 و نه آدرس backend.

ارسال PROXY protocol v1 برای receiverهای قدیمی​

بعضی گیرنده‌های قدیمی فقط فرمت متنی PROXY protocol را می‌پذیرند. در این حالت از "proxy-protocol-v1" استفاده کنید:

{
"name": "proxy-header",
"type": "HeaderClient",
"settings": {
"data": "proxy-protocol-v1",
"frontend-ipv4": "203.0.113.10"
},
"next": "backend"
}

گیرنده باید طوری تنظیم شود که انتظار PROXY protocol v1 داشته باشد. برای گیرنده‌های WaterWall از HeaderServer با settings.override = "proxy-protocol->source-fields" استفاده کنید.

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

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

فیلدنوعتوضیح
namestringنام دلخواه که باید در config یکتا باشد.
typestringباید دقیقاً "HeaderClient" باشد.
settingsobjectاجباری است و باید data داشته باشد.
nextstringnode stream بعدی در لایه ۴ که stream دارای header را می‌گیرد.

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

فیلدنوعتوضیح
datanumber یا stringفرمت header و داده‌ای که در نخستین payloadِ upstream encode می‌شود.

فیلدهای شرطی داخل settings:

فیلدنوعتوضیح
frontend-ipv4stringاگر data برابر "proxy-protocol"، "proxy-protocol-v1" یا "proxy-protocol-v2" باشد، اجباری است. مقدار باید IPv4 مربوط به frontend‌ای باشد که کلاینت‌ها به آن متصل می‌شوند.

HeaderClient باید node قبلی هم داشته باشد. این node برای ابتدا یا انتهای chain نیست.

settings.data​

مقدارهای قابل قبول:

مقدارمعنی
1 تا 65535همین port ثابت را در header دوبایتی WaterWall encode می‌کند.
"src_context->port"مقدار line->routing_context.src_ctx.port را به‌عنوان header دو بایتی WaterWall encode می‌کند.
"line->src_ctx->port"نام مستعار سازگار با "src_context->port".
"src_context->protocol"مقدار src_ctx.protocol را هنگام Init ذخیره و TCP یا UDP را در یک header تک‌بایتی encode می‌کند.
"line->src_ctx->protocol"نام مستعار "src_context->protocol".
"dest_context->protocol"مقدار dest_ctx.protocol را هنگام Init ذخیره و TCP یا UDP را در یک header تک‌بایتی encode می‌کند.
"line->dest_ctx->protocol"نام مستعار "dest_context->protocol".
"proxy-protocol"یک header از نوع PROXY protocol v2 تولید می‌کند.
"proxy-protocol-v1"یک header از نوع PROXY protocol v1 تولید می‌کند.
"proxy-protocol-v2"یک header از نوع PROXY protocol v2 تولید می‌کند.

اگر data وجود نداشته باشد، رشته‌ای ناشناخته باشد یا عددی بیرون بازه 1 تا 65535 داشته باشد، راه‌اندازی شکست می‌خورد.

رفتار روی Wire​

در حالت عددی یا source-port، نخستین payloadِ upstream از این شکل:

payload

به این حالت تبدیل می‌شود:

2-byte port header | payload

header مربوط به port یک مقدار شانزده‌بیتی big-endian است که فقط یک بار برای هر line فرستاده می‌شود. payload‌های بعدی upstream دقیقاً بدون تغییر عبور می‌کنند.

در حالت‌های protocol، نخستین payloadِ upstream به این شکل تبدیل می‌شود:

1-byte protocol header | payload

مقدار بایت برای TCP برابر 6 و برای UDP برابر 17 است و فقط یک بار برای هر line عادی لایه ۴ فرستاده می‌شود. این header آدرس IP، port یا فیلدهای PROXY protocol ندارد. کلاینت تنها context انتخاب‌شده را هنگام upstream Init و پیش از فراخوانی Init بعدی ذخیره می‌کند؛ بنابراین تغییر بعدی routing context توسط connector روی header اثری ندارد. اگر پروتکل انتخاب‌شده تنظیم نشده، مبهم یا پشتیبانی‌نشده باشد، line بدون مقداردهی اولیهٔ next بسته می‌شود.

در حالت‌های PROXY protocol، HeaderClient یک header از نوع IPv4 TCP PROXY می‌سازد. Source IP از line->routing_context.src_ctx و destination IP از settings.frontend-ipv4 گرفته می‌شود. برای مقصد در headerِ PROXY از dest_ctx استفاده نمی‌شود، زیرا ممکن است TcpConnector پیش از ارسال نخستین payload، dest_ctx را به backend مقصد اتصال تغییر داده باشد.

port‌های انتخاب‌شده:

  • source port: اگر routing_context.peer_source_port موجود باشد همان مقدار، وگرنه src_ctx.port
  • destination port: اگر routing_context.local_listener_port موجود باشد همان مقدار، وگرنه src_ctx.port

اگر هنگام رسیدن نخستین payloadِ upstream، آدرس مبدأ از نوع IPv4 نباشد، نسخه v1 مقدار PROXY UNKNOWN و نسخه v2 یک header از نوع LOCAL/UNSPEC تولید می‌کند. header‌های IPv6 و TLVهای PROXY protocol تولید نمی‌شوند.

حتی اگر نخستین payloadِ upstream خالی باشد، همان callbackِ نخست header تنظیم‌شده را می‌فرستد.

نکته‌های Routing Context​

HeaderClient تنها اطلاعاتی را serialize می‌کند که از قبل روی line وجود دارند. domain را resolve نمی‌کند و آدرسی را که وجود ندارد تولید نمی‌کند.

در حالت قدیمی header دوبایتی WaterWall، روند معمول چنین است:

  • TcpListener، port محلی listener را که اتصال روی آن پذیرفته شده در src_ctx.port قرار می‌دهد.
  • HeaderClient می‌تواند آن مقدار را در header دوبایتی کپی کند.
  • HeaderServer می‌تواند همان مقدار را در سمت دیگر در dest_ctx.port بازسازی کند.

در حالت پروتکل PROXY، context آدرس مبدأ باید به socket address از نوع IPv4 تبدیل‌شدنی باشد. آدرس مقصد در headerِ PROXY همان frontend-ipv4 تنظیم‌شده است، نه dest_ctx. این رفتار عمدی است: در chain‌های رایجی مانند TcpListener -> HeaderClient -> TcpConnector، معمولاً dest_ctx آدرس backend انتخاب‌شده توسط TcpConnector را نشان می‌دهد، نه آدرس frontend‌ای که کلاینت به آن رسیده است.

استفاده با HeaderServer​

HeaderClient و HeaderServer را در یکی از حالت‌های سازگار با هم به کار ببرید.

برای header دو بایتی WaterWall:

{
"name": "header-server",
"type": "HeaderServer",
"settings": {
"override": "dest_context->port"
},
"next": "connector"
}

برای header تک‌بایتی پروتکل، یکی از contextهای پروتکل کلاینت را با این تنظیم سرور جفت کنید:

{
"name": "header-server",
"type": "HeaderServer",
"settings": {
"override": "dest_context->protocol"
},
"next": "router-or-connector"
}

سرور پیش از مقداردهی اولیهٔ node بعدی فقط dest_ctx.protocol را تغییر می‌دهد؛ metadata مبدأ، آدرس مقصد و port مقصد حفظ می‌شوند.

برای برگرداندن source fields از PROXY protocol:

{
"name": "header-server",
"type": "HeaderServer",
"settings": {
"override": "proxy-protocol->source-fields"
},
"next": "connector"
}

این حالت را با مقدار HeaderClient.data برابر "proxy-protocol"، "proxy-protocol-v1" یا "proxy-protocol-v2" جفت کنید. reader سمت server برای PROXY فعلاً فقط IPv4 را پشتیبانی می‌کند و تنها src_ctx.ip و src_ctx.port را اعمال می‌کند.

اگر HeaderServer.settings.override عدد باشد، سرور هنگام Init در upstream یک destination port ثابت تنظیم می‌کند و header‌ای از داده نمی‌خواند. در این حالت، اگر پیش از آن HeaderClient قرار دهید، header دوبایتی بخشی از payload برنامه باقی می‌ماند.

رفتار جهت‌ها​

جهترفتار
upstream Initدر حالت protocol، پروتکل انتخاب‌شده را بررسی و ذخیره می‌کند و سپس به next می‌فرستد؛ در حالت‌های دیگر بی‌درنگ عبور می‌دهد.
اولین upstream payloadheader تنظیم‌شده را prepend می‌کند و به next می‌فرستد.
payload‌های upstream بعدیبدون تغییر به next می‌روند.
upstream pause/resumeبه next فرستاده می‌شود.
downstream payloadبدون تغییر به node قبلی برمی‌گردد.
downstream Est / pause / resumeبه node قبلی فرستاده می‌شود.

callback‌های UpStreamEst و DownStreamInit برای این node غیرفعال هستند.

Finish​

پیش از نخستین payload، حالت‌های protocol ابتدا snapshot هر line را صفر می‌کنند و سپس Finish را در جهت دریافت‌شده عبور می‌دهند. حالت‌های دیگر از callbackهای پیش‌فرض Finish استفاده می‌کنند. پس از bypass، Finish مستقیماً به node فعال بعدی در همان جهت می‌رسد و snapshot پروتکل از قبل صفر شده است.

با دریافت Finish در upstream، HeaderClient آن را به next می‌فرستد.

با دریافت Finish در downstream، آن را به node قبلی می‌فرستد.

چون این تونل سازنده line نیست، lineDestroy() را فراخوانی نمی‌کند.

محدودیت‌ها و Padding​

مقداراندازه
header دو بایتی WaterWall برای port2 bytes
header پروتکل WaterWall1 byte
header پروتکل PROXY v1حداکثر 108 bytes
header پروتکل PROXY v216 یا 28 bytes
required_padding_left108 bytes

از آنجا که این node بزرگ‌ترین header پشتیبانی‌شده را با sbufShiftLeft() prepend می‌کند، مقدار required_padding_left = 108 را اعلام می‌کند.

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

  • فقط در chain‌های stream لایه ۴ استفاده شود.
  • نباید به‌عنوان state مشترک packet-line استفاده شود.
  • header دوبایتی port فقط metadata داخلی WaterWall است، نه یک پروتکل عمومی.
  • حالت‌های PROXY protocol فعلاً فقط IPv4 را پشتیبانی می‌کنند.
  • destination IP در PROXY protocol از settings.frontend-ipv4 می‌آید، نه dest_ctx.
  • اگر یک listener روی چند frontend IPv4 ترافیک قبول کند، یک مقدار ثابت frontend-ipv4 ممکن است همه اتصال‌های پذیرفته‌شده را درست توصیف نکند.
  • مقدار پروتکل هنگام upstream Init ذخیره می‌شود؛ فیلدهای port و PROXY هنگام رسیدن نخستین payloadِ upstream خوانده می‌شوند.
  • نشانگر پروتکل مرز datagramهای UDP را حفظ نمی‌کند؛ برای حمل UDP روی TCP از framing جداگانه استفاده کنید.
  • تنها نخستین payloadِ upstream تغییر می‌کند و مسیر پاسخ بدون تغییر است.

متادیتای نود​

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

ویژگیمقدار
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left108 bytes

پشتیبانی splice​

header یک‌باره فقط در padding واقعی و اعلام‌شدهٔ سمت چپ، پیش از prefix موجود نوشته می‌شود. بدنهٔ داده در pipe خصوصی می‌ماند و payload‌های بعدی upstream و همهٔ payload‌های downstream بدون تغییر عبور می‌کنند. HeaderServer نیز splice را پشتیبانی می‌کند: هنگام خواندن header، داده را به buffer عادی تبدیل می‌کند و پس از تکمیل مسیریابی، payloadهای opaque را حفظ می‌کند.

buffer‌های عادیِ fallback همچنان پذیرفته می‌شوند. splice به build پشتیبانی‌شدهٔ Linux، فعال‌بودن misc.splice، نبود node packet و پشتیبانی همهٔ node‌های chainِ نهایی پس از گسترش و ادغام نیاز دارد. flag قابلیت این بررسی‌ها را دور نمی‌زند. بدون اندازه‌گیری، بهبود کارایی تضمین نمی‌شود.