← بازگشت به وبلاگ

متغیرهای HTTP_PROXY، HTTPS_PROXY، NO_PROXY در لینوکس، داکر و CI: ۷ اشتباه رایج

چرا متغیرهای محیطی پروکسی در یک کانتینر کار می‌کنند و در کانتینر دیگر خراب می‌شوند - ۷ مورد واقعی با راه‌حل‌ها و کد را بررسی می‌کنیم.

📅۱۶ آبان ۱۴۰۵

متغیرهای محیطی HTTP_PROXY، HTTPS_PROXY و NO_PROXY به ظاهر بسیار ساده به نظر می‌رسند: متغیر را تنظیم کردید — تمام ترافیک از طریق پروکسی رفت. اما در عمل، در همین نقطه توسعه‌دهندگان ساعت‌ها را صرف اشکال‌زدایی می‌کنند: پروکسی در ترمینال کار می‌کند، اما در کانتینر داکر کار نمی‌کند؛ curl از طریق پروکسی می‌رود، اما برنامه Python این‌طور نیست؛ CI-runner به طور ناگهانی registry داخلی را مسدود می‌کند، زیرا NO_PROXY با سینتکس نادرست مشخص شده است. بیایید 7 تله واقعی را بررسی کنیم و نشان دهیم که چگونه می‌توان آنها را با کد مشخص دور زد.

چگونه برنامه‌ها متغیرهای پروکسی را می‌خوانند

مهم است که بدانید: HTTP_PROXY، HTTPS_PROXY و NO_PROXY استاندارد پروتکل HTTP نیستند، بلکه یک کنوانسیون هستند که هر کتابخانه به شیوه خود آن را پیاده‌سازی می‌کند. هسته لینوکس هیچ اطلاعی از این متغیرها ندارد — آنها توسط یک برنامه یا کتابخانه خاص در مرحله راه‌اندازی کلاینت شبکه خوانده می‌شوند.

به عنوان مثال، curl و ابزارهای مبتنی بر libcurl متغیرها را در هر بار اجرای فرآیند می‌خوانند. کتابخانه Python requests آنها را از طریق ماژول urllib.request.getproxies() می‌خواند، که محیط فرآیند را در زمان ایجاد جلسه اسکن می‌کند. اما fetch() داخلی در Node.js تا نسخه 18 اصلاً این متغیرها را بدون بسته‌های اضافی در نظر نمی‌گیرد.

از این رو، اولین قانون: متغیر پروکسی فقط در جایی کار می‌کند که به وضوح توسط کتابخانه خاصی پشتیبانی می‌شود. بررسی از طریق curl تضمین نمی‌کند که برنامه شما در Go، Java یا Node.js نیز به همین شکل رفتار کند.

export HTTP_PROXY="http://user:[email protected]:8080"
export HTTPS_PROXY="http://user:[email protected]:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.corp"

curl -v https://api.example.com/ping
# curl در لاگ verbose نشان می‌دهد: "Trying proxy.example.com..."

تله 1: ثبت متغیرها و آسیب‌پذیری httpoxy

بیشتر ابزارهای کنسولی در لینوکس متغیرها را در حروف کوچک می‌خوانند: http_proxy، https_proxy، no_proxy. بسیاری از SDKهای مدرن به حروف بزرگ توجه می‌کنند. این تفاوت به نظر جزئی می‌رسد، اما به خاطر آن نیمی از باگ‌ها به سبک "در لوکال کار می‌کند، در سرور کار نمی‌کند" در همان روز اول تنظیمات رخ می‌دهد.

داستان جداگانه‌ای وجود دارد — آسیب‌پذیری httpoxy (CVE-2016-5385 و موارد مرتبط). اصل قضیه این است که محیط CGI (PHP، Python WSGI، برخی از فریم‌ورک‌های Ruby) به طور خودکار هدر HTTP Proxy را از درخواست ورودی به متغیر محیطی HTTP_PROXY تبدیل می‌کند. یک مهاجم هدر Proxy: http://attacker.com را ارسال می‌کند و برنامه سرور شما شروع به ارسال درخواست‌های خروجی از طریق پروکسی سرور خارجی می‌کند، که راه را برای حملات MITM و نشت داده‌ها باز می‌کند.

راه‌حل — همیشه متغیرها را در هر دو حالت حروف بزرگ و کوچک تنظیم کنید و به وضوح HTTP_PROXY را در سطح وب‌سرور برای درخواست‌های ورودی صفر کنید:

# در هر دو حالت حروف بزرگ و کوچک برای سازگاری تنظیم می‌کنیم
export http_proxy="http://proxy.example.com:8080"
export HTTP_PROXY="http://proxy.example.com:8080"
export https_proxy="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"

