Openvpn

如何對每個 OpenVPN 客戶端使用 TC 進行流量整形(速率限制)

  • June 12, 2016

這個問題與來自@Oliver的一個很好的答案和腳本的另一個問題有關。

**目標:**我想修改/擴展此答案中提供的腳本 以滿足我的要求,如下所示:

  1. 我有大量客戶(最多 1000 個)。每個客戶端應根據其 CN(通用名稱)分配一個訂閱類別和相應的最大數據速率。這些速率限制應在客戶端連接時應用,並在其斷開連接時刪除:
  • bronze: 1 兆位
  • silver: 10 兆比特
  • gold: 100 兆位
  1. 我想在客戶端連接到 OpenVPN 伺服器時即時調整每個客戶端的訂閱類別和相應的活動數據速率限制。客戶端不必重新連接到 OpenVPN 伺服器。這是可能的還是我們必須斷開每個客戶端並將其重新連接到 OpenVPN 以導致再次呼叫腳本來更改tc配置?
  2. tc我們將如何從另一台電腦或應用程序(即通過 PHP)即時更新客戶端訂閱類和相應的活動數據速率限制,而不是使用 shell 手動修改配置?

非常感謝

這是一個解決方案,如何tc使用 OpenVPN 呼叫的腳本為單個客戶端的數據速率限制(流量控制)進行流量整形。

流量控制設置在tc.sh具有以下功能的腳本中處理:

  • 由 OpenVPN 使用指令呼叫:up、、downclient-connect``client-disconnect
  • 所有設置都通過環境變數傳遞
  • 理論上最多支持/16子網(最多 65534 個客戶端)
  • 使用散列過濾器進行過濾以實現非常快速的大規模過濾
  • 過濾器和類僅針對目前連接的客戶端設置,並單獨添加和刪除,而不會影響tc使用唯一標識符(hashtableshandlesclassids)的其他設置。這些標識符是從客戶端遠端 vpn IP 的最後 16 位生成的
  • 基於 CN-name(客戶端證書公用名)對客戶端的個別限制/節流
  • 客戶端設置儲存在包含其“訂閱類”( 和 )的文件中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"

將最後 2 行中的 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介面進行優化。

深入了解的相關文件:

引用自:https://serverfault.com/questions/777875