summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFlorian Pritz <bluewind@xinu.at>2016-07-23 13:29:07 +0200
committerFlorian Pritz <bluewind@xinu.at>2016-07-23 13:29:07 +0200
commit38ad80b8672a7ecce6bd9f607900c77dfd70da07 (patch)
tree37fd22f76e8a1babbbd4e6dd41fa13055f71aa25
downloadqos-38ad80b8672a7ecce6bd9f607900c77dfd70da07.tar.gz
qos-38ad80b8672a7ecce6bd9f607900c77dfd70da07.tar.xz
Initial commit
Signed-off-by: Florian Pritz <bluewind@xinu.at>
-rwxr-xr-xhfsc_shaper.sh274
-rwxr-xr-xqos.pl364
2 files changed, 638 insertions, 0 deletions
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 <bluewind@xinu.at>
+# 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 = $+{class};
+ }
+
+ if ($class && defined($classes{$class})) {
+ if (m/ Sent (?<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();