TPROXY: як не дати DNS пройти повз gateway ⚙️
Для перехоплення DNS я використовую окремі ланцюжки iptables ⚙️.
Логіка проста:
1. Ловимо UDP-пакети на порт 53 від тестової підмережі.
2. Запити до стандартних resolver відправляємо на локальний DoH relay.
3. Усе інше перехоплюємо через TPROXY і передаємо у UDP-to-TCP relay 🛡.
Схематично 🔍:
Після цього треба ще додати ip rule і локальний route для позначених пакетів — без цього TPROXY не доставить їх у потрібний socket ✅.
Повний розбір із кодом, схемою та перевірками вже виклав у статті 📄:
https://devdays.net.ua/articles/vitik-dns-abo-buduiemo-velosiped/
Наступною частиною покажу саме проксіювання TCP-трафіку через gateway 🚀 💻
Для перехоплення DNS я використовую окремі ланцюжки iptables ⚙️.
Логіка проста:
1. Ловимо UDP-пакети на порт 53 від тестової підмережі.
2. Запити до стандартних resolver відправляємо на локальний DoH relay.
3. Усе інше перехоплюємо через TPROXY і передаємо у UDP-to-TCP relay 🛡.
Схематично 🔍:
# Стандартний DNS → DoH relay
iptables -t nat -A TG_DNS_DOH \
+ -p udp -d 1.1.1.1 --dport 53 \
+ -j REDIRECT --to-ports 5053
# Інший DNS → UDP-to-TCP relay
iptables -t mangle -A TG_DNS_TPROXY \
+ -p udp --dport 53 \
+ -j TPROXY --on-ip 127.0.0.1 --on-port 5353 \
+ --tproxy-mark 0x1/0x1
Після цього треба ще додати ip rule і локальний route для позначених пакетів — без цього TPROXY не доставить їх у потрібний socket ✅.
Повний розбір із кодом, схемою та перевірками вже виклав у статті 📄:
https://devdays.net.ua/articles/vitik-dns-abo-buduiemo-velosiped/
Наступною частиною покажу саме проксіювання TCP-трафіку через gateway 🚀 💻
Дійшов до звичайного TCP-трафіку 🌐
З DNS розібрались, а отже настав час проксувати все інше ⚙️
Спочатку хотів налаштовувати проксі в кожній окремій тулі. Але швидко зрозумів, що це шлях у нікуди 😬
Одна програма підтримує SOCKS, інша — тільки HTTP, а третя взагалі вирішить піти напряму. Стежити за кожною окремо мені не хотілось.
Тому логіку виніс на gateway:
Робоча VM просто відкриває TCP-з’єднання. Вона не знає, який проксі зараз живий і через кого насправді піде трафік. Усю цю кухню бере на себе Transparent Gateway 🛡
Наступним кроком треба було зробити так, щоб VM фізично не могла обійти gateway.
Посиланя на гітхаб проекту додав нижче, якщо комусь цікаво розібратись як все працює під капотом, або прийняти участь у рощзробці.
https://github.com/Va1b0rt/TransparentGateway
З DNS розібрались, а отже настав час проксувати все інше ⚙️
Спочатку хотів налаштовувати проксі в кожній окремій тулі. Але швидко зрозумів, що це шлях у нікуди 😬
Одна програма підтримує SOCKS, інша — тільки HTTP, а третя взагалі вирішить піти напряму. Стежити за кожною окремо мені не хотілось.
Тому логіку виніс на gateway:
VM → iptables → redsocks → HAProxy → proxy → host
Робоча VM просто відкриває TCP-з’єднання. Вона не знає, який проксі зараз живий і через кого насправді піде трафік. Усю цю кухню бере на себе Transparent Gateway 🛡
Наступним кроком треба було зробити так, щоб VM фізично не могла обійти gateway.
Посиланя на гітхаб проекту додав нижче, якщо комусь цікаво розібратись як все працює під капотом, або прийняти участь у рощзробці.
https://github.com/Va1b0rt/TransparentGateway
GitHub
GitHub - Va1b0rt/TransparentGateway
Contribute to Va1b0rt/TransparentGateway development by creating an account on GitHub.
Як я ізолював тестову VM 🔒
Для нормального тесту недостатньо просто вказати gateway у налаштуваннях 🤔
Якщо в машини залишиться інший маршрут, у якийсь момент трафік може піти повз нашу конструкцію. Тоді всі перевірки втрачають сенс.
Тому між Kali VM і gateway я прокинув окремий VLAN:
Management-інтерфейси залишив у мережі 192.168.88.0/24, а тестовий трафік відправив у VLAN 90 🌐
Додатково обмежив прямий вихід через firewall гіпервізора. Тепер у клієнта фактично один шлях — через 192.168.90.1.
Мережу підготували. Далі треба зловити TCP-пакети й передати їх у redsocks 🔀
Для нормального тесту недостатньо просто вказати gateway у налаштуваннях 🤔
Якщо в машини залишиться інший маршрут, у якийсь момент трафік може піти повз нашу конструкцію. Тоді всі перевірки втрачають сенс.
Тому між Kali VM і gateway я прокинув окремий VLAN:
Kali VM: 192.168.90.10/24
Gateway VM: 192.168.90.1/24
Default via 192.168.90.1
Management-інтерфейси залишив у мережі 192.168.88.0/24, а тестовий трафік відправив у VLAN 90 🌐
Додатково обмежив прямий вихід через firewall гіпервізора. Тепер у клієнта фактично один шлях — через 192.168.90.1.
Мережу підготували. Далі треба зловити TCP-пакети й передати їх у redsocks 🔀
🤔1
Ловимо TCP через iptables 🔀
На gateway TCP-трафік із тестової підмережі ловить окремий ланцюжок iptables ⚙️
Правила вийшли доволі простими:
Клієнт думає, що підключається до звичайного сайту, а gateway непомітно завертає з’єднання на локальний порт 12345. Там уже слухає redsocks.
У моєму лабораторному стенді я ловлю весь TCP. Для production перед REDIRECT краще додати винятки для локальних мереж, management-адрес та іншого трафіку, який проксіювати не потрібно ⚠️
Далі покажу, навіщо в цій схемі redsocks.
На gateway TCP-трафік із тестової підмережі ловить окремий ланцюжок iptables ⚙️
Правила вийшли доволі простими:
iptables -t nat -N TG_TCP
iptables -t nat -A PREROUTING \
-s 192.168.90.0/24 \
-p tcp \
-j TG_TCP
iptables -t nat -A TG_TCP \
-p tcp \
-j REDIRECT \
--to-ports 12345
Клієнт думає, що підключається до звичайного сайту, а gateway непомітно завертає з’єднання на локальний порт 12345. Там уже слухає redsocks.
У моєму лабораторному стенді я ловлю весь TCP. Для production перед REDIRECT краще додати винятки для локальних мереж, management-адрес та іншого трафіку, який проксіювати не потрібно ⚠️
Далі покажу, навіщо в цій схемі redsocks.
Що насправді робить redsocks 🌉
Спочатку назва transparent proxy звучала для мене складніше, ніж воно є насправді 😅
У цій схемі redsocks виконує одну задачу: приймає TCP-з’єднання, яке віддав iptables, і відкриває його через наступний рівень проксі-інфраструктури.
Відповідь повертається тим самим ланцюжком назад до клієнта 🔄
Тобто redsocks тут — звичайний міст. Він не вибирає найкращий проксі й не стежить, хто зараз живий. І це добре: кожна частина конструкції робить тільки свою роботу.
Лінукс стайл 😜.
А вибір конкретного proxy backend я віддав HAProxy ⚖️
Спочатку назва transparent proxy звучала для мене складніше, ніж воно є насправді 😅
У цій схемі redsocks виконує одну задачу: приймає TCP-з’єднання, яке віддав iptables, і відкриває його через наступний рівень проксі-інфраструктури.
client socket
→ REDIRECT :12345
→ redsocks
→ HAProxy
→ upstream proxy
→ destination
Відповідь повертається тим самим ланцюжком назад до клієнта 🔄
Тобто redsocks тут — звичайний міст. Він не вибирає найкращий проксі й не стежить, хто зараз живий. І це добре: кожна частина конструкції робить тільки свою роботу.
Лінукс стайл 😜.
А вибір конкретного proxy backend я віддав HAProxy ⚖️
🔥1
Навіщо мені тут HAProxy ⚖️
Підключити redsocks до одного проксі нескладно. Проблеми починаються, коли проксі стає багато 🤔
Я не хочу вручну перевіряти, хто зараз відповідає, міняти адресу в конфігу й перезапускати все після кожного падіння.
Тому між redsocks і пулом поставив HAProxy. Він:
• розподіляє нові з’єднання;
• перевіряє backend через health check;
• прибирає з роботи ті, що перестали відповідати.
Для перемикання режимів написав невелику REST API-обгортку над HAProxy Runtime API. Зміни можна застосовувати на льоту, без перезапуску балансувальника ⚙️
API залишив у management-мережі, а операції зміни режиму закрив Bearer-токеном 🔐
Для тестів підняв вісім легких LXC-контейнерів. Далі подивимось, як між ними розлітаються запити.
Підключити redsocks до одного проксі нескладно. Проблеми починаються, коли проксі стає багато 🤔
Я не хочу вручну перевіряти, хто зараз відповідає, міняти адресу в конфігу й перезапускати все після кожного падіння.
Тому між redsocks і пулом поставив HAProxy. Він:
• розподіляє нові з’єднання;
• перевіряє backend через health check;
• прибирає з роботи ті, що перестали відповідати.
Для перемикання режимів написав невелику REST API-обгортку над HAProxy Runtime API. Зміни можна застосовувати на льоту, без перезапуску балансувальника ⚙️
API залишив у management-мережі, а операції зміни режиму закрив Bearer-токеном 🔐
Для тестів підняв вісім легких LXC-контейнерів. Далі подивимось, як між ними розлітаються запити.
Roundrobin у дії 🔁
Почав із найпростішого режиму — roundrobin.
Логіка очевидна: нове TCP-з’єднання віддаємо наступному доступному proxy backend. Дійшли до кінця списку — починаємо спочатку 🔄
Для перевірки підняв echo server на 10.10.10.2:8000 і відправив до нього десять запитів через gateway.
Спочатку логи здались трохи хаотичними, бо записи з різних контейнерів виводились не по порядку 😶🌫️
Але після сортування за timestamp усе стало на місце: контейнери проходять послідовно, а дев’ятий і десятий запити починають нове коло ✅
Є нюанс: HAProxy розподіляє саме з’єднання. Якщо програма тримає один keep-alive connection, запити всередині нього не будуть стрибати між backend.
А що робити, якщо нам навпаки потрібна стабільна IP-адреса? Тут з’являється stick-table 📌
Почав із найпростішого режиму — roundrobin.
Логіка очевидна: нове TCP-з’єднання віддаємо наступному доступному proxy backend. Дійшли до кінця списку — починаємо спочатку 🔄
Для перевірки підняв echo server на 10.10.10.2:8000 і відправив до нього десять запитів через gateway.
Спочатку логи здались трохи хаотичними, бо записи з різних контейнерів виводились не по порядку 😶🌫️
Але після сортування за timestamp усе стало на місце: контейнери проходять послідовно, а дев’ятий і десятий запити починають нове коло ✅
Є нюанс: HAProxy розподіляє саме з’єднання. Якщо програма тримає один keep-alive connection, запити всередині нього не будуть стрибати між backend.
А що робити, якщо нам навпаки потрібна стабільна IP-адреса? Тут з’являється stick-table 📌
Навіщо я додав stick-table 📌
Roundrobin добре розподіляє навантаження, але підходить не для кожного сценарію.
Уявімо, що програма робить декілька послідовних дій на одному сайті. Якщо між ними зміниться зовнішня IP-адреса, сесія може виглядати підозріло або просто зламатися 🤷♂️
Тому додав режим із прив’язкою клієнта до одного backend.
Важливий момент: stick-table — не алгоритм балансування. Це таблиця стану. Разом зі stick on src вона запам’ятовує, який backend HAProxy обрав для конкретної IP-адреси клієнта 🧠
Перемкнув режим через REST API і повторив десять запитів. Цього разу всі їх записав один контейнер — CT-302 ✅
Отже, прив’язка працює. Тепер треба було перевірити найважливіше: чи справді VM не може вийти напряму.
Roundrobin добре розподіляє навантаження, але підходить не для кожного сценарію.
Уявімо, що програма робить декілька послідовних дій на одному сайті. Якщо між ними зміниться зовнішня IP-адреса, сесія може виглядати підозріло або просто зламатися 🤷♂️
Тому додав режим із прив’язкою клієнта до одного backend.
Важливий момент: stick-table — не алгоритм балансування. Це таблиця стану. Разом зі stick on src вона запам’ятовує, який backend HAProxy обрав для конкретної IP-адреси клієнта 🧠
Перемкнув режим через REST API і повторив десять запитів. Цього разу всі їх записав один контейнер — CT-302 ✅
Отже, прив’язка працює. Тепер треба було перевірити найважливіше: чи справді VM не може вийти напряму.
🧑💻DevDays pinned «Маю цікаву ідею сервісу для проксірування трафіку 🚀💻. Детально розпишу у статті або статтях 📝, інформації для вивчення там немало, а тут опишу суть. А суть в тому, що помітив, що іноді DNS-трафік летить напряму до резолвера, незважаючи на те, що я намагаюся…»
Чи може VM обійти gateway? 🧪
Те, що curl показує адресу проксі, — добре. Але мені хотілось переконатись, що іншого маршруту справді немає 🔍
Спочатку запит до ifconfig.me повернув IP проксі-сервера. Потім я видалив default route:
Після цього curl уже не зміг виконати DNS-запит, а ping відповів Network is unreachable. Тобто запасного виходу в інтернет машина не знайшла ✅
Повернув маршрут тільки через тестовий gateway:
З firewall теж встиг трохи повоювати 😅 Спочатку OutputPolicy=DROP відрізав навіть керування через SSH. Для тестового стенда залишив явний DROP на небажаний вихід через management-інтерфейс.
У production я б усе ж залишав deny-by-default і окремо дозволяв потрібний службовий трафік 🛡
Те, що curl показує адресу проксі, — добре. Але мені хотілось переконатись, що іншого маршруту справді немає 🔍
Спочатку запит до ifconfig.me повернув IP проксі-сервера. Потім я видалив default route:
ip route del default
Після цього curl уже не зміг виконати DNS-запит, а ping відповів Network is unreachable. Тобто запасного виходу в інтернет машина не знайшла ✅
Повернув маршрут тільки через тестовий gateway:
ip route add default via 192.168.90.1 dev eth1
З firewall теж встиг трохи повоювати 😅 Спочатку OutputPolicy=DROP відрізав навіть керування через SSH. Для тестового стенда залишив явний DROP на небажаний вихід через management-інтерфейс.
У production я б усе ж залишав deny-by-default і окремо дозволяв потрібний службовий трафік 🛡
Вимикаю один проксі й повторюю тест 🩺
Остання перевірка — що буде, якщо один proxy backend перестане відповідати.
Вимкнув один із восьми тестових контейнерів і ще раз відправив десять запитів до echo server.
Усі десять повернули HTTP 200 ✅
Нових записів від вимкненого контейнера в логах не з’явилось. HAProxy продовжив віддавати з’єднання тільки доступним backend. Саме такої поведінки я й очікував.
Для лабораторного тесту цього достатньо. У production я б додатково читав статус DOWN через HAProxy Runtime API або сторінку статистики й підключив alerts 📊
І ще одне обмеження: ця гілка перехоплює TCP. Довільний UDP, зокрема QUIC/HTTP3 на UDP/443, через неї не проходить. DNS у моєму gateway обробляється окремо.
Повний розбір зі схемою та скріншотами залишу в наступному пості 🚀
Остання перевірка — що буде, якщо один proxy backend перестане відповідати.
Вимкнув один із восьми тестових контейнерів і ще раз відправив десять запитів до echo server.
Усі десять повернули HTTP 200 ✅
Нових записів від вимкненого контейнера в логах не з’явилось. HAProxy продовжив віддавати з’єднання тільки доступним backend. Саме такої поведінки я й очікував.
Для лабораторного тесту цього достатньо. У production я б додатково читав статус DOWN через HAProxy Runtime API або сторінку статистики й підключив alerts 📊
І ще одне обмеження: ця гілка перехоплює TCP. Довільний UDP, зокрема QUIC/HTTP3 на UDP/443, через неї не проходить. DNS у моєму gateway обробляється окремо.
Повний розбір зі схемою та скріншотами залишу в наступному пості 🚀
Transparent Gateway для TCP — повний розбір 🚀
Зібрав в одну статтю весь шлях від ізольованої VM до тесту падіння proxy backend ⚙️
Всередині:
• окремий VLAN і маршрут через gateway;
• перехоплення TCP через iptables;
• redsocks як міст до проксі;
• HAProxy, roundrobin і stick-table;
• REST API для перемикання режимів;
• перевірка вихідної IP-адреси;
• тест health check на восьми LXC-контейнерах.
Додав команди, схему й реальні скріншоти з тестового стенда 📄
https://devdays.net.ua/articles/prozore-proksiiuvannia-tcp-redsocks-haproxy/
Код проєкту тут:
https://github.com/Va1b0rt/TransparentGateway
Наступний крок — збирати проксі з дозволених джерел, перевіряти їх, ранжувати й уже після цього завантажувати в робочий пул. Там теж вистачає цікавих нюансів 😬
Зібрав в одну статтю весь шлях від ізольованої VM до тесту падіння proxy backend ⚙️
Всередині:
• окремий VLAN і маршрут через gateway;
• перехоплення TCP через iptables;
• redsocks як міст до проксі;
• HAProxy, roundrobin і stick-table;
• REST API для перемикання режимів;
• перевірка вихідної IP-адреси;
• тест health check на восьми LXC-контейнерах.
Додав команди, схему й реальні скріншоти з тестового стенда 📄
https://devdays.net.ua/articles/prozore-proksiiuvannia-tcp-redsocks-haproxy/
Код проєкту тут:
https://github.com/Va1b0rt/TransparentGateway
Наступний крок — збирати проксі з дозволених джерел, перевіряти їх, ранжувати й уже після цього завантажувати в робочий пул. Там теж вистачає цікавих нюансів 😬
З тестовими проксі було аж занадто добре 😅
Під час тестів я вручну додав вісім LXC у HAProxy. Вони стояли на своїх адресах, слухняно відповідали на health check і нікуди не зникали. Краса 😎
А потім я подумав про реальні проксі. Сьогодні адреса жива, за годину гальмує, завтра її вже немає. І спробуй вгадай, яка саме вирішить померти посеред роботи 🫣
Перевіряти це руками після кожного падіння? Не наш метод 😤. Буду автоматизувати.
Замість одного списку зібрав конвеєр:
Останні завершені цикли видно так:
На моєму стенді зараз виходить:
Це один агрегований HAProxy endpoint, тому
Далі пішов підключати джерела. Здавалося б, JSON він і є JSON. Ага, зараз 😅
Під час тестів я вручну додав вісім LXC у HAProxy. Вони стояли на своїх адресах, слухняно відповідали на health check і нікуди не зникали. Краса 😎
А потім я подумав про реальні проксі. Сьогодні адреса жива, за годину гальмує, завтра її вже немає. І спробуй вгадай, яка саме вирішить померти посеред роботи 🫣
Перевіряти це руками після кожного падіння? Не наш метод 😤. Буду автоматизувати.
Замість одного списку зібрав конвеєр:
source → fetcher → validator → ranker → active pool → gateway
Останні завершені цикли видно так:
docker compose logs --no-log-prefix --tail=20 \
fetcher validator ranker
На моєму стенді зараз виходить:
[fetcher] sources=1 candidates=1
[validator] healthy=1/1
[ranker] validated=1 reserve=1 active=1
Це один агрегований HAProxy endpoint, тому
1 → 1 → 1 тут нормально. Головне — три незалежні worker завершили свої цикли.Далі пішов підключати джерела. Здавалося б, JSON він і є JSON. Ага, зараз 😅
Хотів один connector для всіх JSON. Не вийшло 🧩
Першою думкою було зробити універсальний connector: даю йому URL, він читає JSON, усі задоволені 🚀
Перший реальний feed швидко пояснив, що мій оптимізм нічим не підкріплений 😅
Одне джерело повертає
Тому всередині залишив одну модель:
А різницю виніс у adapters. Джерела вмикаються явно:
Автоматично імпортувати всі Python-файли з каталогу теж не став. Випадково підкинутий файл не повинен раптом стати дозволеним мережевим джерелом 🔐
Fetcher збирає записи, прибирає дублікати й публікує
Наступним кроком треба було перевірити, чи вони взагалі проксі, а не просто красиві відкриті порти 🔍
Першою думкою було зробити універсальний connector: даю йому URL, він читає JSON, усі задоволені 🚀
Перший реальний feed швидко пояснив, що мій оптимізм нічим не підкріплений 😅
Одне джерело повертає
ip, інше host, протокол лежить то в type, то в protocol. Якщо додавати всі ці винятки у fetcher, через місяць там буде музей чужих форматів.Тому всередині залишив одну модель:
host, port, protocol, source, credential_ref, metadata
А різницю виніс у adapters. Джерела вмикаються явно:
{
"allowed_connector_types": [
"jsonl_inventory", "http_json_feed"
],
"active_connectors": [{
"type": "jsonl_inventory",
"name": "lab-lxc",
"path": "/inventory/upstreams.jsonl"
}]
}
Автоматично імпортувати всі Python-файли з каталогу теж не став. Випадково підкинутий файл не повинен раптом стати дозволеним мережевим джерелом 🔐
Fetcher збирає записи, прибирає дублікати й публікує
proxy_pool:raw:v1. Але raw — це ще не пул. Це список адрес, яким поки що ніхто не вірить.Наступним кроком треба було перевірити, чи вони взагалі проксі, а не просто красиві відкриті порти 🔍
Порт відкритий. Проксі живий? Не факт 🔌
Спочатку хотів перевіряти кандидатів через
Socket може прийняти з’єднання, а потім відмовити на HTTP CONNECT, зламати SOCKS handshake або просто не дістатися цільового сервера.
Тому validator проходить весь маршрут:
Щоб під час перевірки не покласти власний gateway, обмежив конкуренцію:
Semaphore тримає максимум 100 одночасних перевірок. Для сотень і тисяч адрес цього вистачає. Перед сотнями тисяч доведеться замінити
З proxy auth теж без магії. У snapshot лежить лише
Немає посилання або пароль неправильний — кандидат вилітає. Тихого fallback без авторизації немає.
Handshake пройшов. Уже можна радіти? Ще ні. Проксі може працювати й акуратно здавати мою адресу далі 🫣
Спочатку хотів перевіряти кандидатів через
nc -z. Порт відкрився — значить проксі живий. Логіка залізна. Майже 😅Socket може прийняти з’єднання, а потім відмовити на HTTP CONNECT, зламати SOCKS handshake або просто не дістатися цільового сервера.
Тому validator проходить весь маршрут:
candidate
→ HTTP CONNECT / SOCKS4 / SOCKS5 handshake
→ echo endpoint
→ HTTP status + JSON + latency
Щоб під час перевірки не покласти власний gateway, обмежив конкуренцію:
VALIDATION_CONCURRENCY=100
PROXY_VALIDATION_TIMEOUT_SECONDS=8
Semaphore тримає максимум 100 одночасних перевірок. Для сотень і тисяч адрес цього вистачає. Перед сотнями тисяч доведеться замінити
asyncio.gather() на bounded queue, інакше coroutine самі з’їдять пам’ять 🤷♂️З proxy auth теж без магії. У snapshot лежить лише
credential_ref, а secret дістається перед handshake:auth = credentials.get(candidate.credential_ref)
Немає посилання або пароль неправильний — кандидат вилітає. Тихого fallback без авторизації немає.
Handshake пройшов. Уже можна радіти? Ще ні. Проксі може працювати й акуратно здавати мою адресу далі 🫣
Проксі працює, але здає мою IP 🕵️
Після успішного handshake я майже записав кандидата в робочий пул. Але ж проксі може додати мою реальну адресу в
Тому validator спочатку йде до echo напряму й запам’ятовує адресу клієнта. Потім повторює запит через проксі.
Якщо direct IP знову видно як peer або вона з’явилась у
Для разової зовнішньої перевірки використовую:
Саме
У лабораторії в мене інший endpoint:
Він перевіряє HAProxy та контейнери 301–308, але нічого не доводить про публічний Internet. Для production потрібен ще власний HTTPS echo. Чужий безкоштовний сервіс сьогодні працює, а завтра вводить rate limit — і знову весело 🙃
Адресу отримали. Тепер можна ранжувати. І тут найшвидші кандидати підготували ще один сюрприз.
Після успішного handshake я майже записав кандидата в робочий пул. Але ж проксі може додати мою реальну адресу в
X-Forwarded-For і формально все одно повернути HTTP 200 😬Тому validator спочатку йде до echo напряму й запам’ятовує адресу клієнта. Потім повторює запит через проксі.
Якщо direct IP знову видно як peer або вона з’явилась у
Forwarded, X-Forwarded-For, X-Real-IP чи Client-IP — кандидат відхиляється.Для разової зовнішньої перевірки використовую:
ANONYMITY_CHECK_URL=https://httpbingo.org/get
Саме
/get, а не /ip. Поточному parser потрібні і origin, і об’єкт headers. Тому ipify та ifconfig.me сюди без доробки не підходять.У лабораторії в мене інший endpoint:
ANONYMITY_CHECK_URL=http://10.10.10.2:8000/
Він перевіряє HAProxy та контейнери 301–308, але нічого не доводить про публічний Internet. Для production потрібен ще власний HTTPS echo. Чужий безкоштовний сервіс сьогодні працює, а завтра вводить rate limit — і знову весело 🙃
Адресу отримали. Тепер можна ранжувати. І тут найшвидші кандидати підготували ще один сюрприз.
Десять IP в одному плащі 🧥
Відсортував кандидатів за latency, подивився на красиву верхівку списку й уже хотів радіти 🚀
Потім перевірив ASN. Виявилось, кілька найшвидших адрес сидять в одного провайдера й навіть в одній підмережі. На папері проксі різні, а впадуть дружною компанією 😅
Для цього зробив окремий demo:
Результат скорочено:
Повільніший AS2 піднявся між двома AS1 через ASN/subnet penalty. Це синтетичний тест: production snapshot він не чіпає, зате добре показує логіку ranker 🔍
Ще додав
З ранжуванням розібрався. Залишилось безпечно передати результат між worker. Один ключ у Redis? Ну звісно, ні 😬
Відсортував кандидатів за latency, подивився на красиву верхівку списку й уже хотів радіти 🚀
Потім перевірив ASN. Виявилось, кілька найшвидших адрес сидять в одного провайдера й навіть в одній підмережі. На папері проксі різні, а впадуть дружною компанією 😅
Для цього зробив окремий demo:
./scripts/run-diversity-ranking-demo.sh
Результат скорочено:
INPUT
proxy-1: ASN=AS1, latency=10ms
proxy-2: ASN=AS1, latency=11ms
proxy-3: ASN=AS2, latency=20ms
OUTPUT
1. proxy-1: ASN=AS1, score=92.909091
2. proxy-3: ASN=AS2, score=49.619048
3. proxy-2: ASN=AS1, score=28.444444
Повільніший AS2 піднявся між двома AS1 через ASN/subnet penalty. Це синтетичний тест: production snapshot він не чіпає, зате добре показує логіку ranker 🔍
Ще додав
MIN_SCORE_THRESHOLD. Якщо нормальних кандидатів лише 40, POOL_SIZE=150 не змушує добирати 110 трупів для красивої цифри. Порожнє місце в пулі краще за проксі, який не пройшов поріг.З ранжуванням розібрався. Залишилось безпечно передати результат між worker. Один ключ у Redis? Ну звісно, ні 😬
Один ключ у Redis? Ага, зараз 😅
Спочатку хотів тримати весь стан в одному Redis key. Fetcher записав список, validator його оновив, ranker відсортував. Просто ж 🤔
А потім два worker одночасно торкнуться цього ключа — і спробуй зрозумій, чий там зараз результат.
Тому розділив конвеєр на повні snapshots:
Перевірити поточний стан можна так:
На лабораторному стенді зараз:
Ці числа показують, що ключі містять повні списки. Чи належать вони одному циклу — окремо звіряю через
Запис теж зробив без проміжного стану: спочатку тимчасовий key, потім атомарний Redis
Active готовий. Тепер просто віддам його HAProxy й піду пити каву. Принаймні такий був план ☕️
Спочатку хотів тримати весь стан в одному Redis key. Fetcher записав список, validator його оновив, ranker відсортував. Просто ж 🤔
А потім два worker одночасно торкнуться цього ключа — і спробуй зрозумій, чий там зараз результат.
Тому розділив конвеєр на повні snapshots:
proxy_pool:raw:v1
proxy_pool:validated:v1
proxy_pool:reserve:v1
proxy_pool:active:v1
Перевірити поточний стан можна так:
./scripts/run-redis-rows.sh
На лабораторному стенді зараз:
proxy_pool:raw:v1: 1
proxy_pool:validated:v1: 1
proxy_pool:reserve:v1: 1
proxy_pool:active:v1: 1
transparent-gateway:validated:v1: 1
Ці числа показують, що ключі містять повні списки. Чи належать вони одному циклу — окремо звіряю через
generated_at, source_generated_at і logs.Запис теж зробив без проміжного стану: спочатку тимчасовий key, потім атомарний Redis
RENAME. Читач бачить старий або новий повний JSON, а не половину нового пулу 🛡Active готовий. Тепер просто віддам його HAProxy й піду пити каву. Принаймні такий був план ☕️
Віддав mixed pool у HAProxy. Майже 🚧
План здавався нормальним: ranker формує active pool, HAProxy підхоплює backend через config або Runtime API, я задоволений і йду далі 😎
Але пул змішаний: HTTP, HTTPS, SOCKS4, SOCKS5, ще й credentials подекуди є. HAProxy як TCP backend не виконає SOCKS handshake і не дістане пароль за
От і приїхали 😬
Між redsocks та upstream довелось додати protocol-aware relay:
Перед новим з’єднанням relay перевіряє active snapshot, але не частіше ніж раз на три секунди. Потім вибирає кандидата, виконує правильний handshake і тільки тоді підставляє credentials. Restart або reload не потрібен ⚙️
Є чесне обмеження: у живому active pool поки один агрегований HAProxy endpoint. Тому показати реальний cooldown між двома direct candidates на цьому стенді я не можу. Логіка покрита unit-тестами, а HAProxy failover окремо перевірений на восьми LXC.
Отака вийшла проста задача «автоматично оновлювати список проксі» 😅
У наступному пості зберу код, тести й повний матеріал.
План здавався нормальним: ranker формує active pool, HAProxy підхоплює backend через config або Runtime API, я задоволений і йду далі 😎
Але пул змішаний: HTTP, HTTPS, SOCKS4, SOCKS5, ще й credentials подекуди є. HAProxy як TCP backend не виконає SOCKS handshake і не дістане пароль за
credential_ref.От і приїхали 😬
Між redsocks та upstream довелось додати protocol-aware relay:
VM → iptables → redsocks :12345
→ egress_relay :12000
→ HTTP / HTTPS / SOCKS4 / SOCKS5 upstream
→ destination
Перед новим з’єднанням relay перевіряє active snapshot, але не частіше ніж раз на три секунди. Потім вибирає кандидата, виконує правильний handshake і тільки тоді підставляє credentials. Restart або reload не потрібен ⚙️
Є чесне обмеження: у живому active pool поки один агрегований HAProxy endpoint. Тому показати реальний cooldown між двома direct candidates на цьому стенді я не можу. Логіка покрита unit-тестами, а HAProxy failover окремо перевірений на восьми LXC.
Отака вийшла проста задача «автоматично оновлювати список проксі» 😅
У наступному пості зберу код, тести й повний матеріал.
Автоматичний пул проксі — код, тести й граблі 🚀
Зібрав увесь шлях в одну статтю: від першого JSON до active pool, який gateway підхоплює без рестарту. Інформації вийшло нормально так 😅
Перед запуском стенда перевірив протокольну логіку окремо:
Результат:
Усередині статті:
• connector adapters;
• HTTP, HTTPS, SOCKS4 і SOCKS5 validation;
• authenticated proxy без secrets у Redis;
• перевірка витоку IP;
• ASN diversity та quality threshold;
• атомарні snapshots;
• глухий кут із HAProxy;
• protocol-aware egress relay;
• шість практичних перевірок.
Обмеження теж не ховав: стенд поки має один агрегований HAProxy endpoint, для реальної різноманітності потрібні direct candidates, а для постійної зовнішньої перевірки — власний HTTPS echo.
Стаття, до якої вела ця серія:
https://devdays.net.ua/articles/avtomatychnyi-pul-proksi-zbir-validatsiia-ranzhuvannia/
Код:
https://github.com/Va1b0rt/TransparentGateway
Далі буду відучувати gateway від ручного налаштування однієї VM: Proxmox, cloud-init і deployment. Там теж без граблів не обійдеться ⚙️😬
Зібрав увесь шлях в одну статтю: від першого JSON до active pool, який gateway підхоплює без рестарту. Інформації вийшло нормально так 😅
Перед запуском стенда перевірив протокольну логіку окремо:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements-dev.txt
python -m pytest -q
Результат:
............................ [100%]
28 passed
Усередині статті:
• connector adapters;
• HTTP, HTTPS, SOCKS4 і SOCKS5 validation;
• authenticated proxy без secrets у Redis;
• перевірка витоку IP;
• ASN diversity та quality threshold;
• атомарні snapshots;
• глухий кут із HAProxy;
• protocol-aware egress relay;
• шість практичних перевірок.
Обмеження теж не ховав: стенд поки має один агрегований HAProxy endpoint, для реальної різноманітності потрібні direct candidates, а для постійної зовнішньої перевірки — власний HTTPS echo.
Стаття, до якої вела ця серія:
https://devdays.net.ua/articles/avtomatychnyi-pul-proksi-zbir-validatsiia-ranzhuvannia/
Код:
https://github.com/Va1b0rt/TransparentGateway
Далі буду відучувати gateway від ручного налаштування однієї VM: Proxmox, cloud-init і deployment. Там теж без граблів не обійдеться ⚙️😬
❤1
Gateway працює. А повторити його я зможу? 🤔
Transparent Gateway уже маршрутизує DNS і TCP, оновлює active pool та переживає перезапуск контейнерів. На схемі — майже production. На диску — кілька днів моїх ручних пригод 😅
Поки VM жива, різниці не видно. Але якщо диск завтра вирішить піти у відпустку, чи вдасться відтворити той самий gateway на чистій машині без історії shell і магічного "десь я це вже правив"?
Почав із простого аудиту на gateway: перевірив середовище, стан cloud-init, версію Debian і запущені сервіси.
Ключові рядки виглядали так:
Docker також показав усі шість сервісів запущеними. Здавалося б, де проблема?
Проблема в одному слові:
Transparent Gateway уже маршрутизує DNS і TCP, оновлює active pool та переживає перезапуск контейнерів. На схемі — майже production. На диску — кілька днів моїх ручних пригод 😅
Поки VM жива, різниці не видно. Але якщо диск завтра вирішить піти у відпустку, чи вдасться відтворити той самий gateway на чистій машині без історії shell і магічного "десь я це вже правив"?
Почав із простого аудиту на gateway: перевірив середовище, стан cloud-init, версію Debian і запущені сервіси.
systemd-detect-virt
cloud-init status
. /etc/os-release; echo "$PRETTY_NAME"
docker compose ps
Ключові рядки виглядали так:
kvm
status: done
Debian GNU/Linux 13 (trixie)
Docker також показав усі шість сервісів запущеними. Здавалося б, де проблема?
Проблема в одному слові:
done стосується cloud-init, а не всього gateway. VM створена автоматично, застосунок — ще ні. Далі пішов шукати, де саме закінчується автоматизація 🔍❤1