Привет,Хабр! Меня зовут Сергей, я инженер отдела поддержки, но эксплуатация у нас работает так себе, поэтому чаще мы что то делаем сами. Решил рассказать о том, как мы решали проблему прокси в корпоративной сети. Тема для нашей команды болезненная, но интересная: мы прошли путь от ручного редактирования /etc/environment до создания собственного сервиса, который живёт в systemd и умеет валидировать конфигурации, делать бэкапы и даже стучаться в MATE с Cinnamon.

Сегодня расскажу, почему мы свернули с пути Ansible, почему не стали внедряться в процессы через gdb (спойлер: это был плохой сон) и как в итоге получили систему, которая позволяет нам забыть про прокси как про проблему. И да, мы знаем о WPAD, оно тоже не заработало.

У самурая нет цели, есть только путь от /etc/environment до systemd

Всё началось с масштабного проекта импортозамещения. Нам предстояло заменить 500+ рабочих станций с Windows на РЕД ОС. План был амбициозный: около 50 машин в месяц. Был развернут PXE и настроен практически стандартный конфиг для Ред ОС.

Файл /etc/environment используется модулем pam_env. Он не привязан к командным оболочкам, поэтому скрипты или glob-выражения использовать здесь нельзя (кстати, мы этого не знали :-) ). Здесь можно указывать только пары имя=значение.

Типичный /etc/environment выглядит так:

http_proxy=http://proxy.corp.company.ru:8082
https_proxy=http://proxy.corp.company.ru:8082
no_proxy=localhost,127.0.0.1,*.corp.company.ru

Еще есть /etc/profile который устанавливает переменные только в оболочках входа (когда пользователь выполняет вход в систему). В нём можно запускать скрипты (например, запускаются скрипты из каталога /etc/profile.d/) и он может использоваться в оболочках.

Вначале все шло хорошо, но через несколько месяцев поменялся состав отдела сети, и оказалось, что все внутренние ресурсы необходимо заносить в исключение. Не будем обсуждать как раньше все работало, но раз старшие товарищи сказали, то мы делаем.

Естественно вначале ни о какой автоматизации у нас не шло и речи.

Далее все персонажи и события вымышлены, любые совпадения случайны.

Вася (глядя на 20-е рабочее место с необходимостью внести изменение): «Слушай, может, ну его? Пусть без интернета сидят, продуктивнее работать будут!»

Я: «Вася, ты же сам на прошлой неделе жаловался, что без интернета не можешь найти решение своей проблемы на StackOverflow».

Вася: «Это другое! Мне нужно для работы. А бухгалтеру зачем интернет?»

Спорить с Васей бесполезно. Но проблема была очевидна: ручная настройка на 500 машин — это не просто боль, это клиническая смерть. Причём боль физическая (пальцы устают печатать) и моральная (каждый раз объяснять, почему без прокси не работает 1С).

Вася придумал, что нам в системе нужен еще один файл /etc/profile.d/proxyhost.sh в котором будем записывать все переменные в формате:

LOCAL=127.0.0.1,localhost,::1
CORP_IP=10.51.0.1,10.52.10.25
CORP_DOMAINS=*.corp.company.ru
ANY_HOSTS=*.anycompany.ru

а /etc/environment и /etc/profile.d/proxy.sh собирают из него данные

PROXYNAME="http://proxy.corp.company.ru:8082"
NO_PROXY="${LOCAL},${CORP_IP},${CORP_DOMAINS},${ANY_HOSTS}"
http_proxy="$PROXYNAME"
https_proxy="$PROXYNAME"
no_proxy="$NO_PROXY"
HTTP_PROXY="$PROXYNAME"
HTTPS_PROXY="$PROXYNAME"
NO_PROXY="$NO_PROXY"

И вот тут мы узнали, что не все так просто. Часть исключений не работают именно по причине /etc/environment.

Смотря какой UX, смотря сколько фреймворков

«Я слышал, есть такая штука — Ansible!» — гордо заявил Петя, который пришёл в команду как раз под этот проект импортозамещения.

Вася: «Ты про того клоуна из мультика?»

Петя: «Нет, это такая программа, которая сама всё настраивает!»

Мы дали Пете шанс, и он написал playbook.