# در nginx — هدر Proxy را از کلاینت مسدود می‌کنیم
proxy_set_header Proxy "";

تله 2: سینتکس NO_PROXY بین ابزارها متفاوت است

NO_PROXY یک لیست استثناها است، اما فرمت نوشتن دامنه‌ها و ماسک‌ها در آن استاندارد نشده است. برخی ابزارها .example.com را به عنوان "هر زیر دامنه" درک می‌کنند، در حالی که برخی دیگر به *.example.com نیاز دارند، و برخی دیگر فقط تطابق دقیق میزبان را بدون wildcard درک می‌کنند.

به عنوان مثال، کتابخانه Go net/http از CIDR notation در NO_PROXY (10.0.0.0/8) پشتیبانی می‌کند، در حالی که curl کلاسیک تا نسخه‌های خاصی — نه. این باعث می‌شود که یک NO_PROXY یکسان به درستی registry داخلی را برای یک مرحله CI مستثنی کند و برای مرحله دیگری که به زبان دیگری نوشته شده است مستثنی نکند.

ابزار Wildcard زیر دامنه CIDR-mask
curl / libcurl از طریق .example.com خیر
Go net/http از طریق .example.com بله
Python requests از طریق .example.com خیر
npm / Node.js به *.example.com نیاز دارد خیر

نکته عملی: در NO_PROXY هم دامنه و هم wildcard را به طور همزمان ذکر کنید تا پارسرهای مختلف را پوشش دهید:

export NO_PROXY="localhost,127.0.0.1,.internal.corp,*.internal.corp,10.0.0.0/8"

تله 3: Docker build در مقابل Docker run — متغیرهای محیطی متفاوت

این احتمالاً رایج‌ترین تله در داکر است. متغیرهایی که از طریق docker run -e HTTP_PROXY=... تنظیم می‌شوند، فقط در حین اجرای کانتینر عمل می‌کنند. آنها به طور خودکار به مرحله ساخت تصویر منتقل نمی‌شوند. اگر Dockerfile شما درون خود apt-get install یا pip install را فراخوانی کند و ساخت تحت پروکسی شرکتی انجام شود، build به سادگی در تایم‌اوت متوقف می‌شود.

برای ساخت، باید build-args را منتقل کنید، که داکر از نسخه 1.9+ به طور "از جعبه" بدون اعلام در Dockerfile درک می‌کند:

docker build \
  --build-arg HTTP_PROXY=http://proxy.example.com:8080 \
  --build-arg HTTPS_PROXY=http://proxy.example.com:8080 \
  --build-arg NO_PROXY=localhost,127.0.0.1 \
  -t my-app:latest .

اگر می‌خواهید متغیر در داخل کانتینر در حال اجرا نیز در دسترس باشد، باید به وضوح آن را از طریق ENV در Dockerfile تکرار کنید — ARG و ENV در داکر به طور اصولی مکانیزم‌های متفاوتی هستند و ARG بلافاصله پس از ساخت لایه ناپدید می‌شود:

ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY

ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV NO_PROXY=${NO_PROXY}

RUN apt-get update && apt-get install -y curl

یک نکته دیگر: داکر دیمون ممکن است خود از پروکسی برای دانلود تصاویر از registry استفاده کند — این در /etc/systemd/system/docker.service.d/http-proxy.conf در تنظیمات unit systemd تنظیم می‌شود، که اغلب با متغیرهای داخل کانتینر اشتباه گرفته می‌شود.

تله 4: docker-compose و وراثت متغیرها بین سرویس‌ها

در docker-compose، متغیرهای محیطی میزبان به طور خودکار به کانتینرها منتقل نمی‌شوند، مگر اینکه شما به وضوح آنها را در بخش environment یا فایل .env مشخص کنید. یک اشتباه رایج — تنظیم پروکسی در ترمینال قبل از اجرای docker-compose up و انتظار اینکه همه سرویس‌ها آن را دریافت کنند.

version: "3.9"
services:
  scraper:
    build:
      context: .
      args:
        HTTP_PROXY: "${HTTP_PROXY}"
        HTTPS_PROXY: "${HTTPS_PROXY}"
    environment:
      - HTTP_PROXY=${HTTP_PROXY}
      - HTTPS_PROXY=${HTTPS_PROXY}
      - NO_PROXY=localhost,127.0.0.1,db,redis
    depends_on:
      - db
      - redis

