Недавно обзавёлся задачей по балансировке трафика между несколькими usb-модемами. В итоге родилось решение коим и хочу поделиться с Хабрасообществом.

На момент написания статьи это balancing_v0.5.2-alpha.

Изначально задача формулировалась примерно так:
Есть пучёк armhf девайсов c Ubuntu Trusty на борту.
У них есть несколько подключений к интернету. Обычно это основное проводное подключение (eth0) и несколько HiLink usb-модемов Huawei E303 (eth1-eth5). Через каждое из этих подключений нужно поднять openvpn-клиентов к единственному серверу и через них уже балансировать трафик.

Всё бы ничего, но у этих модемов нет возможности изменения подсети и шлюза (гвоздями прибиты 192.168.1.1/24), причём прошивок с реализацией этой возможности тоже не нашлось (в отличии, например от E3272 для которого есть прошивки с таким функционалом). Кроме того даже если бы и нашлись, то vpn-подключения всё равно были бы в одной подсети и с одинаковым шлюзом. Т.е. без продвинутой маршрутизации (policy routing) не обойтись.

Ах, да, ещё надо мониторить каждое подключение и отключать/включать, если порвалось/возобновилось. Т.е. маршрутизацией нужно управлять динамически.

Готовых решений под «обычный» Linux не нашёл. Киньте в меня ссылкой если они есть. Обычно публикуют свои собственные велосипеды на базе iproute2, вот и я туда же.

Есть парочка OpenWrt-специфичных:

В комментариях подкинули ещё решений:
  • MPTCP: Только TCP, требует патченое ядро на обоих концах, максимально плавные переключения между интерфейсами;
  • MULTI Network Manager;

Основной трафик будет адресован сервису на openvpn-сервере, и для него достаточно будет балансировать соединения. Имейте ввиду, что такой способ балансировки не очень хорошо подходит для веб-сёрфинга, т.к. некоторые соединения внутри https-сессии могут быть направлены в разные интерфейсы. Тут нужно балансировать сессии (несколько соединений кряду, flow-based), поправьте меня, если не прав. В планах реализовать этот способ, вкупе с per-packet- и hash-based.

Фичи этой реализации


  • Может использоваться для балансировки трафика на интерфейсах в одной подсети с одинаковым IP шлюза. Это полезно не только для usb-модемов, но и для других подключений, доступа к перенастройке которых у тебя нет, или перенастройка не желательна;
  • Поддержка балансировки поверх vpn;
  • Мониторинг состояния подключения. Есть два типа:
    1. multi: connection-based балансировка;
    2. solo: redundancy mode, балансировка не используется, а используется только живой интерфейс с максимальным приоритетом.
    Живость подключения определяется минимальным объёмом трафика за период между проверками, и если он меньше порогового значения, то посредством пинга внешнего хоста.

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

Как это работает?


При поднятии или опускании интерфейса, который участвует в балансировке, автоматически должен рестартовать скрипт balancing (с помощью balancing_restart). Это обеспечивается соответствующей настройкой интерфейсов.

Скрипт инициализирует все упомянутые в настройках интерфейсы (и в то же время доступные) для балансировки: добавляет соответствующие правила маршрутизации (ip rule), маршруты (ip route), настраивает firewall (iptables).

В зависимости от настроек режима (solo или multi, которые, кстати, можно менять на лету записав соответствующее слово в balancing_mode), будет либо производиться балансировка между интерфейсами с указанным весом (multi), либо использоваться живой интерфейс с максимальным приоритетом (solo).

Через определённый период времени скрипт проверяет все интерфейсы на живость (а также сменился ли режим), и включает/отключает их соответственно посредством редактирования таблиц маршрутизации.

Примерные требования


  • Ubuntu Trusty 14.04 LTS (другие linux также могут быть использованы, но все настройки и тесты проводились именно в этой ОС)
  • Ядро Linux с поддержкой CONFIG_IP_ROUTE_MULTIPATH, CONFIG_IP_MULTIPLE_TABLES, CONFIG_IP_ADVANCED_ROUTER:
    for conf in /proc/config.gz /boot/config-$(uname -r) /boot/config; do zgrep -e CONFIG_IP_ROUTE_MULTIPATH -e CONFIG_IP_MULTIPLE_TABLES -e CONFIG_IP_ADVANCED_ROUTER $conf 2>/dev/null; done
    

  • Пакеты:
    apt-get install iproute2 iptables coreutils iputils-ping grep sed
    