# # playbooks/proxy-config.yml
---
- name: "Магия настройки прокси (почти как у Гарри Поттера)"
  hosts: all
  become: yes
  vars:
    proxy_server: "http://proxy.corp.company.ru:8082"
    no_proxy_list:
      - "localhost"
      - "127.0.0.1"
      - "*.corp.company.ru"
      - "10.0.0.0/8"
  tasks:
    - name: "Создаём конфиг (надеемся, что он кому-то нужен)"
      template:
        src: templates/proxyhost.conf.j2
        dest: /etc/profile.d/proxyhost.sh
        mode: '0644'
    - name: "Правим /etc/environment (молимся, чтобы ничего не сломать)"
      lineinfile:
        path: /etc/environment
        regexp: "^{{ item.key }}="
        line: "{{ item.key }}={{ item.value }}"
      loop:
        - { key: "http_proxy", value: "{{ proxy_server }}" }
        - { key: "https_proxy", value: "{{ proxy_server }}" }
        - { key: "no_proxy", value: "{{ no_proxy_list | join(',') }}" }

Вася (прочитав playbook): "А где тут проверка на дурака? Что, если я опечатаюсь в адресе?"

Петя: "А зачем опечатываться?"

Я: "Вася, ты у нас главный пессимист. Но в чём-то прав..."

Ansible решил проблему развертывания: теперь настройка 50 машин занимала не 3 часа, а 15 минут (пока Ansible думал, мы успевали выпить кофе). Все конфиги оказались в Git — можно было посмотреть, кто и когда что менял (и потом этого человека… наградить премией).

Но параллельно с ростом парка рос и список исключений. Новые сервисы, новые подсети, специфические хосты для разных отделов. В начале у нас было 10 хостов в no_proxy. Через месяц — 50. А ещё через три месяца мы уже хотели работать с подсетями и масками.

Добавление каждого нового исключения превращалось в ритуал: правим playbook, пушим в Git, ждём, пока Ansible достучится до всех машин, молимся, чтобы никто не выключил компьютер. И всё это — пока 100 машин уже введены в эксплуатацию, а новые продолжают прибывать.

Реальный кейс (он же «Почему мы возненавидели Ansible в 3 часа ночи»):

Звонит начальник отдела разработки: «Срочно! У нас новый GitLab, нужно добавить его в исключения прокси! Иначе разработчики бунтуют!»

Я (в 3 часа ночи): «Ща, минут 20…»

К 3:20 я:

  1. Открыл ноутбук

  2. Нашёл playbook

  3. Добавил gitlab.dev.company.ru в исключения

  4. Запушил в Git

  5. Запустил Ansible на 500 машинах

  6. Понял, что треть машин офлайн

  7. Отправил письмо: «Кто не выключил компьютер — получат обновления утром»

  8. Не выспался

Утром разработчики всё равно бунтовали, потому что GitLab всё равно не работал (оказывается, нужно было ещё и DNS прописать).

Вася (злорадствуя): «А я говорил! Ansible — это не серебряная пуля. Это просто автоматизированный способ делать то же самое, только быстрее и с большим количеством поломок!»

Левый — коронный, правый — Winbindовый

«А может, сделать так, чтобы конфиг сам подтягивался?» — предложил Петя, начитавшись статей про DevOps.

«Как это — сам? Он что, умный?» — недоверчиво спросил Вася.

«Ну, например, каждые 30 минут!» — Петя уже открывал редактор.

Так появился первый cron-скрипт, который каждые полчаса качал файл с сервера и раскладывал по машинам.

# /etc/cron.d/proxy-update
*/30 * * * * root /usr/local/bin/update-proxy-config.sh

Сам скрипт загрузки:

#!/bin/bash
# /usr/local/bin/update-proxy-config.sh

CONFIG_URL="http://repo.company.ru/proxy/proxyhost.conf"
CONFIG_FILE="/etc/proxy/proxyhost.conf"

# Загрузка новой конфигурации
curl -s -f -o "$CONFIG_FILE.tmp" "$CONFIG_URL" && \
    cp "$CONFIG_FILE.tmp" "$CONFIG_FILE" && \
    rm -f "$CONFIG_FILE.tmp"

