Назад | Перейти на главную страницу

Как формировать трафик (ограничение скорости) с TC для каждого клиента OpenVPN

Этот вопрос связан с Другой вопрос с отличный ответ и сценарий из @Оливер.

Цель: Я хочу изменить / расширить сценарий, представленный в этом ответе в соответствии с моими требованиями, а именно:

  1. У меня большое количество клиентов (до 1000). Каждому клиенту должен быть назначен класс подписки и соответствующая максимальная скорость передачи данных на основе его CN (общего имени). Эти ограничения скорости применяются при подключении клиента и снимаются при отключении:

    • bronze: 1 мбит
    • silver: 10 мбит
    • gold: 100 мбит
  2. Я хотел бы настроить класс подписки каждого клиента и соответствующий лимит активной скорости передачи данных на лету, пока клиент подключен к серверу OpenVPN. Клиенту не нужно повторно подключаться к серверу OpenVPN. Возможно ли это, или нам нужно отключать и повторно подключать каждого клиента к OpenVPN, чтобы снова вызвать скрипт для изменения tc конфигурация?

  3. Вместо изменения tc конфигурация вручную с помощью оболочки, как бы мы обновили класс подписки клиента и соответствующее ограничение активной скорости передачи данных на лету с другого компьютера или приложения (то есть через PHP)?

Большое спасибо

Вот решение, как сделать формирование трафика для ограничения скорости передачи данных отдельных клиентов с tc (контроль трафика) с помощью скрипта, вызываемого OpenVPN.

Настройки управления трафиком обрабатываются в скрипте. tc.sh со следующими функциями:

  • Вызывается OpenVPN с использованием директив: up, down, client-connect и client-disconnect
  • Все настройки передаются через переменные среды
  • Теоретически поддерживает до /16 подсети (до 65534 клиентов)
  • Фильтрация с использованием фильтры хеширования для очень быстрой массовой фильтрации
  • Фильтры и классы устанавливаются только для подключенных в данный момент клиентов и добавляются и удаляются индивидуально, не затрагивая другие tc настройки с использованием уникальных идентификаторов (hashtables, handles, classids). Эти идентификаторы генерируются из последних 16 бит удаленного IP-адреса vpn клиента.
  • Индивидуальное ограничение / дросселирование клиентов на основе CN-имени (общее имя сертификата клиента)
  • Настройки клиентов хранятся в файлах, содержащих их «класс подписки» (bronze, silver и gold), чтобы использовать другие классы, просто отредактируйте сценарий и внесите необходимые изменения.
  • «Класс подписки» и соответствующая скорость передачи данных («полоса пропускания») могут быть изменены на лету из внешних приложений, когда клиент подключен.

Конфигурация

Конфигурация OpenVPN сервера /etc/openvpn/tc/conf:

port 1194
proto udp
dev tun
sndbuf 0
rcvbuf 0
ca ca.crt
cert server.crt
key server.key
dh dh.pem
tls-auth ta.key 0
topology subnet
server 10.8.0.0 255.255.0.0
keepalive 10 60
comp-lzo
persist-key
persist-tun
status /var/log/openvpn-tc-status.log
log /var/log/openvpn-tc.log
verb 3
script-security 2
down-pre
up /etc/openvpn/tc/tc.sh
down /etc/openvpn/tc/tc.sh
client-connect /etc/openvpn/tc/tc.sh
client-disconnect /etc/openvpn/tc/tc.sh
push "redirect-gateway def1"
push "dhcp-option DNS 8.8.8.8"
push "dhcp-option DNS 8.8.4.4"

Замените DNS-серверы в последних 2 строках на правильные IP-адреса.

Скрипт управления трафиком /etc/openvpn/tc/tc.sh:

#!/bin/bash

ipdir=/etc/openvpn/tc/ip
dbdir=/etc/openvpn/tc/db
ip="$ifconfig_pool_remote_ip"
cn="$common_name"
ip_local="$ifconfig_local"

debug=0
log=/tmp/tc.log

if [[ "$debug" > 0 ]]; then
  exec >>"$log" 2>&1
  chmod 666 "$log" 2>/dev/null
  if [[ "$debug" > 1 ]]; then
    date
    id
    echo "PATH=$PATH"
    [[ "$debug" > 2 ]] && printenv
  fi
  echo
  echo "script_type=$script_type"
  echo "dev=$dev"
  echo "ip=$ip"
  echo "user=$cn"
  echo "\$1=$1"
  echo "\$2=$2"
  echo "\$3=$3"
