#
# Shorewall-perl 4.4 -- /usr/share/shorewall-perl/Shorewall/Tc.pm
#
#     This program is under GPL [http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt]
#
#     (c) 2007,2008,2009 - Tom Eastep (teastep@shorewall.net)
#
#     Traffic Control is from tc4shorewall Version 0.5
#     (c) 2005 Arne Bernin <arne@ucbering.de>
#     Modified by Tom Eastep for integration into the Shorewall distribution
#     published under GPL Version 2#
#
#       Complete documentation is available at http://shorewall.net
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of Version 2 of the GNU General Public License
#       as published by the Free Software Foundation.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
#   This module deals with Traffic Shaping and the tcrules file.
#
package Shorewall::Tc;
require Exporter;
use Shorewall::Config qw(:DEFAULT :internal);
use Shorewall::IPAddrs;
use Shorewall::Zones;
use Shorewall::Chains qw(:DEFAULT :internal);
use Shorewall::Providers;

use strict;

our @ISA = qw(Exporter);
our @EXPORT = qw( setup_tc );
our @EXPORT_OK = qw( process_tc_rule initialize );
our $VERSION = '4.3_7';

our %tcs = ( T => { chain  => 'tcpost',
		    connmark => 0,
		    fw       => 1
		  } ,
	    CT => { chain  => 'tcpost' ,
		    target => 'CONNMARK --set-mark' ,
		    connmark => 1 ,
		    fw       => 1
		    } ,
	    C  => { target => 'CONNMARK --set-mark' ,
		    connmark => 1 ,
		    fw       => 1
		    } ,
	    P  => { chain    => 'tcpre' ,
		    connmark => 0 ,
		    fw       => 0
		    } ,
	    CP => { chain    => 'tcpre' ,
		    target => 'CONNMARK --set-mark' ,
		    connmark => 1 ,
		    fw       => 0
		    } ,
	    F =>  { chain    => 'tcfor' ,
		    connmark => 0 ,
		    fw       => 0
		    } ,
	    CF => { chain    => 'tcfor' ,
		    connmark => 1 ,
		    fw       => 0 ,
		    } ,
	    );

use constant { NOMARK    => 0 ,
	       SMALLMARK => 1 ,
	       HIGHMARK  => 2
	       };

our @tccmd = ( { match     => sub ( $ ) { $_[0] eq 'SAVE' } ,
		 target    => 'CONNMARK --save-mark --mask' ,
		 mark      => SMALLMARK ,
		 mask      => '0xFF' ,
		 connmark  => 1
	       } ,
	      { match     => sub ( $ ) { $_[0] eq 'RESTORE' },
		target    => 'CONNMARK --restore-mark --mask' ,
		mark      => SMALLMARK ,
		mask      => '0xFF' ,
		connmark  => 1
		} ,
	      { match     => sub ( $ ) { $_[0] eq 'CONTINUE' },
		target    => 'RETURN' ,
		mark      => NOMARK ,
		mask      => '' ,
		connmark  => 0
		} ,
	      { match     => sub ( $ ) { $_[0] eq 'SAME' },
		target    => 'sticky' ,
		mark      => NOMARK ,
		mask      => '' ,
		connmark  => 0
		} ,
	      { match     => sub ( $ ) { $_[0] =~ /^IPMARK/ },
		target    => 'IPMARK' ,
		mark      => NOMARK,
		mask      => '',
		connmark  => 0
	        } ,
	      { match     => sub ( $ ) { $_[0] =~ '\|.*'} ,
		target    => 'MARK --or-mark' ,
		mark      => HIGHMARK ,
		mask      => '' } ,
	      { match     => sub ( $ ) { $_[0] =~ '&.*' },
		target    => 'MARK --and-mark ' ,
		mark      => HIGHMARK ,
		mask      => '' ,
		connmark  => 0
		}
	      );

our %flow_keys = ( 'src'            => 1,
		   'dst'            => 1,
		   'proto'          => 1,
		   'proto-src'      => 1,
		   'proto-dst'      => 1,
		   'iif'            => 1,
		   'priority'       => 1,
		   'mark'           => 1,
		   'nfct'           => 1,
		   'nfct-src'       => 1,
		   'nfct-dst'       => 1,
		   'nfct-proto-src' => 1,
		   'nfct-proto-dst' => 1,
		   'rt-classid'     => 1,
		   'sk-uid'         => 1,
		   'sk-gid'         => 1,
		   'vlan-tag'       => 1 );

our %classids;

our @deferred_rules;

