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" استفاده کنید.
فیلدهای لازم
فیلدهای سطح اصلی:
| فیلد | نوع | توضیح |
|---|---|---|
name | string | نام دلخواه که باید در config یکتا باشد. |
type | string | باید دقیقاً "HeaderClient" باشد. |
settings | object | اجباری است و باید data داشته باشد. |
next | string | node stream بعدی در لایه ۴ که stream دارای header را میگیرد. |
فیلدهای لازم داخل settings:
| فیلد | نوع | توضیح |
|---|---|---|
data | number یا string | فرمت header و دادهای که در نخستین payloadِ upstream encode میشود. |
فیلدهای شرطی داخل settings:
| فیلد | نوع | توضیح |
|---|---|---|
frontend-ipv4 | string | اگر 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 payload | header تنظیمشده را 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 برای port | 2 bytes |
| header پروتکل WaterWall | 1 byte |
| header پروتکل PROXY v1 | حداکثر 108 bytes |
| header پروتکل PROXY v2 | 16 یا 28 bytes |
required_padding_left | 108 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 flags | kNodeFlagSupportsSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 108 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 قابلیت این بررسیها را دور نمیزند. بدون اندازهگیری، بهبود کارایی تضمین نمیشود.