diff --git a/net/trafficshaper/Makefile b/net/trafficshaper/Makefile new file mode 100644 index 000000000..939c37a97 --- /dev/null +++ b/net/trafficshaper/Makefile @@ -0,0 +1,50 @@ +# +# Copyright (C) 2018 Luiz Angelo Daros de Luca +# +# This is free software, licensed under the GNU General Public License v2. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=trafficshaper +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=Luiz Angelo Daros de Luca +PKG_LICENSE:=GPLv2 +PKG_ARCH:=all + +PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) +include $(INCLUDE_DIR)/package.mk + +include $(INCLUDE_DIR)/package.mk + +define Package/trafficshaper + SECTION:=net + CATEGORY:=Network + DEPENDS:=+tc +kmod-sched-core +kmod-sched-connmark +kmod-ifb +iptables +kmod-sched-cake +iptables-mod-conntrack-extra + TITLE:=WAN traffic shaper based on LAN addresses + MAINTAINER:=Luiz Angelo Daros de Luca + PKGARCH:=all +endef + +define Package/trafficshaper/description +Setup QoS rules to limit (or reserve) traffic used by classes of clients. +Uplink and downlink can be controled (or not controlled) independently. +Client classes are defined by its network addresses (IPv4 or IPv6). Each +client class can define absolute or relative (to wan) bandwith, and also +the use (or not) of spare wan bandwidth when avaiable. + +endef + +define Package/trafficshaper/conffiles +/etc/config/trafficshaper +endef + +define Build/Compile +endef + +define Package/trafficshaper/install +$(CP) ./files/* $(1) +endef + +$(eval $(call BuildPackage,trafficshaper)) diff --git a/net/trafficshaper/files/etc/config/trafficshaper b/net/trafficshaper/files/etc/config/trafficshaper new file mode 100644 index 000000000..daee10332 --- /dev/null +++ b/net/trafficshaper/files/etc/config/trafficshaper @@ -0,0 +1,42 @@ +package trafficshaper + +config globals 'globals' + option mark_mask '0xFF' + +config wan 'wan' + option downlink '20000' + option uplink '20000' + +config wan 'wanb' + option downlink '15000' + option uplink '5000' + +config class 'corp' + list network '192.168.1.0/24' + list network 'fdc8:1234:1234:1::/64' + option reserved_downlink '50%' + option reserved_uplink '35%' + option allowed_downlink '100%' + option allowed_uplink '100%' + +config class 'vpn' + list network '192.168.2.0/24' + list network 'fdc8:1234:1234:2::/64' + option reserved_downlink '25%' + option reserved_uplink '50%' + option allowed_downlink '100%' + option allowed_uplink '100%' + +config class 'guest' + list network '192.168.3.0/24' + list network 'fdc8:1234:1234:3::/64' + option reserved_downlink '25%' + option reserved_uplink '15%' + option allowed_downlink '25%' + option allowed_uplink '15%' + +config class 'default' + option reserved_downlink '1000' + option reserved_uplink '1000' + option allowed_downlink '100%' + option allowed_uplink '100%' diff --git a/net/trafficshaper/files/etc/init.d/trafficshaper b/net/trafficshaper/files/etc/init.d/trafficshaper new file mode 100755 index 000000000..445f50e38 --- /dev/null +++ b/net/trafficshaper/files/etc/init.d/trafficshaper @@ -0,0 +1,477 @@ +#!/bin/sh /etc/rc.common + +# Internal uci firewall chains are flushed and recreated on reload, so +# put custom rules into the root chains e.g. INPUT or FORWARD or into the +# special user chains, e.g. input_wan_rule or postrouting_lan_rule. + +START=25 +USE_PROCD=1 + +echo_err() { + echo "$@" >&2 +} + +msg() { + local level=$1; shift + echo_err "$APPNAME[$level]: $*" +} + +LOGLEVEL=${LOGLEVEL:-2} + +die() { + local err=$1; shift + e "$*" + exit $err +} + +APPNAME="trafficshaper" +IPT_CHAIN=$APPNAME + +debug_exec(){ + local err + d "exec: $*" + if "$@"; then + return 0 + else + err="$?" + fi + e "exec[err=$err]: $*" + return "$err" +} + +IP="debug_exec ip" +TC="debug_exec tc" +IP4T="debug_exec iptables -w 5" +IP6T="debug_exec ip6tables -w 5" + +#QDISC="cake autorate_ingress internet ethernet diffserv4 triple-isolate" +QDISC="cake" + +REQ_MODULES="sch_htb sch_cake act_connmark act_mirred em_u32" +REQ_CMDS="ip tc iptables" + +preinit(){ + [ "$LOGLEVEL" -ge 1 ] && e() { msg ERROR "$@"; } || e() { true; } + [ "$LOGLEVEL" -ge 2 ] && v() { msg INFO "$@"; } || v() { true; } + [ "$LOGLEVEL" -ge 3 ] && d() { msg DEBUG "$@"; } || d() { true; } + [ "$LOGLEVEL" -ge 4 ] && set -x + set -e +} + +requires() { + for module in $REQ_MODULES; do + [ -d /sys/module/$module ] || insert_modules "$module" || + die 2 "cannot load $module. Please install kmod-$module" + done + for cmd in $REQ_CMDS; do + which $cmd &>/dev/null || + die 2 "cannot find command $cmd. Please install $cmd" + done + + if ! which ip6tables &>/dev/null; then + v "Disabling IPv6 as ip6tables was not found" + IP6T=true + fi + + . /lib/functions/network.sh + + config_load $APPNAME +} + +do_stop() { + local only_int=$1 + + preinit + requires + + v "Stopping $APPNAME${only_int:+ for interface $only_int}" + if [ -z "$only_int" ]; then + d "Cleaning iptables" + # Cleaning iptables + for IPT in "$IP4T" "$IP6T"; do + $IPT -t mangle -D FORWARD -j $IPT_CHAIN &>/dev/null || : + $IPT -t mangle -F $IPT_CHAIN &>/dev/null || : + $IPT -t mangle -X $IPT_CHAIN &>/dev/null || : + $IPT -t mangle -F $IPT_CHAIN-classify &>/dev/null || : + $IPT -t mangle -X $IPT_CHAIN-classify &>/dev/null || : + done + fi + + d "Cleaning tc" + local dev_done int dev ifb interfaces + if [ "$only_int" ]; then + config_get type $only_int TYPE + if [ "$type" != "wan" ]; then + d "interface $only_int not found in trafficshaper config. Ignoring" + return 0 + fi + interfaces="$only_int" + + else + interfaces="$(config_foreach echo wan)" + fi + + for int in $interfaces; do + d "Cleaning tc for interface $int" + network_get_physdev dev "$int" || + die 1 "failed to get physical dev of interface $int" + + if echo "$dev_done" | grep -x -F -q "$dev"; then + continue + fi + ifb="ifb_$dev" + if [ ${#ifb} -gt 15 ]; then + die 1 "ifb name too long: ${ifb}" + fi + + $TC qdisc del dev ${ifb} root 2> /dev/null || : + $TC qdisc del dev ${dev} root 2> /dev/null || : + $TC qdisc del dev ${dev} ingress 2> /dev/null || : + + d "Removing ${ifb}..." + $IP link set dev ${ifb} down 2>/dev/null || : + $IP link delete dev ${ifb} 2>/dev/null || : + + intdev_done="$(echo "$dev_done"; echo -n $dev)" + done +} + + +calc_bw() { + local value=$1 reference=$2 + case "${value}" in + *%) echo "$((${value%\%} * reference / 100 ))";; + *) echo ${value};; + esac +} + +mask_range() { + local mask=$(($1)) n=0 fsb + if [ $mask -le 0 ]; then + e "mask '$1' must be greater than 0 (have a sequence of set bit)" + return 2 + fi + while [ "$((mask & 0x1))" -eq 0 ]; do + mask=$((mask >> 1)) + : $((n++)) + done + fsb="$n" + while [ "$((mask & 0x1))" -eq 1 ]; do + mask=$((mask >> 1)) + : $((n++)) + done + if [ $mask -ne 0 ]; then + e "mask '$1' must be a continuos sequence of set bit" + return 2 + fi + echo $fsb $((n-1)) + return 0 +} + +start_iptables(){ + d "Creating iptables mangle rules" + + config_get mark_mask globals mark_mask 0xFF + mark_mask=$(printf '0x%X\n' $(($mark_mask))) + + local fsb_lst class_id_max class_id_shift + fsb_lst=$(mask_range $mark_mask) + class_id_max=$(((1<<(${fsb_lst#* } - ${fsb_lst% *} +1))+1)) + class_id_shift=$((${fsb_lst% *})) + + d "General iptables rules:" + for IPT in "$IP4T" "$IP6T"; do + $IPT -t mangle -N $IPT_CHAIN + $IPT -t mangle -N $IPT_CHAIN-classify + + $IPT -t mangle -A FORWARD -j $IPT_CHAIN + $IPT -t mangle -A $IPT_CHAIN -j CONNMARK --restore-mark --nfmask $mark_mask --ctmask $mark_mask \ + -m comment --comment "Get previous class" + $IPT -t mangle -A $IPT_CHAIN -m mark --mark 0x0/$mark_mask -j $IPT_CHAIN-classify \ + -m comment --comment "If no class, try to classify" + done + + d "Classes iptables rules:" + local class_reserved_uplink class_reserved_downlink class_nets i=2 xi default_class_id + for class in $(config_foreach echo class); do + config_get class_reserved_uplink $class reserved_uplink + config_get class_reserved_downlink $class reserved_downlink + config_get class_nets $class network + if [ "$class" = default ]; then + default_class_id=$i + if [ -z "$class_reserved_uplink" -a -z "$class_reserved_downlink" ] ; then + die 2 "class default must defined either reserved uplink or downlink!" + fi + if [ "$class_nets" ]; then + die 2 "class default must not have any network defined!" + fi + else + if [ "$i" -ge "$class_id_max" ]; then + die 1 "Max client classes reached. Please, use less classes or increase option mark_mask '$mark_mask' in globals. Current mask allows only $((class_id_max-2)) classes if default is the last one." + fi + fi + + xi=$(printf '0x%X\n' $(((i-1)</dev/null +} + +reload_service() { + preinit + if ! is_running; then + d "Not running. Nothing to reload" + return 0 + fi + logger -t "$APPNAME" "Reloading $*..." + ( do_start "$@" ) +} + +add_interface_trigger() { + procd_add_interface_trigger "interface.update" "$1" /etc/init.d/$APPNAME reload $1 +} + +service_triggers() { + preinit; set +e + requires + + procd_add_reload_trigger "$APPNAME" + config_foreach add_interface_trigger wan + + procd_open_validate + validate_trafficshaper_global + validate_trafficshaper_wan + validate_trafficshaper_class + procd_close_validate +} + +validate_trafficshaper_global() { + uci_validate_section $APPNAME global "${1}" \ + 'mark_mask:uinteger:0xFF' +} + +validate_trafficshaper_wan() { + uci_validate_section "$APPNAME" wan "${1}" \ + 'downlink:uinteger' \ + 'uplink:uinteger' +} + +validate_trafficshaper_class() { + uci_validate_section "$APPNAME" class "${1}" \ + 'network:cidr' \ + 'reserved_downlink:or(uinteger, string)' \ + 'reserved_uplink:or(uinteger, string)' \ + 'allowed_downlink:or(uinteger, string)' \ + 'allowed_uplink:or(uinteger, string)' +} + +boot() { + LOGLEVEL=1 start +}