#
# Perl version of Arn Bernin's 'tc4shorewall'.
#
# TCDevices Table
#
# %tcdevices { <interface> -> {in_bandwidth  => <value> ,
#                              out_bandwidth => <value> ,
#                              number        => <number>,
#                              classify      => 0|1
#                              tablenumber   => <next u32 table to be allocated for this device>
#                              default       => <default class mark value>
#                              redirected    => [ <dev1>, <dev2>, ... ]
#                                               }
#
our @tcdevices;
our %tcdevices;
our @devnums;
our $devnum;
our $sticky;


#
# TCClasses Table
#
# %tcclasses { device    => <device> ,
#              mark      => <mark> ,
#              number    => <number> ,
#              rate      => <rate> ,
#              ceiling   => <ceiling> ,
#              priority  => <priority> ,
#              options   => { tos  => [ <value1> , <value2> , ... ];
#                             tcp_ack => 1 ,
#                             ...
#

our @tcclasses;
our %tcclasses;

our %restrictions = ( tcpre      => PREROUTE_RESTRICT ,
		      tcpost     => POSTROUTE_RESTRICT ,
		      tcfor      => NO_RESTRICT ,
		      tcout      => OUTPUT_RESTRICT );

our $family;

#
# Initialize globals -- we take this novel approach to globals initialization to allow
#                       the compiler to run multiple times in the same process. The
#                       initialize() function does globals initialization for this
#                       module and is called from an INIT block below. The function is
#                       also called by Shorewall::Compiler::compiler at the beginning of
#                       the second and subsequent calls to that function.
#

sub initialize( $ ) {
    $family   = shift;
    %classids = ();
    @deferred_rules = ();
    @tcdevices = ();
    %tcdevices = ();
    @tcclasses = ();
    %tcclasses = ();
    @devnums   = ();
    $devnum = 0;
    $sticky = 0;
}

INIT {
    initialize( F_IPV4 );
}

