Prechádzať zdrojové kódy

Updated the script for iptables

Roman Dmitriev 1 mesiac pred
rodič
commit
389cbcc8d5

+ 0 - 291
docs/iptables/parse_ulog.pl

@@ -1,291 +0,0 @@
-#!/usr/bin/perl
-
-#
-# Copyright (C) Roman Dmitiriev, rnd@rajven.ru
-#
-
-use FindBin '$Bin';
-use lib "$Bin/";
-use strict;
-use DBI;
-use Time::Local;
-use Net::Patricia;
-use Data::Dumper;
-use Date::Parse;
-use Socket;
-use eyelib::config;
-use eyelib::main;
-use eyelib::net_utils;
-use eyelib::database;
-
-setpriority(0,0,19);
-
-
-my $router_id;
-if (scalar @ARGV>1) { $router_id=shift(@ARGV); } else { $router_id=$ARGV[0]; }
-
-if (!$router_id) {
-    db_log_error($dbh,"Router id not defined! Bye...");
-    exit 110;
-    }
-
-my $timeshift = get_option($dbh,55)*60;
-
-db_log_debug($dbh,"Import traffic from router id: $router_id start. Timestep $timeshift sec.") if ($debug);
-
-my %stats;
-$stats{pkt}{all}=0;
-$stats{pkt}{user}=0;
-$stats{pkt}{user_in}=0;
-$stats{pkt}{user_out}=0;
-$stats{pkt}{free}=0;
-$stats{pkt}{unknown}=0;
-
-$stats{line}{all}=0;
-$stats{line}{user}=0;
-$stats{line}{free}=0;
-$stats{line}{unknown}=0;
-
-# net objects
-my $users = new Net::Patricia;
-
-InitSubnets();
-
-#get userid list
-my $user_auth_list = $dbh->prepare( "SELECT id,ip,user_id,save_traf FROM user_auth where deleted=0 ORDER by user_id,ip" );
-if ( !defined $user_auth_list ) { die "Cannot prepare statement: $DBI::errstr\n"; }
-
-$user_auth_list->execute;
-
-# user auth list
-my $authlist_ref = $user_auth_list->fetchall_arrayref();
-$user_auth_list->finish();
-
-my %user_stats;
-
-foreach my $row (@$authlist_ref) {
-$users->add_string($row->[1],$row->[0]);
-$user_stats{$row->[0]}{net}=$row->[1];
-$user_stats{$row->[0]}{id}=$row->[0];
-$user_stats{$row->[0]}{user_id}=$row->[2];
-$user_stats{$row->[0]}{save_traf}=$row->[3];
-$user_stats{$row->[0]}{in}=0;
-$user_stats{$row->[0]}{out}=0;
-$user_stats{$row->[0]}{pkt_in}=0;
-$user_stats{$row->[0]}{pkt_out}=0;
-}
-
-my $last_time = localtime();
-
-my $time_string;
-my $dbtime;
-my $hour_date;
-my $minute_date;
-
-my @batch_sql_traf=();
-
-open(FH,"-");
-
-while (my $line=<FH>) {
-$stats{line}{all}++;
-#1555573194.980;17   ;     77.243.0.12;   172.20.178.71;    53; 43432;       1;     134;     2;     1
-$line=~s/\s+//g;
-
-my ($l_time,$l_proto,$l_src_ip,$l_src_port,$l_dst_ip,$l_dst_port,$l_packets,$l_bytes,$l_input,$l_output,$l_prefix) = split(/ /,$line);
-$stats{pkt}{all}+=$l_packets;
-
-if (!$l_time) { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-if ($l_src_ip eq '0.0.0.0') { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-if ($l_dst_ip eq '0.0.0.0') { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-if ($l_src_ip eq '255.255.255.255') { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-if ($l_dst_ip eq '255.255.255.255') { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-if ($Special_Nets->match_string($l_src_ip) or $Special_Nets->match_string($l_dst_ip)) { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-
-#unknown networks
-if (!$office_networks->match_string($l_src_ip) and !$office_networks->match_string($l_dst_ip)) { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-
-#local forward
-if ($office_networks->match_string($l_src_ip) and $office_networks->match_string($l_dst_ip)) { $stats{line}{free}++; $stats{line}{free}+=$l_packets; next; }
-
-#free forward
-if ($office_networks->match_string($l_src_ip) and $free_networks->match_string($l_dst_ip)) { $stats{line}{free}++; $stats{line}{free}+=$l_packets; next; }
-if ($free_networks->match_string($l_src_ip) and $office_networks->match_string($l_dst_ip)) { $stats{line}{free}++; $stats{line}{free}+=$l_packets; next; }
-
-my $l_src_ip_aton=StrToIp($l_src_ip);
-my $l_dst_ip_aton=StrToIp($l_dst_ip);
-
-$last_time = $l_time;
-my ($sec,$min,$hour,$day,$month,$year,$zone) = (localtime($l_time))[0,1,2,3,4,5];
-$month++;
-$year += 1900;
-
-$time_string = sprintf "%04d-%02d-%02d %02d:%02d:%02d",$year,$month,$day,$hour,$min,$sec;
-$dbtime = $dbh->quote($time_string);
-$hour_date = $dbh->quote(sprintf "%04d-%02d-%02d %02d:00:00",$year,$month,$day,$hour);
-$minute_date = $dbh->quote(sprintf "%04d-%02d-%02d %02d:%02d:00",$year,$month,$day,$hour,$min);
-
-my $user_found = 0;
-# find user id
-
-if ($office_networks->match_string($l_src_ip)) {
-    my $out_user = $users->match_string($l_src_ip);
-    if ($out_user) {
-        $user_stats{$out_user}{out} += $l_bytes;
-        $user_stats{$out_user}{dbtime} = $minute_date;
-        $user_stats{$out_user}{htime} = $hour_date;
-        $user_stats{$out_user}{pkt_out} +=$l_packets;
-        $user_found = 1;
-        $stats{line}{user}++;
-        $stats{pkt}{user_out}+=$l_packets;
-        if ($save_detail and $user_stats{$out_user}{save_traf}) {
-            my $dSQL="INSERT INTO traffic_detail (auth_id,router_id,timestamp,proto,src_ip,dst_ip,src_port,dst_port,bytes,pkt) VALUES($out_user,$router_id,$dbtime,'$l_proto',$l_src_ip_aton,$l_dst_ip_aton,'$l_src_port','$l_dst_port','$l_bytes','$l_packets')";
-            push (@batch_sql_traf,$dSQL);
-            }
-        }
-    }
-
-if ($office_networks->match_string($l_dst_ip)) {
-    my $in_user = $users->match_string($l_dst_ip);
-    if ($in_user) {
-        $user_stats{$in_user}{in} += $l_bytes;
-        $user_stats{$in_user}{dbtime} = $minute_date;
-        $user_stats{$in_user}{htime} = $hour_date;
-        $user_stats{$in_user}{pkt_in} +=$l_packets;
-        $stats{line}{user}++;
-        $stats{pkt}{user_in}+=$l_packets;
-        $user_found = 1;
-        if ($save_detail and $user_stats{$in_user}{save_traf}) {
-            my $dSQL="INSERT INTO traffic_detail (auth_id,router_id,timestamp,proto,src_ip,dst_ip,src_port,dst_port,bytes,pkt) VALUES($in_user,$router_id,$dbtime,'$l_proto',$l_src_ip_aton,$l_dst_ip_aton,'$l_src_port','$l_dst_port','$l_bytes','$l_packets')";
-            push (@batch_sql_traf,$dSQL);
-            }
-        }
-    }
-
-if (scalar(@batch_sql_traf)>10000) {
-    $dbh->{AutoCommit} = 0;
-    my $f_sth;
-    foreach my $sSQL(@batch_sql_traf) {
-        $f_sth = $dbh->prepare($sSQL);
-        $f_sth->execute;
-        }
-    $f_sth->finish;
-    $dbh->{AutoCommit} = 1;
-    @batch_sql_traf=();
-    }
-
-if ($users->match_string($l_src_ip) or $users->match_string($l_dst_ip)) { next; }
-if (!$add_unknown_user) { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-
-# find user ip
-my $user_ip;
-my $user_ip_aton;
-undef $user_ip;
-
-#add user by src ip only if dst not office network!!!!
-if (!$office_networks->match_string($l_dst_ip) and $office_networks->match_string($l_src_ip)) { $user_ip = $l_src_ip; }
-
-#skip unknown packet
-if (!$user_ip) { $stats{line}{illegal}++; $stats{pkt}{illegal}+=$l_packets; next; }
-
-$stats{line}{user}++;
-
-$user_ip_aton=StrToIp($user_ip);
-
-#new user
-my $auth_id=new_auth($dbh,$user_ip);
-next if (!$auth_id);
-
-my $new_user = get_record_sql($dbh,"SELECT * FROM user_auth WHERE id=$auth_id");
-
-$users->add_string($user_ip,$auth_id);
-$user_stats{$auth_id}{net}=$user_ip;
-$user_stats{$auth_id}{user_id}=$new_user->{user_id};
-$user_stats{$auth_id}{id}=$auth_id;
-$user_stats{$auth_id}{in}=0;
-$user_stats{$auth_id}{out}=0;
-$user_stats{$auth_id}{pkt_in}=0;
-$user_stats{$auth_id}{pkt_out}=0;
-
-db_log_info($dbh,"Added user_auth id: $auth_id ip: $user_ip user_id: $new_user->{user_id}");
-
-if ($auth_id) {
-        if ($save_detail) {
-            my $dSQL="INSERT INTO traffic_detail (auth_id,router_id,timestamp,proto,src_ip,dst_ip,src_port,dst_port,bytes) VALUES($auth_id,$router_id,$dbtime,'$l_proto',$l_src_ip_aton,$l_dst_ip_aton,'$l_src_port','$l_dst_port','$l_bytes')";
-            push (@batch_sql_traf,$dSQL);
-            }
-        if ($l_src_ip eq $user_ip) {
-            $user_stats{$auth_id}{out} += $l_bytes;
-            $user_stats{$auth_id}{pkt_out} += $l_bytes;
-            }
-        if ($l_dst_ip eq $user_ip) {
-            $user_stats{$auth_id}{in} += $l_bytes;
-            $user_stats{$auth_id}{pkt_in} += $l_bytes;
-            }
-        $user_stats{$auth_id}{dbtime} = $minute_date;
-        $user_stats{$auth_id}{htime} = $hour_date;
-        } else {
-        undef $user_ip;
-        undef $user_ip_aton;
-        }
-}
-
-#start hour
-my ($min,$hour,$day,$month,$year) = (localtime($last_time))[1,2,3,4,5];
-my $hour_date1 = $dbh->quote(sprintf "%04d-%02d-%02d %02d:00:00",$year+1900,$month+1,$day,$hour);
-my $flow_date = $dbh->quote(sprintf "%04d-%02d-%02d %02d:%02d:00",$year+1900,$month+1,$day,$hour,$min);
-
-#end hour
-($min,$hour,$day,$month,$year) = (localtime($last_time+3600))[1,2,3,4,5];
-my $hour_date2 = $dbh->quote(sprintf "%04d-%02d-%02d %02d:00:00",$year+1900,$month+1,$day,$hour);
-
-# update database
-foreach my $row (keys %user_stats) {
-next if (!$user_stats{$row}{htime});
-
-#current stats
-
-my $tSQL="INSERT INTO user_stats_full (timestamp,auth_id,router_id,byte_in,byte_out,pkt_in,pkt_out,step) VALUES($flow_date,'$user_stats{$row}{id}','$router_id','$user_stats{$row}{in}','$user_stats{$row}{out}','$user_stats{$row}{pkt_in}','$user_stats{$row}{pkt_out}','$timeshift')";
-push (@batch_sql_traf,$tSQL);
-
-#hour stats
-
-# get current stats
-my $sql = "SELECT id, byte_in, byte_out FROM user_stats
-WHERE ts>=$hour_date1 AND ts<$hour_date2 AND router_id=$router_id AND auth_id=$user_stats{$row}{id}";
-my $hour_stat = get_record_sql($dbh,$sql);
-if (!$hour_stat) {
-    my $dSQL="INSERT INTO user_stats (timestamp,auth_id,router_id,byte_in,byte_out,pkt_in,pkt_out) VALUES($user_stats{$row}{htime},'$user_stats{$row}{id}','$router_id','$user_stats{$row}{in}','$user_stats{$row}{out}','$user_stats{$row}{pkt_in}','$user_stats{$row}{pkt_out}')";
-    push (@batch_sql_traf,$dSQL);
-    next;
-    }
-if (!$hour_stat->{byte_in}) { $hour_stat->{byte_in}=0; }
-if (!$hour_stat->{byte_out}) { $hour_stat->{byte_out}=0; }
-$hour_stat->{byte_in} += $user_stats{$row}{in};
-$hour_stat->{byte_out} += $user_stats{$row}{out};
-my $ssql="UPDATE user_stats SET byte_in='".$hour_stat->{byte_in}."', byte_out='".$hour_stat->{byte_out}."' WHERE id=".$hour_stat->{id};
-my $res = $dbh->do($ssql);
-}
-
-$dbh->{AutoCommit} = 0;
-my $sth;
-foreach my $sSQL(@batch_sql_traf) {
-$sth = $dbh->prepare($sSQL);
-$sth->execute;
-}
-$sth->finish;
-$dbh->{AutoCommit} = 1;
-
-db_log_debug($dbh,"Import traffic from router id: $router_id stop") if ($debug);
-
-db_log_verbose($dbh,"Recalc quotes started");
-recalc_quotes($dbh,$router_id);
-db_log_verbose($dbh,"Recalc quotes stopped");
-
-db_log_verbose($dbh,"router id: $router_id stop Traffic statistics, lines: all => $stats{line}{all}, user=> $stats{line}{user}, free => $stats{line}{free}, illegal=> $stats{line}{illegal}");
-db_log_verbose($dbh,sprintf("router id: %d stop Traffic speed, line/s: all => %.2f, user=> %.2f, free => %.2f, unknown=> %.2f", $router_id, $stats{line}{all}/$timeshift, $stats{line}{user}/$timeshift, $stats{line}{free}/$timeshift, $stats{line}{illegal}/$timeshift));
-db_log_verbose($dbh,"router id: $router_id stop Traffic statistics, pkt: all => $stats{pkt}{all}, user_in=> $stats{pkt}{user_in}, user_in=> $stats{pkt}{user_out}, free => $stats{pkt}{free}, illegal=> $stats{pkt}{illegal}");
-db_log_verbose($dbh,sprintf("router id: %d stop Traffic speed, pkt/s: all => %.2f, user_in=> %.2f, user_out=> %.2f, free => %.2f, unknown=> %.2f", $router_id, $stats{pkt}{all}/$timeshift, $stats{pkt}{user_in}/$timeshift, $stats{pkt}{user_out}/$timeshift, $stats{pkt}{free}/$timeshift, $stats{pkt}{illegal}/$timeshift));
-
-$dbh->disconnect;
-
-exit 0;

+ 534 - 263
docs/iptables/sync_iptables.pl

@@ -1,11 +1,19 @@
-#!/usr/bin/perl -w
+#!/usr/bin/perl
 
 #
-# Copyright (C) Roman Dmitiriev, rnd@rajven.ru
+# Copyright (C) Roman Dmitriev, rnd@rajven.ru
 #
 
+use utf8;
+use warnings;
+use Encode;
+use open qw(:std :encoding(UTF-8));
+no warnings 'utf8';
+
+use English;
+use base;
 use FindBin '$Bin';
-use lib "$Bin/";
+use lib "/opt/Eye/scripts";
 use strict;
 use Time::Local;
 use FileHandle;
@@ -17,348 +25,611 @@ use Net::Patricia;
 use Date::Parse;
 use eyelib::net_utils;
 use eyelib::database;
-use IPTables::libiptc;
+use eyelib::common;
 use DBI;
-use utf8;
-use open ":encoding(utf8)";
+use Fcntl qw(:flock);
 use Net::DNS;
+use File::Path qw(make_path);
+
+#$debug = 1;
 
-#exit;
+open(SELF,"<",$0) or die "Cannot open $0 - $!";
+flock(SELF, LOCK_EX|LOCK_NB) or exit 1;
 
 $|=1;
 
-my $gate = get_record_sql($dbh,"SELECT * FROM devices WHERE device_type=2 and user_acl=1 and deleted=0 and vendor_id=19 and device_name='".$HOSTNAME."'");
+if (IsNotRun($SPID)) { Add_PID($SPID); }  else { die "Warning!!! $SPID already running!\n"; }
+
+my $router_id    = $ARGV[0];
+
+$debug = 1;
+
+# ============================================================================
+# КОНСТАНТЫ ДЛЯ IPTABLES/IPSET
+# ============================================================================
+my $IPSET_CMD = '/usr/sbin/ipset';
+my $IPTABLES_CMD = '/usr/sbin/iptables';
+my $IPTABLES_TABLE_NAME = 'eye_acl';
+my $IPTABLES_CHAIN_PREFIX = 'EYE_';
+my $IPSET_SAVE_DIR = '/etc/ipset.d';
+my $IPSET_SAVE_EXTENSION = '.conf';
+
+# ============================================================================
+# ОСНОВНАЯ ЛОГИКА
+# ============================================================================
 
-if (!$gate) { exit 0; }
+my $gate = get_record_sql($dbh, 'SELECT * FROM devices WHERE id=?', $router_id );
+
+exit 100 if (!$gate);
+
+my $gate_ident = $gate->{device_name}." [$gate->{ip}]:: ";
+
+my @cmd_list=();
+my @cmd_ipset_list=();
 
 my $router_name=$gate->{device_name};
 my $router_ip=$gate->{ip};
-my $shaper_enabled = $gate->{queue_enabled};
 my $connected_users_only = $gate->{connected_user_only};
 
+my @changed_ref=();
+
+# Лог начала обработки
+log_verbose($gate_ident."=== ACL Sync STARTED ===");
+log_info($gate_ident."Starting ACL synchronization for router $router_name [$router_ip]");
+
+# все сети роутера, которые к нему подключены по информации из БД
+my $connected_users = new Net::Patricia;
+my %connected_nets_hash;
+my %hotspot_exceptions;
 my @lan_int=();
 my @wan_int=();
 
-my @l3_int = get_records_sql($dbh,'SELECT * FROM device_l3_interfaces WHERE device_id='.$gate->{'id'});
+# настройки используемых l3-интерфейсов
+my %l3_interfaces;
+
+my @l3_int = get_records_sql($dbh,'SELECT * FROM device_l3_interfaces WHERE device_id=?',$gate->{'id'});
 foreach my $l3 (@l3_int) {
+    $l3->{'name'}=~s/\"//g;
+    $l3_interfaces{$l3->{'name'}}{type} = $l3->{'interface_type'};
+    $l3_interfaces{$l3->{'name'}}{bandwidth} = 0;
+    if ($l3->{'bandwidth'}) { $l3_interfaces{$l3->{'name'}}{bandwidth} = $l3->{'bandwidth'}; }
     if ($l3->{'interface_type'} eq '0') { push(@lan_int,$l3->{'name'}); }
     if ($l3->{'interface_type'} eq '1') { push(@wan_int,$l3->{'name'}); }
 }
 
-my $connected_users = new Net::Patricia;
+log_verbose($gate_ident."Loaded ".scalar(@l3_int)." L3 interfaces (".scalar(@lan_int)." LAN, ".scalar(@wan_int)." WAN)");
 
-if ($connected_users_only) {
-    foreach my $int (@lan_int) {
-    $int=trim($int);
-    next if (!$int);
-    #get ip addr at interface
-    foreach my $int_str (@lan_int) {
-	$int_str=trim($int_str);
-	my $int_addr=do_exec('/sbin/ip addr show '.$int_str.' | grep "scope global"');
-	foreach my $address (split(/\n/,$int_addr)) {
-	    if ($address=~/inet\s+(.*)\s+brd/i) {
-		if ($1) { $connected_users->add_string($1); }
-		}
-	    }
-	}
+# формируем список подключенных к роутеру сетей
+my @gw_subnets = get_records_sql($dbh,"SELECT gateway_subnets.*,subnets.subnet FROM gateway_subnets LEFT JOIN subnets ON gateway_subnets.subnet_id = subnets.id WHERE gateway_subnets.device_id=?",$gate->{'id'});
+if (@gw_subnets and scalar @gw_subnets) {
+    foreach my $gw_subnet (@gw_subnets) {
+        if ($gw_subnet and $gw_subnet->{'subnet'}) {
+            $connected_users->add_string($gw_subnet->{'subnet'});
+            $connected_nets_hash{$gw_subnet->{'subnet'}} = $gw_subnet;
+        }
     }
 }
 
-db_log_verbose($dbh,"Sync user state at router $router_name started.");
-
-#get userid list
-my $user_auth_sql="SELECT user_auth.ip, user_auth.filter_group_id, user_auth.queue_id, user_auth.id
-FROM user_auth, user_list
-WHERE user_auth.user_id = user_list.id
-AND user_auth.deleted =0
-AND user_auth.enabled =1
-AND user_auth.blocked =0
-AND user_list.blocked =0
-AND user_list.enabled =1
-AND user_auth.ou_id <> $default_hotspot_ou_id
-ORDER BY ip_int";
-
-my @authlist_ref = get_records_sql($dbh,$user_auth_sql);
+log_verbose($gate_ident."Loaded ".scalar(@gw_subnets)." gateway subnets");
+
 my %users;
 my %lists;
-my %found_users;
-
-foreach my $row (@authlist_ref) {
-if ($connected_users_only) { next if (!$connected_users->match_string($row->{ip})); }
-#skip not office ip's
-next if (!$office_networks->match_string($row->{ip}));
-$found_users{$row->{'id'}}=$row->{ip};
-#filter group acl's
-$users{'group_'.$row->{filter_group_id}}->{$row->{ip}}=1;
-$users{'group_all'}->{$row->{ip}}=1;
-$lists{'group_'.$row->{filter_group_id}}=1;
-#queue acl's
-if ($row->{queue_id}) { $users{'queue_'.$row->{queue_id}}->{$row->{ip}}=1; }
-}
 
-log_debug("Users status:".Dumper(\%users));
 
-#full list
+my $group_sql = "SELECT DISTINCT filter_group_id FROM user_auth WHERE deleted = 0 ORDER BY filter_group_id;";
+my @grouplist_ref = get_records_sql($dbh,$group_sql);
+foreach my $row (@grouplist_ref) {
+    $lists{'group_'.$row->{filter_group_id}}=1;
+}
 $lists{'group_all'}=1;
 
-#get queue list
-my @queuelist_ref = get_records_sql($dbh,"SELECT * FROM queue_list");
+log_verbose($gate_ident."Loaded ".scalar(keys %lists)." filter groups");
 
-my %queues;
-foreach my $row (@queuelist_ref) {
-$lists{'queue_'.$row->{id}}=1;
-next if ((!$row->{Download}) and !($row->{Upload}));
-$queues{'queue_'.$row->{id}}{id}=$row->{id};
-$queues{'queue_'.$row->{id}}{down}=$row->{Download};
-$queues{'queue_'.$row->{id}}{up}=$row->{Upload};
-}
+my $chains_created = 0;
+my $chains_removed = 0;
+my $rules_added = 0;
+my $rules_removed = 0;
+
+my $ipset_created = 0;
+my $ipset_added = 0;
+my $ipset_removed = 0;
+
+# ============================================================================
+# ACCESS LISTS CONFIG
+# ============================================================================
+if ($gate->{user_acl}) {
+
+
+    log_verbose($gate_ident."Sync user state at router $router_name [".$router_ip."] started.");
+    db_log_verbose($dbh,$gate_ident."Sync user state at router $router_name [".$router_ip."] started.");
+
+    my $user_auth_sql="SELECT user_auth.ip, user_auth.filter_group_id, user_auth.id
+    FROM user_auth, user_list
+    WHERE user_auth.user_id = user_list.id
+    AND user_auth.deleted =0
+    AND user_auth.enabled =1
+    AND user_auth.blocked =0
+    AND user_list.blocked =0
+    AND user_list.enabled =1
+    AND user_auth.ou_id <> ?
+    ORDER BY ip_int";
+
+    my @authlist_ref = get_records_sql($dbh,$user_auth_sql,$default_hotspot_ou_id);
 
-log_debug("Queues status:".Dumper(\%queues));
+    foreach my $row (@authlist_ref) {
+        if ($connected_users_only) { next if (!$connected_users->match_string($row->{ip})); }
+        next if (!$office_networks->match_string($row->{ip}));
+        $users{'group_'.$row->{filter_group_id}}->{$row->{ip}}=1;
+        $users{'group_all'}->{$row->{ip}}=1;
+    }
+
+    # Подсчёт пользователей
+    my $users_count = 0;
+    foreach my $group (values %users) {
+        $users_count += scalar keys %$group;
+    }
+    log_verbose($gate_ident."Loaded $users_count user IPs for ACL processing");
 
-my @filterlist_ref = get_records_sql($dbh,"SELECT * FROM filter_list where type=0");
+    log_debug($gate_ident."Users status by ACL:".Dumper(\%users));
 
-my %filters;
-my %dyn_filters;
+    my @filter_instances = get_records_sql($dbh,"SELECT * FROM filter_instances");
+    my @filterlist_ref = get_records_sql($dbh,"SELECT * FROM filter_list where filter_type=0");
 
-my $max_filter_rec = get_record_sql($dbh,"SELECT MAX(id) FROM filter_list");
-my $max_filter_id = $max_filter_rec->{id};
+    my %filters;
+    my %dyn_filters;
 
-my $dyn_filters_base = $max_filter_id+1000;
-my $dyn_filters_index = $dyn_filters_base;
+    my $max_filter_rec = get_record_sql($dbh,"SELECT MAX(id) as max_filter FROM filter_list");
+    my $max_filter_id = $max_filter_rec->{max_filter};
+    my $dyn_filters_base = $max_filter_id+1000;
+    my $dyn_filters_index = $dyn_filters_base;
 
-foreach my $row (@filterlist_ref) {
-    #if dst - ip address
-    if (is_ip($row->{dst})) {
-        $filters{$row->{id}}->{id}=$row->{id};
-        $filters{$row->{id}}->{proto}=$row->{proto};
-        $filters{$row->{id}}->{dst}=$row->{dst};
-        $filters{$row->{id}}->{dstport}=$row->{dstport};
-        $filters{$row->{id}}->{srcport}=$row->{srcport};
-        #set false for dns dst flag
-        $filters{$row->{id}}->{dns_dst}=0;
+    foreach my $row (@filterlist_ref) {
+        if (is_ip($row->{dst})) {
+            $filters{$row->{id}}->{id}=$row->{id};
+            $filters{$row->{id}}->{proto}=$row->{proto};
+            $filters{$row->{id}}->{dst}=$row->{dst};
+            $filters{$row->{id}}->{dstport}=$row->{dstport};
+            $filters{$row->{id}}->{srcport}=$row->{srcport};
+            $filters{$row->{id}}->{dns_dst}=0;
         } else {
-        #if dst not ip - check dns record
-        my @dns_record=ResolveNames($row->{dst},undef);
-        my $resolved_ips = (scalar @dns_record>0);
-        next if (!$resolved_ips);
-        foreach my $resolved_ip (sort @dns_record) {
+            my @dns_record=ResolveNames($row->{dst},undef);
+            my $resolved_ips = (scalar @dns_record>0);
+            next if (!$resolved_ips);
+            foreach my $resolved_ip (sort @dns_record) {
                 next if (!$resolved_ip);
-                #enable dns dst filters
                 $filters{$row->{id}}->{dns_dst}=1;
-                #add dynamic dns filter
                 $filters{$dyn_filters_index}->{id}=$row->{id};
                 $filters{$dyn_filters_index}->{proto}=$row->{proto};
                 $filters{$dyn_filters_index}->{dst}=$resolved_ip;
                 $filters{$dyn_filters_index}->{dstport}=$row->{dstport};
                 $filters{$dyn_filters_index}->{srcport}=$row->{srcport};
                 $filters{$dyn_filters_index}->{dns_dst}=0;
-                #save new filter dns id for original filter id
                 push(@{$dyn_filters{$row->{id}}},$dyn_filters_index);
                 $dyn_filters_index++;
             }
         }
-}
+    }
 
-log_debug("Filters status:". Dumper(\%filters));
-log_debug("DNS-filters status:". Dumper(\%dyn_filters));
+    log_debug($gate_ident."Filters status:". Dumper(\%filters));
+    log_debug($gate_ident."DNS-filters status:". Dumper(\%dyn_filters));
+    log_verbose($gate_ident."Loaded ".scalar(keys %filters)." filters (".scalar(keys %dyn_filters)." with DNS resolution)");
 
-#clean unused filter records
-do_sql($dbh,"DELETE FROM group_filters WHERE group_id NOT IN (SELECT id FROM group_list)");
-do_sql($dbh,"DELETE FROM group_filters WHERE filter_id NOT IN (SELECT id FROM filter_list)");
+    do_sql($dbh,"DELETE FROM group_filters WHERE group_id NOT IN (SELECT id FROM group_list)");
+    do_sql($dbh,"DELETE FROM group_filters WHERE filter_id NOT IN (SELECT id FROM filter_list)");
 
-my @grouplist_ref = get_records_sql($dbh,"SELECT group_id,filter_id,rule_order,action FROM group_filters ORDER BY group_filters.group_id,group_filters.rule_order");
+    my @groups_list = get_records_sql($dbh,"SELECT * FROM group_list");
+    my %groups;
+    foreach my $group (@groups_list) { $groups{'group_'.$group->{id}}=$group; }
 
-my %group_filters;
-my $index=0;
-foreach my $row (@grouplist_ref) {
-    #if dst dns filter not found
-    if (!$filters{$row->{filter_id}}->{dns_dst}) {
-        $group_filters{'group_'.$row->{group_id}}->{$index}->{filter_id}=$row->{filter_id};
-        $group_filters{'group_'.$row->{group_id}}->{$index}->{action}=$row->{action};
-        $index++;
-    } else {
-        #if found dns dst filters - add
-	    if (exists $dyn_filters{$row->{filter_id}}) {
-	        my @dyn_ips = @{$dyn_filters{$row->{filter_id}}};
-	        if (scalar @dyn_ips >0) {
-		        for (my $i = 0; $i < scalar @dyn_ips; $i++) {
-        	        $group_filters{'group_'.$row->{group_id}}->{$index}->{filter_id}=$dyn_ips[$i];
-                    $group_filters{'group_'.$row->{group_id}}->{$index}->{action}=$row->{action};
-        	        $index++;
-        	    }
-	        }
+    my @grouplist_ref = get_records_sql($dbh,"SELECT group_id,filter_id,rule_order,action FROM group_filters ORDER BY group_filters.group_id,group_filters.rule_order");
+
+    my %group_filters;
+    my $index = 0;
+    my $cur_group;
+
+    foreach my $row (@grouplist_ref) {
+        if (!$cur_group) { $cur_group = $row->{group_id}; }
+        if ($cur_group != $row->{group_id}) {
+            $index = 0;
+            $cur_group = $row->{group_id};
+        }
+        if (!$filters{$row->{filter_id}}->{dns_dst}) {
+            $group_filters{'group_'.$row->{group_id}}->{$index}->{filter_id}=$row->{filter_id};
+            $group_filters{'group_'.$row->{group_id}}->{$index}->{action}=$row->{action};
+            $index++;
+        } else {
+            if (exists $dyn_filters{$row->{filter_id}}) {
+                my @dyn_ips = @{$dyn_filters{$row->{filter_id}}};
+                if (scalar @dyn_ips >0) {
+                    for (my $i = 0; $i < scalar @dyn_ips; $i++) {
+                        $group_filters{'group_'.$row->{group_id}}->{$index}->{filter_id}=$dyn_ips[$i];
+                        $group_filters{'group_'.$row->{group_id}}->{$index}->{action}=$row->{action};
+                        $index++;
+                    }
+                }
+            }
         }
     }
-}
 
-log_debug("Group filters: ".Dumper(\%group_filters));
-
-my %cur_users;
-
-my @new_iptables_users=();
-foreach my $group_name (keys %lists) {
-#new users chains
-push(@new_iptables_users,"-A USERS -m set --match-set $group_name src -j $group_name");
-push(@new_iptables_users,"-A USERS -m set --match-set $group_name dst -j $group_name");
-#current user chains members
-my $address_lists=do_exec('/sbin/ipset list '.$group_name.' 2>/dev/null');
-$cur_users{$group_name}{found}=0;
-foreach my $row (split(/\n/,$address_lists)) {
-    $row=trim($row);
-    next if (!$row);
-    if ($row=~/^Error$/i) {  $cur_users{$group_name}{found}=0; last; }
-    next if ($row !~ /^[0-9]/);
-    $cur_users{$group_name}{ips}{$row}=1;
-    $cur_users{$group_name}{found}=1;
+    log_debug($gate_ident."Group filters: ".Dumper(\%group_filters));
+    log_verbose($gate_ident."Prepared ".scalar(keys %group_filters)." group filter configurations");
+
+    # ============================================================================
+    # СИНХРОНИЗАЦИЯ IPSET
+    # ============================================================================
+
+    my %cur_users;
+    my %final_ipsets;  # Хранилище для финальных IPset данных
+
+
+    # Создаем таблицу ipset если не существует
+    log_verbose($gate_ident."Ensure ipset exists: ${IPTABLES_TABLE_NAME}_group_all");
+    push(@cmd_ipset_list, "$IPSET_CMD create ${IPTABLES_TABLE_NAME}_group_all hash:net family inet hashsize 1024 maxelem 2655360");
+#    log_debug(Dumper(\%lists));
+    foreach my $group_name (keys %lists) {
+        my $set_name = $IPTABLES_TABLE_NAME . '_' . $group_name;
+        log_verbose($gate_ident."Ensure ipset exists: $set_name");
+        push(@cmd_ipset_list, "$IPSET_CMD create $set_name hash:net family inet hashsize 1024 maxelem 2655360");
+
+        my @address_list = get_ipset_members($set_name);
+        foreach my $ip (@address_list) {
+            $cur_users{$group_name}{$ip} = 1;
+            $final_ipsets{$group_name}{$ip} = 1;  # Сохраняем в финальный хэш
+        }
+        log_verbose($gate_ident."Current ipset $group_name has ".scalar(@address_list)." entries");
     }
-}
 
-#recreate ipsets if not found
-foreach my $group_name (keys %lists) {
-next if ($cur_users{$group_name}{found});
-do_exec("/sbin/ipset create $group_name hash:net family inet maxelem 2655360 2>/dev/null");
-}
+    # Добавляем новые IP
+    foreach my $group_name (keys %users) {
+        my $set_name = $IPTABLES_TABLE_NAME . '_' . $group_name;
+        foreach my $user_ip (keys %{$users{$group_name}}) {
+            if (!exists($cur_users{$group_name}{$user_ip})) {
+                log_info($gate_ident."ADD ipset entry: $user_ip -> $group_name");
+                db_log_verbose($dbh, $gate_ident."Add user with ip: $user_ip to ipset $group_name");
+                push(@cmd_ipset_list, "$IPSET_CMD add $set_name $user_ip");
+                $final_ipsets{$group_name}{$user_ip} = 1;  # Добавляем в финальный хэш
+                $ipset_added++;
+            }
+        }
+    }
 
-my @cmd_list=();
+    # Удаляем старые IP
+    foreach my $group_name (keys %cur_users) {
+        my $set_name = $IPTABLES_TABLE_NAME . '_' . $group_name;
+        foreach my $user_ip (keys %{$cur_users{$group_name}}) {
+            if (!exists($users{$group_name}{$user_ip})) {
+                log_info($gate_ident."REMOVE ipset entry: $user_ip <- $group_name");
+                db_log_verbose($dbh, $gate_ident."Remove user with ip: $user_ip from ipset $group_name");
+                push(@cmd_ipset_list, "$IPSET_CMD del $set_name $user_ip");
+                delete $final_ipsets{$group_name}{$user_ip};  # Удаляем из финального хэша
+                $ipset_removed++;
+            }
+        }
+    }
+
+    log_verbose($gate_ident."IPset changes: $ipset_added added, $ipset_removed removed");
+
+    # ============================================================================
+    # СОХРАНЕНИЕ IPSET В ФАЙЛЫ
+    # ============================================================================
 
-#new-ips
-foreach my $group_name (keys %users) {
-    next if (!$users{$group_name}{ips});
-    foreach my $user_ip (keys %{$users{$group_name}{ips}}) {
-    if (!exists($cur_users{$group_name}{ips}{$user_ip})) {
-	db_log_verbose($dbh,"Add user with ip: $user_ip to access-list $group_name");
-	do_exec("/sbin/ipset add $group_name $user_ip");
-	}
+    save_ipsets_to_files(\%final_ipsets);
+
+    timestamp;
+
+
+    # ========================================================================
+    # СИНХРОНИЗАЦИЯ IPTABLES
+    # ========================================================================
+
+    foreach my $filter_instance (@filter_instances) {
+        my $instance_name = 'Users';
+        if ($filter_instance->{id}>1) {
+            $instance_name = 'Users-'.$filter_instance->{name};
+            my $instance_ok = get_record_sql($dbh,"SELECT * FROM device_filter_instances WHERE device_id= ? AND instance_id=?", $gate->{'id'}, $filter_instance->{id});
+            if (!$instance_ok) {
+                log_verbose($gate_ident."Skip filter instance '$instance_name' - not assigned to this device");
+                next;
+            }
+        }
+
+        # Создаем цепочку iptables если не существует
+        my $chain_name = $IPTABLES_CHAIN_PREFIX . $instance_name;
+        log_verbose($gate_ident."Ensure chain exists: $chain_name");
+        push(@cmd_list, "$IPTABLES_CMD -N $chain_name 2>/dev/null || true");
+
+        # Проверяем существующие цепочки для групп
+        my %cur_chain = get_iptables_jumps($chain_name);
+
+        # Удаляем старые цепочки
+        foreach my $group_name (keys %cur_chain) {
+            if (!exists($group_filters{$group_name}) or $groups{$group_name}->{instance_id} ne $filter_instance->{id}) {
+                my $filter_chain_name = $IPTABLES_CHAIN_PREFIX . $group_name;
+                my $user_ipset_group = $IPTABLES_TABLE_NAME . "_" . $group_name;
+                log_info($gate_ident."REMOVE iptables chain link: $group_name -> $instance_name");
+                push(@cmd_list, "$IPTABLES_CMD -D $chain_name -m set --match-set $user_ipset_group src -j $filter_chain_name");
+                push(@cmd_list, "$IPTABLES_CMD -D $chain_name -m set --match-set $user_ipset_group dst -j $filter_chain_name");
+                push(@cmd_list, "$IPTABLES_CMD -F $filter_chain_name 2>/dev/null");
+                push(@cmd_list, "$IPTABLES_CMD -X $filter_chain_name 2>/dev/null");
+                $chains_removed++;
+            }
+        }
+
+        # Добавляем новые цепочки
+        foreach my $group_name (keys %group_filters) {
+            # Пропускаем фильтры, которых нет у пользователей данного шлюза
+            next if (!exists $lists{$group_name});
+            if (!exists($cur_chain{$group_name}) and $groups{$group_name}->{instance_id} eq $filter_instance->{id}) {
+                my $filter_chain_name = $IPTABLES_CHAIN_PREFIX . $group_name;
+                my $user_ipset_group = $IPTABLES_TABLE_NAME . "_" . $group_name;
+                log_info($gate_ident."ADD iptables chain link: $group_name -> $instance_name");
+                push(@cmd_list, "$IPTABLES_CMD -N $filter_chain_name 2>/dev/null || true");
+                push(@cmd_list, "$IPTABLES_CMD -A $chain_name -m set --match-set $user_ipset_group src -j $filter_chain_name");
+                push(@cmd_list, "$IPTABLES_CMD -A $chain_name -m set --match-set $user_ipset_group dst -j $filter_chain_name");
+                $chains_created++;
+            }
+        }
     }
-}
 
-#old-ips
-foreach my $group_name (keys %cur_users) {
-    next if (!$cur_users{$group_name}{ips});
-    foreach my $user_ip (keys %{$cur_users{$group_name}{ips}}) {
-    if (!exists($users{$group_name}{ips}{$user_ip})) {
-	db_log_verbose($dbh,"Remove user with ip: $user_ip from access-list $group_name");
-        do_exec("/sbin/ipset del $group_name $user_ip");
-	}
+    # Формируем правила для цепочек
+    my %chain_rules;
+    foreach my $group_name (sort keys %group_filters) {
+        next if (!$group_name);
+
+        if (!exists $lists{$group_name}) {
+            log_info($gate_ident."Filter group $group_name not found at users this device. Skip create");
+            next;
+            }
+
+        my %group_filter = %{$group_filters{$group_name}};
+        my $group_rules_count = 0;
+
+        foreach my $filter_index (sort keys %group_filter) {
+            my $filter = $group_filter{$filter_index};
+            my $filter_id=$filter->{filter_id};
+
+            next if (!$filters{$filter_id});
+            next if ($filters{$filter_id}->{dns_dst});
+
+            my $chain_name = $IPTABLES_CHAIN_PREFIX . $group_name;
+            my $action = $filter->{action} ? 'ACCEPT' : 'REJECT';
+            my $proto = $filters{$filter_id}->{proto};
+            my $dstport = $filters{$filter_id}->{dstport} || 0;
+            my $srcport = $filters{$filter_id}->{srcport} || 0;
+
+            $dstport=~s/\-/:/g;
+            $srcport=~s/\-/:/g;
+
+            my $src_rule = '-A '.$chain_name;
+            my $dst_rule = '-A '.$chain_name;
+
+
+            if ($filters{$filter_id}->{dst} and $filters{$filter_id}->{dst} ne '0/0' and $filters{$filter_id}->{dst} !~ /\/\d{1,2}/) { $filters{$filter_id}->{dst} .='/32'; }
+            my $dst = $filters{$filter_id}->{dst};
+
+            if (defined $dst && $dst ne '' && $dst ne '0/0') {
+                $src_rule .= " -s $dst";
+                $dst_rule .= " -d $dst";
+            }
+
+            if ($filters{$filter_id}->{proto} and ($filters{$filter_id}->{proto}!~/all/i)) {
+                $src_rule=$src_rule." -p ".$filters{$filter_id}->{proto};
+                $dst_rule=$dst_rule." -p ".$filters{$filter_id}->{proto};
+                }
+
+            if ($dstport ne '0' and $srcport ne '0') {
+                        $src_rule=$src_rule." -m multiport --dports ".trim($srcport)." --sports ".trim($dstport);
+                        $dst_rule=$dst_rule." -m multiport --sports ".trim($srcport)." --dports ".trim($dstport);
+                        }
+            if ($dstport eq '0' and $srcport ne '0') {
+                        $src_rule=$src_rule." -m multiport --dports ".trim($srcport);
+                        $dst_rule=$dst_rule." -m multiport --sport ".trim($srcport);
+                        }
+            if ($dstport ne '0' and $srcport eq '0') {
+                        $src_rule=$src_rule." -m multiport --sports ".trim($dstport);
+                        $dst_rule=$dst_rule." -m multiport --dports ".trim($dstport);
+                        }
+
+            if ($filter->{action}) {
+                $src_rule=$src_rule." -j ACCEPT";
+                $dst_rule=$dst_rule." -j ACCEPT";
+                } else {
+                $src_rule=$src_rule." -j REJECT --reject-with icmp-port-unreachable";
+                $dst_rule=$dst_rule." -j REJECT --reject-with icmp-port-unreachable";
+                }
+
+            if ($src_rule ne $dst_rule) {
+                push(@{$chain_rules{$group_name}},$src_rule);
+                push(@{$chain_rules{$group_name}},$dst_rule);
+                $group_rules_count += 2;
+                } else {
+                push(@{$chain_rules{$group_name}},$src_rule);
+                $group_rules_count++;
+                }
+        }
+        log_verbose($gate_ident."Prepared $group_rules_count rules for chain $IPTABLES_CHAIN_PREFIX$group_name");
+        $rules_added += $group_rules_count;
     }
-}
 
-timestamp;
-
-#filters
-my %chain_rules;
-foreach my $group_name (keys %lists) {
-next if (!$group_name);
-next if (!exists($group_filters{$group_name}));
-push(@{$chain_rules{$group_name}},"-N $group_name");
-foreach my $filter_index (sort keys %{$group_filters{$group_name}}) {
-    my $filter_id=$group_filters{$group_name}->{$filter_index}->{filter_id};
-    next if (!$filters{$filter_id});
-    my $src_rule='-A '.$group_name;
-    my $dst_rule='-A '.$group_name;
-    if ($filters{$filter_id}->{proto} and ($filters{$filter_id}->{proto}!~/all/i)) {
-	$src_rule=$src_rule." -p ".$filters{$filter_id}->{proto};
-	$dst_rule=$dst_rule." -p ".$filters{$filter_id}->{proto};
-	}
-    if ($filters{$filter_id}->{dst} and $filters{$filter_id}->{dst} ne '0/0') {
-	$src_rule=$src_rule." -s ".trim($filters{$filter_id}->{dst});
-	$dst_rule=$dst_rule." -d ".trim($filters{$filter_id}->{dst});
-	}
-    if ($filters{$filter_id}->{port} and $filters{$filter_id}->{port} ne '0') {
-	my $module=" -m ".$filters{$filter_id}->{proto};
-	if ($filters{$filter_id}->{port}=~/\-/ or $filters{$filter_id}->{port}=~/\,/ or $filters{$filter_id}->{port}=~/\:/) {
-	    $module=" -m multiport";
-	    $filters{$filter_id}->{port}=~s/\-/:/g;
-	    }
-	$src_rule=$src_rule.$module." --sport ".trim($filters{$filter_id}->{port});
-	$dst_rule=$dst_rule.$module." --dport ".trim($filters{$filter_id}->{port});
-	}
-    if ($group_filters{$group_name}->{$filter_index}->{action}) {
-	$src_rule=$src_rule." -j ACCEPT";
-	$dst_rule=$dst_rule." -j ACCEPT";
-	} else {
-	$src_rule=$src_rule." -j REJECT";
-	$dst_rule=$dst_rule." -j REJECT";
-	}
-    if ($src_rule ne $dst_rule) {
-        push(@{$chain_rules{$group_name}},$src_rule);
-        push(@{$chain_rules{$group_name}},$dst_rule);
+    # Применяем правила цепочек
+    foreach my $group_name (sort keys %group_filters) {
+        next if (!$group_name);
+        next if (!exists $lists{$group_name});
+
+        my $chain_name = $IPTABLES_CHAIN_PREFIX . $group_name;
+        my @cur_filter = get_iptables_chain_rules($chain_name);
+        my $chain_ok = 1;
+
+        if (scalar @cur_filter != scalar @{$chain_rules{$group_name}}) {
+            $chain_ok = 0;
+            log_verbose($gate_ident."Chain $chain_name rules count mismatch (current: ".scalar(@cur_filter).", expected: ".scalar(@{$chain_rules{$group_name}}).")");
+        } else {
+            # Проверка текущих правил
+            for (my $f_index=0; $f_index<scalar(@cur_filter); $f_index++) {
+                my $filter_str = trim($cur_filter[$f_index]);
+                if (!$chain_rules{$group_name}[$f_index] or $filter_str !~ /$chain_rules{$group_name}[$f_index]/i) {
+                    log_error($gate_ident."Check chain $chain_name error! Rule mismatch at position $f_index");
+                    log_verbose($gate_ident."Expected: $chain_rules{$group_name}[$f_index]");
+                    log_verbose($gate_ident."Current: $filter_str");
+                    $chain_ok = 0;
+                    last;
+                }
+            }
+        }
+
+        if (!$chain_ok) {
+            log_info($gate_ident."RECREATE iptables chain: $chain_name (".scalar(@{$chain_rules{$group_name}})." rules)");
+            push(@cmd_list, "$IPTABLES_CMD -F $chain_name 2>/dev/null || true");
+            push(@cmd_list, "$IPTABLES_CMD -N $chain_name 2>/dev/null || true");
+            foreach my $filter_str (@{$chain_rules{$group_name}}) {
+                push(@cmd_list, "$IPTABLES_CMD $filter_str");
+            }
         } else {
-        push(@{$chain_rules{$group_name}},$src_rule);
+            log_verbose($gate_ident."Chain $chain_name is up to date");
         }
     }
+
+    log_verbose($gate_ident."IPTables chains: $chains_created created, $chains_removed removed, $rules_added rules total");
 }
 
-######## get current iptables USERS chain state
+# ============================================================================
+# ВЫПОЛНЕНИЕ КОМАНД
+# ============================================================================
+
+my $cmd_executed = 0;
+my $cmd_failed = 0;
 
-my $cur_iptables = do_exec("/sbin/iptables --list-rules USERS 2>/dev/null");
-my @cur_iptables_users = split(/\n/,$cur_iptables);
+log_verbose($gate_ident."Executing ipset ".scalar(@cmd_ipset_list)." commands...");
 
-my $users_chain_ok=(scalar @cur_iptables_users eq scalar @new_iptables_users);
-#if count records in chain ok - check stuff
-if ($users_chain_ok) {
-    for (my $i = 0; $i <= $#cur_iptables_users; $i++) {
-	if ($cur_iptables_users[$i]!~/$new_iptables_users[$i]/i) { $users_chain_ok=0; last; }
-	}
+foreach my $cmd (@cmd_ipset_list) {
+    log_debug($gate_ident."EXEC: $cmd");
+    my %result = do_exec_ref($cmd);
+    $cmd_executed++;
+    if (%result && $result{status} && $result{status} != 0) {
+        log_error($gate_ident."Command failed (exit code $result{status}): $cmd");
+        $cmd_failed++;
     }
+}
+
+log_verbose($gate_ident."Ipset ommands executed: $cmd_executed total, $cmd_failed failed");
+
+$cmd_executed = 0;
+$cmd_failed = 0;
+
+log_verbose($gate_ident."Executing iptables ".scalar(@cmd_list)." commands...");
 
-#group rules
-my %cur_chain_rules;
-foreach my $group_name (keys %lists) {
-next if (!$group_name);
-my $tmp=do_exec("/sbin/iptables --list-rules $group_name 2>/dev/null");
-foreach my $rule (split(/\n/,$tmp)) {
-    if ($rule=~/Error/i) {
-	$lists{$group_name}=0;
-	last;
-	}
-    push(@{$cur_chain_rules{$group_name}},$rule);
+foreach my $cmd (@cmd_list) {
+    log_debug($gate_ident."EXEC: $cmd");
+    my %result = do_exec_ref($cmd);
+    $cmd_executed++;
+    if (%result && $result{status} && $result{status} != 0) {
+        log_error($gate_ident."Command failed (exit code $result{status}): $cmd");
+        $cmd_failed++;
     }
 }
 
-#check filter group chain
-foreach my $group_name (keys %lists) {
-my @tmp = ();
-if ($chain_rules{$group_name}) { @tmp = @{$chain_rules{$group_name}}; }
-my @cur_tmp = ();
-if ($cur_chain_rules{$group_name}) { @cur_tmp=@{$cur_chain_rules{$group_name}}; }
-my $group_chain_ok=($#tmp eq $#cur_tmp);
-#if count records in chain ok - check stuff
-if ($group_chain_ok) {
-    for (my $i = 0; $i <= $#tmp; $i++) {
-	    if ($tmp[$i]!~/$cur_tmp[$i]/i) { $group_chain_ok=0; last; }
-	    }
+log_verbose($gate_ident."Iptables commands executed: $cmd_executed total, $cmd_failed failed");
+
+# ============================================================================
+# ЗАВЕРШЕНИЕ
+# ============================================================================
+
+if (IsMyPID($SPID)) { Remove_PID($SPID); };
+
+log_info($gate_ident."=== ACL Sync COMPLETED ===");
+log_verbose($gate_ident."Summary: chains=$chains_created created/$chains_removed removed, rules=$rules_added, ipset=$ipset_added added/$ipset_removed removed, commands=$cmd_executed executed/$cmd_failed failed");
+
+do_exit 0;
+
+# ============================================================================
+# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
+# ============================================================================
+
+sub get_iptables_jumps {
+    my ($chain) = @_;
+    my %jumps;
+    my %result = do_exec_ref("$IPTABLES_CMD --list-rules $chain");
+    my $output = $result{output};
+    my @lines = split(/\n/, $output);
+    foreach my $line (@lines) {
+        next if ($line !~/^\-A\s+/);
+        if ($line =~ /\-j\s+(\S+)$/) {
+            my $target = $1;
+            $target =~ s/^${IPTABLES_CHAIN_PREFIX}//;
+            $jumps{$target}++ if ($target);
+            }
         }
-if (!$group_chain_ok) {
-    if ($lists{$group_name}) {
-        push(@cmd_list,"-D USERS -m set --match-set $group_name src -j $group_name");
-	push(@cmd_list,"-D USERS -m set --match-set $group_name dst -j $group_name");
-        push(@cmd_list,"-D $group_name");
-	}
-    push(@cmd_list,@{$chain_rules{$group_name}});
-    if ($users_chain_ok) {
-	push(@cmd_list,"-A USERS -m set --match-set $group_name src -j $group_name");
-	push(@cmd_list,"-A USERS -m set --match-set $group_name dst -j $group_name");
-	}
-    }
+    log_debug($gate_ident."Get target chain for $chain: ".Dumper(\%jumps));
+    return %jumps;
 }
 
-#recreate users chain
-if (!$users_chain_ok) {
-    for (my $i = 0; $i <= $#new_iptables_users; $i++) {
-	push(@cmd_list,$new_iptables_users[$i]);
-	}
+sub get_iptables_chain_rules {
+    my ($chain) = @_;
+    my @rules;
+    my %result = do_exec_ref("$IPTABLES_CMD --list-rules  $chain");
+    my $output = $result{output};
+    my @lines = split(/\n/, $output);
+    foreach my $line (@lines) {
+        next if ($line !~ /^\-A/);
+        push(@rules, trim($line));
     }
+    log_debug($gate_ident."Current rules for $chain: ".Dumper(\@rules));
+    return @rules;
+}
 
-my $table = IPTables::libiptc::init('filter');
-foreach my $row (@cmd_list) {
-print "$row\n" if ($debug);
-my @cmd_array = split(" ",$row);
-$table->iptables_do_command(\@cmd_array);
+sub get_ipset_members {
+    my ($set_name) = @_;
+    my @members;
+    my %result = do_exec_ref("$IPSET_CMD list $set_name");
+    if ($result{status} != 0) {
+        do_exec("$IPSET_CMD create $set_name hash:net family inet hashsize 1024 maxelem 2655360 2>/dev/null || true");
+        do_exec("$IPSET_CMD flush $set_name 2>/dev/null || true");
+        return @members;
+        }
+    my $output = $result{output};
+    while ($output =~ /^(\d+\.\d+\.\d+\.\d+(?:\/\d+)?)\s*$/gm) {
+        push(@members, $1);
+    }
+    log_debug($gate_ident."Current ipset $set_name members:".Dumper(\@members));
+    return @members;
 }
-$table->commit();
 
-db_log_verbose($dbh,"Sync user state at router $router_name stopped.");
-$dbh->disconnect();
+# ============================================================================
+# ФУНКЦИИ ДЛЯ СОХРАНЕНИЯ IPSET В ФАЙЛЫ
+# ============================================================================
 
-exit;
+sub save_ipsets_to_files {
+    my ($ipsets_ref) = @_;
+    # Создаем директорию если не существует
+    unless (-d $IPSET_SAVE_DIR) {
+        make_path($IPSET_SAVE_DIR) or die "Cannot create $IPSET_SAVE_DIR: $!";
+        log_verbose($gate_ident."Created directory: $IPSET_SAVE_DIR");
+    }
+    # Сохраняем каждый ipset в отдельный файл
+    my $files_saved = 0;
+    foreach my $group_name (sort keys %$ipsets_ref) {
+        my $set_name = $IPTABLES_TABLE_NAME . '_' . $group_name;
+        my $filename = $IPSET_SAVE_DIR . '/' . $group_name . $IPSET_SAVE_EXTENSION;
+        my $fh = FileHandle->new();
+        if ($fh->open(">$filename")) {
+            # Заголовок файла с метаинформацией
+            print $fh "# ipset configuration file\n";
+            print $fh "# Generated: " . scalar(localtime) . "\n";
+            print $fh "# Router: $router_name ($router_ip)\n";
+            print $fh "# Set name: $set_name\n";
+            print $fh "#\n\n";
+            print $fh "create $set_name hash:net family inet hashsize 1024 maxelem 2655360\n";
+            # Добавляем все IP адреса
+            foreach my $ip (sort keys %{$ipsets_ref->{$group_name}}) {
+                print $fh "add $set_name $ip\n";
+            }
+            $fh->close();
+            log_verbose($gate_ident."Saved ipset $group_name to $filename (".scalar(keys %{$ipsets_ref->{$group_name}})." entries)");
+            log_info($gate_ident."SAVE ipset file: $filename (".scalar(keys %{$ipsets_ref->{$group_name}})." entries)");
+            $files_saved++;
+        } else {
+            log_error($gate_ident."ERROR: Cannot write to $filename: $!");
+        }
+    }
+    log_verbose($gate_ident."Saved $files_saved ipset configuration files");
+}

+ 0 - 36
docs/iptables/ulog-save

@@ -1,36 +0,0 @@
-#!/bin/sh
-#
-# make traffic statistics snapshot
-
-SCRIPTPATH=$( cd "$(dirname "$0")" ; pwd -P )
-
-if [ ! -e "${SCRIPTPATH}/config" ]; then
-    echo "Config file not found!"
-    exit 120
-    fi
-
-. ${SCRIPTPATH}/config
-
-exit_ifrun
-create_lock
-
-YY=`date +%Y`
-MM=`date +%m`
-DD=`date +%d`
-
-SAVE_PATH=/mnt/md0/stat/${YY}/${MM}/${DD}/
-[ ! -e "${SAVE_PATH}" ] && mkdir -p ${SAVE_PATH}
-
-TM=`date +%Y%m%d-%H%M`
-
-###skill -HUP -c ulog-acctd
-skill -TSTP -c ulog-acctd
-mv /var/log/ulog-acctd/account.log ${SAVE_PATH}/${TM} --backup --suffix="-`date +%s`" -f
-skill -CONT -c ulog-acctd
-
-# create statistics
-cat ${SAVE_PATH}/${TM} | /opt/Eye/scripts/parse_ulog.pl
-
-/opt/Eye/scripts/sync_iptables.pl
-
-exit

+ 55 - 42
docs/systemd/init.d/ipset

@@ -1,61 +1,69 @@
 #! /bin/bash
-#
-# ipset       Create ipset before start iptables
-#
-# chkconfig: 2345 07 93
-# description: Activates/Deactivates ipset lists
-#
-#
 
-# Source function library.
-. /etc/init.d/functions
+#
+### BEGIN INIT INFO
+# Provides: ipset
+# Required-Start: $local_fs $network $remote_fs $syslog
+# Required-Stop: $local_fs $network $remote_fs $syslog
+# Default-Start:  2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: start and stop the ipset lists
+# Description: start and stop the ipset lists
+### END INIT INFO
 
-if [ ! -f /etc/sysconfig/network ]; then
-    exit 0
+if [ -r "/lib/lsb/init-functions" ]; then
+        . /lib/lsb/init-functions
+else
+        log_success_msg() {
+                echo "$@"
+        }
+        log_warning_msg() {
+                echo "$@" >&2
+        }
+        log_failure_msg() {
+                echo "$@" >&2
+        }
 fi
 
-# Check that networking is up.
-[ "${NETWORKING}" = "no" ] && exit 0
 
 IPSET='/sbin/ipset'
-config_dir='/etc/ipset.d'
+IPSET_DIR='/etc/ipset.d'
 
 # if the ip configuration utility isn't around we can't function.
 [ -x ${IPSET} ] || exit 1
 
-[ ! -e "${config_dir}" ] && mkdir -p "${config_dir}"
-
 stop_ipset() {
-if [ -d "${config_dir}" ] ; then
-        CONFS=`ls ${config_dir}/*.save 2>/dev/null`
-        [ -z "${CONFS}" ] && exit 6
-        for i in $CONFS; do
-            ipset_name=`basename $i .save`
-            echo -n $"Destroy ipset $ipset_name: "
-	    ${IPSET} destroy $ipset_name >/dev/null 2>&1
-            echo
-            done
-        else
-        RETVAL=1
-        fi
+ls -x -1 "${IPSET_DIR}/"*.conf | while read IPSET_FILE; do
+ipset_name=`grep -P "^create\s+(\S+)\s+" "${IPSET_FILE}"  | awk '{ print $2 }' | sed 's/_new//'`
+[ -z "${ipset_name}" ] && continue
+echo -n $"Destroy ${ipset_name} ipset"
+${IPSET} destroy ${ipset_name} >/dev/null 2>&1
+echo
+done
 return 0
 }
 
 start_ipset() {
-[ ! -e "/run/ipstate" ] && ln -s /usr/local/ipstate /run/ipstate
-if [ -d "${config_dir}" ] ; then
-        CONFS=`ls ${config_dir}/*.save 2>/dev/null`
-        [ -z "${CONFS}" ] && exit 6
-        for i in $CONFS; do
-            ipset_name=`basename $i .save`
-	    ${IPSET} destroy $ipset_name >/dev/null 2>&1
-            echo -n $"Loading ipset $ipset_name: "
-	    cat "${config_dir}/${i}" | ${IPSET} restore >/dev/null 2>&1
-            echo
-            done
-        else
-        RETVAL=1
-        fi
+ls -x -1 "${IPSET_DIR}/"*.conf | while read IPSET_FILE; do
+ipset_name=`grep -P "^create\s+(\S+)\s+" "${IPSET_FILE}"  | awk '{ print $2 }' | sed 's/_new//'`
+if [ ! -e "${IPSET_DIR}/${ipset_name}.ipset" ]; then
+    cat "${IPSET_FILE}" | sed 's/_new//' >"${IPSET_DIR}/${ipset_name}.ipset"
+    fi
+echo -n $"Load ${ipset_name} ipset"
+${IPSET} restore -file "${IPSET_DIR}/${ipset_name}.ipset" >/dev/null 2>&1
+echo
+done
+return 0
+}
+
+save_ipset() {
+ls -x -1 "${IPSET_DIR}/"*.conf | while read IPSET_FILE; do
+ipset_name=`grep -P "^create\s+(\S+)\s+" "${IPSET_FILE}"  | awk '{ print $2 }' | sed 's/_new//'`
+[ -z "${ipset_name}" ] && continue
+echo -n $"Save ${ipset_name} ipset"
+${IPSET} save ${ipset_name} -file "${IPSET_DIR}/${ipset_name}.ipset" >/dev/null 2>&1
+echo
+done
 return 0
 }
 
@@ -69,6 +77,10 @@ case "$1" in
         stop_ipset
         RET=$?
         ;;
+  save)
+        save_ipset
+        RET=$?
+        ;;
   restart|reload)
         stop_ipset
         start_ipset
@@ -80,3 +92,4 @@ case "$1" in
 esac
 
 exit ${RET}
+

+ 3 - 0
docs/systemd/netfilter-persistent.service.d/override.conf

@@ -0,0 +1,3 @@
+[Service]
+ExecStartPre=/etc/init.d/ipset start
+ExecStopPost=/etc/init.d/ipset stop

+ 1 - 1
html/inc/footer.php

@@ -1,6 +1,6 @@
 <br style="clear: both">
 
-<div id="copyright">Copyright &copy; 2008-2025 Eye v<?php print $config["version"]; ?> &nbsp<a href="https://github.com/rajven/Eye">rnd@rajven.ru</a></div>
+<div id="copyright">Copyright &copy; 2008-2026 Eye v<?php print $config["version"]; ?> &nbsp<a href="https://github.com/rajven/Eye">rnd@rajven.ru</a></div>
 <hr>
 <div>
 <?php

+ 1 - 1
html/inc/footer.simple.php

@@ -1,5 +1,5 @@
 <br style="clear: both">
-<div id="copyright">Copyright &copy; 2008-2025 Eye v<?php print $config["version"]; ?> &nbsp<a href="https://github.com/rajven/Eye">rnd@rajven.ru</a></div>
+<div id="copyright">Copyright &copy; 2008-2026 Eye v<?php print $config["version"]; ?> &nbsp<a href="https://github.com/rajven/Eye">rnd@rajven.ru</a></div>
 </div>
 </body>
 </html>

+ 20 - 4
install-eye.sh

@@ -323,7 +323,7 @@ install_deps_altlinux() {
 
     # === Сетевой бэкенд (если нужен) ===
     if [[ "$INSTALL_TYPE" == "full" || "$INSTALL_TYPE" == "backend" ]]; then
-        apt-get install -y fping
+        apt-get install -y fping iptables ipset
 
         # Общие Perl-модули (независимо от СУБД)
         apt-get install -y perl \
@@ -337,7 +337,7 @@ install_deps_altlinux() {
             perl-Crypt-Rijndael perl-Crypt-CBC perl-CryptX perl-Crypt-DES \
             perl-File-Path-Tiny perl-Expect perl-Proc-ProcessTable \
             perl-Text-CSV \
-            perl-DBD-Pg perl-DBD-mysql
+            perl-DBD-Pg perl-DBD-mysql \
     fi
 
     # Дополнительные проверки (например, fping — нужны только бэкенду)
@@ -383,7 +383,7 @@ install_deps_debian() {
 
     # === Сетевой бэкенд (если нужен) ===
     if [[ "$INSTALL_TYPE" == "full" || "$INSTALL_TYPE" == "backend" ]]; then
-        apt-get install -y fping
+        apt-get install -y fping ipset netfilter-persistent
 
         # Perl и обязательные модули (имена корректны для Ubuntu 24.04)
         apt-get install -y perl \
@@ -400,7 +400,6 @@ install_deps_debian() {
     fi
 
     # === Дополнительно (если нужно) ===
-    # Раскомментируйте, если требуется DNS-сервер
     # apt-get install -y bind9 bind9-utils bind9-host
 }
 
@@ -1580,6 +1579,23 @@ setup_additional_services() {
         print_info "eye-statd service (NetFlow) enabled"
     fi
 
+    # add ipset
+    if [[ -f "/opt/Eye/docs/systemd/init.d/ipset" ]]; then
+        mkdir -p /etc/init.d
+        cp /opt/Eye/docs/systemd/init.d/ipset /etc/init.d
+        if [[ -f "/opt/Eye/docs/systemd/netfilter-persistent.service.d" ]]; then
+           if [[ "$OS_FAMILY" == "alt" ]]; then
+               mkdir -p /etc/systemd/system/iptables.service.d
+               cp /opt/Eye/docs/systemd/netfilter-persistent.service.d/override.conf /etc/systemd/system/iptables.service.d
+               else
+               mkdir -p /etc/systemd/system/netfilter-persistent.service.d
+               cp /opt/Eye/docs/systemd/netfilter-persistent.service.d/override.conf /etc/systemd/system/netfilter-persistent.service.d
+           fi
+        $SERVICE_MANAGER daemon-reload
+        fi
+        print_info "ipset installed"
+    fi
+
     # Configure DHCP
     setup_dhcp_server