← العودة إلى المدونة

متغيرات HTTP_PROXY و HTTPS_PROXY و NO_PROXY في لينكس ودوكر و CI: 7 أخطاء شائعة

لماذا تعمل متغيرات البيئة الوكيلة في حاوية واحدة وتفشل في أخرى - نستعرض 7 حالات حقيقية مع الحلول والكود.

📅٢٧ جمادى الأولى ١٤٤٨ هـ

تبدو متغيرات البيئة HTTP_PROXY و HTTPS_PROXY و NO_PROXY بسيطة للغاية: قمت بتعيين المتغير — كل حركة المرور تمر عبر البروكسي. لكن في الواقع، هنا بالضبط يفقد المطورون ساعات في تصحيح الأخطاء: البروكسي يعمل في الطرفية، لكنه لا يعمل في حاوية Docker؛ curl يمر عبر البروكسي، لكن التطبيق المكتوب بلغة Python لا يفعل ذلك؛ و CI-رانر يحظر فجأة السجل الداخلي، لأن NO_PROXY تم تحديده بصيغة خاطئة. دعونا نستعرض 7 فخاخ حقيقية ونظهر كيف يمكن تجاوزها مع كود محدد.

كيف تقرأ التطبيقات متغيرات البروكسي

من المهم أن نفهم: HTTP_PROXY و HTTPS_PROXY و NO_PROXY ليست معيارًا لبروتوكول HTTP، بل هي اتفاقية تطبقها كل مكتبة بطريقتها الخاصة. نواة Linux لا تعرف شيئًا عن هذه المتغيرات — يتم قراءتها بواسطة تطبيق أو مكتبة معينة في مرحلة تهيئة عميل الشبكة.

على سبيل المثال، 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

معظم أدوات الطرفية في Linux تقرأ المتغيرات بحروف صغيرة: 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 في NO_PROXY (10.0.0.0/8)، بينما لا تدعم curl الكلاسيكية حتى إصدارات معينة. هذا يؤدي إلى موقف حيث يستثني نفس NO_PROXY السجل الداخلي بشكل صحيح لخطوة CI واحدة ولا يستثنيه لخطوة أخرى مكتوبة بلغة مختلفة.

الأداة نطاق فرعي wildcard قناع CIDR
curl / libcurl من خلال .example.com لا
Go net/http من خلال .example.com نعم
Python requests من خلال .example.com لا
npm / Node.js يحتاج إلى *.example.com لا

نصيحة عملية: قم بإدراج كل من النطاق والدومين في NO_PROXY في نفس الوقت لتغطية المحللات المختلفة:

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

الفخ 3: Docker build مقابل Docker run — متغيرات بيئة مختلفة

هذه، على الأرجح، هي أكثر الفخاخ شيوعًا في Docker. المتغيرات التي تم تعيينها عبر docker run -e HTTP_PROXY=... تعمل فقط أثناء تشغيل الحاوية. إنها لا تنتقل تلقائيًا إلى مرحلة بناء الصورة. إذا كان Dockerfile الخاص بك يستدعي apt-get install أو pip install، وتتم عملية البناء خلف بروكسي الشركة، فإن البناء سيتوقف ببساطة على المهلة.

للبناء، تحتاج إلى تمرير build-args، التي يفهمها Docker من الإصدار 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 في Docker آليتين مختلفتين، و 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

هناك نقطة أخرى: يمكن أن يستخدم خادم Docker البروكسي لتنزيل الصور من السجل — يتم إعداد ذلك ليس في Dockerfile، بل في تكوين وحدة systemd /etc/systemd/system/docker.service.d/http-proxy.conf، وهو ما يخلط بينه وبين المتغيرات داخل الحاوية.

الفخ 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-وظيفة.

# .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: اختبار الاتصال
        run: curl -sS https://api.example.com/health

قاعدة الأمان: احفظ عنوان URL للبروكسي فقط في أسرار CI (أسرار GitHub، متغيرات CI/CD في GitLab مع علامة "masked")، ولا تطبع أبدًا متغيرات البيئة بالكامل في سجلات التصحيح، وحدد NO_PROXY بحيث لا تذهب الطلبات إلى منصة CI نفسها (مثل github.com و gitlab.com) عبر البروكسي — وإلا قد يتوقف البناء إذا كان البروكسي غير متاح في وقت الذروة.