fi

cut_ip_local() {
  if [ -n "$ip_local" ]; then
    ip_local_byte1=`echo "$ip_local" | cut -d. -f1`
    ip_local_byte2=`echo "$ip_local" | cut -d. -f2`
  fi

  [[ "$debug" > 0 ]] && echo "ip_local_byte1=$ip_local_byte1"
  [[ "$debug" > 0 ]] && echo "ip_local_byte2=$ip_local_byte2"
}

create_identifiers() {
  if [ -n "$ip" ]; then
    ip_byte3=`echo "$ip" | cut -d. -f3`
    handle=`printf "%x\n" "$ip_byte3"`
    ip_byte4=`echo "$ip" | cut -d. -f4`
    hash=`printf "%x\n" "$ip_byte4"`
    classid=`printf "%x\n" $((256*ip_byte3+ip_byte4))`
  fi

  [[ "$debug" > 0 ]] && echo "ip_byte3=$ip_byte3"
  [[ "$debug" > 0 ]] && echo "ip_byte4=$ip_byte4"
  [[ "$debug" > 0 ]] && echo "handle=$handle"
  [[ "$debug" > 0 ]] && echo "hash=$hash"
}

start_tc() {
  [[ "$debug" > 1 ]] && echo "start_tc()"

  cut_ip_local

  echo "$dev" > "$ipdir"/dev

  tc qdisc add dev "$dev" root handle 1: htb
  tc qdisc add dev "$dev" handle ffff: ingress

  tc filter add dev "$dev" parent 1:0 prio 1 protocol ip u32
  tc filter add dev "$dev" parent 1:0 prio 1 handle 2: protocol ip u32 divisor 256
  tc filter add dev "$dev" parent 1:0 prio 1 protocol ip u32 ht 800:: \
      match ip dst "${ip_local_byte1}"."${ip_local_byte2}".0.0/16 \
      hashkey mask 0x000000ff at 16 link 2:

  tc filter add dev "$dev" parent ffff:0 prio 1 protocol ip u32
  tc filter add dev "$dev" parent ffff:0 prio 1 handle 3: protocol ip u32 divisor 256
  tc filter add dev "$dev" parent ffff:0 prio 1 protocol ip u32 ht 800:: \
      match ip src "${ip_local_byte1}"."${ip_local_byte2}".0.0/16 \
      hashkey mask 0x000000ff at 12 link 3:
}

stop_tc() {
  [[ "$debug" > 1 ]] && echo "stop_tc()"

  tc qdisc del dev "$dev" root
  tc qdisc del dev "$dev" handle ffff: ingress

  [ -e "$ipdir"/dev ] && rm "$ipdir"/dev
}

function bwlimit-enable() {
  [[ "$debug" > 1 ]] && echo "bwlimit-enable()"

  create_identifiers

  echo "$ip" > "$ipdir"/"$cn".ip

  # Find this user's bandwidth limit
  [[ "$debug" > 0 ]] && echo "userdbfile=${dbdir}/${cn}"
  user=`cat "${dbdir}/${cn}"`
  [[ "$debug" > 0 ]] && echo "subscription=$user"

  if [ "$user" == "gold" ]; then
    downrate=100mbit
    uprate=100mbit
  elif [ "$user" == "silver" ]; then
    downrate=10mbit
    uprate=10mbit
  elif [ "$user" == "bronze" ]; then
    downrate=1mbit
    uprate=1mbit
  else
    downrate=10kbit
    uprate=10kbit
  fi

  # Limit traffic from VPN server to client
  tc class add dev "$dev" parent 1: classid 1:"$classid" htb rate "$downrate"
  tc filter add dev "$dev" parent 1:0 protocol ip prio 1 \
      handle 2:"${hash}":"${handle}" \
      u32 ht 2:"${hash}": match ip dst "$ip"/32 flowid 1:"$classid"

  # Limit traffic from client to VPN server
  # Maybe better use ifb for ingress? See: https://serverfault.com/a/386791/209089
  tc filter add dev "$dev" parent ffff:0 protocol ip prio 1 \
      handle 3:"${hash}":"${handle}" \
      u32 ht 3:"${hash}": match ip src "$ip"/32 \
      police rate "$uprate" burst 80k drop flowid :"$classid"
}