Настройка


  • Скопировать файлы в /etc/network:
    mkdir temp && cd temp
    git clone https://github.com/vmspike/balancing
    cd balancing
    chmod +x balancing/balancing{_restart,} add_rt_table get_ovpn_by_base_ip bandwidth-measure
    cp balancing/balancing* get_ovpn_by_base_ip add_rt_table bandwidth-measure /etc/network/
    cd ../../ && rm -rf temp
    

  • Отключить или снести к чертям свинячим NetworkManager, если наличествует, а то всё испортит:
    apt-get install usb-modeswitch  # Нужно для большинства usb-модемов
    apt-get purge network-manager network-manager-gnome
    apt-get autoremove  # Аккуратно с этим, проверь точно ли всё это тебе не нужно.
    

  • Настроить параметры ядра в /etc/sysctl.conf:
    • Если какие-либо интерфейсы находятся в одной подсети:
      # ARP kernel settings for multiple interfaces in the same subnet
      net.ipv4.conf.all.arp_ignore = 1
      net.ipv4.conf.all.arp_filter = 1
      net.ipv4.conf.all.arp_announce = 1
      # Enable Loose Reverse Path
      net.ipv4.conf.all.rp_filter = 2
      

    • Убрать routing cache для ядер <3.6 (в ядрах >=3.6 routing cache для ipv4 уже не используется):
      # Remove routing cache if exists
      net.ipv4.route.max_size = 0
      

    • Отключить IPv6, если нигде больше не используется, для балансировки используется только IPv4:
      # Disable IPv6
      net.ipv6.conf.all.disable_ipv6 = 1
      net.ipv6.conf.default.disable_ipv6 = 1
      net.ipv6.conf.lo.disable_ipv6 = 1

    • Применить изменения:
      sysctl -p
      

  • Настроить интерфейсы:
    • Если настройка происходит локально, рекомендую положить все интерфейсы во избежание проблем с поднятием:
      service networking stop
      
      ну или
      ifdown eth1 eth2 ... ethN
      

    • Адаптировать примеры из interfaces.d/* под себя.
      пример для eth0
      auto eth0
      allow-hotplug eth0
      iface eth0 inet static
          address 192.168.1.10
          network 255.255.255.0
          dns-nameservers 8.8.8.8 208.67.222.222
          pre-up /etc/network/add_rt_table eth0
          # Gateway setup
          up ip route add default via 192.168.1.1 dev eth0 src 192.168.1.10 proto static table eth0
          up ip route add default via 192.168.1.1 dev eth0 src 192.168.1.10 proto static table default metric 2000
          # IP rules setup for separate routing table
          up ip rule add priority 10 from 192.168.1.10 lookup eth0
          up ip rule add priority 110 from all oif eth0 lookup eth0
          down while ip rule delete lookup eth0; do :; done || exit 0
          # Start/stop OpenVPN
          up service openvpn start $(/etc/network/get_ovpn_by_base_ip 192.168.1.10) || exit 0
          down service openvpn stop $(/etc/network/get_ovpn_by_base_ip 192.168.1.10) || exit 0
          # Restart balancing
          up /etc/network/balancing_restart
          down /etc/network/balancing_restart
      
          # If it's WiFi interface
          #wpa-driver nl80211
          #wpa-key-mgmt WPA-PSK
          #wpa-proto WPA2
          #wpa-ssid SSID
          #wpa-psk PASSWORD
      

  • Настроить OpenVPN клиентов, если используются:
    • Установить OpenVPN:
      ## Repo for amd64 and i386 arch
      # wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add -
      # echo "deb http://swupdate.openvpn.net/apt trusty main" > /etc/apt/sources.list.d/swupdate.openvpn.net.list
      apt-get update
      apt-get install openvpn
      

    • Настроить OS:
      adduser --system --no-create-home --home /nonexistent --disabled-login --group openvpn
      mkdir /var/log/openvpn
      chown openvpn:openvpn /var/log/openvpn
      

    • Отключить автозапуск всех клиентов при старте. В /etc/default/openvpn раскомментировать строку:
      AUTOSTART="none"
      

    • Примеры конфигурации клиентов есть в openvpn/*:
      пример клиентского конфига tun0 поверх eth0
      # OpenVPN-client config example for balancing
      
      client
      remote openvpn.example.com 1194
      local 192.168.1.10  # bind to eth0
      ;nobind
      dev tun0
      proto udp
      resolv-retry infinite
      remote-cert-tls server
      comp-lzo
      log-append /var/log/openvpn/ovpn-client-example.log
      verb 3
      ;daemon
      
      # Commented because balancing_restart require root permissions
      ;user openvpn
      ;group openvpn
      ;persist-key
      ;persist-tun
      
      up "/etc/network/balancing_restart tun0 start"
      down "/etc/network/balancing_restart tun0 stop"
      
      ;ca /etc/openvpn/ca.crt
      <ca>
      CA CERT HERE
      </ca>
      
      ;cert /etc/openvpn/ovpn-client-example.crt
      <cert>
      CERT HERE
      </cert>
      
      ;key /etc/openvpn/ovpn-client-example.key
      <key>
      PRIVATE KEY HERE
      </key>
      
      ;tls-auth /etc/openvpn/ta.key 1
      key-direction 1
      <tls-auth>
      STATIC TLS KEY HERE
      </tls-auth>

  • Отредактировать переменные под себя в balancing_vars (см. комментарии к файлу)
  • Поднять интерфейсы для балансировки (/etc/network/balancing_restart должен запускаться при поднятии/опускании интерфейса):
  • Проверить /var/log/balancing.log на наличие ошибок:
    tail -f -n30 /var/log/balancing.log
    

  • Техническую информацию о текущем состоянии балансировки можно посмотреть с помощью такого вот однострочника:
    echo ======Current state======; echo ===Addresses===; ip a; echo; echo ===Rules===; ip rule; echo; echo ===Routing tables===; for TBL in $(ip rule | rev | cut -d' ' -f2 | rev | sort -u); do echo ==$TBL==; ip r l t $TBL; echo; done; echo; echo ===IPTABLES===; for table in raw mangle nat filter; do echo ==$table==; iptables -vnL -t $table; echo; done; echo; echo ===MODE===; cat /etc/network/balancing_mode;
    
    пример состояния для пары интерфейсов
    ======Current state======
    ===Addresses===
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
    3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
        link/ether 1a:2b:3c:4d:5e:6c brd ff:ff:ff:ff:ff:ff
        inet 192.168.1.10/24 brd 192.168.1.255 scope global eth0
           valid_lft forever preferred_lft forever
    7: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000
        link/ether 12:34:56:78:90:12 brd ff:ff:ff:ff:ff:ff
        inet 192.168.1.11/24 brd 192.168.1.255 scope global eth1
           valid_lft forever preferred_lft forever
    8: tun1: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 100
        link/none
        inet 172.22.0.3/16 brd 172.22.255.255 scope global tun1
           valid_lft forever preferred_lft forever
    9: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 100
        link/none
        inet 172.22.0.2/16 brd 172.22.255.255 scope global tun0
           valid_lft forever preferred_lft forever
    
    ===Rules===
    0:      from all lookup local
    10:     from 192.168.1.10 lookup eth0
    11:     from 192.168.1.11 lookup eth1
    110:    from all oif eth0 lookup eth0
    111:    from all oif eth1 lookup eth1
    1000:   from all fwmark 0x6a lookup tun0
    1001:   from 172.22.0.2 lookup tun0
    1002:   from all oif tun0 lookup tun0
    1003:   from all fwmark 0x6b lookup tun1
    1004:   from 172.22.0.3 lookup tun1
    1005:   from all oif tun1 lookup tun1
    20000:  from all lookup main
    30000:  from all lookup balancing
    32767:  from all lookup default
    
    ===Routing tables===
    ==balancing==
    default  proto static  metric 1
            nexthop via 172.22.0.1  dev tun0 weight 18
            nexthop via 172.22.0.1  dev tun1 weight 1
    default via 172.22.0.1 dev tun0  proto static  src 172.22.0.2  metric 2
    default via 172.22.0.1 dev tun1  proto static  src 172.22.0.3  metric 4
    default via 172.22.0.1 dev tun0  proto static  src 172.22.0.2  metric 1002
    default via 172.22.0.1 dev tun1  proto static  src 172.22.0.3  metric 1004
    
    ==default==
    default via 192.168.1.1 dev eth0  src 192.168.1.10  metric 2000
    default via 192.168.1.1 dev eth1  src 192.168.1.11  metric 2001
    
    ==eth0==
    default via 192.168.1.1 dev eth0  src 192.168.1.10
    
    ==eth1==
    default via 192.168.1.1 dev eth1  src 192.168.1.11
    
    ==local==
    broadcast 127.0.0.0 dev lo  proto kernel  scope link  src 127.0.0.1
    local 127.0.0.0/8 dev lo  proto kernel  scope host  src 127.0.0.1
    local 127.0.0.1 dev lo  proto kernel  scope host  src 127.0.0.1
    broadcast 127.255.255.255 dev lo  proto kernel  scope link  src 127.0.0.1
    broadcast 172.22.0.0 dev tun1  proto kernel  scope link  src 172.22.0.3
    broadcast 172.22.0.0 dev tun0  proto kernel  scope link  src 172.22.0.2
    local 172.22.0.2 dev tun0  proto kernel  scope host  src 172.22.0.2
    local 172.22.0.3 dev tun1  proto kernel  scope host  src 172.22.0.3
    broadcast 172.22.255.255 dev tun1  proto kernel  scope link  src 172.22.0.3
    broadcast 172.22.255.255 dev tun0  proto kernel  scope link  src 172.22.0.2
    broadcast 192.168.1.0 dev eth0  proto kernel  scope link  src 192.168.1.10
    broadcast 192.168.1.0 dev eth1  proto kernel  scope link  src 192.168.1.11
    local 192.168.1.10 dev eth0  proto kernel  scope host  src 192.168.1.10
    local 192.168.1.11 dev eth1  proto kernel  scope host  src 192.168.1.11
    broadcast 192.168.1.255 dev eth0  proto kernel  scope link  src 192.168.1.10
    broadcast 192.168.1.255 dev eth1  proto kernel  scope link  src 192.168.1.11
    
    ==main==
    169.254.0.0/16 dev eth0  scope link  metric 1000
    172.22.0.0/16 dev tun1  proto kernel  scope link  src 172.22.0.3
    172.22.0.0/16 dev tun0  proto kernel  scope link  src 172.22.0.2
    192.168.1.0/24 dev eth0  proto kernel  scope link  src 192.168.1.10
    192.168.1.0/24 dev eth1  proto kernel  scope link  src 192.168.1.11
    
    ==tun0==
    default via 172.22.0.1 dev tun0  proto static  src 172.22.0.2  metric 2
    
    ==tun1==
    default via 172.22.0.1 dev tun1  proto static  src 172.22.0.3  metric 4
    
    
    ===IPTABLES===
    ==mangle==
    Chain PREROUTING (policy ACCEPT 5473 packets, 631K bytes)
     pkts bytes target     prot opt in     out     source               destination
     5473  631K CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0            CONNMARK restore
       19  1140 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctorigdst 172.22.0.2 mark match 0x0 MARK set 0x6a
        7  420 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctorigdst 172.22.0.3 mark match 0x0 MARK set 0x6b
    
    Chain INPUT (policy ACCEPT 4621 packets, 587K bytes)
     pkts bytes target     prot opt in     out     source               destination
    
    Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
     pkts bytes target     prot opt in     out     source               destination
    
    Chain OUTPUT (policy ACCEPT 3344 packets, 541K bytes)
     pkts bytes target     prot opt in     out     source               destination
    
    Chain POSTROUTING (policy ACCEPT 3344 packets, 541K bytes)
     pkts bytes target     prot opt in     out     source               destination
      590 92460 MARK       all  --  *      tun0    0.0.0.0/0            0.0.0.0/0            mark match 0x0 MARK set 0x6a
       40  6268 MARK       all  --  *      tun1    0.0.0.0/0            0.0.0.0/0            mark match 0x0 MARK set 0x6b
     3344  541K CONNMARK   all  --  *      *       0.0.0.0/0            0.0.0.0/0            CONNMARK save
    
    
    ===MODE===
    multi
    

Известные «особенности»


  • Если один из балансируемых интерфейсов лёг/исчез, а скрипт не перезапустился (с помощью balancing_restart), то скрипт падает;
  • OpenVPN клиент должен быть запущен под root, чтобы позволять запускать balancing_restart от root;
  • В момент перезапуска скрипта пакеты из установленных соединений могут пропасть, т.к. не будет существовать подходящих маршрутов в таблицах маршрутизации. Т.е. поднятие/опускание одного из балансируемых интерфейсов влияет на соединения в других, что не есть гуд. Возможно в дальнейшем это будет исправлено или хотя бы максимально минимизировано;
  • При vpn-балансировке если на момент инициализации не удалось поднять vpn-интерфейс (кончился трафик, или не было сигнала, ...) на каком-то из базовых интерфейсов, он пропускается, и если в дальнейшем такая возможность появляется, vpn-интерфейс не будет поднят автоматически.

Планы на будущее

(если проект будет развиваться)

  • Позволить выбирать использовать ли для балансировки дефолтный маршрут или только определённую подсеть/IP (сейчас используется только маршрут по-умолчанию);
  • Добавить маршруты scope link дабы была возможность общаться с хостами своей подсети минуя шлюз;
  • Уточнить вычисления ширины канала (сейчас считается будто все проверки в скрипте мгновенные, а они секунды могут отъедать);
  • Добавить типы балансировки: flow/session-based, hash-based (require kernels >=4.4 or patch), packet-based;

Комментарии/предложения/критика горячо приветствуются!
Поделиться с друзьями
-->

Комментарии (5)


  1. ValdikSS
    10.06.2016 23:50

    Готовых решений под «обычный» Linux не нашёл. Киньте в меня ссылкой если они есть. Обычно публикуют свои собственные велосипеды на базе ip route, вот и я туда же.
    А их и нет! Удивительно, но это так. Можно кое-как настроить Bird, но это все же демон маршрутизации, а не конфигуратор policy routing.

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


    1. vmspike
      11.06.2016 08:14

      Спасибо, добавил в статью и README.

      На MPTCP натыкался, но оттолкнуло, что только TCP поддерживается, и ядро нужно специальное. Хотя так-то интересное решение (судя по ролику на главной странице), если позволяет очень плавно переключаться между интерфейсами, что пользователю это даже не заметно.


  1. phantasm1c
    13.06.2016 08:18

    А почему нельзя поднять несколько туннелей к одному серверу, каждый туннель на отдельном порту сервера, со стороны клиента сделать 3 дополнительных таблицы маршрутизации с дефолтроутом через нужного аплинка, трафик к каждому dst addr + dst port openvpn сервера заворачивать в нужную таблицу маршрутизации через ip rule. Поверх туннелей поднять OSPF и анонсировать сервером дефолтроут или нужную сеть.
    В таком случае не нужно будет никаких скриптов, все будет переключаться автоматически с помощью динамической маршрутизации хоть через несколько секунд.


  1. vmspike
    13.06.2016 10:44

    Скрипт нужен по большей части для начальной автоматической конфигурации всех интерфейсов (и vpn, и базовых, в зависимости от режима), а затем лишь для проверки состояния туннеля/интерфейса, вся балансировка осуществляется ядром. См. «пример состояния для пары интерфейсов» под спойлером. Это похоже на то, что ты описал, только каждый туннель запускать к отдельному порту сервера не требуется, тем более, что доступа к конфигурации сервера может и не быть.

    Состояние всех интерфейсов обновляется каждые TIME_GAP секунд (60 по-умолчанию, меньше 30 лучше не ставить, особенное если много интерфейсов — проверки могут не уложиться в это время). Хочешь сказать, что с помощью OSPF можно организовать проверку туннеля гораздо быстрее и качественнее чем данными о трафике и пингом? Не использовал пока OSPF нигде.

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

    Да, сейчас проверки интерфейса на живость происходят последовательно, хорошо было бы их сделать параллельными, чтобы сократить время на проверку, тогда можно чаще проверять (хотя не факт, что это будет лучше, особенно при не стабильном соединении).


    1. phantasm1c
      13.06.2016 23:23

      Конечно, это одна из задач OSPF — отслеживание изменения топологии сети. Интервал — зависит от того какой сконфигурируете.
      У меня так бегала по двум нестабильным каналам IP-телефония и RDP-сессии к датацентру: если канал падает, трафик перетекает на резервный через порядка 0.8 секунды. При этом не страдают ни TCP-сесии (не разрываются), ни звонки (крякнет легонько, и дальше пойдет).

      Проблема «интерфейс вроде жив, но трафик не идёт» в OSPF решается непрерывшим обменом Hello-сообщениями между OSPF-роутерами. Только роутер не получает hello-шек больше чем dead timer — канал считается мёртвым.

      В OSPF проверки всех каналов идут параллельно друг другу. Это чуть ли не негласный стандарт interior роутинга, конечно же там всё это предусмотрено :)

      Если Вам интересно, вот ключевые слова для того чтобы разобраться: linux advanced routing, ospf, настройка OSPF в quagga (статья для cisco, но квагга копирует их синтаксис. Мне лично для динамической маршрутизации нравится bird).