# Применение
source /etc/profile.d/proxy.sh

Преимущества

  • Автоматическое обновление: каждые 30 минут

  • Централизованный источник: один файл на сервере

  • Гибкость: изменения применяются без Ansible

Работало. Но параллельно с ростом парка и усложнением конфигурации вылезла новая проблема.

Реальный кейс (он же «И снова Петя»):

Петя решил «оптимизировать» файл конфигурации на сервере, случайно удалив оттуда всё содержимое. Cron-скрипт послушно разнёс пустой файл на 200 машин. Интернет пропал везде и сразу. В 10 утра.

Мы потратили полдня, чтобы вручную восстановить конфигурацию на ключевых машинах. Так мы получили ценный опыт: автоматизация без валидации — это не ускоритель, а ускоритель катастрофы.

Следующую версию мы писали уже с человеческим лицом. Добавили проверки: не пустая ли строка, есть ли там http://, не забыли ли localhost в исключениях.

Рождение "убийцы прокси" (версия 1.0)

#!/bin/bash
# /usr/local/bin/update-proxy-config.sh
# Скрипт, который мы назвали "Монолит", но потом переименовали в "Плачущий админ"

CONFIG_URL="http://repo.company.ru/proxy/proxyhost.conf"
CONFIG_FILE="/etc/proxy/proxyhost.conf"
BACKUP_DIR="/etc/proxy/backups"

# Создаём бэкап (на всякий случай, вдруг Вася опять что-то сломает)
cp "$CONFIG_FILE" "$BACKUP_DIR/proxyhost.conf.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true

# Скачиваем новую конфигурацию
curl -s -f -o "$CONFIG_FILE.tmp" "$CONFIG_URL"

if [ $? -eq 0 ]; then
    # Проверка на наличие пустого файла (бывало и такое)
    if [ -s "$CONFIG_FILE.tmp" ]; then
        mv "$CONFIG_FILE.tmp" "$CONFIG_FILE"
        echo "$(date): Конфигурация обновлена" >> /var/log/proxy-update.log
        
        # Применяем (надеемся)
        source /etc/profile.d/proxy.sh
    else
        echo "$(date): Получили пустой файл, не обновляем" >> /var/log/proxy-update.log
        rm -f "$CONFIG_FILE.tmp"
    fi
else
    echo "$(date): Не удалось скачать конфигурацию" >> /var/log/proxy-update.log
fi

Большой секрет автоматизации:

Волшебной таблетки тут нет. За магией автоматизации кроется ручная работа: нам пришлось выстроить систему валидации, которая проверяет синтаксис конфигурации, наличие обязательных переменных и корректность форматов

validate_config() {
    local file=$1
    
    # Проверка синтаксиса
    if ! bash -n "$file" 2>/dev/null; then
        echo "ERROR: Синтаксическая ошибка в конфигурации!"
        return 1
    fi
    
    # Загружаем и проверяем переменные
    source "$file"
    if [ -z "${PROXYNAME:-}" ]; then
        echo "ERROR: PROXYNAME не задана! Это как интернет без провайдера!"
        return 1
    fi
    
    # Проверка формата URL
    if ! [[ "${PROXYNAME:-}" =~ ^(http|https):// ]]; then
        echo "ERROR: Неверный формат прокси. Пример: http://proxy:8082"
        return 1
    fi
    
    # Проверка наличия localhost в исключениях
    NO_PROXY="${CORP_IP:-},${CORP_DOMAINS:-},${ANY_HOSTS:-}"
    if ! echo "$NO_PROXY" | grep -q "localhost"; then
        echo "WARNING: localhost не в исключениях! Будут проблемы с локальными сервисами."
    fi
    
    return 0
}

С тех пор ни одна битая конфигурация не ушла в прод. Но нам этого было мало.

Петя (снова пытается всё сломать, но уже безуспешно):

Решил «оптимизировать» конфигурацию, удалив localhost из исключений «потому что он и так работает».

Скрипт валидации выдал предупреждение, и мы получили:

WARNING: localhost не в исключениях! Будут проблемы с локальными сервисами.

Мы увидели, кто автор изменений (спасибо Git!), и откатили конфиг за 5 минут, а не за полдня, как в прошлый раз.

Вася (с удивлением): «Оно работает! Ты представляешь, оно работает!»

Что под капотом: архитектура финального решения

Вдохновившись утилитой join-to-domain от Ред Софт (не реклама), которая умеет работать по нескольким сценариям, мы захотели такой же гибкости. Наш прокси-сервис должен был одинаково хорошо работать и на серверах без графики, и на рабочих станциях с MATE или Cinnamon, и учитывать особенности разных приложений.

Что под капотом:

Сначала скрипт скачивает актуальную конфигурацию с центрального репозитория. Потом проверяет её синтаксис и корректность. Если всё хорошо — создаёт резервную копию старой конфигурации (мы храним последние 5 версий, чтобы можно было откатиться). И только затем обновляет файлы /etc/environment и /etc/profile.d/proxy.sh.

Но самое интересное начинается после:

# 1. Обновляем окружение systemd
systemctl set-environment http_proxy="$PROXYNAME" https_proxy="$PROXYNAME" no_proxy="$NO_PROXY"

# 2. Обновляем пользовательские сессии
for uid in $(awk -F: '$3>=1000 {print $3}' /etc/passwd); do
    user=$(id -nu $uid)
    sudo -u $user systemctl --user import-environment http_proxy https_proxy no_proxy
done

# 3. Уведомляем рабочие столы (да-да, и MATE, и Cinnamon)
dbus-send --type=signal / org.mate.SessionManager.Presence.StatusChanged &>/dev/null || true
dbus-send --type=signal / org.Cinnamon.SessionManager.Presence.StatusChanged &>/dev/null || true

# 4. Настраиваем Docker
cat > /etc/systemd/system/docker.service.d/proxy.conf << EOF
[Service]
Environment="HTTP_PROXY=$PROXYNAME"
Environment="HTTPS_PROXY=$PROXYNAME"
Environment="NO_PROXY=$NO_PROXY"
EOF
systemctl daemon-reload
# Перезапускаем, только если нет запущенных контейнеров
if ! docker ps -q 2>/dev/null | grep -q .; then
    systemctl restart docker
fi

Мы долго спорили, стоит ли пытаться обновлять переменные в уже запущенных процессах. В интернете советовали использовать gdb, чтобы «внедриться» в работающий bash и изменить его окружение. Это было бы красиво… и абсолютно безумно. gdb в боевом скрипте — верный способ устроить ад на пустом месте. Поэтому мы выбрали путь дзен: новые процессы получат новые настройки, старые пусть живут своей жизнью. Главное, что после перезапуска приложения или открытия нового терминала всё заработает как надо.

Мануал для админа: как забыть про мануалы, используя proxy-generator

Небольшой дисклеймер. Наш прокси-генератор — это хорошее решение для быстрого управления настройками прокси на клиентских машинах. Поставил — и забыл. Но если вам нужно что-то более глобальное, умеющее в централизованное управление политиками, инвентаризацию и интеграцию с доменом, то возможно необходимо иное решение. А для повседневных задач — добро пожаловать в мир автоматизации.

А теперь я расскажу вам, как избавиться от стопки мануалов и настроить прокси на всех устройствах с помощью простого советского нашего решения.

На сегодняшний день наша утилита состоит из трёх компонентов:

  • основной скрипт генерации (/usr/local/bin/proxy-generate-config.sh);

  • три systemd-юнита (сервис, таймер, path unit);

  • конфигурация logrotate для логов.

Шаг 2. Подготовьте центральный репозиторий

На внутреннем веб-сервере разместите файл конфигурации /proxy/proxyhost.conf примерно такого содержания:

PROXYNAME=http://proxy.corp.company.ru:8082
LOCAL=127.0.0.1,localhost,::1
CORP_IP=10.0.0.0/8,172.16.0.0/12
CORP_DOMAINS=*.corp.company.ru,*.dev.company.ru
ANY_HOSTS=gitlab.anycompany.ru,jira.anycompany.ru

Шаг 3. Запустите и забудьте

После установки сервис сам подхватит конфигурацию, запустит таймер и начнёт отслеживать изменения.

# Проверить статус
systemctl status proxy-generate-config.service

# Посмотреть логи
journalctl -u proxy-generate-config.service -f

# Если нужно применить вручную
/usr/local/bin/proxy-update-config

Теперь всё работает само. Вы меняете конфигурацию на сервере — и через 30 минут (или раньше, если система заметит изменения) настройки обновляются на всех машинах. Без беготни, без Ansible, без ручного перезапуска сессий.

Типовые ошибки и как с ними быть

Что-то пошло не по плану? Идём изучать журнал логов: /var/log/proxy/proxy-config.log.

Ошибка №1. Не удалось скачать конфигурацию

Проверьте, доступен ли ваш веб-сервер из сети:

curl -I http://repo.company.ru/proxy/proxyhost.conf

Если сервер не отвечает — возможно, проблема в DNS или в том, что вы забыли добавить его в исключения прокси. Да-да, бывает и такое: прокси не может скачать конфигурацию для самого себя, потому что сам себе же мешает.

Ошибка №2. Синтаксическая ошибка в конфигурации

Скрипт откатится к предыдущей рабочей версии, а в логах появится запись:

ERROR: Синтаксическая ошибка в файле конфигурации /etc/proxy/proxyhost.conf

Идите править файл на сервере. Резервная копия предыдущей версии уже ждёт вас в /etc/proxy/backups/.

Ошибка №3. Прокси не работает в браузере, но curl работает

Скорее всего, ваш рабочий стол (MATE или Cinnamon) использует собственные настройки прокси, которые не подхватывают переменные окружения. Наш сервис умеет стучаться в D-Bus и обновлять эти настройки, но если не помогло — зайдите в настройки сети и убедитесь, что там стоит «Использовать системные настройки».

Вместо заключения

Что в итоге

Кодовая база:

  • Основной скрипт: 300 строк на bash (вместе с валидацией и комментариями)

  • Systemd units: 3 файла (service, timer, path)

  • Logrotate конфиг: 1 файл

  • Общий размер: ~15 КБ

Функциональность:

✅ Автоматическое обновление каждые 30 минут
✅ Мгновенное применение при изменении файла (path unit)
✅ Валидация конфигурации перед применением
✅ Резервное копирование (5 последних версий)
✅ Логирование с ротацией (7 дней)
✅ Применение к systemd, PAM, shell
✅ Поддержка NetworkManager, Docker
✅ Уведомления D-Bus для MATE и Cinnamon

Вася (сидя за чашкой кофе после завершения проекта): «Слушай, а помнишь, как мы начинали? Вручную на 50 компьютерах, потом Ansible, потом этот… наш сервис.»

Я: «Помню. Руки до сих пор болят от тех первых партий.»

Вася: «А сейчас у нас 800+ машин, и мы вообще про прокси не думаем. Оно само как-то работает. И эти твои исключения… их стало 300 штук, но мы про них даже не вспоминаем.»

Петя (подходя к нам): «Ребята, я тут подумал… А может, добавим поддержку etcd? Или веб-интерфейс для управления исключениями?»

Вася и я (хором): «Петя, дай отдохнуть хоть неделю! Проект импортозамещения закрыли, 500 машин работают, прокси настроен. Всё. Отдыхай.»

Мы не претендуем на создание универсального решения для всех дистрибутивов. Но для нашей инфраструктуры, где теперь крутятся 800+ машин на РЕД ОС, прокси-генератор стал настоящим спасением. Теперь мы не бегаем с флешкой, не ждём, пока Ansible достучится до выключенных компьютеров, и не боимся ошибиться в IP-адресе. Конфигурация обновляется сама, логи пишутся в journald, а мы спим спокойно.

Самое важное, что мы поняли: автоматизация должна расти вместе с инфраструктурой. Нельзя один раз написать скрипт и забыть — нужно постоянно добавлять в него «защиту от дурака», валидацию и механизмы восстановления. Но когда это сделано — можно действительно забыть о проблеме и заняться чем-то более интересным.

Конечно, утилита в текущем виде — не финальная версия. Мы постоянно придумываем что-то новенькое: в планах интеграция с РЕД АДМ, но об этом в следующий раз.

Если вам интересно узнать подробности или вы хотите посмотреть полный исходный код — пишите в комментариях. Обязательно отвечу!