الفخ 6: الرموز الخاصة في اسم المستخدم وكلمة المرور للبروكسي

إذا كانت كلمة المرور للبروكسي تحتوي على رموز مثل @ و : و / أو #، فإن محلل URL يكسر السلسلة في أماكن خاطئة. الخطأ الكلاسيكي: كلمة المرور P@ssw0rd في السلسلة http://user:P@[email protected]:8080 يقرأها المحلل كما لو أن المضيف هو "[email protected]" وليس "proxy.com".

الحل هو دائمًا ترميز اسم المستخدم وكلمة المرور قبل إدخالهما في المتغير:

# 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);

تتطلب Java أيضًا خصائص نظام منفصلة بدلاً من متغيرات البيئة: -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

إذا كانت المهمة هي إجراء طلبات جماعية إلى واجهات برمجة التطبيقات الخارجية، أو تحليل الأسواق، أو اختبار استهداف الإعلانات الجغرافية من خط أنابيب CI، فمن المهم اختيار النوع الصحيح من البروكسي تحت الحمل. عادةً ما تكون بروكسيات مراكز البيانات كافية للسيناريوهات المستقرة ذات الحمل العالي في CI — فهي أسرع وأرخص من السكنية عند حركة المرور الكبيرة للبناء والاختبارات. وإذا كان خط أنابيبك يتحقق من سلوك الموقع من عنوان IP لمستخدم حقيقي من منطقة معينة، فمن الأفضل توصيل بروكسيات سكنية — فهي تقلل من خطر الحظر بسبب عنوان IP "مركز البيانات".

قائمة التحقق قبل النشر

  • تم تعيين المتغيرات في الحالتين العليا والسفلى في نفس الوقت
  • تم ترميز اسم المستخدم وكلمة المرور في عنوان URL للبروكسي عبر urllib.parse.quote أو encodeURIComponent
  • يتضمن NO_PROXY كل من صيغة النطاق والصيغة wildcard للتوافق بين الأدوات
  • في docker-compose، يتضمن NO_PROXY أسماء الخدمات من شبكة Compose، وليس فقط localhost
  • تم تمرير build-args بشكل منفصل عن runtime env عبر ENV في Dockerfile
  • تم إخفاء الأسرار مع عنوان URL للبروكسي في CI ولا تتم طباعتها في سجلات التصحيح
  • لتطبيقات Java، يتم تعيين البروكسي عبر خصائص النظام -D، وليس فقط عبر env
  • تم التحقق من الحماية من httpoxy — يتم إلغاء رأس Proxy من العميل على مستوى خادم الويب

الخاتمة

تعتبر متغيرات HTTP_PROXY و HTTPS_PROXY و NO_PROXY اتفاقية بسيطة في المظهر، لكنها في الواقع تتعطل بسبب حالة الحروف، وصيغة الاستثناءات، والحدود بين مراحل Docker build و Docker run، ووراثة البيئة في docker-compose، وتسرب الأسرار في سجلات CI، وأخيرًا، ليس كل مكتبة تقرأ هذه المتغيرات على الإطلاق. تحقق من كل متغير بشكل معزول عبر curl و docker exec قبل البحث عن خطأ في منطق العمل للتطبيق.

إذا كانت بنيتك تعتمد على تحليل المواقع الخارجية، أو اختبار استهداف الإعلانات الجغرافية، أو تجاوز الحظر بناءً على IP من خط أنابيب CI، فمن المهم ليس فقط تمرير متغيرات البيئة بشكل صحيح، ولكن أيضًا اختيار النوع المناسب من البروكسي للمهمة. لاختبار حركة المرور المحمولة والتطبيقات التي تتحقق من السلوك من مشغلي الهاتف المحمول، ستكون بروكسيات المحمول مناسبة — فهي توفر IP من شبكات الهاتف الحقيقية وتقلل من فرص الوقوع تحت فلاتر أنظمة مكافحة الاحتيال.