sub process_tc_rule( $$$$$$$$$$$$ ) {
    my ( $originalmark, $source, $dest, $proto, $ports, $sports, $user, $testval, $length, $tos , $connbytes , $helper ) = @_;

    my ( $mark, $designator, $remainder ) = split( /:/, $originalmark, 3 );

    fatal_error "Invalid MARK ($originalmark)" if defined $remainder || ! defined $mark || $mark eq '';

    my $chain  = $globals{MARKING_CHAIN};
    my $target = 'MARK --set-mark';
    my $tcsref;
    my $connmark = 0;
    my $classid  = 0;
    my $device   = '';
    my $fw       = firewall_zone;
    my $list;

    if ( $source ) {
	if ( $source eq $fw ) {
	    $chain = 'tcout';
	    $source = '';
	} else {
	    $chain = 'tcout' if $source =~ s/^($fw)://;
	}
    }

    if ( $designator ) {
	$tcsref = $tcs{$designator};

	if ( $tcsref ) {
	    if ( $chain eq 'tcout' ) {
		fatal_error "Invalid chain designator for source $fw" unless $tcsref->{fw};
	    }

	    $chain    = $tcsref->{chain}  if $tcsref->{chain};
	    $target   = $tcsref->{target} if $tcsref->{target};
	    $mark     = "$mark/0xFF"      if $connmark = $tcsref->{connmark};

	    require_capability ('CONNMARK' , "CONNMARK Rules", '' ) if $connmark;

	} else {
	    fatal_error "Invalid MARK ($originalmark)"   unless $mark =~ /^([0-9]+|0x[0-9a-f]+)$/ and $designator =~ /^([0-9]+|0x[0-9a-f]+)$/;

	    if ( $config{TC_ENABLED} eq 'Internal' ) {
		fatal_error "Unknown Class ($originalmark)}" unless ( $device = $classids{$originalmark} );
	    }

	    $chain   = 'tcpost';
	    $classid = 1;
	    $mark    = $originalmark;
	    $target  = 'CLASSIFY --set-class';
	}
    }

    my $mask = 0xffff;

    my ($cmd, $rest) = split( '/', $mark, 2 );

    $list = '';

    unless ( $classid ) {
      MARK:
	{
	    for my $tccmd ( @tccmd ) {
		if ( $tccmd->{match}($cmd) ) {
		    fatal_error "$mark not valid with :C[FPT]" if $connmark;

		    require_capability ('CONNMARK' , "SAVE/RESTORE Rules", '' ) if $tccmd->{connmark};

		    $target      = "$tccmd->{target} ";
		    my $marktype = $tccmd->{mark};

		    if ( $marktype == NOMARK ) {
			$mark = ''
		    } else {
			$mark =~ s/^[|&]//;
		    }

		    if ( $target eq 'sticky ' ) {
			if ( $chain eq 'tcout' ) {
			    $target = 'sticko';
			} else {
			    fatal_error "SAME rules are only allowed in the PREROUTING and OUTPUT chains" if $chain ne 'tcpre';
			}

			$sticky++;
		    } elsif ( $target eq 'IPMARK ' ) {
			my ( $srcdst, $mask1, $mask2, $shift ) = ('src', 255, 0, 0 );

			require_capability 'IPMARK_TARGET', 'IPMARK', 's';

			if ( $cmd =~ /^IPMARK\((.+?)\)$/ ) {
			    my $params = $1;
			    my $val;

			    my ( $sd, $m1, $m2, $s , $bad ) = split ',', $params;

			    fatal_error "Invalid IPMARK parameters ($params)" if $bad;
			    fatal_error "Invalid IPMARK parameter ($sd)" unless ( $sd eq 'src' || $sd eq 'dst' );
			    $srcdst = $sd;

			    if ( defined $m1 && $m1 ne '' ) {
				$val = numeric_value ($m1);
				fatal_error "Invalid Mask ($m1)" unless defined $val;
				$mask1 = $m1;
			    }

			    if ( defined $m2 && $m2 ne '' ) {
				$val = numeric_value ($m2);
				fatal_error "Invalid Mask ($m2)" unless defined $val;
				$mask2 = $m2;
			    }
				
			    if ( defined $s ) {
				$val = numeric_value ($s);
				fatal_error "Invalid Shift Bits ($s)" unless defined $val;
				$shift = $s;
			    }
			} else {
			    fatal_error "Invalid MARK/CLASSIFY ($cmd)" unless $cmd eq 'IPMARK';
			}
			    
			$target = "IPMARK --addr $srcdst --and-mask $mask1 --or-mask $mask2 --shift $shift";
		    }

		    if ( $rest ) {
			fatal_error "Invalid MARK ($originalmark)" if $marktype == NOMARK;

			$mark = $rest if $tccmd->{mask};

			if ( $marktype == SMALLMARK ) {
			    verify_small_mark $mark;
			} else {
			    validate_mark $mark;
			}
		    } elsif ( $tccmd->{mask} ) {
			$mark = $tccmd->{mask};
		    }

		    last MARK;
		}
	    }

	    validate_mark $mark;

	    if ( $config{HIGH_ROUTE_MARKS} ) {
		my $val = numeric_value( $cmd );
		fatal_error "Invalid MARK/CLASSIFY ($cmd)" unless defined $val;
		fatal_error 'Marks < 256 may not be set in the PREROUTING or OUTPUT chains when HIGH_ROUTE_MARKS=Yes'
		    if $cmd && ( $chain eq 'tcpre' || $chain eq 'tcout' ) && $val <= 0xFF;
	    }
	}
    }

    if ( ( my $result = expand_rule( ensure_chain( 'mangle' , $chain ) ,
				     $restrictions{$chain} ,
				     do_proto( $proto, $ports, $sports) . 
				     do_user( $user ) . 
				     do_test( $testval, $mask ) . 
				     do_length( $length ) . 
				     do_tos( $tos ) . 
				     do_connbytes( $connbytes ) . 
				     do_helper( $helper ),
				     $source ,
				     $dest ,
				     '' ,
				     "-j $target $mark" ,
				     '' ,
				     '' ,
				     '' ) )
	  && $device ) {
	#
	# expand_rule() returns destination device if any
	#
	fatal_error "Class Id $originalmark is not associated with device $result" if $device ne $result;
    }

    progress_message "  TC Rule \"$currentline\" $done";

}

sub rate_to_kbit( $ ) {
    my $rate = $_[0];

    return 0           if $rate eq '-';
    return $1          if $rate =~ /^(\d+)kbit$/i;
    return $1 * 1000   if $rate =~ /^(\d+)mbit$/i;
    return $1 * 8000   if $rate =~ /^(\d+)mbps$/i;
    return $1 * 8      if $rate =~ /^(\d+)kbps$/i;
    return int($1/125) if $rate =~ /^(\d+)(bps)?$/;
    fatal_error "Invalid Rate ($rate)";
}

sub calculate_r2q( $ ) {
    my $rate = rate_to_kbit $_[0];
    my $r2q= $rate / 200 ;
    $r2q <= 5 ? 5 : $r2q;
}

sub calculate_quantum( $$ ) {
    my ( $rate, $r2q ) = @_;
    $rate = rate_to_kbit $rate;
    int( ( $rate * 125 ) / $r2q );
}