function bwlimit-disable() {
  [[ "$debug" > 1 ]] && echo "bwlimit-disable()"

  create_identifiers

  tc filter del dev "$dev" parent 1:0 protocol ip prio 1 \
      handle 2:"${hash}":"${handle}" u32 ht 2:"${hash}":
  tc class del dev "$dev" classid 1:"$classid"
  tc filter del dev "$dev" parent ffff:0 protocol ip prio 1 \
      handle 3:"${hash}":"${handle}" u32 ht 3:"${hash}":

  # Remove .ip
  [ -e "$ipdir"/"$cn".ip ] && rm "$ipdir"/"$cn".ip
}

case "$script_type" in
  up)
    start_tc
    ;;
  down)
    stop_tc
    ;;
  client-connect)
    bwlimit-enable
    ;;
  client-disconnect)
    bwlimit-disable
    ;;
  *)
    case "$1" in
      update)
        [ -z "$2" ] && echo "$0 $1: missing argument [client-CN]" >&2 && exit 1
        [ ! -e "$ipdir"/"$2".ip ] &&  \
            echo "$0 $1 $2: file $ipdir/$2.ip not found" >&2 && exit 1
        [ ! -e "$ipdir"/dev ] && \
            echo "$0 $1: file $ipdir/dev not found" >&2 && exit 1
        ip=`cat "$ipdir/$2.ip"`
        dev=`cat "$ipdir/dev"`
        cn="$2"
        bwlimit-disable
        bwlimit-enable
        ;;
      *)
        echo "$0: unknown operation [$1]" >&2
        exit 1
        ;;
    esac
    ;;
esac

exit 0

Сделайте его исполняемым:

chmod +x /etc/openvpn/tc/tc.sh

Каталог базы данных подписки /etc/openvpn/tc/db/:

Этот каталог содержит файл для каждого клиента, названный в честь его CN-имя содержащий строку "класс подписки", настройте следующим образом:

mkdir -p /etc/openvpn/tc/db
echo bronze > /etc/openvpn/tc/db/client1
echo silver > /etc/openvpn/tc/db/client2
echo gold > /etc/openvpn/tc/db/client3

Каталог базы данных IP /etc/openvpn/tc/ip/:

Этот каталог будет содержать CN-name <-> IP-address отношения и tun interface во время выполнения, который должен быть предоставлен для внешнего приложения, обновляющего tc настройки при подключении клиентов.

mkdir -p /etc/openvpn/tc/ip

Это будет выглядеть так:

root@ubuntu:/etc/openvpn/tc/ip# ls -l
-rw-r--r-- 1 root root    9 Jun  1 08:31 client1.ip
-rw-r--r-- 1 root root    9 Jun  1 08:30 client2.ip
-rw-r--r-- 1 root root    9 Jun  1 08:30 client3.ip
-rw-r--r-- 1 root root    5 Jun  1 08:25 dev
root@ubuntu:/etc/openvpn/tc/ip# cat *
10.8.0.2
10.8.1.0
10.8.2.123
tun0

Включить переадресацию IP:

echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
sysctl -p

Настройка NAT (трансляция сетевых адресов):

Если у вас есть статический внешний IP-адрес, используйте SNAT:

iptables -t nat -A POSTROUTING -s 10.8.0.0/16 -o <if> -j SNAT --to <ip>

Или, если у вас есть динамически назначаемый IP-адрес, используйте MASQUERADE (помедленнее):

iptables -t nat -A POSTROUTING -s 10.8.0.0/16 -o <if> -j MASQUERADE

пока

  • <if> это имя внешнего интерфейса (т.е. eth0)
  • <ip> это IP-адрес внешнего интерфейса

Использование скрипта и отображение конфигурации tc

Обновление "класса подписки" и tc настройки из внешнего приложения:

Пока сервер OpenVPN включен и подключен клиент, выполните следующие команды (пример для обновления client1 к "gold" подписка):

echo gold > /etc/openvpn/tc/db/client1
/etc/openvpn/tc/tc.sh update client1

tc команды для отображения настроек:

tc -s qdisc show dev tun0
tc class show dev tun0
tc filter show dev tun0

Дополнительная информация

Примечания и возможные оптимизации:

  • Сценарий и tc настройки были протестированы только на небольшом количестве клиентов
  • Необходимо провести крупномасштабное тестирование с одновременным массовым клиентским трафиком и, возможно, tc настройки должны быть оптимизированы
  • Не совсем понимаю, как работают настройки ingress. Вероятно, их следует оптимизировать с помощью ifb интерфейс, как описано в этот ответ.

Связанная документация для более глубокого понимания: