From 38ad80b8672a7ecce6bd9f607900c77dfd70da07 Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Sat, 23 Jul 2016 13:29:07 +0200 Subject: Initial commit Signed-off-by: Florian Pritz --- hfsc_shaper.sh | 274 +++++++++++++++++++++++++++++++++++++++++++ qos.pl | 364 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 638 insertions(+) create mode 100755 hfsc_shaper.sh create mode 100755 qos.pl diff --git a/hfsc_shaper.sh b/hfsc_shaper.sh new file mode 100755 index 0000000..3504c68 --- /dev/null +++ b/hfsc_shaper.sh @@ -0,0 +1,274 @@ +#!/bin/bash +#---------------------------------------------------- +# File: hfsc_shaper.sh +# Version: 0.2 +# Edited: Florian "Bluewind" Pritz +# Author: Maciej Bliziński, http://automatthias.wordpress.com/ +#---------------------------------------------------- +# +# Special Thanks to +# Maciej Bliziński, http://automatthias.wordpress.com/ +# +# References: +# http://www.voip-info.org/wiki/view/QoS+Linux+with+HFSC +# http://www.nslu2-linux.org/wiki/HowTo/EnableTrafficShaping +# http://www.cs.cmu.edu/~hzhang/HFSC/main.html + +######################################################################## +# CONFIGURATION +######################################################################## + +# Uplink and downlink speeds +# Normally use a bit lower values than your real speed, but +# you should experiment a bit +# downlink is unused +#DOWNLINK=75000 +UPLINK=7400 + +# Device that connects you to the Internet +DEV="extern0" + +# Traffic classes: +# 1:2 Interactive (SSH, DNS, ACK, Quake) +# 1:3 Low latency (VoIP) +# 1:4 Browsing (HTTP, HTTPs) +# 1:5 Default +# 1:6 Middle-low priority +# 1:7 Lowest priority + +# Interactive class: SSH Terminal, DNS and gaming (Quake) +INTERACTIVEPORTS="22 23 53 3389 5900 22222 6667 7000 44400 7776 4949 11030 11239 143 445 25765" + +# VoIP telephony +#VOIPPORTS="5060:5100 10000:11000 5000:5059 8000:8016 5004 1720 1731" +VOIPPORTS="7977 9987" +# IP addresses of the VoIP phones, +# if none, set VOIPIPS="" +VOIPIPS="" + + +# WWW, jabber and IRC +BROWSINGPORTS="80 443 8080 993" + +# Everything unspecified will be here (inbetween Browsing and Data) + +# FTP, Mail... +DATAPORTS="110 25 21 137:139 4662 4664 " + +# The lowest priority traffic: eDonkey, Bittorrent, etc. +#P2PPORTS="6881:6999 36892 8333" +P2PPORTS="" + +######################################################################## +# CONFIGURATION ENDS HERE +######################################################################## + +if [ -z "$DEV" ] ; then + echo "$0: device not set, aborting." + exit -1 +fi + +function stop() { + # Reset everything to a known state (cleared) + tc qdisc del dev $DEV root &> /dev/null + tc qdisc del dev $DEV ingress &> /dev/null + + # Flush and delete tables + iptables -t mangle --delete POSTROUTING -o $DEV -j THESHAPER &> /dev/null + iptables -t mangle --flush THESHAPER &> /dev/null + iptables -t mangle --delete-chain THESHAPER &> /dev/null +} + +function start() { + #if [ -z "$DOWNLINK" ] ; then + #echo "$0: start requires a downlink speed, aborting." + #exit -1 + #fi + if [ -z "$UPLINK" ] ; then + echo "$0: start requires an uplink speed, aborting." + exit -1 + fi + + # add HFSC root qdisc + tc qdisc add dev $DEV root handle 1: hfsc default 5 + + # add main rate limit class + tc class add dev $DEV parent 1: classid 1:1 hfsc \ + sc rate ${UPLINK}kbit ul rate ${UPLINK}kbit + + # Interactive traffic: guarantee full uplink for 50ms, then + # 5/10 of the uplink + tc class add dev $DEV parent 1:1 classid 1:2 hfsc \ + sc m1 ${UPLINK}kbit d 50ms m2 $((5*$UPLINK/10))kbit \ + ul rate ${UPLINK}kbit + + # VoIP: guarantee full uplink for 200ms, then 3/10 + tc class add dev $DEV parent 1:1 classid 1:3 hfsc \ + sc m1 ${UPLINK}kbit d 200ms m2 $((3*$UPLINK/10))kbit \ + ul rate ${UPLINK}kbit + + # Browsing: guarantee 3/10 uplink for 200ms, then + # guarantee 1/10 + tc class add dev $DEV parent 1:1 classid 1:4 hfsc \ + sc m1 $((3*$UPLINK/10))kbit d 200ms m2 $((1*$UPLINK/10))kbit \ + ul rate ${UPLINK}kbit + + # Default traffic: guarantee 1/10 uplink for 100ms, + # then guarantee 3/20 + tc class add dev $DEV parent 1:1 classid 1:5 hfsc \ + sc m1 $((1*$UPLINK/10))kbit d 100ms m2 $((3*$UPLINK/20))kbit \ + ul rate ${UPLINK}kbit + + # Middle-low taffic: don't guarantee anything for the first 5 seconds, + # then guarantee 1/10 + tc class add dev $DEV parent 1:1 classid 1:6 hfsc \ + sc m1 0 d 5s m2 $((1*$UPLINK/10))kbit \ + ul rate ${UPLINK}kbit + + # Lowest taffic: don't guarantee anything for the first 10 seconds, + # then guarantee 1/20 + #ls m2 $((1*$UPLINK/200))kbit \ + tc class add dev $DEV parent 1:1 classid 1:7 hfsc \ + sc m1 0 d 5s m2 10kbit \ + ul rate $((UPLINK-200))kbit + + # add THESHAPER chain to the mangle table in iptables + iptables -t mangle --new-chain THESHAPER + iptables -t mangle --insert POSTROUTING -o $DEV -j THESHAPER + + # Type of service filters (see /etc/iproute2/rt_dsfield) + iptables -t mangle -A THESHAPER \ + -m tos --tos 0x10 \ + -j CLASSIFY --set-class 1:2 + + iptables -t mangle -A THESHAPER \ + -m tos --tos 0x08 \ + -j CLASSIFY --set-class 1:7 + + # To speed up downloads while an upload is going on, put short ACK + # packets in the interactive class + iptables -t mangle -A THESHAPER \ + -p tcp \ + -m tcp --tcp-flags FIN,SYN,RST,ACK ACK \ + -m length --length :64 \ + -j CLASSIFY --set-class 1:2 + + # put large (512+) icmp packets in default category + iptables -t mangle -A THESHAPER \ + -p icmp \ + -m length --length 512: \ + -j CLASSIFY --set-class 1:5 + + # small ICMP in the interactive class + iptables -t mangle -A THESHAPER \ + -p icmp \ + -m length --length :512 \ + -j CLASSIFY --set-class 1:2 + + setclassbyport() { + port=$1 + CLASS=$2 + iptables -t mangle -A THESHAPER -p udp --sport $port -j CLASSIFY --set-class $CLASS + iptables -t mangle -A THESHAPER -p udp --dport $port -j CLASSIFY --set-class $CLASS + iptables -t mangle -A THESHAPER -p tcp --sport $port -j CLASSIFY --set-class $CLASS + iptables -t mangle -A THESHAPER -p tcp --dport $port -j CLASSIFY --set-class $CLASS + } + + for port in $INTERACTIVEPORTS; do setclassbyport $port 1:2; done + for port in $VOIPPORTS; do setclassbyport $port 1:3; done + for port in $BROWSINGPORTS; do setclassbyport $port 1:4; done + for port in $DATAPORTS; do setclassbyport $port 1:6; done + for port in $P2PPORTS; do setclassbyport $port 1:7; done + + for VOIP in $VOIPIPS; do + iptables -t mangle -A THESHAPER --src $VOIP -j CLASSIFY --set-class 1:3 + iptables -t mangle -A THESHAPER --dst $VOIP -j CLASSIFY --set-class 1:3 + done + + # put large (1024+) https packets in default category + iptables -t mangle -A THESHAPER \ + -p tcp --dport 443 \ + -m length --length 1024: \ + -j CLASSIFY --set-class 1:6 + + # put large (1024+) http packets in default category + iptables -t mangle -A THESHAPER \ + -p tcp --dport 80 \ + -m length --length 1024: \ + -j CLASSIFY --set-class 1:6 + + # put large (1024+) packets in default category + #iptables -t mangle -A THESHAPER \ + #-p tcp \ + #-m length --length 1024: \ + #-j CLASSIFY --set-class 1:6 + + # put large (1024+) ssh packets in default category + iptables -t mangle -A THESHAPER \ + -p tcp --dport 22 \ + -m length --length 1024: \ + -j CLASSIFY --set-class 1:6 + + # put all traffic from user torrent into p2p category (only works for the host this script runs on) + #iptables -t mangle -A THESHAPER \ + #--match owner --uid-owner 169 -j CLASSIFY --set-class 1:7 + + # Try to control the incoming traffic as well. + # Set up ingress qdisc + #tc qdisc add dev $DEV handle ffff: ingress + + # Filter everything that is coming in too fast + # It's mostly HTTP downloads that keep jamming the downlink, so try to restrict + # them to 95/100 of the downlink. + + # FIXME: slows down too much + #tc filter add dev $DEV parent ffff: protocol ip prio 50 \ + #u32 match ip src 0.0.0.0/0 \ + #match ip protocol 6 0xff \ + #match ip sport 80 0xffff \ + #police rate $((95*${DOWNLINK}/100))kbit \ + #burst 10k drop flowid :1 + + #tc filter add dev $DEV parent ffff: protocol ip prio 50 \ + #u32 match ip src 0.0.0.0/0 \ + #police rate $((95*${DOWNLINK}/100))kbit \ + #burst $((95*${DOWNLINK}/100*2)) drop flowid :1 +} + +function status() { + echo "[qdisc]" + tc -s qdisc show dev $DEV + + echo "" + echo "[class]" + tc -s class show dev $DEV + + echo "" + echo "[filter]" + tc -s filter show dev $DEV + + echo "" + echo "[iptables]" + iptables -n -t mangle -L THESHAPER -v -x +} + +case "$1" in + status) + status + ;; + stop) + stop + ;; + start) + start + ;; + restart) + stop + start + ;; + *) + echo "$0 [ACTION] [device]" + echo "ACTION := { start | stop | status | restart }" + exit + ;; +esac diff --git a/qos.pl b/qos.pl new file mode 100755 index 0000000..c874159 --- /dev/null +++ b/qos.pl @@ -0,0 +1,364 @@ +#!/usr/bin/perl -T +use warnings; +use strict; + +use v5.10; +use autodie; +use Data::Dumper; +use File::Slurp; +use List::Util qw(reduce); +use POSIX; +use Time::HiRes qw(sleep time); +use Term::ANSIColor qw(colored); + +=head1 NAME + +qos.pl - Show some network QoS statistics + +=head1 SYNOPSIS + +qos.pl [interval [history_size]] + + Options: + interval: Set the sampling interval in seconds + history_size: Set the number of samples to produce averages with + +=head1 DESCRIPTION + +Use with hfsc_shaper.sh. + +This programm will output QoS metrics. If an interval is set, it will output +the metrics roughly once per interval. If a history size N is set, it will +display the average traffic per second using N samples. + +The output can be adjusted in the source code of this script. + +=cut + +my $device = "extern0"; +my %classes = ( + "1:2" => "interactive", + "1:3" => "voip", + "1:4" => "browsing", + "1:5" => "default", + "1:6" => "mid-low", + "1:7" => "low/data", +); +my %bandwidth = ( + "up" => 5500, + "down" => 97280, +); +$bandwidth{"1:2"} = 6*$bandwidth{up}/10; +$bandwidth{"1:3"} = 5*$bandwidth{up}/10; +$bandwidth{"1:4"} = 1*$bandwidth{up}/10; +$bandwidth{"1:5"} = 1*$bandwidth{up}/10; +$bandwidth{"1:6"} = 1*$bandwidth{up}/10; +$bandwidth{"1:7"} = 1*$bandwidth{up}/20; + +my %bwthresh = ( + warn => 60, + crit => 80, +); +my %bwcolors = ( + ok => "green", + warn => "rgb531", + crit => "red", +); + +my $colored_rates = 1; +my $show_debug_info = 0; + +my @display_rows = ( + [qw(time space interface-name)], + #[qw(1:2 1:5 space total-up-tc)], + #[qw(1:3 1:6 space total-up)], + #[qw(1:4 1:7 space total-down)], + [qw(1:2 1:5)], + [qw(1:3 1:6)], + [qw(1:4 1:7)], + [], + [qw(total-up-tc total-up total-down)], +); + +sub untaint { + my $data = shift; + my $regex = shift; + + $data =~ m/^($regex)$/ or die "Failed to untaint: $data"; + return $1; +} + +sub format_bytes { + my $bytes = shift; + my $is_rate = shift; + my $boundry = 2048; + my $format; + my $unit; + + my @suffix = qw(B KiB MiB GiB TiB); + + for (@suffix) { + $unit = $_; + last if (abs($bytes) < $boundry); + $bytes /= 1024; + } + + if ($unit eq "B") { + $format = "%.0f"; + } else { + $format = "%.2f"; + } + + if ($is_rate) { + return sprintf $format." %-5s", $bytes, $unit."/s"; + } else { + return sprintf $format." %-3s", $bytes, $unit; + } +} + +sub format_rate_color { + my $bytes = shift; + my $direction = shift; + my $format; + my $relrate = 0; + my $color = $bwcolors{ok}; + my $formatted_bytes = format_bytes($bytes, 1); + + if ($colored_rates) { + $relrate = $bytes/128 * 100.00 / $bandwidth{$direction}; + if ($relrate >= $bwthresh{crit}) { + $color = $bwcolors{crit}; + } elsif ($relrate >= $bwthresh{warn}) { + $color = $bwcolors{warn}; + } else { + $color = $bwcolors{ok}; + } + } + + return colored( sprintf("%13s", $formatted_bytes), $color); +} + +sub parse_tc_output { + my $output = shift; + my $timestamp = shift; + my $history_values = shift; + my $history_size_limit = shift; + + my $class = undef; + my %results = (); + + for (split /^/, $output) { + if (m/^class [a-z]+ (?[^ ]+)/) { + $class = $+{class}; + } + + if ($class && defined($classes{$class})) { + if (m/ Sent (?[0-9]+) bytes/) { + $results{$class} = $+{sent}; + + if ($history_size_limit > 0) { + # keep history of previous values + if (!defined($history_values->{$class})) { + $history_values->{$class} = []; + push @{$history_values->{$class}}, { + value => $+{sent}, + value_diff => 0, + time_diff => 0, + time => $timestamp, + }; + } else { + my $last_time = $history_values->{$class}[-1]{time}; + my $last_value = $history_values->{$class}[-1]{value}; + push @{$history_values->{$class}}, { + value => $+{sent}, + value_diff => $+{sent} - $last_value, + time_diff => $timestamp - $last_time, + time => $timestamp, + }; + } + + # limit history size + if (0+@{$history_values->{$class}} > $history_size_limit) { + splice @{$history_values->{$class}}, 0, 1; + } + } + } + } + + if (m/^$/) { + $class = undef; + } + } + + return \%results; +} + +sub get_interface_speed { + my $interface = shift; + my $history_size_limit = shift; + + state $speed_history = {}; + + my $tx_bytes = read_file("/sys/class/net/$interface/statistics/tx_bytes"); + my $rx_bytes = read_file("/sys/class/net/$interface/statistics/rx_bytes"); + my $timestamp = time; + + if (!defined($speed_history->{$interface})) { + $speed_history->{$interface} = []; + push @{$speed_history->{$interface}}, { + rx_value => $rx_bytes, + tx_value => $tx_bytes, + rx_diff => 0, + tx_diff => 0, + time => $timestamp, + time_diff => 0, + }; + } else { + my $last_time = $speed_history->{$interface}[-1]{time}; + my $last_rx = $speed_history->{$interface}[-1]{rx_value}; + my $last_tx = $speed_history->{$interface}[-1]{tx_value}; + push @{$speed_history->{$interface}}, { + rx_value => $rx_bytes, + tx_value => $tx_bytes, + rx_diff => $rx_bytes - $last_rx, + tx_diff => $tx_bytes - $last_tx, + time => $timestamp, + time_diff => $timestamp - $last_time, + }; + } + + # limit history size + if (0+@{$speed_history->{$interface}} > $history_size_limit) { + splice @{$speed_history->{$interface}}, 0, 1; + } + + my $total_time = reduce {$a + $b->{time_diff}} 0, @{$speed_history->{$interface}}; + my $total_rx = reduce {$a + $b->{rx_diff}} 0, @{$speed_history->{$interface}}; + my $total_tx = reduce {$a + $b->{tx_diff}} 0, @{$speed_history->{$interface}}; + + my $rx_speed = 0; + my $tx_speed = 0; + + $rx_speed = $total_rx/$total_time if $total_time != 0; + $tx_speed = $total_tx/$total_time if $total_time != 0; + + return ($rx_speed, $tx_speed); +} + +sub print_output_table { + my $results = shift; + my $history_values = shift; + my $history_size_limit = shift; + my $starttime = shift; + my $after_tc = shift; + my $interval = shift; + my $first_run = shift; + + my $output_buffer = ""; + + my $global_speed = 0; + my ($rx_speed, $tx_speed); + + if ($history_size_limit > 0) { + ($rx_speed, $tx_speed) = get_interface_speed($device, $history_size_limit); + } + + for my $class_id (keys %classes) { + if ($history_values->{$class_id}) { + my $total_time = reduce {$a + $b->{time_diff}} 0, @{$history_values->{$class_id}}; + my $total_value = reduce {$a + $b->{value_diff}} 0, @{$history_values->{$class_id}}; + my $speed = 0; + + $speed = $total_value / $total_time if $total_time != 0; + $global_speed += $speed; + } + } + + for my $row (@display_rows) { + for my $col (@{$row}) { + if ($col =~ /^total-.*/) { + if ($history_size_limit > 0) { + if ($col eq "total-up") { + $output_buffer .= sprintf "%s up (tc)", format_rate_color($global_speed, "up"); + } elsif ($col eq "total-up-tc") { + $output_buffer .= sprintf "%s up (interface)", format_rate_color($tx_speed, "up"); + } elsif ($col eq "total-down") { + $output_buffer .= sprintf "%s down (interface)", format_rate_color($rx_speed, "down"); + } + } + } elsif ($col eq "space") { + $output_buffer .= " "x4; + } elsif ($col eq "spaaaaaace") { + if ($interval) { + $output_buffer .= " "x81; + } else { + $output_buffer .= " "x14; + } + } elsif ($col eq "interface-name") { + $output_buffer .= $device; + } elsif ($col eq "time") { + $output_buffer .= POSIX::strftime("%Y-%m-%d %H:%M:%S ", localtime); + } else { + my $class_id = $col; + if ($history_values->{$class_id}) { + my $total_time = reduce {$a + $b->{time_diff}} 0, @{$history_values->{$class_id}}; + my $total_value = reduce {$a + $b->{value_diff}} 0, @{$history_values->{$class_id}}; + my $speed = 0; + + $speed = $total_value / $total_time if $total_time != 0; + + $output_buffer .= sprintf "%14s (%s): %11s %s", $classes{$class_id}, $class_id, format_bytes($results->{$class_id}), format_rate_color($speed, $class_id); + } else { + $output_buffer .= sprintf "%14s (%s): %11s", $classes{$class_id}, $class_id, format_bytes($results->{$class_id}); + } + } + } + $output_buffer .= "\n"; + } + + my $runtime = time - $starttime; + my $runtime_tc = $after_tc - $starttime; + printf "WARNING: processing took %0.4fs, skipping sleep. Consider raising interval!\n", $runtime if $runtime > $interval and $interval > 0; + + $output_buffer = sprintf "Runtime for this iteration: %0.4fs (tc: %0.4fs = %0.2f%%)\n%s", $runtime, $runtime_tc, $runtime_tc / $runtime * 100, $output_buffer if $show_debug_info; + if (!$first_run) { + my $newline_count = (split /^/, $output_buffer); + # move cursor to start of previous table and clear screen + printf "[%dF", $newline_count; + } + print $output_buffer; +} + +sub main { + my $interval = 0; + my %history_values = (); + my $history_size_limit = 0; + my $first_run = 1; + + if (0+@ARGV >= 1) { + $interval = $ARGV[0]; + } + + if (0+@ARGV >= 2) { + $history_size_limit = $ARGV[1]; + } + + $ENV{PATH} = untaint($ENV{PATH}, qr(.*)); + + while (1) { + my $starttime = time; + my $stats = `tc -s class show dev $device`; + my $after_tc = time; + + my $results = parse_tc_output($stats, $after_tc, \%history_values, $history_size_limit); + + print_output_table($results, \%history_values, $history_size_limit, $starttime, $after_tc, $interval, $first_run); + + $first_run = 0; + last unless $interval > 0; + my $runtime = time - $starttime; + sleep($interval - $runtime) unless $runtime > $interval; + } +} + +main(); -- cgit v1.2.3-24-g4f1b