sub process_flow($) {
    my $flow = shift;

    $flow =~ s/^\(// if $flow =~ s/\)$//;

    my @flow = split /,/, $flow;

    for ( @flow ) {
	fatal_error "Invalid flow key ($_)" unless $flow_keys{$_};
    }

    $flow;
}

sub validate_tc_device( $$$$$ ) {
    my ( $device, $inband, $outband , $options , $redirected ) = @_;

    my $devnumber;

    if ( $device =~ /:/ ) {
	( my $number, $device, my $rest )  = split /:/, $device, 3;

	fatal_error "Invalid NUMBER:INTERFACE ($device:$number:$rest)" if defined $rest;

	if ( defined $number ) {
	    $devnumber = numeric_value( $number );
	    fatal_error "Invalid interface NUMBER ($number)" unless defined $devnumber && $devnumber;
	    fatal_error "Duplicate interface number ($number)" if defined $devnums[ $devnumber ];
	    $devnum = $devnumber if $devnumber > $devnum;
	} else {
	    fatal_error "Missing interface NUMBER";
	}
    } else {
	$devnumber = ++$devnum;
    }

    $devnums[ $devnumber ] = $device;

    fatal_error "Duplicate INTERFACE ($device)"    if $tcdevices{$device};
    fatal_error "Invalid INTERFACE name ($device)" if $device =~ /[:+]/;

    my ( $classify, $pfifo, $flow)  = (0, 0, '' );

    if ( $options ne '-' ) {
	for my $option ( split_list1 $options, 'option' ) {
	    if ( $option eq 'classify' ) {
		$classify = 1;
	    } elsif ( $option =~ /^flow=(.*)$/ ) {
		fatal_error "The 'flow' option is not allowed with 'pfifo'" if $pfifo;
		$flow = process_flow $1;
	    } elsif ( $option eq 'pfifo' ) {
		fatal_error "The 'pfifo'' option is not allowed with 'flow='" if $flow;
		$pfifo = 1;
	    } else {
		fatal_error "Unknown device option ($option)";
	    }
	}
    }

    my @redirected = ();

    @redirected = split_list( $redirected , 'device' ) if defined $redirected && $redirected ne '-';

    if ( @redirected ) {
	fatal_error "IFB devices may not have IN-BANDWIDTH" if $inband ne '-' && $inband;
	$classify = 1;
    }	

    for my $rdevice ( @redirected ) {
	fatal_error "Invalid device name ($rdevice)" if $rdevice =~ /[:+]/;
	my $rdevref = $tcdevices{$rdevice};
	fatal_error "REDIRECTED device ($rdevice) has not been defined in this file" unless $rdevref;
	fatal_error "IN-BANDWIDTH must be zero for REDIRECTED devices" if $rdevref->{in_bandwidth} ne '0kbit';
    }

    $tcdevices{$device} = { in_bandwidth  => rate_to_kbit( $inband ) . 'kbit' ,
			    out_bandwidth => rate_to_kbit( $outband ) . 'kbit' ,
			    number        => $devnumber,
			    classify      => $classify ,
			    flow          => $flow ,
			    pfifo         => $pfifo ,
			    tablenumber   => 1 ,
			    redirected    => \@redirected ,
			  } ,

    push @tcdevices, $device;

    progress_message "  Tcdevice \"$currentline\" $done.";
}

sub convert_rate( $$$ ) {
    my ($full, $rate, $column) = @_;

    if ( $rate =~ /\bfull\b/ ) {
	$rate =~ s/\bfull\b/$full/g;
	fatal_error "Invalid $column ($_[1])" if $rate =~ m{[^0-9*/+()-]};
	no warnings;
	$rate = eval "int( $rate )";
	use warnings;
	fatal_error "Invalid $column ($_[1])" unless defined $rate;
    } else {
	$rate = rate_to_kbit $rate
    }

    fatal_error "$column may not be zero" unless $rate;
    fatal_error "$column ($_[1]) exceeds OUT-BANDWIDTH" if $rate > $full;

    $rate;
}

sub dev_by_number( $ ) {
    my $dev = $_[0];
    my $devnum = numeric_value( $dev );
    my $devref;

    if ( defined $devnum ) {
	$dev = $devnums[ $devnum ];
	fatal_error "Undefined INTERFACE number ($_[0])" unless defined $dev;
	$devref = $tcdevices{$dev};
	assert( $devref );
    } else {
	$devref = $tcdevices{$dev};
	fatal_error "Unknown INTERFACE ($dev)" unless $devref;
    }

    ( $dev , $devref );

}

sub validate_tc_class( $$$$$$ ) {
    my ( $devclass, $mark, $rate, $ceil, $prio, $options ) = @_;

    my %tosoptions = ( 'tos-minimize-delay'       => 'tos=0x10/0x10' ,
		       'tos-maximize-throughput'  => 'tos=0x08/0x08' ,
		       'tos-maximize-reliability' => 'tos=0x04/0x04' ,
		       'tos-minimize-cost'        => 'tos=0x02/0x02' ,
		       'tos-normal-service'       => 'tos=0x00/0x1e' );

    my $classnumber = 0;
    my $devref;
    my $device = $devclass;

    if ( $devclass =~ /:/ ) {
	( $device, my ($number, $rest ) )  = split /:/, $device, 3;
	fatal_error "Invalid INTERFACE:CLASS ($devclass)" if defined $rest;

	( $device , $devref) = dev_by_number( $device );

	if ( defined $number ) {
	    if ( $devref->{classify} ) {
		$classnumber = numeric_value( $number );
		fatal_error "Invalid interface NUMBER ($number)" unless defined $classnumber && $classnumber;
		fatal_error "Duplicate interface/class number ($number)" if defined $devnums[ $classnumber ];
	    } else {
		warning_message "Class NUMBER ignored -- INTERFACE $device does not have the 'classify' option";
	    }
	} else {
	    fatal_error "Missing interface NUMBER";
	}
    } else {
	($device, $devref ) = dev_by_number( $device );
	fatal_error "Missing class NUMBER" if $devref->{classify};
    }

    my $full  = rate_to_kbit $devref->{out_bandwidth};

    $tcclasses{$device} = {} unless $tcclasses{$device};
    my $tcref = $tcclasses{$device};

    my $markval = 0;

    if ( $mark ne '-' ) {
	if ( $devref->{classify} ) {
	    warning_message "INTERFACE $device has the 'classify' option - MARK value ($mark) ignored";
	} else {
	    fatal_error "Invalid Mark ($mark)" unless $mark =~ /^([0-9]+|0x[0-9a-fA-F]+)$/ && numeric_value( $mark ) <= 0xff;

	    $markval = numeric_value( $mark );
	    fatal_error "Invalid MARK ($markval)" unless defined $markval;
	    fatal_error "Duplicate MARK ($mark)" if $tcref->{$classnumber};
	    $classnumber = $devnum . $mark;
	}
    } else {
	fatal_error "Missing MARK" unless $devref->{classify};
	fatal_error "Duplicate Class NUMBER ($classnumber)" if $tcref->{$classnumber};
    }

    $tcref->{$classnumber} = { tos      => [] ,
			       rate     => convert_rate( $full, $rate, 'RATE' ) ,
			       ceiling  => convert_rate( $full, $ceil, 'CEIL' ) ,
			       priority => $prio eq '-' ? 1 : $prio ,
			       mark     => $markval ,
			       flow     => '' ,
			       pfifo    => 0
			     };

    $tcref = $tcref->{$classnumber};

    fatal_error "RATE ($tcref->{rate}) exceeds CEIL ($tcref->{ceiling})" if $tcref->{rate} > $tcref->{ceiling};

    unless ( $options eq '-' ) {
	for my $option ( split_list1 "\L$options", 'option' ) {
	    my $optval = $tosoptions{$option};

	    $option = $optval if $optval;

	    if ( $option eq 'default' ) {
		fatal_error "Only one default class may be specified for device $device" if $devref->{default};
		$devref->{default} = $classnumber;
	    } elsif ( $option eq 'tcp-ack' ) {
		$tcref->{tcp_ack} = 1;
	    } elsif ( $option =~ /^tos=0x[0-9a-f]{2}$/ ) {
		( undef, $option ) = split /=/, $option;
		push @{$tcref->{tos}}, "$option/0xff";
	    } elsif ( $option =~ /^tos=0x[0-9a-f]{2}\/0x[0-9a-f]{2}$/ ) {
		( undef, $option ) = split /=/, $option;
		push @{$tcref->{tos}}, $option;
	    } elsif ( $option =~ /^flow=(.*)$/ ) {
		fatal_error "The 'flow' option is not allowed with 'pfifo'" if $tcref->{pfifo};
		$tcref->{flow} = process_flow $1;
	    } elsif ( $option eq 'pfifo' ) {
		fatal_error "The 'pfifo'' option is not allowed with 'flow='" if $tcref->{flow};
		$tcref->{pfifo} = 1;
	    } else {
		fatal_error "Unknown option ($option)";
	    }
	}
    }

    $tcref->{flow}  = $devref->{flow}  unless $tcref->{flow};
    $tcref->{pfifo} = $devref->{pfifo} unless $tcref->{flow} || $tcref->{pfifo};

    push @tcclasses, "$device:$classnumber";
    progress_message "  Tcclass \"$currentline\" $done.";
}

#
# Process a record from the tcfilters file
#
sub process_tc_filter( $$$$$$ ) {
    my ($devclass , $source, $dest , $proto, $portlist , $sportlist ) = @_;

    my ($device, $class, $rest ) = split /:/, $devclass, 3;

    fatal_error "Invalid INTERFACE:CLASS ($devclass)" if defined $rest || ! ($device && $class );

    ( $device , my $devref ) = dev_by_number( $device );

    my $devnum = $devref->{number};

    my $tcref = $tcclasses{$device};

    fatal_error "No Classes were defined for INTERFACE $device" unless $tcref;

    $tcref = $tcref->{$class};

    fatal_error "Unknown CLASS ($devclass)" unless $tcref; 

    my $rule = "filter add dev $device protocol ip parent $devnum:0 pref 10 u32";

    my ( $net , $mask ) = decompose_net( $source );

    $rule .= "\\\n   match u32 $net $mask at 12" unless $mask eq '0x00000000';

    ( $net , $mask ) = decompose_net( $dest );

    $rule .= "\\\n   match u32 $net $mask at 16" unless $mask eq '0x00000000';

    my $protonumber = 0;

    unless ( $proto eq '-' ) {
	$protonumber = resolve_proto $proto;
	fatal_error "Unknown PROTO ($proto)" unless defined $protonumber;

	if ( $protonumber ) {
	    my $pnumber = in_hex2 $protonumber;
	    $rule .= "\\\n   match u8 $pnumber 0xff at 9";
	}
    }

    if ( $portlist eq '-' && $sportlist eq '-' ) {
	emit( "\nrun_tc $rule\\" ,
	      "   flowid $devref->{number}:$class" ,
	      '' );
    } else {
	our $lastrule;
	our $lasttnum;
	#
	# In order to be able to access the protocol header, we must create another hash table and link to it.
	#
	# Create the Table.
	#
	my $tnum;

	if ( $lastrule eq $rule ) {
	    #
	    # The source, dest and protocol are the same as the last rule that specified a port
	    # Use the same table
	    #
	    $tnum = $lasttnum
	} else {
	    $tnum     = in_hex3 $devref->{tablenumber}++;
	    $lasttnum = $tnum;
	    $lastrule = $rule;

	    emit( "\nrun_tc filter add dev $device parent $devnum:0 protocol ip pref 10 handle $tnum: u32 divisor 1" );
	}
	#
	# And link to it using the current contents of $rule
	#
	emit( "\nrun_tc $rule\\" ,
	      "   link $tnum:0 offset at 0 mask 0x0F00 shift 6 plus 0 eat" );
	#
	# The rule to match the port(s) will be inserted into the new table
	#
	$rule     = "filter add dev $device protocol ip parent $devnum:0 pref 10 u32 ht $tnum:0";

	if ( $portlist eq '-' ) {
	    fatal_error "Only TCP, UDP and SCTP may specify SOURCE PORT" 
		unless $protonumber == TCP || $protonumber == UDP || $protonumber == SCTP;

	    for my $sportrange ( split_list $sportlist , 'port list' ) {
		my @sportlist = expand_port_range $protonumber , $sportrange;

		while ( @sportlist ) {
		    my ( $sport, $smask ) = ( shift @sportlist, shift @sportlist );
		    emit( "\nrun_tc $rule\\" ,
			  "   match u32 0x${sport}0000 0x${smask}0000 at nexthdr+0\\" ,
			  "   flowid $devref->{number}:$class" );
		}
	    }
	} else {
	    fatal_error "Only TCP, UDP, SCTP and ICMP may specify DEST PORT" 
		unless $protonumber == TCP || $protonumber == UDP || $protonumber == SCTP || $protonumber == ICMP;

	    for my $portrange ( split_list $portlist, 'port list' ) {
		if ( $protonumber == ICMP ) {
		    fatal_error "SOURCE PORT(S) are not allowed with ICMP" if $sportlist ne '-';

		    my ( $icmptype , $icmpcode ) = split '//', validate_icmp( $portrange );

		    $icmptype = in_hex2 numeric_value1 $icmptype;
		    $icmpcode = in_hex2 numeric_value1 $icmpcode if defined $icmpcode;

		    my $rule1 = "   match u8 $icmptype 0xff at nexthdr+0";
		    $rule1   .= "\\\n   match u8 $icmpcode 0xff at nexthdr+1" if defined $icmpcode;
		    emit( "\nrun_tc ${rule}\\" ,
			  "$rule1\\" ,
			  "   flowid $devref->{number}:$class" );
		} else {
		    my @portlist = expand_port_range $protonumber , $portrange;

		    while ( @portlist ) {
			my ( $port, $mask ) = ( shift @portlist, shift @portlist );

			my $rule1 = "match u32 0x0000${port} 0x0000${mask} at nexthdr+0";

			if ( $sportlist eq '-' ) {
			    emit( "\nrun_tc ${rule}\\" ,
				  "   $rule1\\" ,
				  "   flowid $devref->{number}:$class" );
			} else {
			    for my $sportrange ( split_list $sportlist , 'port list' ) {
				my @sportlist = expand_port_range $protonumber , $sportrange;

				while ( @sportlist ) {
				    my ( $sport, $smask ) = ( shift @sportlist, shift @sportlist );

				    emit( "\nrun_tc ${rule}\\",
					  "   $rule1\\" ,
					  "   match u32 0x${sport}0000 0x${smask}0000 at nexthdr+0\\" ,
					  "   flowid $devref->{number}:$class" );
				}
			    }
			}   
		    }
		}    
	    }
	}
    }

    emit '';

    progress_message "  TC Filter \"$currentline\" $done";

    $currentline =~ s/\s+/ /g;

    save_progress_message_short qq("   TC Filter \"$currentline\" defined.");

    emit '';

}   

sub setup_traffic_shaping() {
    our $lastrule = '';

    save_progress_message "Setting up Traffic Control...";

    my $fn = open_file 'tcdevices';

    if ( $fn ) {
	first_entry "$doing $fn...";

	while ( read_a_line ) {

	    my ( $device, $inband, $outband, $options , $redirected ) = split_line 3, 5, 'tcdevices';

	    fatal_error "Invalid tcdevices entry" if $outband eq '-';
	    validate_tc_device( $device, $inband, $outband , $options , $redirected );
	}
    }

    $devnum = $devnum > 10 ? 10 : 1;

    $fn = open_file 'tcclasses';

    if ( $fn ) {
	first_entry "$doing $fn...";

	while ( read_a_line ) {

	    my ( $device, $mark, $rate, $ceil, $prio, $options ) = split_line 4, 6, 'tcclasses file';

	    validate_tc_class( $device, $mark, $rate, $ceil, $prio, $options );
	}
    }

    for my $device ( @tcdevices ) {
	my $dev     = chain_base( $device );
	my $devref  = $tcdevices{$device};
	my $defmark = $devref->{default} || 0;
	my $devnum  = $devref->{number};

	emit "if interface_is_up $device; then";

	push_indent;

	emit ( "${dev}_exists=Yes",
	       "qt \$TC qdisc del dev $device root",
	       "qt \$TC qdisc del dev $device ingress",
	       "run_tc qdisc add dev $device root handle $devnum: htb default $defmark",
	       "${dev}_mtu=\$(get_device_mtu $device)",
	       "${dev}_mtu1=\$(get_device_mtu1 $device)",
	       "run_tc class add dev $device parent $devnum: classid $devnum:1 htb rate $devref->{out_bandwidth} \$${dev}_mtu1"
	       );

	my $inband = rate_to_kbit $devref->{in_bandwidth};

	if ( $inband ) {
	    emit ( "run_tc qdisc add dev $device handle ffff: ingress",
		   "run_tc filter add dev $device parent ffff: protocol ip pref 10 u32 match ip src 0.0.0.0/0 police rate ${inband}kbit burst 10k drop flowid :1"
		   );
	}

	for my $rdev ( @{$devref->{redirected}} ) {
	    emit ( "run_tc qdisc add dev $rdev handle ffff: ingress" );
	    emit( "run_tc filter add dev $rdev parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev $device > /dev/null" );
	}

	save_progress_message_short "   TC Device $device defined.";

	pop_indent;
	emit 'else';
	push_indent;

	emit qq(error_message "WARNING: Device $device is not in the UP state -- traffic-shaping configuration skipped");
	emit "${dev}_exists=";
	pop_indent;
	emit "fi\n";
    }

    my $lastdevice = '';

    for my $class ( @tcclasses ) {
	my ( $device, $classnum ) = split /:/, $class;
	my $devref  = $tcdevices{$device};
	my $tcref   = $tcclasses{$device}{$classnum};
	my $mark    = $tcref->{mark};
	my $devicenumber  = $devref->{number};
	my $classid = join( '', $devicenumber, ':', $classnum);
	my $rate    = "$tcref->{rate}kbit";
	my $quantum = calculate_quantum $rate, calculate_r2q( $devref->{out_bandwidth} );
	my $dev     = chain_base $device;

	$classids{$classid}=$device;

	if ( $lastdevice ne $device ) {
	    if ( $lastdevice ) {
		pop_indent;
		emit "fi\n";
	    }

	    emit qq(if [ -n "\$${dev}_exists" ]; then);
	    push_indent;
	    $lastdevice = $device;
	}

	emit ( "[ \$${dev}_mtu -gt $quantum ] && quantum=\$${dev}_mtu || quantum=$quantum",
	       "run_tc class add dev $device parent $devref->{number}:1 classid $classid htb rate $rate ceil $tcref->{ceiling}kbit prio $tcref->{priority} \$${dev}_mtu1 quantum \$quantum" );

	emit( "run_tc qdisc add dev $device parent $classid handle ${classnum}: sfq quantum \$quantum limit 127 perturb 10" ) unless $tcref->{pfifo};
	#
	# add filters
	#
	emit "run_tc filter add dev $device protocol ip parent $devicenumber:0 prio 1 handle $mark fw classid $classid" unless $devref->{classify};
	emit "run_tc filter add dev $device protocol ip pref 1 parent $classnum: protocol ip handle $classnum flow hash keys $tcref->{flow} divisor 1024" if $tcref->{flow};
	#
	#options
	#
	emit "run_tc filter add dev $device parent $devref->{number}:0 protocol ip prio 10 u32 match ip protocol 6 0xff match u8 0x05 0x0f at 0 match u16 0x0000 0xffc0 at 2 match u8 0x10 0xff at 33 flowid $classid" if $tcref->{tcp_ack};

	for my $tospair ( @{$tcref->{tos}} ) {
	    my ( $tos, $mask ) = split q(/), $tospair;
	    emit "run_tc filter add dev $device parent $devicenumber:0 protocol ip prio 10 u32 match ip tos $tos $mask flowid $classid";
	}

	save_progress_message_short qq("   TC Class $class defined.");
	emit '';
    }

    if ( $lastdevice ) {
	pop_indent;
	emit "fi\n";
    }

    if ( $family == F_IPV4 ) {
	$fn = open_file 'tcfilters';

	if ( $fn ) {
	    first_entry( sub { progress_message2 "$doing $fn..."; save_progress_message "Adding TC Filters"; } );

	    while ( read_a_line ) {

		my ( $devclass, $source, $dest, $proto, $port, $sport ) = split_line 2, 6, 'tcfilters file';

		process_tc_filter( $devclass, $source, $dest, $proto, $port, $sport );
	    }
	}
    }
}

#
# Process the tcrules file and setup traffic shaping
#
sub setup_tc() {

    if ( $capabilities{MANGLE_ENABLED} && $config{MANGLE_ENABLED} ) {
	ensure_mangle_chain 'tcpre';
	ensure_mangle_chain 'tcout';

	if ( $capabilities{MANGLE_FORWARD} ) {
	    ensure_mangle_chain 'tcfor';
	    ensure_mangle_chain 'tcpost';
	}

	my $mark_part = '';

	if ( @routemarked_interfaces && ! $config{TC_EXPERT} ) {
	    $mark_part = $config{HIGH_ROUTE_MARKS} ? '-m mark --mark 0/0xFF00' : '-m mark --mark 0/0xFF';

	    for my $interface ( @routemarked_interfaces ) {
		add_rule $mangle_table->{PREROUTING} , "-i $interface -j tcpre";
	    }
	}

	add_rule $mangle_table->{PREROUTING} , "$mark_part -j tcpre";
	add_rule $mangle_table->{OUTPUT} ,     "$mark_part -j tcout";

	if ( $capabilities{MANGLE_FORWARD} ) {
	    add_rule $mangle_table->{FORWARD} ,     '-j tcfor';
	    add_rule $mangle_table->{POSTROUTING} , '-j tcpost';
	}

	if ( $config{HIGH_ROUTE_MARKS} ) {
	    for my $chain qw(INPUT FORWARD POSTROUTING) {
		insert_rule1 $mangle_table->{$chain}, 0, '-j MARK --and-mark 0xFF';
	    }
	}
    }

    if ( $globals{TC_SCRIPT} ) {
	save_progress_message 'Setting up Traffic Control...';
	append_file $globals{TC_SCRIPT};
    } elsif ( $config{TC_ENABLED} eq 'Internal' ) {
	setup_traffic_shaping;
    }

    if ( $config{TC_ENABLED} ) {
	if ( my $fn = open_file 'tcrules' ) {

	    first_entry( sub { progress_message2 "$doing $fn..."; require_capability 'MANGLE_ENABLED' , 'a non-empty tcrules file' , 's'; } );

	    while ( read_a_line ) {

		my ( $mark, $source, $dest, $proto, $ports, $sports, $user, $testval, $length, $tos , $connbytes, $helper ) = split_line1 2, 12, 'tcrules file';

		if ( $mark eq 'COMMENT' ) {
		    process_comment;
		} else {
		    process_tc_rule $mark, $source, $dest, $proto, $ports, $sports, $user, $testval, $length, $tos, $connbytes, $helper;
		}

	    }

	    clear_comment;
	}
    }

    for ( @deferred_rules ) {
	add_rule ensure_chain( 'mangle' , 'tcpost' ), $_;
    }

    handle_stickiness( $sticky );
}

1;