به NO_PROXY توجه کنید: در شبکه‌های docker-compose، کانتینرها به یکدیگر از طریق نام‌های سرویس‌ها (db، redis) دسترسی پیدا می‌کنند، نه از طریق localhost. اگر فراموش کنید که نام‌های سرویس‌ها را به استثنائات اضافه کنید، تمام ترافیک داخلی بین کانتینرها نیز از طریق پروکسی خارجی خواهد رفت، که تأخیرها را افزایش می‌دهد و ممکن است اتصال را خراب کند، اگر پروکسی پروتکل‌های داخلی مانند PostgreSQL یا Redis را عبور ندهد.

تله 5: CI/CD رانرها و نشت اسرار پروکسی

در GitHub Actions، GitLab CI و Jenkins، متغیرهای پروکسی اغلب در سطح رانر یا سازمان به طور کلی تنظیم می‌شوند، و این دو مشکل ایجاد می‌کند. اولاً، اگر URL پروکسی شامل نام کاربری و رمز عبور به صورت واضح باشد، در هر دستوری که محیط را چاپ می‌کند (مثل env، printenv، خروجی اشکال‌زدایی در اسکریپت‌ها) به لاگ ساخت می‌رسد. ثانیاً، رانرهای خود میزبان گاهی اوقات پروکسی سیستم میزبان را به ارث می‌برند که برای ترافیک CI-job طراحی نشده است.

# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      HTTP_PROXY: ${{ secrets.PROXY_URL }}
      HTTPS_PROXY: ${{ secrets.PROXY_URL }}
      NO_PROXY: "localhost,127.0.0.1,github.com"
    steps:
      - uses: actions/checkout@v4
      - name: Test connection
        run: curl -sS https://api.example.com/health

قاعده امنیتی: URL پروکسی را فقط در اسرار CI (GitHub Secrets، GitLab CI/CD Variables با علامت "masked") ذخیره کنید، هرگز متغیرهای محیطی را به طور کامل در لاگ‌های اشکال‌زدایی چاپ نکنید، و NO_PROXY را به گونه‌ای محدود کنید که درخواست‌ها به خود پلتفرم CI (مثلاً github.com، gitlab.com) از طریق پروکسی نروند — در غیر این صورت، ساخت ممکن است در صورت عدم دسترسی پروکسی در زمان بار ترافیکی متوقف شود.

تله 6: کاراکترهای خاص در نام کاربری و رمز عبور پروکسی

اگر رمز عبور پروکسی شامل کاراکترهای @، :، / یا # باشد، URL-parser به سادگی رشته را در مکان‌های نادرست تقسیم می‌کند. اشتباه کلاسیک: رمز عبور P@ssw0rd در رشته http://user:P@[email protected]:8080 به گونه‌ای خوانده می‌شود که گویی میزبان "[email protected]" است، نه "proxy.com".

راه‌حل — همیشه نام کاربری و رمز عبور را قبل از جایگذاری در متغیر URL-encode کنید:

# Python: رمز عبور را قبل از ساخت رشته پروکسی کدگذاری کنید
import urllib.parse

user = "login"
password = "P@ss:w0rd/1"
encoded_password = urllib.parse.quote(password, safe="")
proxy_url = f"http://{user}:{encoded_password}@proxy.example.com:8080"

import os
os.environ["HTTPS_PROXY"] = proxy_url
# Node.js: ساخت مشابه از طریق encodeURIComponent
const user = "login";
const password = "P@ss:w0rd/1";
const encoded = encodeURIComponent(password);
const proxyUrl = `http://${user}:${encoded}@proxy.example.com:8080`;

process.env.HTTPS_PROXY = proxyUrl;

تله 7: برنامه‌هایی که متغیرهای محیطی را نادیده می‌گیرند

همه کلاینت‌های HTTP به متغیرهای پروکسی سیستم احترام نمی‌گذارند. در Python، کتابخانه requests آنها را به طور خودکار از طریق requests.Session() می‌خواند، اما اگر شما یک بار به وضوح دیکشنری proxies={} را در فراخوانی ارسال کنید، این متغیرهای محیطی را بازنویسی می‌کند و ترافیک به طور مستقیم خواهد رفت، حتی اگر HTTP_PROXY در سیستم تنظیم شده باشد.

import requests

# دیکشنری خالی proxies متغیرهای محیطی را قطع می‌کند
response = requests.get("https://api.example.com", proxies={})
# درست — به وضوح پروکسی را ارسال کنید یا اصلاً proxies را ارسال نکنید
proxies = {
    "http": "http://user:[email protected]:8080",
    "https": "http://user:[email protected]:8080",
}
response = requests.get("https://api.example.com", proxies=proxies)

در Node.js، fetch داخلی اصلاً از HTTP_PROXY بدون کتابخانه‌های اضافی اطلاعی ندارد. باید به وضوح undici با ProxyAgent یا https-proxy-agent را متصل کنید:

