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

nginx

نمای کلی​

nginx پیش از پنل XLADA قرار می‌گیرد و سه کار را انجام می‌دهد: ترافیک را روی پورت‌های 80 و 443 می‌پذیرد، TLS را پایان می‌دهد و درخواست‌های پنل را به 127.0.0.1:3000 می‌فرستد و هدرهایی را جایگزین می‌کند که پنل از روی آن‌ها می‌فهمد اتصال محافظت‌شده است و کاربر واقعی کیست.

پروکسی‌کردن اجباری است، نه اختیاری: در کد پنل و صفحه اشتراک میان‌افزاری وجود دارد که اتصال را قطع می‌کند اگر درخواست بدون هدرهای X-Forwarded-For و X-Forwarded-Proto: https برسد. بنابراین پیش از نصب nginx پنل باید از قبل بالا آمده باشد — به نصب پنل مراجعه کنید، و برای میزبان دوم به صفحه اشتراک.

هشدار

به یک دامنه ثبت‌شده و دسترسی به رکوردهای DNS آن نیاز دارید. بدون دامنه نمی‌توان گواهی صادر کرد و بدون HTTPS پنل کار نخواهد کرد.

DNS: دامنه را به سرور اشاره می‌دهیم​

نشانی بیرونی سرور را پیدا کنید. بهتر است یک نشانی ثابت IPv4 باشد. سپس رکوردها را نزد ثبت‌کننده یا ارائه‌دهنده DNS ایجاد کنید:

نوعناممقدارکاربرد
Apanelنشانی IPv4 سرورپنل و API
AAAApanelنشانی IPv6 سرورهمان مورد از طریق IPv6، اگر وجود دارد
Asubنشانی IPv4 سرورصفحه اشتراک

اگر پنل باید روی خود دامنه باشد، در فیلد نام @ را وارد کنید. رکوردهای AAAA را تنها زمانی ایجاد کنید که IPv6 واقعاً روی سرور پیکربندی شده باشد: در غیر این صورت کلاینت‌ها به جایی نامعلوم می‌روند و خطای اتصال می‌گیرند.

به‌روزرسانی رکوردها از چند دقیقه تا چند ساعت طول می‌کشد. نتیجه را می‌توان با این فرمان بررسی کرد:

dig +short panel.example.com

ابر خاکستری و نارنجی​

در پنل مدیریت Cloudflare برای هر رکورد یک کلید پروکسی‌کردن وجود دارد:

  • ابر خاکستری — رکورد نشانی سرور شما را مستقیماً به کلاینت می‌دهد؛ گواهی روی سرور صادر و تمدید می‌شود و فهرست مجاز IP مانند معمول کار می‌کند؛
  • ابر نارنجی — ترافیک از Cloudflare عبور می‌کند و کلاینت نشانی‌های Cloudflare را می‌بیند، نه نشانی شما را. این کار سرور را پنهان و جهش‌ها را هموار می‌کند، اما نیازمند تنظیم جداگانه IP واقعی کاربر است (به بخش زیر مراجعه کنید) و اجازه نمی‌دهد گواهی با بررسی روی پورت 80 و بدون شرایط اضافی صادر شود.

برای پنل معمولاً ابر نارنجی انتخاب می‌شود و اعتماد به هدر CF-Connecting-IP الزاماً پیکربندی می‌گردد. جزئیات در بخش Cloudflare.

گواهی​

certbot — مسیر اصلی​

certbot گواهی Let's Encrypt را صادر می‌کند و آن را با تایمر خودش تمدید می‌کند. ابتدا میزبان کامل HTTP را بسازید: این میزبان بررسی مالکیت دامنه را ارائه می‌دهد و بقیه درخواست‌ها را به HTTPS هدایت می‌کند.

sudo mkdir -p /var/www/certbot
sudo mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
sudo nano /etc/nginx/sites-available/xlada-bootstrap
/etc/nginx/sites-available/xlada-bootstrap
# میزبان موقت برای صدور گواهی‌های پنل و صفحه اشتراک.
server {
listen 80;
listen [::]:80;
server_name panel.example.com sub.example.com;

location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}
sudo ln -sf /etc/nginx/sites-available/xlada-bootstrap /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d panel.example.com
sudo certbot --nginx -d sub.example.com

Certbot برای هر دامنه یک گواهی جداگانه صادر می‌کند: آن‌ها در /etc/letsencrypt/live/panel.example.com/ و /etc/letsencrypt/live/sub.example.com/ قرار می‌گیرند. پس از صدور، میزبان موقت را حذف کنید و پیکربندی‌های کامل بخش‌های زیر را فعال کنید: مسیرهای گواهی در آن‌ها از قبل مشخص شده است.

sudo rm -f /etc/nginx/sites-enabled/xlada-bootstrap

تمدید را خود certbot انجام می‌دهد. آن را این‌گونه بررسی کنید:

sudo certbot renew --dry-run
systemctl list-timers | grep certbot

acme.sh — جایگزین​

اگر certbot در دسترس نیست یا ترجیح می‌دهید برای چند سرور از یک ابزار استفاده کنید، از acme.sh با بررسی مالکیت دامنه از طریق ALPN بهره ببرید. این روش به وب‌سرور وابسته نیست، اما به پورت آزاد 8443 نیاز دارد.

sudo apt-get install -y cron socat
curl https://get.acme.sh | sh -s email=admin@example.com
source ~/.bashrc

sudo mkdir -p /opt/xlada/nginx && cd /opt/xlada/nginx

# یک گواهی برای هر دو دامنه به‌طور همزمان: صدور و تثبیت مسیرها
acme.sh --issue --standalone -d 'panel.example.com' -d 'sub.example.com' \
--key-file /opt/xlada/nginx/privkey.key \
--fullchain-file /opt/xlada/nginx/fullchain.pem \
--alpn --tlsport 8443 \
--reloadcmd "systemctl reload nginx"

# تثبیت مسیرها و فرمان‌های بارگذاری مجدد برای تمدید خودکار
acme.sh --install-cert -d 'panel.example.com' \
--key-file /opt/xlada/nginx/privkey.key \
--fullchain-file /opt/xlada/nginx/fullchain.pem \
--reloadcmd "systemctl reload nginx"

# نام فایل‌ها را به نام‌های ذکرشده در پیکربندی‌های کامل زیر می‌رسانیم
sudo mkdir -p /etc/letsencrypt/live/panel.example.com
sudo mkdir -p /etc/letsencrypt/live/sub.example.com
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/panel.example.com/fullchain.pem
sudo ln -sf /opt/xlada/nginx/privkey.key /etc/letsencrypt/live/panel.example.com/privkey.pem
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/panel.example.com/chain.pem
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/sub.example.com/fullchain.pem
sudo ln -sf /opt/xlada/nginx/privkey.key /etc/letsencrypt/live/sub.example.com/privkey.pem
sudo ln -sf /opt/xlada/nginx/fullchain.pem /etc/letsencrypt/live/sub.example.com/chain.pem

پورت 8443 باید باز و توسط فرایند دیگری اشغال نشده باشد. acme.sh گواهی را خودش تقریباً هر 60 روز یک‌بار از طریق cron تمدید می‌کند — بررسی کنید که سرویس cron اجرا شده باشد.

توجه

اگر nginx در کانتینر اجرا می‌شود (به زیر مراجعه کنید)، در --reloadcmd به‌جای بارگذاری مجدد سرویس، بارگذاری مجدد درون کانتینر را مشخص کنید: docker exec xlada-nginx nginx -s reload.

هشدار

محدودیت مرکز صدور گواهی را در نظر بگیرید: دامنه‌های .ru، .su و .рф هنگام صدور از طریق ZeroSSL که acme.sh به‌طور پیش‌فرض از آن استفاده می‌کند، پشتیبانی نمی‌شوند. برای چنین دامنه‌هایی certbot یا مرکز صدور گواهی دیگری انتخاب کنید.

پیکربندی nginx برای پنل​

فایل میزبان مجازی را بسازید و آن را فعال کنید:

sudo nano /etc/nginx/sites-available/xlada-panel
sudo ln -sf /etc/nginx/sites-available/xlada-panel /etc/nginx/sites-enabled/
/etc/nginx/sites-available/xlada-panel
# میزبان مجازی پنل XLADA.

upstream xlada_panel {
server 127.0.0.1:3000;
keepalive 32;
}

# برای WebSocket مقدار upgrade لازم است و برای درخواست‌های معمولی close.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

# ─── HTTP: بررسی مالکیت دامنه و هدایت به HTTPS ─────────────
server {
listen 80;
listen [::]:80;
server_name panel.example.com;

location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}

# ─── HTTPS ───────────────────────────────────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name panel.example.com;

# گواهی Let's Encrypt
ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/panel.example.com/chain.pem;

# پروتکل‌ها و رمزها: توصیه‌های Mozilla Intermediate
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

# نشست‌ها و بررسی ابطال گواهی
ssl_session_timeout 1d;
ssl_session_cache shared:XLADA_TLS:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 valid=60s;
resolver_timeout 2s;

# هدرهای امنیتی. HSTS را تنها زمانی فعال کنید که مطمئن شده‌اید HTTPS
# کار می‌کند: مرورگر این الزام را برای مدت طولانی به خاطر می‌سپارد.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# اندازه بدنه درخواست: با حاشیه اطمینان برای بارگذاری فایل از رابط کاربری
client_max_body_size 25m;
client_body_timeout 60s;

# فشرده‌سازی پاسخ‌ها
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/wasm
application/xhtml+xml
application/xml
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;

# ─── فایل‌های ایستای رابط: بیلد Vite در /assets/ قرار دارد ─────────────
# نام فایل‌ها شامل هش است، بنابراین می‌توان آن‌ها را برای مدت طولانی کش کرد.
location ^~ /assets/ {
proxy_pass http://xlada_panel;
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

# ─── API و پاسخ‌های جریانی: بدون بافر ──────────────────────
location /api/ {
proxy_pass http://xlada_panel;
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
}

# ─── رابط پنل ────────────────────────────────────────────
location / {
proxy_pass http://xlada_panel;
proxy_http_version 1.1;

# بدون این هدرها پنل اتصال را قطع می‌کند
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# WebSocket: ترمینال SSH به نودها و به‌روزرسانی‌های زنده رابط
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

# ترمینال اتصال را ساعت‌ها باز نگه می‌دارد
proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;

proxy_buffering on;
}
}

یک بلوک جایگزین (stub) جداگانه درخواست‌هایی را می‌بندد که با نشانی IP یا با نام سرور بیگانه می‌آیند: به چنین کلاینت‌هایی اصلاً گواهی داده نمی‌شود.

/etc/nginx/conf.d/00-default.conf
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;

ssl_reject_handshake on;
}

پیکربندی را بررسی و nginx را بارگذاری مجدد کنید:

sudo nginx -t && sudo systemctl reload nginx

محدودسازی دسترسی با فهرست مجاز IP​

در پیکربندی پایه هیچ محدودیت دسترسی بر اساس IP وجود ندارد: پنل با احراز هویت خودش محافظت می‌شود و صفحه اشتراک باید برای همه کلاینت‌ها باز باشد. اگر پنل فقط برای شما در دسترس است، از پیکربندی کامل همان میزبان با فیلتر نشانی‌ها استفاده کنید: در زیر آمده و در همان فایل /etc/nginx/sites-available/xlada-panel نوشته می‌شود.

/etc/nginx/sites-available/xlada-panel
# میزبان مجازی پنل XLADA با محدودسازی دسترسی بر اساس IP.

upstream xlada_panel {
server 127.0.0.1:3000;
keepalive 32;
}

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

# ─── HTTP: بررسی مالکیت دامنه و هدایت به HTTPS ─────────────
server {
listen 80;
listen [::]:80;
server_name panel.example.com;

location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}

# ─── HTTPS فقط برای نشانی‌های مورد اعتماد ─────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name panel.example.com;

# تنها نشانی‌های مورد اعتماد را می‌پذیریم، بقیه 403 می‌گیرند
allow 203.0.113.0/24;
allow 198.51.100.7;
deny all;

ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/panel.example.com/chain.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

ssl_session_timeout 1d;
ssl_session_cache shared:XLADA_TLS:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 valid=60s;
resolver_timeout 2s;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

client_max_body_size 25m;
client_body_timeout 60s;

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/wasm
application/xhtml+xml
application/xml
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;

location ^~ /assets/ {
proxy_pass http://xlada_panel;
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

location /api/ {
proxy_pass http://xlada_panel;
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
}

location / {
proxy_pass http://xlada_panel;
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;

proxy_buffering on;
}
}

چنین فیلتری را فقط روی میزبان پنل بگذارید. روی میزبان صفحه اشتراک این فیلتر ارائه لینک‌ها به کاربران را خراب می‌کند و روی API، وب‌هوک‌ها و یکپارچه‌سازی‌های بیرونی را. Basic auth به‌عنوان لایه دوم مناسب نیست: کار توکن‌های API و درخواست‌های سرویس را خراب می‌کند، بنابراین به‌جای آن به گذرواژه مدیر و TOTP اعتماد کنید.

nginx در کانتینر​

اگر nginx شما هم در Docker است، برای آن حالت شبکه host را فعال کنید: آن‌گاه کانتینر همان 127.0.0.1:3000 و 127.0.0.1:3010 را می‌بیند که میزبان می‌بیند، و پیکربندی‌های کامل این صفحه بدون هیچ ویرایشی مناسب‌اند.

/opt/xlada/nginx/docker-compose.yml
services:
xlada-nginx:
image: nginx:1.30
container_name: xlada-nginx
restart: unless-stopped
# حالت host: upstreamهای 127.0.0.1:3000 و 127.0.0.1:3010 مانند میزبان در دسترس هستند
network_mode: host
volumes:
- /etc/nginx/sites-available/xlada-panel:/etc/nginx/conf.d/xlada-panel.conf:ro
- /etc/nginx/sites-available/xlada-subpage:/etc/nginx/conf.d/xlada-subpage.conf:ro
- /etc/nginx/conf.d/00-default.conf:/etc/nginx/conf.d/00-default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/www/certbot:/var/www/certbot:ro
cd /opt/xlada/nginx
docker compose up -d
docker exec xlada-nginx nginx -t
docker exec xlada-nginx nginx -s reload

به‌روزرسانی پیکربندی به ویرایش فایل‌ها در /etc/nginx/sites-available/ و بارگذاری مجدد درون کانتینر خلاصه می‌شود:

docker exec xlada-nginx nginx -t && docker exec xlada-nginx nginx -s reload

صفحه اشتراک​

صفحه اشتراک روی 127.0.0.1:3010 در کانتینر xpanel-subpage کار می‌کند و روی زیردامنه جداگانه قرار دارد. الزامات همان است: HTTPS و هدرهای X-Forwarded-For و X-Forwarded-Proto: https، وگرنه کانتینر اتصال را قطع می‌کند. WebSocket اینجا لازم نیست و می‌توان تایم‌اوت را کوتاه‌تر کرد.

/etc/nginx/sites-available/xlada-subpage
# میزبان مجازی صفحه اشتراک XLADA.

upstream xlada_subpage {
server 127.0.0.1:3010;
keepalive 16;
}

# صفحه اشتراک به WebSocket نیازی ندارد، اما هدرها باقی مانده‌اند:
# تا پیکربندی در صورت استفاده برنامه از آن‌ها همچنان کار کند.
map $http_upgrade $subpage_connection_upgrade {
default upgrade;
'' close;
}

