Linux, Docker, OpenVPN client and source-based routing

Автор: Admin | 30.03.2023

Linux, Docker, OpenVPN клиент и маршрутизация от источника

Условия

Имеется Linux сервер с веб приложением, работающее в Docker'е. Существует стороннее API на которое приложение шлет запросы. Этот API заблокирован из места расположения сервера. Надо сделать так, чтобы запросы доходили. Самым простым решением кажется использовать уже существующий OpenVPN, просто подключив к нему целевой сервер в качестве клиента, а не придумывать более сложные решения, типа использования прокси, обратного прокси или переноса сервера.

Задача

Есть как минимум два решения:

  • Можно и нужно добавить маршруты для конкретных адресов API, если есть уверенность в их статичности. Если же они могут меняться - на той стороне round-robin DNS или просто динамически меняющийся IP, то придется пускать через туннель весь трафик, поскольку адрес резолвится в момент добавления правил и не может учесть изменений на той стороне. Данный вариант не подходит.
  • При поднятии туннеля весь трафик направлять в него, перезаписывая шлюз по умолчанию в системе. Веб приложение как и прежде продолжает работать на своем интерфейсе. Клиенты приходя на него не получают ответа, поскольку он шлется в дефолт гетвей через туннель. Надо настроить маршрутизацию от источника, чтобы ответ отправлялся с того же интерфейса, откуда и пришел.

В широком смысле задача сводится к доступу до сервера по двум аплинкам. Не важно чем именно является второй - провайдером или туннелем.

Решение

Предосторожность

Перед выполнением скрипта или команд имеет смысл сделать роут до хоста с которого происходит коннект к серверу, чтобы не потерять соединение, если что-то пойдет не так:

ip route add z.z.z.z via $GW1

Где z.z.z.z - белый IP адрес с которого происходит подключение к серверу, можно узнать выполнив запрос curl http://ipecho.net/plain

Переменные

IF - имя интерфейса. В конфиге OpenVPN клиента стоит указать конкретный номер интерфейса dev tun0, чтобы он не мог измениться.
IP - IP адрес на этом интерфейсе.
NET - Подсеть к которой принадлежит IP. В целом, если за интерфейсом нет подсети которую надо маршрутизировать, нужно указать /32.
GW - Адрес шлюза для интерфейса.
T - Имя таблицы, произвольное понятное имя. Можно оперировать просто цифрами, но так удобнее.

Создание кастомных таблиц

Создать две таблицы маршрутизации, тут они только объявляются и не влияют на маршрутизация пока не будут заполнены, в конец файла /etc/iproute2/rt_tables добавив две строки:

1 Ethernet
2 OpenVPN

Число - это номер таблицы и далее ее название. Номер может принимать значения от 1 до 252, 253-255 зарезервированы. Основная (дефолтная) таблица main имеет номер 254. Чем меньше число, тем выше приоритет, т.е. когда пакет совпадет с правилом описанным в таблице, то будет отправлен в нее и не дойдет до дефолтной. Если же пакет не попадет ни в одно из правил, описанных в таблицах 1-252 то отправится в дефолтную. Подробнее в документации

Маршрутизация от источника (source-based routing)

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

Маршрут к шлюзу для Ethernet подсети:

ip route add $NET1 dev $IF1 src $IP1 table $T1
ip route add $NET2 dev $IF2 src $IP2 table $T2

Маршрут по-умолчанию через этот шлюз в свою таблицу:

ip route add default via $GW1 table $T1
ip route add default via $GW2 table $T2

В main таблицу добавить роут, что от если пакет идет от определенной подсети через определенный интерфейс, то он должен отправляться с конкретного IP адреса:

ip route add $NET1 dev $IF1 src $IP1
ip route add $NET2 dev $IF2 src $IP2

Правило маршрутизации от каждого источника через свою таблицу:

ip rule add from $IP1 table $T1
ip rule add from $IP2 table $T2

И это работает, если целью является слушаемый в системе порт, но не работает когда этот порт слушается в Докере, поскольку у него отдельная подсеть маршрутизируемая в дефолт гетвей, идущий в туннель.

Маркировка трафика

Пометить входящие соединения в зависимости от интерфейса на который они пришли:

iptables -t mangle -A PREROUTING -m conntrack --ctstate NEW -i $IF1 -j CONNMARK --set-mark 1
iptables -t mangle -A PREROUTING -m conntrack --ctstate NEW -i $IF2 -j CONNMARK --set-mark 2

Добавить правило маршрутизации для маркированных пакетов. Пакеты с определенной меткой отправлять в свою таблицу:

ip rule add fwmark 1 lookup $T1
ip rule add fwmark 2 lookup $T2

Восстановление метки пакета при пути обратно, чтобы они пошли по нужному правилу:

iptables -t mangle -A PREROUTING -i docker0 -j CONNMARK --restore-mark

Выбор интерфейса docker0 зависит от типа Докер сети и может принимать разные значения, такие как docker0, docker_gwbridge, br-328920957a2e и т.д., нужно подставить свое значение и/или сделать несколько правил, если Докер подсеть не одна.

Выбор числа set-mark 1 ни от чего не зависит - это просто маркировка трафика неким числом, которое будет учитывать при выборе маршрута в параметре fwmark, они просто должны совпадать.

Про маркировку пакетов хорошо написано тут

Полный скрипт

#!/bin/bash

# Ethernet interface
IF1="eth0"
IP1="x.x.x.x"
NET1="x.x.x.x/32"
GW1="x.x.x.z"
T1="Ethernet"

# OpenVPN interface
IF2="tun0"
IP2="10.0.0.10"
NET2="10.0.0.10/32"
GW2="10.0.0.1"
T2="OpenVPN"

# Docker interface
DOCKER_IF="docker0"

# Rules for system
ip route add $NET1 dev $IF1 src $IP1 table $T1
ip route add default via $GW1 table $T1

ip route add $NET2 dev $IF2 src $IP2 table $T2
ip route add default via $GW2 table $T2

ip route add $NET1 dev $IF1 src $IP1
ip route add $NET2 dev $IF2 src $IP2

ip rule add from $IP1 table $T1
ip rule add from $IP2 table $T2

# Rules for Docker
ip rule add fwmark 1 lookup $T1
ip rule add fwmark 2 lookup $T2

iptables -t mangle -A PREROUTING -m conntrack --ctstate NEW -i $IF1 -j CONNMARK --set-mark 1
iptables -t mangle -A PREROUTING -m conntrack --ctstate NEW -i $IF2 -j CONNMARK --set-mark 2

iptables -t mangle -A PREROUTING -i $DOCKER_IF -j CONNMARK --restore-mark

Обращаю внимание, что в скрипте не устанавливается дефолт гетвей для main таблицы и это значит, что при отключении OpenVPN туннеля ничего не сломается и будет работать.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *