Skip to content

Instantly share code, notes, and snippets.

@illarionov
Created January 19, 2013 22:54
Show Gist options
  • Save illarionov/4575711 to your computer and use it in GitHub Desktop.
Save illarionov/4575711 to your computer and use it in GitHub Desktop.
Cкрипт, синхронизирующий состояние IPFW с текстовым конфигом с шейперами sync_shapers.pl. Описание: http://forum.nag.ru/forum/index.php?showtopic=54379&view=findpost&p=518655
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use Class::Struct;
use Data::Dumper;
use Fcntl qw(:flock);
use Getopt::Long;
use Pod::Usage;
struct (
Host => {
ip=>'$',
kbps_in=>'$',
kbps_out=>'$',
shaper_name=>'$'
}
);
package ShaperConfig;
use Class::Struct;
#Конфиг с шейперами
#ips - Хеш с IP адресами, к которым применяются шейперы
#Ключ - ip (addr/masklen).
#Значение - Host.
#
# shapers - хеш по имени шейпера
#Ключ - имя шейпера
#Значение - хеш с хостами: ключ - ip (addr/masklen), значение - Host
#По ключу "" хранятся общие шейперы
struct (
'ShaperConfig' => {
ips=>'*%',
shapers=>'*%'
}
);
#load(fname)
#
#Загрузка файла fname - конфига с шейперами.
#
#Каждая строка файла в формате
#ip speed [shaper_name]
#
#IP - IP адрес в формате addr[/masklen]
#speed - скорость в килобитах. unlim - не шейпировать
#shaper_name - уникальное имя шейпера, если необходим один шейпер на
# несколько IP адресов
sub load {
my ($class, $fname) = @_;
my (%ips, %shapers);
open(FH, $fname) || die("Cannot open config file `$fname`: $!");
while (<FH>) {
chomp;
next if (/^\s?$/);
next if (/^#/);
my ($ip, $speed, $shaper_name) = split(/\s+/);
if ($ip !~ /\S+\/\S+/) {
$ip .= "/32";
}
if (!defined($speed)
|| (lc($speed) eq 'unlim')
|| (lc($speed) eq 'unlimited')) {
$speed = 0;
}
my $h = Host->new(
ip=>$ip,
kbps_in=>$speed,
kbps_out=>$speed,
shaper_name=>$shaper_name||""
);
$ips{$ip} = $h;
$shapers{$shaper_name||''}->{$ip} = $h;
}
close(FH);
return $class->new(
ips=>\%ips, shapers=>\%shapers
);
}
package Pipe;
use Class::Struct;
struct (
'Pipe' => {
'bw_kbps' => '$',
'mask_src_proto' => '$',
'mask_src_ip' => '$',
'mask_src_port' => '$',
'mask_dst_ip' => '$',
'mask_dst_port' => '$',
'mask_proto' => '$',
'delay' => '$',
'burst' => '$',
}
);
sub eq {
my ($self, $pipe) = @_;
return 0 if (!defined($self->bw_kbps) || !defined($pipe->bw_kbps));
foreach (qw/bw_kbps mask_src_proto mask_src_ip mask_src_port
mask_dst_ip mask_dst_port mask_proto delay burst/) {
my $p0 = defined($self->$_) ? $self->$_ : 0;
my $p1 = defined($pipe->$_) ? $pipe->$_ : 0;
return 0 if ($p0 != $p1);
}
return 1;
}
package Ipfw;
use Class::Struct;
struct (
'Ipfw' => {
ipfw_binary => '$',
dry_run => '$',
start_pipe => '$',
last_pipe_num => '$',
pipes=>'*%'
}
);
sub ipfw {
return shift->ipfw_binary || '/sbin/ipfw';
}
sub table_list {
my ($self, $num) = @_;
my %res;
open(IPFW, $self->ipfw . " table $num list|") or die("IPFW `table $num list` error: $!");
while (<IPFW>) {
chomp;
my ($ip, $tablearg) = split(/\s+/);
$res{$ip} = $tablearg+0;
}
close(IPFW) || die($!);
return \%res;
}
sub pipe_show {
my ($self, $num) = @_;
return $self->pipes->{$num} if (defined $self->pipes->{$num});
my $pipe = Pipe->new;
open(IPFW, $self->ipfw . " pipe $num show|") or die("IPFW `pipe $num show` error: $!");
while (<IPFW>) {
chomp;
if ($_ =~
/^(\d+)\:\s+(?:(\d+\.\d{1,4})\s+([KM]bit\/s)|(unlimited))\s+(\d+)\s+ms/) {
my $n = $1+0;
if ($n == $num) {
if (defined($4) && $4 eq 'unlimited') {
$pipe->bw_kbps(0);
}else {
my $kbps = $2;
my $unit = $3;
if ($unit eq 'Mbit/s') {
$kbps = $kbps * 1000.00;
}
$pipe->bw_kbps(int($kbps));
}
$pipe->delay($5);
}
}elsif ($_ =~ /^\s+mask\:\s+(0x[0-9a-fA-F]{2})\s+(0x[0-9a-fA-F]{8})\/(0x[0-9a-fA-F]{4})\s+->\s+(0x[0-9a-fA-F]{8})\/(0x[0-9a-fA-F]{4})$/) {
$pipe->mask_src_proto(hex($1)) if (hex($1) != 0);
$pipe->mask_src_ip(hex($2)) if (hex($2) != 0);
$pipe->mask_src_port(hex($3)) if (hex($3) != 0);
$pipe->mask_dst_ip(hex($4)) if (hex($4) != 0);
$pipe->mask_dst_port(hex($5)) if (hex($5) != 0);
}
}
close(IPFW);
$self->pipes->{$num} = $pipe if (defined($pipe->bw_kbps));
return $pipe;
}
sub find_n_create_pipe {
my ($self, $p) = @_;
my $num;
while (my ($n0, $pipe) = each(%{$self->pipes})) {
if (!$num && ($pipe->eq($p))) {
$num = $n0;
}
}
return $num if ($num);
return $self->pipe_config(0, $p);
}
sub pipe_config {
my ($self, $num, $pipe) = @_;
if (!$num) {
$num = defined($self->last_pipe_num) ? $self->last_pipe_num : $self->start_pipe;
while (exists($self->pipes->{$num})) {
$num++;
}
$self->last_pipe_num($num+1);
}
my @cmd = ($self->ipfw,
'pipe', $num, 'config','bw',$pipe->bw_kbps."Kbit/s");
if ($pipe->mask_src_ip || $pipe->mask_dst_ip) {
push (@cmd, 'mask');
if ($pipe->mask_src_ip) {
push (@cmd, 'src-ip', sprintf('0x%x', $pipe->mask_src_ip));
}
if ($pipe->mask_dst_ip) {
push (@cmd, 'dst-ip', sprintf('0x%x', $pipe->mask_dst_ip));
}
}
print("exec: ", join(" ", @cmd), "\n");
$self->pipes->{$num} = $pipe;
return $num if ($self->dry_run);
system (@cmd);
if ($? != 0) {
delete($self->pipes->{$num});
warn("Cannot set pipe `$num`");
return 0;
}
return $num;
}
sub table_add {
my ($self, $table_num, $ip, $tablearg) = @_;
my @cmd = ($self->ipfw,
'table', $table_num, 'add',$ip);
push (@cmd, $tablearg) if (defined $tablearg);
print("exec: ", join(" ", @cmd), "\n");
return 1 if ($self->dry_run);
system (@cmd);
if ($? != 0) {
warn("Cannot add host `$ip` to table `$table_num`");
return 0;
}
return 1;
}
sub table_delete {
my ($self, $table_num, $ip, $tablearg) = @_;
my @cmd = ($self->ipfw,
'table', $table_num, 'delete',$ip);
print("exec: ", join(" ", @cmd), "\n");
return 1 if ($self->dry_run);
system (@cmd);
if ($? != 0) {
warn("Cannot delete host `$ip` from table `$table_num`");
return 0;
}
return 1;
}
package main;
#Синхронизация шейперов
sub sync {
my ($ipfw, $shapers_conf, $tin_num, $tout_num) = @_;
my %ipfw_shapers;
print("Sync\n");
#Удаляем все IP адреса, которые есть в ipfw, но нет в конфиге
sub delete_unknown_ips {
my ($ipfw, $shapers_conf, $ips, $table_num, $ipfw_shapers) = @_;
foreach my $ip (keys(%$ips)) {
if (!exists($shapers_conf->ips->{$ip})) {
$ipfw->table_delete($table_num, $ip);
delete($ips->{$ip});
}else {
#Если Ip остается, загружаем конфиг его шейпера
my $pipe_num = $ips->{$ip};
if (defined($pipe_num) && ($pipe_num != 0)) {
$ipfw->pipe_show($pipe_num);
$ipfw_shapers->{$pipe_num}->{ips}->{$ip} = 1;
}
}
}
}
my $table_in = $ipfw->table_list($tin_num);
delete_unknown_ips($ipfw, $shapers_conf, $table_in, $tin_num,
\%ipfw_shapers);
my $table_out = $ipfw->table_list($tout_num);
delete_unknown_ips($ipfw, $shapers_conf, $table_out, $tout_num,
\%ipfw_shapers);
#Сравнимаем общие шейперы
while (my ($ip, $host) = each(%{$shapers_conf->shapers->{""}})) {
#pipe-in
my $pipe_in = $table_in->{$ip};
my $p_in = Pipe->new(bw_kbps=>int($host->kbps_in),
mask_dst_ip=>0xffffffff);
if (defined($pipe_in)) {
if (!defined($ipfw->pipes->{$pipe_in})
|| (!$p_in->eq($ipfw->pipes->{$pipe_in}))
) {
$ipfw->table_delete($tin_num, $ip);
$pipe_in = undef;
}
}
if (!defined($pipe_in)) {
#Ищем подходящий шейпер. Если не находим, создаем новый
$pipe_in = $ipfw->find_n_create_pipe($p_in);
$ipfw->table_add($tin_num, $ip, $pipe_in);
}
#pipe-out
my $pipe_out = $table_out->{$ip};
my $p_out = Pipe->new(bw_kbps=>int($host->kbps_out),
mask_src_ip=>0xffffffff);
if (defined($pipe_out)) {
if (!defined($ipfw->pipes->{$pipe_out})
|| (!$p_out->eq($ipfw->pipes->{$pipe_out}))
) {
$ipfw->table_delete($tout_num, $ip);
$pipe_out = undef;
}
}
if (!defined($pipe_out)) {
#Ищем подходящий шейпер. Если не находим, создаем новый
$pipe_out = $ipfw->find_n_create_pipe($p_out);
$ipfw->table_add($tout_num, $ip, $pipe_out);
}
} #shapers ''
delete($shapers_conf->shapers->{""});
#Индивидуальные шейперы
#Сравниваем IP адреса: ищм шейперы с одинаковыми списками IP адресов
foreach my $shaper_name (keys(%{$shapers_conf->shapers})) {
my $is_eq = 0;
my $iplist_cfg = $shapers_conf->shapers->{$shaper_name};
next if (keys(%$iplist_cfg)==0);
my $first_ip = (keys(%$iplist_cfg))[0];
my $host = $iplist_cfg->{$first_ip};
my $pipe_in = $table_in->{$first_ip};
my $pipe_out = $table_out->{$first_ip};
IFEQ: {
last IFEQ if (!$pipe_in || !$pipe_out);
my @ips_conf = keys(%{$iplist_cfg});
my %ips_conf_h = map({$_=>1} @ips_conf);
#pipe-in
my @ips_ipfw = keys(%{$ipfw_shapers{$pipe_in}->{ips}});
last IFEQ if (scalar(@ips_ipfw) != scalar(@ips_conf));
foreach my $ip (@ips_ipfw) {
last IFEQ if (!exists($ips_conf_h{$ip}));
#XXX: Создаем новый шейпер, если данный шейпер используется как
#для входящего, так и для исходящего трафика
last IFEQ if (exists($table_out->{$ip})
&& ($table_out->{$ip} == $pipe_in));
}
#pipe-out
@ips_ipfw = keys(%{$ipfw_shapers{$pipe_out}->{ips}});
last IFEQ if (scalar(@ips_ipfw) != scalar(@ips_conf));
foreach my $ip (@ips_ipfw) {
last IFEQ if (!exists($ips_conf_h{$ip}));
last IFEQ if (exists($table_in->{$ip})
&& ($table_in->{$ip} == $pipe_out));
}
$is_eq=1;
}
my $new_pipe = Pipe->new(bw_kbps=>$host->kbps_in);
if ($is_eq) {
#Сверяем параметры шейперов. Меняем их, если не совпадают
#pipe_in
my $pipe = $ipfw_shapers{$pipe_in};
if (!defined($ipfw->pipes->{$pipe_in})
|| !$ipfw->pipes->{$pipe_in}->eq($new_pipe)) {
$ipfw->pipe_config($pipe_in, $new_pipe);
}
if (!defined($ipfw->pipes->{$pipe_out})
|| !$ipfw->pipes->{$pipe_out}->eq($new_pipe)) {
$ipfw->pipe_config($pipe_out, $new_pipe);
}
}else {
#Создаем новые шейперы
$pipe_in = $ipfw->pipe_config(0, $new_pipe);
$pipe_out = $ipfw->pipe_config(0, $new_pipe);
#Заливаем списки IP адресов
foreach my $ip (keys(%$iplist_cfg)) {
#in
if (defined $table_in->{$ip}) {
$ipfw->table_delete($tin_num, $ip);
}
$ipfw->table_add($tin_num, $ip, $pipe_in);
#out
if (defined $table_out->{$ip}) {
$ipfw->table_delete($tout_num, $ip);
}
$ipfw->table_add($tout_num, $ip, $pipe_out);
} #foreach my $ip (keys(%$iplist_cfg))
}#else is_eq
} #shaper_name
}
my $help;
my $man;
my %params=(
lock_file=>'/tmp/sync_shapers.lock',
shapers_file=>'/var/db/shapers',
start_pipe=>1000,
table_in=>126,
table_out=>127,
dry_run=>0
);
GetOptions(
'help|?' => \$help,
'man' => \$man,
'lock_file=s' => \$params{lock_file},
'shapers=s' => \$params{shapers_file},
'start_pipe=i' => \$params{start_pipe},
'table_in=i' => \$params{table_in},
'table_out=i' => \$params{table_out},
'dry_run' => \$params{dry_run}
) or pod2usage(2);
pod2usage(-verbose => 1) if $help;
pod2usage(-verbose => 2) if $man;
open(LOCK_FILE, ">", $params{lock_file}) || die("cannot open lock file
`".$params{lock_file}."`: $!");
flock(LOCK_FILE, LOCK_EX);
my $ipfw = Ipfw->new(start_pipe=>$params{start_pipe}, dry_run=>$params{dry_run});
my $shapers_conf = ShaperConfig->load($params{shapers_file});
sync($ipfw, $shapers_conf, $params{table_in}, $params{table_out});
flock(LOCK_FILE, LOCK_UN);
close(LOCK_FILE);
=head1 sync_shapers.pl
sync_shapers.pl - sync IPFW shapers
=head1 SYNOPSIS
sync_shapers.pl [-h] [params]
-help brief help message
-man full documentation
-shapers <fname> shapers config file. (defaukt: /var/db/shapers)
-start_pipe <pipe_num> first IPFW pipe num (default: 1000)
-table_in <table_num> IPFW table for incoming data (default: 126)
-table_out <table_num> IPFW table for outgoing data (default: 127)
-dry_run Print the commands that would be executed but do not execute them.
-lock_file <fname> lock file (default: /tmp/sync_shapers.lock)
=head1 OPTIONS
=over 8
=item B<-help>
Print help message and exits.
=item B<-shapers> <fname>
Shapers config file. (default: /var/db/shapers)
Format:
addr[/masklen] speed [shaper_name]
=item B<-start_pipe> <pipe_num>
First IPFW pipe num (default: 1000)
=item B<-table_in> <table_num>
Table for incoming data (default: 126)
=item B<-table_out> <table_num>
Table for outgoing data (default: 127)
=item B<-dry_run>
Print the commands that would be executed but do not execute them.
=item B<-lock_file> <fname>
Lock file (default: /tmp/sync_shapers.lock)
=back
=head1 DESCRIPTION
Sync IPFW from B<shapers> file.
=head1 EXAMPLES
=over 8
=item B<IPFW rules:>
pipe tablearg ip from any to table(<table_in>) in
pipe tablearg ip from table(<table_out>) to any out
=item B<Shapers file:>
192.168.0.1/32 1000
192.168.0.2/32 1000
192.168.0.3/32 2000
192.168.0.4/32 500 shaper0
192.168.0.5/32 500 shaper0
=back
=head1 VERSION
0.1 (06 jun 2010)
=head1 AUTHOR
Alexey Illarionov <littlesavage@rambler.ru>
=head1 LICENSE
Public Domain
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment