如何使用 TC 对每个 OpenVPN 客户端进行流量整形(速率限制)

如何使用 TC 对每个 OpenVPN 客户端进行流量整形(速率限制)

这个问题与另一个问题很棒的答案和脚本@奥利弗

目标:我想修改/扩展此答案中提供的脚本 以满足我的要求,如下所示:

  1. 我有大量客户端(最多 1000 个)。应根据每个客户端的 CN(通用名称)为其分配一个订阅类别和相应的最大数据速率。这些速率限制应在客户端连接时应用,并在客户端断开连接时删除:

    • bronze:1兆位
    • silver:10兆位
    • gold:100兆位
  2. 我想在客户端连接到 OpenVPN 服务器时动态调整每个客户端的订阅类别和相应的活动数据速率限制。客户端不必重新连接到 OpenVPN 服务器。这可行吗?还是我们必须断开每个客户端与 OpenVPN 的连接并重新连接,以再次调用脚本来更改配置tc

  3. 我们不需要tc使用 shell 手动修改配置,如何从另一台计算机或应用程序(即通过 PHP)动态更新客户端订阅类别和相应的活动数据速率限制?

非常感谢

答案1

这是一个解决方案,tc如何使用 OpenVPN 调用的脚本对单个客户端的数据速率进行流量整形(流量控制)

流量控制设置在脚本中处理,tc.sh具有以下特点:

  • 由OpenVPN 使用指令调用:updownclient-connectclient-disconnect
  • 所有设置都通过环境变量传递
  • 理论上支持最多/16子网(最多 65534 个客户端)
  • 使用以下方式进行过滤哈希过滤器用于非常快速的大规模过滤
  • 过滤器和类别仅针对当前连接的客户端进行设置,并单独添加和删除,而不会影响tc使用唯一标识符(hashtableshandlesclassids)的其他设置。这些标识符由客户端的远程 VPN IP 的最后 16 位生成
  • 根据 CN 名称(客户端证书通用名称)对客户端进行单独限制/节流
  • 客户端设置存储在包含其“订阅类”(bronzesilvergold)的文件中,要使用其他类只需编辑脚本并根据需要进行修改。
  • 当客户端连接时,可以从外部应用程序动态修改“订阅类别”和相应的数据速率(“带宽”)。

配置

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 服务器替换为正确的 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需要优化设置
  • 我不完全理解入口设置的工作原理。它们可能应该通过使用ifb界面进行优化,如这个答案

更深入理解的相关文档:

相关内容