From : Alexander Osin (snark@kikg.ifmo.ru, 2:5030/1360@fidonet)
Subj : Как организовать связку samba-ipfw-mysql
-------------------------------------------------------------------------------
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!
!! Все права на данный текст принадлежат автору.
!! Автор: Александр Осин (Alexander Osin)
!! snark@kikg.ifmo.ru, 2:5030/1360@fidonet
!! Последняя версия доступна на
!! http://kikg.ifmo.ru/snark/unix/samba-ipfw-mysql.txt
!! ftp://kikg.ifmo.ru/pub/unix/samba-ipfw-mysql.txt.zip
!! PGP Fingerprint: 7df221cb667b34980887a4629a873bf6
!! Разрешается любое некоммерческое использование данного текста.
!! Обо всех ляпах пишите мне.
!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Сага о том, как я организовывал связку samba-ipfw-mysql
Постановка задачи:
Есть сеть из нескольких десятков мастдаек, подключенных к серверу на базе
юникс (FreeBSD 3.3) с целью использования ресурсов, таких как:
- доступ в Internet
- доступ к файлам, хранящимся на этом сервере
То есть сервер играет роль как файл-сервера, так и роутера.
Нужно организовать работу сервера таким образом, чтобы оные ресурсы
отдавались пользователям в объеме, положенным им по социальному статусу.
Доступ пользователя к тем или иным ресурсам определяется username/password,
который он использовал при входе в сеть.
Требовалось:
- обеспечить определенных юзеров правами доступа в internet по
определенным протоколам
- ограничивать количество одновременно вошедших пользователей
- считать трафик для каждого пользователя
- выводить статистику о трафике через www
Уточнения:
В рассмотренном ниже примере сеть сидела под локальными фиксированными
ip-адресами из множества 192.168.0.0/16
Локалка состояла из двух сегментов, подключенных к интерфейсам ed2 и de0.
Выход во внешний мир осуществлялся через ed1 по выделенке.
Серверный софт:
FreeBSD 3.3, ipfw, natd
Perl с DBI
Samba
MySQL
Apache
Настройку фрюникса, файрвола и ната пропустим, ибо информации об этом полно ;)
MySQL:
Вдаваться в подробности установки и настройки MySQL'я, равно как
рассказывать основы языка SQL и строить инфологические модели я не буду,
ибо нет времени. Вобщем: create database control в mysql-клиенте и вперед!
Итак, таблицы:
- сетевые протоколы (Номер, Имя); // tcp, udp, icmp, all
create table proto (
id integer primary key,
name varchar (5)
);
- права на доступ (Номер, Название, Номер протокола, Port, Hostname);
create table rule (
id integer primary key,
title varchar (10),
proto integer references proto (id),
port varchar (30),
host varchar (30)
);
- группы пользователей (Номер, Название);
create table grp (
id integer primary key,
title varchar (15)
);
- пользователи (Номер, Usename, Полное имя, Номер группы);
create table user (
id integer primary key,
name varchar (15),
full_name varchar (50),
grp integer references grp (id)
);
- назначения группам прав (Номер группы, Номер права);
Заполняем таблицы:
- протоколы:
insert into proto values (1, 'ip');
insert into proto values (2, 'tcp');
insert into proto values (3, 'udp');
insert into proto values (4, 'icmp');
- права
insert into rule values (1, 'my_server', 2, '80,20,21,22,25,110', 'my_server.domain.com');
// доступ к серверу my_server по куче протоколов
insert into rule values (2, 'icq', 3, '4000', 'any');
// аська
insert into rule values (3, 'http', 2, '80', 'any');
insert into rule values (4, 'ssh', 2, '22', 'friend_server.domain.com');
- интерфейсы
insert into interface values (1, 'ed2');
insert into interface values (2, 'de0');
- клиентские ip-адреса
insert into local_ip values (1, '192.168.0.101', 1);
insert into local_ip values (2, '192.168.0.102', 1);
insert into local_ip values (3, '192.168.0.103', 1);
insert into local_ip values (4, '192.168.0.104', 1);
insert into local_ip values (5, '192.168.0.105', 1);
// пять машин на первом сегменте
insert into local_ip values (6, '192.168.1.101', 2);
insert into local_ip values (7, '192.168.1.102', 2);
insert into local_ip values (8, '192.168.1.103', 2);
// три машины на втором сегменте
- группы юзеров
insert into grp values (1, 'teacher');
insert into grp values (2, 'student');
insert into grp values (3, 'admin');
- собственно юзера
insert into user values (1, 'snark', 'Alexander Osin', 3);
// админ
insert into user values (2, 'vanya', 'Ivan Pupkin', 2);
insert into user values (3, 'lena', 'Lena Golovach', 2);
// студенты
insert into user values (4, 'muh', 'Peter A. Muhosranskiy', 1);
// препод
- права для групп
insert into grp_rights values (3, 1);
insert into grp_rights values (3, 2);
insert into grp_rights values (3, 3);
insert into grp_rights values (3, 4);
// админ может все ;)
insert into grp_rights values (2, 1);
insert into grp_rights values (2, 3);
// студенты могут веб-серфить, читать/отправлять почту через my_server.domain.com и прочее
insert into grp_rights values (1, 1);
insert into grp_rights values (1, 2);
insert into grp_rights values (1, 3);
// препод может еще и по аське трепаться
Samba:
Ниже приведенный конфиг самбы не является оптимальным, он является моим
рабочим конфигом:
[global]
netbios name = ecgnew
workgroup = ecgdown
domain master = yes
local master = yes
preferred master = yes
domain logons = yes
logon script = %u.bat
# батничек, подключающий сетевые диски, либо выполняющий logoff,
# если выявлен повторный вход того же пользователя
os level = 65
public = yes
preserve case = yes
default case = upper
case sensitive = no
character set = KOI8-R
client code page = 866
encrypt passwords = yes
max open files = 1000
time server = True
[netlogon]
path = /usr/local/samba/netlogon
writable = no
public = no
root preexec = /usr/local/bin/samba_netlogon.pl %u %I
# скрипт, запускаемый при входе в сеть
# %u - username клиента
# %I - ip-адрес клиента
[user]
path = /usr/local/samba/vol_user
public = no
read only = no
create mask = 0664
directory mode = 0775
preexec = /usr/local/bin/samba_firewall.pl %u %I up
# скрипт, запускаемый при подключении тома user, т.е. при входе в систему
postexec = /usr/local/bin/samba_firewall.pl %u %I down
# скрипт, запускаемый при отключении тома user, т.е. при выходе из системы
[soft]
path = /usr/local/samba/vol_soft
public = yes
read only = no
[games]
path = /usr/local/samba/vol_games
public = yes
read only = yes
[distrib]
path = /usr/local/samba/vol_distrib
public = yes
read only = no
На клиентских машинах, в свойствах сети нужно указать:
Вход в сеть = Сеть Microsoft
Клиент для сетей Microsoft = Поставить галочку "Вход в домен", домен: ecgdown
Прописать ip-адреса (если не используется DHCP, у меня - не используется)
Запретить трогать сетевые настройки
[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionPoliciesNetwork]
"NoNetSetup"=dword:00000001
Запретить вход в систему без проверки сетевого пароля (чтобы юзер не мог
забить на Logon Window, нажать ESC, и подключить диски вручную)
[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionPoliciesNetworkLogon]
"MustBeValidated"=dword:00000001
Скрипты:
Вобщем суть такова, при входе в домен запускется
/usr/local/bin/samba_netlogon.pl username ipaddress
который смотрит (через скрипт samba_user.pl), нет ли еще одного такого юзера,
зашедшего с другого ip, если есть, создает флажок
/usr/local/samba/netlogon/$user.off
который затем анализируется login script'ом
--- begin: samba_netlogon.pl ---
#!/usr/bin/perl
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime (time);
$mon++;
$year+=1900;
open logfile, ">>/tmp/netlogon.log";
$user=$ARGV[0];
$ip=$ARGV[1];
print logfile "$year-$mon-$mday $hour:$min:$sec $user from $ipn";
unlink "/usr/local/samba/netlogon/$user.off" or print logfile "tunlink errorn";
$pres = `/usr/local/bin/samba_user.pl $user $ip`;
print logfile "tgetting $presn";
if ($pres < 1) {
print logfile "tallown";
} else {
open off, ">/usr/local/samba/netlogon/$user.off";
print off "n";
close off;
print logfile "tdenyn";
}
close logfile;
--- end: samba_netlogon.pl ---
Скрипт, анализирующий выход smbstatus'а, возвращает количество пользователей
подключенных не с заданного ip-адреса.
--- begin: samba_user.pl ---
#!/usr/bin/perl
$u=$ARGV[0];
$m=$ARGV[1];
$c=0;
open file, "smbstatus|";
$state=0;
$line=0;
while ($data = <file>) {
chop $data;
if (length ($data) == 0) {
$state++;
next;
}
if (substr ($data, 1, 3) eq '---') {
$state++;
next;
}
if ($state < 1) {
next;
}
if ($state == 3) {
next;
}
if ($state == 2) {
($serv, $user, $group, $pid, $machine, $ip_addr, $wday, $mon, $day, $time, $year) = split (' ', $data);
if ($u eq $user) {
chop ($ip_addr);
$ip_addr = substr ($ip_addr, 1);
if ($m ne $ip_addr) {$IP{$ip_addr}++;}
}
}
if ($state == 4) {
}
$line++;
}
close file;
$count = scalar (keys (%IP));
print "$count";
--- end: samba_user.pl ---
Логин скрипт выглядит следующим образом:
--- begin: username.bat ---
@echo off
if not exist \ecgnewnetlogonusername.off goto logon
\ecgnewnetlogonlogoff.exe
goto exit
:logon
echo Welcome!
c:windowsnet use u: \ecgnewuser
c:windowsnet use s: \ecgnewsoft
c:windowsnet use x: \ecgnewgames
c:windowsnet time \ecgnew /set /yes
:exit
--- end: username.bat ---
Logoff.exe написан на паскале (компилирован консольным dcc32 от Delphi4) и
выглядит так:
--- begin: logoff.pas ---
Uses Windows;
begin
Sleep (3000);
ExitWindowsEx (EWX_FORCE or EWX_LOGOFF, 0);
end.
--- end: logoff.pas ---
Теперь самое главное, допусти юзер прошел логон, теперь нужно подключить его
к интернету. Пусть у нас стоит по умолчанию deny from any to any via any
Несколько статических правил:
# netbios.data
ipfw -q add 1000 allow tcp from 192.168.0.1/24 to 192.168.0.1 139 in via ed2
ipfw -q add 1010 allow tcp from 192.168.0.1 139 to 192.168.0.1/24 out via ed2
ipfw -q add 1050 allow tcp from 192.168.1.1/24 to 192.168.1.1 139 in via de0
ipfw -q add 1060 allow tcp from 192.168.1.1 139 to 192.168.1.1/24 out via de0
# netbios.name
ipfw -q add 1100 allow udp from 192.168.0.1 138 to 192.168.0.1/24 138 out via ed2
ipfw -q add 1110 allow udp from 192.168.0.1/24 138 to 192.168.0.1 138 in via ed2
ipfw -q add 1120 allow udp from 192.168.0.1 137 to 192.168.0.1/24 137 out via ed2
ipfw -q add 1130 allow udp from 192.168.0.1/24 137 to 192.168.0.1 137 in via ed2
ipfw -q add 1150 allow udp from 192.168.1.1 138 to 192.168.1.1/24 138 out via de0
ipfw -q add 1160 allow udp from 192.168.1.1/24 138 to 192.168.1.1 138 in via de0
ipfw -q add 1170 allow udp from 192.168.1.1 137 to 192.168.1.1/24 137 out via de0
ipfw -q add 1180 allow udp from 192.168.1.1/24 137 to 192.168.1.1 137 in via de0
# name service
ipfw -q add 1300 allow udp from my_dns_server.domain.com 53 to my_router.domain.com in via ed1
ipfw -q add 1300 allow udp from my_router.domain.com to my_dns_server.domain.com 53 out via ed1
ipfw -q add 1400 allow udp from 192.168.0.1/24 to my_dns_server.domain.com 53 in via ed2
ipfw -q add 1410 allow udp from my_dns_server.domain.com 53 to 192.168.0.1/24 in via ed1
ipfw -q add 1420 allow udp from my_dns_server.domain.com 53 to 192.168.0.1/24 out via ed2
ipfw -q add 1450 allow udp from 192.168.1.1/24 to my_dns_server.domain.com 53 in via de0
ipfw -q add 1460 allow udp from my_dns_server.domain.com 53 to 192.168.1.1/24 in via ed1
ipfw -q add 1470 allow udp from my_dns_server.domain.com 53 to 192.168.1.1/24 out via de0
# broadcasts
ipfw -q add 1300 allow udp from 192.168.0.1/24 to 192.168.0.1/24 in via ed2
ipfw -q add 1300 allow udp from 192.168.1.1/24 to 192.168.1.1/24 in via de0
# smtp, pop3, http, ftp, ssh
ipfw -q add 1400 allow tcp from my_router.domain.com to any 25,110,80,20,21,22 out via ed1
ipfw -q add 1410 allow tcp from any 25,110,80,20,21,22 to my_router.domain.com in via ed1
ipfw -q add 1500 allow tcp from any 25,110,80,20,21,22 to 192.168.0.1/24 in via ed1
ipfw -q add 1510 allow tcp from any 25,110,80,20,21,22 to 192.168.1.1/24 in via ed1
# icq
ipfw -q add 1600 allow udp from any 4000 to my_router.domain.com in via ed1
ipfw -q add 1610 allow udp from my_router.domain.com to any 4000 out via ed1
ipfw -q add 1620 allow udp from any 4000 to 192.168.0.1/24 in via ed1
ipfw -q add 1630 allow udp from any 4000 to 192.168.1.1/24 in via ed1
Теперь скрипты, которые будут генерить динамические правила:
- samba_firewall.pl - запускается при подключении тома user и передает
скрипту control-daemon имя юзер и ip-адрес.
- control-daemon - сидит в памяти (запускать от рута!), принимает запросы,
запускает обработчик запросов: control-agent
Самое тяжелое - на последок ;)
Из базы данных выбирается список правил для данного юзера, в соответствии с
ip-адресом вычисляется номер файрвольных рулесов для этого ip/юзера.
Перед добавлением, вызнается размер перекаченного и заноситься в таблицу
traffic. Таким образом, даже если юзер отресетит свою тачку, мы все равно
узнаем сколько порнухи он выкачал после следующего входа с этой машины ;)
--- begin: control-agent ---
#!/usr/bin/perl
use DBI;
$user=$ARGV[0];
$ip=$ARGV[1];
$action=$ARGV[2];
open file, ">>/tmp/samba.log";
$mysqldate = &mysql_date();
print file "$mysqldate $action: $user from $ipn";
$query_user_src=<<Q1;
select proto.name, rule.port, user.id, rule.host, rule.id from proto, rule, grp_rights, grp, user where
user.grp = grp.id and
grp.id = grp_rights.grp and
grp_rights.rule = rule.id and
rule.proto = proto.id and
Q1
$query_ip_src=<<Q2;
select 10000+local_ip.id*100, interface.name from interface, local_ip where
interface.id = local_ip.interface and
Q2
$query_user=$query_user_src." user.name='${user}'n";
$query_ip=$query_ip_src." local_ip.value='${ip}'n";
print file "topen database...n";
print file "tquery_interface...n";
$dbh = DBI->connect("DBI:mysql:control", "control", "controlisimo");
$sthi = $dbh->prepare($query_ip) or die "prepare: $dbh->errstrn";
$rvi = $sthi->execute or die "execute: $sthi->errstrn";
if (@rowi = $sthi->fetchrow_array) {
$rule_num = $rowi[0];
$interface = $rowi[1];
} else {
die "bad ip";
}
print file "tquery_user...n";
$sthu = $dbh->prepare($query_user) or die "prepare: $dbh->errstrn";
$rvi = $sthu->execute or die "execute: $sthu->errstrn";
while (@rowu = $sthu->fetchrow_array) {
$proto = $rowu[0];
$port = $rowu[1];
$user_id = $rowu[2];
$host = $rowu[3];
$rule_id = $rowu[4];
delete_rule ($rule_num);
if ($action eq "up") {
add_rule ($rule_num, $user_id, $rule_id);
$this_rule = "$rule_num allow $proto from $ip to $host $port in via $interface";
print file "tadding rule $this_rule...n";
system ("/sbin/ipfw a $this_rule");
}
$rule_num += 5;
delete_rule ($rule_num);
if ($action eq "up") {
add_rule ($rule_num, $user_id, $rule_id);
$this_rule = "$rule_num allow $proto from $host $port to $ip out via $interface";
print file "tadding rule $this_rule...n";
system ("/sbin/ipfw a $this_rule");
}
$rule_num += 5;
}
$rc = $dbh->disconnect;
print file "tclose database ...n";
close file;
sub delete_rule {
print file "tdeleting rule $_[0]...n";
print "rule: $_[0]";
system ("/sbin/ipfw show $_[0]| /usr/bin/awk '{ print $3 }'");
if ($size_list = `/sbin/ipfw show $_[0]| /usr/bin/awk '{ print $3 }'`) {
@rules_list = split ('n', $size_list);
$size = $rules_list[0];
} else {
$size="0";
}
print file "tquery_logout...n";
print "rule: $_[0]n";
$sths = $dbh->prepare("select user, start, rule from login where rule_num=$_[0] order by start desc") or die "prepare: $dbh->errstrn";
$rvs = $sths->execute or die "execute: $sths->errstrn";
if (@rows = $sths->fetchrow_array) {
$id = $rows[0];
$start = $rows[1];
$r_id = $rows[2];
$mysqldate = &mysql_date();
print file "tquery_traffic...n";
$sthd = $dbh->prepare("insert into traffic values ($id, $size, '$start', '$mysqldate', $r_id)") or die "prepare: $dbh->errstrn";
$rvd = $sthd->execute or die "execute: $sthd->errstrn";
}
system ("/sbin/ipfw -q d $_[0]");
}
sub add_rule {
$mysqldate = &mysql_date();
print file "tquery_login...n";
print ("insert into login values ($_[0], $_[1], '$mysqldate', $_[2])n");
$stha = $dbh->prepare("insert into login values ($_[0], $_[1], '$mysqldate', $_[2])") or die "prepare: $dbh->errstrn";
$rva = $stha->execute or die "execute: $stha->errstrn";
}
sub mysql_date {
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime (time);
$year += 1900;
$mon++;
return ("$year-$mon-$mday $hour:$min:$sec");
}
--- end: control-agent ---
Работать очень удобно, главное сначала немного помучиться ;)
Например, хотим открыть группе admin чего-нибудь еще, например ftp:
insert into rule values (5, 'ftp', 2, '20,21', 'any');
insert into grp_rights values (3, 5);
Хотим отобрать у teacher аську:
delete from grp_rights where grp=1 and rule=2
Добавление нового пользователя $username в группу student:
Apache и статистика:
Маленький cgi-скриптик, выводящий кто скоко накачал.
--- begin: /cgi-bin/stat.pl ---
#!/usr/bin/perl
use DBI;
$query=<<Q1;
select user.name, size, start, finish, rule.title from rule, user, traffic where
user.id = traffic.user and rule.id = traffic.rule
Q1
$o_head=<<Ohead;
Content-type: text/html
<html>
<body>
Ohead
$o_tbl=<<Otbl;
<table border=1 width="100%">
<tr width="100%">
<td width="50%">User</td>
<td width="50%">Size</td>
</tr>
Otbl
$o_tail=<<Otail;
</body>
</html>
Otail
$dbh = DBI->connect("DBI:mysql:control", "control", "controlisimo");
$sth = $dbh->prepare("select distinct (start) from traffic order by start") or die "prepare: $dbh->errstrn";
$rv = $sth->execute or die "execute: $sth->errstrn";
$i=0;
while (@row = $sth->fetchrow_array) {
($dt1, $td1) = split (' ', $row[0]);
$dates{$dt1} ++;
if ($dates{$dt1} == 1) {
$Dates[$i] = $dt1;
$i++;
}
}
print $o_head;
for ($i=0; $i<=$#Dates; $i++) {
print "$Dates[$i] n";
}
for ($i=0; $i<=$#Dates; $i++) {
$q=$query."and start between '$Dates[$i] 00:00:00' and '$Dates[$i] 23:59:59' order by start";
$sth = $dbh->prepare($q) or die "prepare: $dbh->errstrn";
$rv = $sth->execute or die "execute: $sth->errstrn";
while (@row = $sth->fetchrow_array) {
$user = $row[0];
$size = $row[1];
$start = $row[2];
$finish = $row[3];
$rule = $row[4];
$Size{$user} += $size;
}
print "<h3>$Dates[$i]</h3>n";
print $o_tbl;
foreach $u (keys (%Size)) {
print "<tr><td>$u</td><td>$Size{$u}</td></tr>n";
$Size{$u} = 0;
}
print "</table>n";
}
print $o_tail;
$rc = $dbh->disconnect;
sub format_size {
$sz = $_[0];
if ($sz < 1024) {return $sz;}
$sz = int ($sz / 1024);
if ($sz < 1024) {return "${sz}K";}
$sz = int ($sz / 1024);
if ($sz < 1024) {return "${sz}M";}
$sz = int ($sz / 1024);
if ($sz < 1024) {return "${sz}G";}
}
--- end: /cgi-bin/stat.pl ---
Последнее.
Разумеется можно сделать прямее, лучше и проще. Но у меня работает, причем,
что удивительно без сбоев ;)
В следующей версии сделаю непуск юзера по временному периоду, отключ юзера
по тайму и превышению трафика и еще чего-нибудь.
Кто захочет принять участие - присоединяйтесь.
Все! Дополнения, разъяснения -> мыло.
Удачи!
// The Snark, 11.08.2000