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

HeaderClient

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

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

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

HeaderClient socket باز نمی‌کند و line نمی‌سازد. تنها نخستین payloadِ upstream را تغییر می‌دهد و در ادامه عمر line، داده را بدون تغییر عبور می‌دهد.

عملکرد

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

جایگاه رایج

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 وصل می‌شود.

اجبار همه ترافیک 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نام دلخواه که باید در پیکربندی یکتا باشد.
typestringباید دقیقاً "HeaderClient" باشد.
settingsobjectاجباری است و باید data داشته باشد.
nextstringنود 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 باید نود قبلی هم داشته باشد. این نود برای ابتدا یا انتهای 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".
"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 دقیقاً بدون تغییر عبور می‌کنند.

در حالت‌های 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"
}

برای برگرداندن 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 Initstate محلی را مقداردهی اولیه می‌کند و callback را به next می‌فرستد.
اولین upstream payloadheader تنظیم‌شده را prepend می‌کند و به next می‌فرستد.
payloadهای upstream بعدیبدون تغییر به next می‌روند.
upstream pause/resumeبه next فرستاده می‌شود.
downstream payloadبدون تغییر به نود قبلی برمی‌گردد.
downstream Est / pause / resumeبه نود قبلی فرستاده می‌شود.

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

Finish

با دریافت Finish در upstream، HeaderClient state محلی خود را از بین می‌برد و finish را به next می‌فرستد.

با دریافت Finish در downstream، state محلی از بین می‌رود و finish به نود قبلی فرستاده می‌شود.

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

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

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

از آنجا که این نود بزرگ‌ترین 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 ممکن است همه اتصال‌های پذیرفته‌شده را درست توصیف نکند.
  • مقدار encodeشده هنگام رسیدن نخستین payloadِ upstream خوانده می‌شود.
  • تنها نخستین payloadِ upstream تغییر می‌کند و مسیر پاسخ بدون تغییر است.