server {
listen 80;
listen [::]:80;
server_name sub.example.com;

location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name sub.example.com;

ssl_certificate /etc/letsencrypt/live/sub.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sub.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/sub.example.com/chain.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

ssl_session_timeout 1d;
ssl_session_cache shared:XLADA_TLS:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=60s;
resolver_timeout 2s;

add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

client_max_body_size 10m;

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
application/javascript
application/json
application/manifest+json
application/xml
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;

# فایل‌های ایستای بیلد صفحه اشتراک
location ^~ /assets/ {
proxy_pass http://xlada_subpage;
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

location / {
proxy_pass http://xlada_subpage;
proxy_http_version 1.1;

# بدون این هدرها صفحه اشتراک اتصال را قطع می‌کند
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# WebSocket: اکنون استفاده نمی‌شود، اما پیکربندی برای آن آماده است
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $subpage_connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;

proxy_buffering on;
}
}

میزبان را فعال و nginx را بارگذاری مجدد کنید:

sudo ln -sf /etc/nginx/sites-available/xlada-subpage /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot renew --dry-run

اگر در پنل CUSTOM_SUB_PREFIX تنظیم شده است، همان مسیر باید به SUB_PUBLIC_DOMAIN هم اضافه شود — جزئیات در بخش صفحه اشتراک. و در فایل محیطی خود صفحه TRUST_PROXY=1 را بگذارید تا به یک hop پروکسی اعتماد کند و IP کاربر را از هدر بگیرد.

IP واقعی کاربر​

پنل باید نشانی واقعی کاربر را ببیند، زیرا موارد زیر به آن وابسته‌اند:

  • ثبت دستگاه‌ها بر اساس HWID — اتصال دستگاه و تاریخچه دسترسی‌ها نشانی درخواست را ذخیره می‌کنند (محدودیت دستگاه‌ها)؛
  • گزارش اقدامات مدیر: از روی نشانی معلوم می‌شود چه کسی و از کجا وارد شده است (امنیت پنل)؛
  • محافظت از ورود در برابر حمله جست‌وجوی فراگیر: تلاش‌های ورود بر اساس نشانی شمرده می‌شوند و اگر همه درخواست‌ها از یک نشانی پروکسی بیایند، محدودیت یا برای همه هم‌زمان فعال می‌شود یا اصلاً فعال نمی‌شود.

اگر پیش از nginx، Cloudflare قرار دارد، X-Forwarded-For معمولی بی‌فایده است: Cloudflare نشانی کاربر را در هدر CF-Connecting-IP منتقل می‌کند. اعتماد به این هدر تنها از زیرشبکه‌های Cloudflare ممکن است — وگرنه هر کسی می‌تواند نشانی دیگری را در درخواست جایگزین کند.

/etc/nginx/conf.d/real-ip.conf
# نشانی کاربر را از هدر Cloudflare می‌گیریم.
# فهرست محدوده‌های Cloudflare هر چند وقت یک‌بار تغییر می‌کند — آن را بررسی کنید.
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

real_ip_header CF-Connecting-IP;
real_ip_recursive on;

اگر پیش از nginx، متعادل‌کننده بار خودتان یا پروکسی دومی قرار دارد، به زیرشبکه آن و هدر معمولی اعتماد کنید:

/etc/nginx/conf.d/real-ip.conf
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
خطر

هرگز set_real_ip_from 0.0.0.0/0 را مشخص نکنید و به X-Forwarded-For از کل اینترنت اعتماد نکنید. همچنین X-Forwarded-Proto را دستی با مقدار http جایگزین نکنید: پنل از کار می‌افتد.

بررسی​

# 1. نحو و بارگذاری مجدد
sudo nginx -t && sudo systemctl reload nginx

# 2. هدایت از HTTP به HTTPS — انتظار 301
curl -sI http://panel.example.com | head -1

# 3. HTTPS: کد پاسخ و هدر HSTS
curl -sI https://panel.example.com | grep -Ei 'HTTP/|strict-transport'

# 4. مدت اعتبار گواهی
echo | openssl s_client -connect panel.example.com:443 \
-servername panel.example.com 2>/dev/null | openssl x509 -noout -dates

# 5. صفحه اشتراک — انتظار 200 و هدر profile-web-page-url
curl -sI https://sub.example.com/AbCdEf1234567890

اگر چیزی کار نمی‌کند، این‌ها را ببینید:

sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
sudo journalctl -u nginx -n 50 --no-pager
docker compose logs --tail 50 xpanel

نشانه‌های معمول: 502 Bad Gateway — کانتینر پنل اجرا نشده یا روی پورت اشتباه گوش می‌دهد؛ خطاهای handshake در TLS — مسیر نادرست گواهی یا انقضای آن؛ قطع اتصال بلافاصله پس از درخواست — از دست رفتن هدرهای X-Forwarded-For یا X-Forwarded-Proto. در لاگ دسترسی، نشانی کاربر باید با نشانی واقعی او مطابقت داشته باشد، نه با نشانی Cloudflare.

قدم‌های بعدی​