const { ProxyAgent, setGlobalDispatcher } = require("undici");

const proxyUrl = process.env.HTTPS_PROXY;
if (proxyUrl) {
  setGlobalDispatcher(new ProxyAgent(proxyUrl));
}

fetch("https://api.example.com/data")
  .then(res => res.json())
  .then(console.log);

جاوا نیز به خواص سیستمی جداگانه‌ای به جای متغیرهای محیطی نیاز دارد: -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080. متغیرهای محیطی HTTP_PROXY به طور پیش‌فرض در JVM اصلاً خوانده نمی‌شوند.

چگونه متغیرهای پروکسی را به درستی آزمایش کنیم

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

# 1. بررسی می‌کنیم که متغیر واقعاً در جلسه فعلی تنظیم شده است
env | grep -i proxy

# 2. بررسی می‌کنیم که پروکسی به طور مستقیم از طریق curl کار می‌کند
curl -x "$HTTP_PROXY" -v https://api.ipify.org

# 3. بررسی می‌کنیم که از داخل کانتینر، متغیر به درستی منتقل شده است
docker exec -it my_container env | grep -i proxy

# 4. بررسی می‌کنیم که NO_PROXY واقعاً میزبان مورد نظر را مستثنی می‌کند
curl -v --noproxy "internal.corp" https://service.internal.corp/health

اگر هدف — درخواست‌های انبوه به APIهای خارجی، پارس کردن بازارها یا آزمایش جغرافیایی تبلیغات از CI-pipeline باشد، مهم است که نوع مناسب پروکسی را تحت بار انتخاب کنید. برای سناریوهای پایدار با بار بالا در CI معمولاً پروکسی‌های دیتا سنتر کافی هستند — آنها سریع‌تر و ارزان‌تر از پروکسی‌های مسکونی در ترافیک انبوه ساخت و آزمایش هستند. و اگر pipeline شما رفتار سایت را از IP واقعی کاربر در یک منطقه خاص بررسی می‌کند، بهتر است از پروکسی‌های مسکونی استفاده کنید — آنها خطر مسدود شدن به دلیل IP "دیتا سنتر" را کاهش می‌دهند.

چک‌لیست قبل از استقرار

  • متغیرها هم در حروف بزرگ و هم در حروف کوچک به طور همزمان تنظیم شده‌اند
  • نام کاربری و رمز عبور در URL پروکسی URL-encode شده‌اند از طریق urllib.parse.quote یا encodeURIComponent
  • NO_PROXY شامل هم دامنه و هم فرمت wildcard برای سازگاری بین ابزارها است
  • در docker-compose، NO_PROXY شامل نام‌های سرویس‌ها از شبکه Compose است، نه فقط localhost
  • برای Docker build، build-args به طور جداگانه از runtime env از طریق ENV در Dockerfile منتقل شده‌اند
  • Secrets با URL پروکسی در CI مخفی شده و در لاگ‌های اشکال‌زدایی چاپ نمی‌شوند
  • برای برنامه‌های جاوا، پروکسی از طریق خواص سیستمی -D تنظیم می‌شود، نه فقط از طریق env
  • از httpoxy محافظت شده است — هدر Proxy از کلاینت در سطح وب‌سرور صفر می‌شود

نتیجه‌گیری

متغیرهای HTTP_PROXY، HTTPS_PROXY و NO_PROXY — به نظر ساده می‌رسند، اما در عمل به دلیل ثبت حروف، سینتکس استثنائات، مرزهای بین مراحل Docker build و Docker run، وراثت محیط در docker-compose، نشت اسرار در لاگ‌های CI و در نهایت این که هر کتابخانه‌ای اصلاً این متغیرها را نمی‌خواند، خراب می‌شوند. هر متغیر را به طور جداگانه از طریق curl و docker exec بررسی کنید، قبل از اینکه به دنبال باگ در منطق کسب و کار برنامه باشید.

اگر زیرساخت شما به پارس کردن سایت‌های خارجی، آزمایش جغرافیایی تبلیغات یا دور زدن مسدودیت‌ها از طریق IP از CI-pipeline وابسته است، مهم است که نه تنها متغیرهای محیطی را به درستی منتقل کنید، بلکه نوع مناسب پروکسی را برای وظیفه انتخاب کنید. برای آزمایش ترافیک موبایل و برنامه‌هایی که رفتار را از اپراتورهای موبایل بررسی می‌کنند، پروکسی‌های موبایل مناسب هستند — آنها IP واقعی شبکه‌های تلفن همراه را ارائه می‌دهند و کمتر تحت فیلترهای سیستم‌های ضد تقلب قرار می‌گیرند.