Я пишу собственный сервис Incident Garden — on-call систему для дежурных девопсов и SRE. Одной из важнейших функций системы являются звонки на мобильные телефоны. Это позволяет разбудить дежурного хоть ночью и это гораздо надежней, чем пуши в ТГ или сообщения на почту.
Запросы к API через прокси на другом сервере
В качестве SIP-провайдера используется МТС Exolve и вот в один прекрасный момент запросы с нашей виртуальной машины в Яндекс.Облаке перестали проходить, хотя с других серверов API отвечал нормально, то есть проблема была именно в связанности сетей двух провайдеров.
В поддержке МТС проблему подтвердили и исправили, но на это ушло два дня, а звонки были нужны сразу. Пришлось городить решение для обхода буквально на коленке: поднял обычный forward-прокси на другом сервере, пустил на него запросы только к API, весь остальной трафик шел как раньше.
В статье расскажу про эксперименты с прокси и то как вы можете у себя это воспроизвести.
Идея
Нужен второй сервер с другим IP с развернутым прокси. В него отправляем только запросы к api.exolve.ru.
Открытую проксю выставлять в интернет было бы безумием, поэтому ограничим подключения только для одного клиента.
Прокси на базе Squid в Docker
Создайте на втором сервере файл squid.conf (укажите вместо api.exolve.ru любой ваш адрес):
|
1 2 3 4 5 6 |
http_port 3128 acl vm src <IP_КЛИЕНТА_ПРОКСИ> acl api dstdomain api.exolve.ru acl tls port 443 http_access allow vm CONNECT api tls http_access deny all |
Подробнее о каждой строке:
- http_port 3128 прокси слушает порт 3128 (значение по умолчанию, выставлено для наглядности).
- acl vm src <IP_КЛИЕНТА_ПРОКСИ> клиент, которому разрешено пользоваться прокси. Подставьте внешний IP вашей ВМ.
- acl api dstdomain api.exolve.ru единственный домен, куда можно ходить.
- acl tls port 443 порт назначения. Без него разрешённый домен открыл бы туннель на любой порт, например api.exolve.ru:8443.
- http_access allow vm CONNECT api tls разрешаем только туннель CONNECT от нашего клиента к целевому домену на 443 порт.
- http_access deny all всё остальное запрещено.
Запускаем контейнер:
|
1 2 3 |
docker run -d --name squid -p 3128:3128 \ -v $PWD/squid.conf:/etc/squid/squid.conf:ro \ ubuntu/squid:6.6-24.04_beta |
Проверить синтаксис конфига можно командой docker exec squid squid -k parse. Применить правки без перезапуска позволяет docker exec squid squid -k reconfigure.
Файрвол
Не стоит полагаться только на правила Squid по src, порт лучше закрыть и на сетевом уровне.
|
1 |
iptables -I DOCKER-USER -i <ВНЕШНИЙ_ИНТЕРФЕЙС> -p tcp --dport 3128 ! -s <IP_КЛИЕНТА_ПРОКСИ> -j DROP |
Если накосячили с правилами, то увидите следующую ошибку. Чужой клиент вместо 403 не получает ничего и ждёт до таймаута:
|
1 |
curl: (28) Connection timed out after 6000 milliseconds |
Создаваемые через iptables правила обычно не переживают перезагрузку сервера, сохраните их иными инструментами, например iptables-save.
Проверяем через curl
Попробуем проверить подключение с исходной виртуалки, для которой мы и строим этот прокси. Ошибки самого сервиса нас тут не волнуют. Если получим их, то как минимум будем уверены, что запрос доходит до конечного сервиса (это 7 уровень OSI). В противном случае получили бы ошибку сетевого уровня/транспортного:
|
1 |
curl -sS -x http://<IP_ПРОКСИ>:3128 -X POST https://api.exolve.ru/call/v1/MakeCall -d '{}' |
Ответ API:
|
1 |
{"error": "error while trying to get token from header"} |
Теперь запрещённый вызов (другой домен через тот же прокси, вспомните список разрешенных доменов в конфиге сквида выше):
|
1 |
curl -sS -x http://<IP_ПРОКСИ>:3128 https://ya.ru/ |
Ожидаемый ответ и уже от прокси:
|
1 |
curl: (7) CONNECT tunnel failed, response 403 |
Тот же текст получит и чужой клиент, пытающийся обратиться даже к api.exolve.ru. Простой http без туннеля тоже отклоняется (последняя строка лога).
Наблюдать происходящее нужно в логе внутри контейнера (да, вот такой вот бред), docker logs покажет только запуск:
|
1 |
docker exec squid tail -f /var/log/squid/access.log |
Так выглядит лог (видны все случаи: разрешённый, чужой домен, чужой клиент и простой http без туннеля):
|
1 2 3 4 |
1791634432.470 335 172.30.0.10 TCP_TUNNEL/200 3659 CONNECT api.exolve.ru:443 - HIER_DIRECT/80.75.132.6 - 1791634432.547 0 172.30.0.10 TCP_DENIED/403 3382 CONNECT ya.ru:443 - HIER_NONE/- text/html 1791634432.606 0 172.30.0.11 TCP_DENIED/403 3398 CONNECT api.exolve.ru:443 - HIER_NONE/- text/html 1791634445.375 0 172.30.0.10 TCP_DENIED/403 3394 GET http://api.exolve.ru/ - HIER_NONE/- text/html |
Первый столбец это unix-время, третий — IP клиента. TCP_TUNNEL/200 — запрос прошёл, а TCP_DENIED/403 — прокси его отклонил.
Подключаем к сервису
Простейший способ это выставить переменную HTTPS_PROXY, но есть нюанс. Дело в том, что она действует на все HTTPS-запросы процесса, а не только на звонки. Любой другой исходящий вызов тоже пойдёт в прокси и упрётся в наш белый список:
|
1 |
HTTPS_PROXY=http://<IP_ПРОКСИ>:3128 curl -sS https://ya.ru/ |
Ответ прокси:
|
1 |
curl: (7) CONNECT tunnel failed, response 403 |
Например в Go такая переменная читается стандартным клиентом (http.DefaultTransport использует ProxyFromEnvironment). Всё, что в сервисе ходит через http.Get и другие клиенты по умолчанию, сломается. Исключения можно перечислить в NO_PROXY, но это путь в никуда.
Надёжнее прописать прокси только в том HTTP-клиенте, который общается с целевым сервисом и чтобы он сам управлял маршрутом. Например на том же Go это будет выглядеть вот так:
|
1 2 3 4 |
u, _ := url.Parse("http://<IP_ПРОКСИ>:3128") t := http.DefaultTransport.(*http.Transport).Clone() t.Proxy = http.ProxyURL(u) exolve := &http.Client{Transport: t, Timeout: 10 * time.Second} |
Запросы к Exolve отправляем через exolve, остальной код по-прежнему пользуется http.DefaultClient и ходит напрямую. Clone() сохраняет таймауты и пул соединений стандартного транспорта, а Proxy подменяет только адрес прокси.
Если IP сервиса сменится, а прокси защищён только правилами Squid, клиент получит:
|
1 |
Get "https://api.exolve.ru/": Forbidden |
С правилом файрвола запрос повиснет до таймаута клиента, поэтому таймаут в клиенте обязателен:
|
1 |
Get "https://api.exolve.ru/": context deadline exceeded (Client.Timeout exceeded while awaiting headers) |
Вместо вывода
Для меня было большим сюрпризом, что между двумя огромными провайдерами могут быть такие проблемы с сетевой связанностью и что для их устранения может потребоваться почти двое суток.
С тех пор для подобных случаев держу под боком сценарий с прокси.






