Created
January 19, 2013 22:54
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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