воскресенье, 27 апреля 2008 г.

PPTP VPN-server on Linux

Вдоволь накушавшись проблем с mpd+dummynet на новых FreeBSD (новых = 6.3 и 7.0), да и просто, чтобы не терять квалификации, решил соорудить PPTP VPN-Server на базе Linux. И по старой памяти и долгим летам саппорта был выбран RHEL 5.1, а если быть точным - CentOS 5.1. Интересно - возможно в будущем будет смысл сколотить подобное на Ubuntu LTS одной из версий.

Чтобы не изобретать велосипеда - было решено попытаться реализовать все то, что имелось в работающих серверах на базе FreeBSD с mpd. А именно - обжимать клиентские каналы ответом от freeradius (см. ng_car), обжимать несколько ай-пи в одну трубу (см. dummynet), сбрасывать клиента с линии ответом от радиус (см. mpd-drop), собирать детальную статистику по трафику в формате netflow (см. ng_netflow).


Итак - в распоряжении у нас был iproute2 вместе со своим tc и htb, pppd, модуль ifb, perl, accel-pptp. Причем accel-pptp был выбран не случайно - использование обычного poptop с его user-level обработкой gre-трафика было способно уложить на лопатки даже при относительно незначительном трафике относительно мощный сервер.

В первую очередь необходимо было добиться работы самого pptp. Был стянут архив исходников accel-pptp с SourceForge, распакован и сделан make server_install. Незначительно поправлены конфиги. В итоге /etc/ppp/options.pptpd:

plugin radius.so
plugin radattr.so
name pptpd
refuse-pap
require-chap
nobsdcomp
nodeflate
novj
novjccomp
nologfd
ms-dns
ms-dns
mtu 1496
mru 1496
lcp-echo-failure 6
lcp-echo-interval 10
и /etc/pptpd.conf:
option /etc/ppp/options.pptpd
connections 500
localip

Еще необходимо указать адрес радиус-сервера в /etc/radiusclient/*.
После чего соединение успешно завелось. И встала задача реализовать шейпинг трафика по радиус параметрам. Был выбран классический вариант решения - в скрипте ip-up парсить /var/run/radattr.pppX. Тут стоит заметить - в радиус словарь к параметрам mpd был также добавлен параметр ifb_shape, который должен был содержать имя ifb-интерфейса, на котором обжат клиент. Это было необходимо для обжатия группы айпи-адресов на общую скорость. Скрипт получился следующий:
#!/usr/bin/perl

$DEV = $ARGV[0];
$REMOTE_IP = $ARGV[4];
$tc = "/sbin/tc";

open(F, "/var/run/radattr.$DEV");
@lines = ;
close(F);

%params = ();

foreach $line (@lines)
{
chomp($line);
$line =~ /(\S+) (.+)/;
$param = $1;
$value = $2;
if($param eq "mpd-limit")
{
if($value =~ /^in#1=all shape (\d+) (\d+) pass/)
{
$params{'speed_in'} = $1;
$params{'burst_in'} = $2;
}
if($value =~ /^out#1=all shape (\d+) (\d+) pass/)
{
$params{'speed_out'} = $1;
$params{'burst_out'} = $2;
}
}
if($param eq "ifb_shape")
{
$params{'ifb_shape'} = $value;
}
}

if(exists($params{'ifb_shape'}))
{
$ifb_name = $params{'ifb_shape'};
# move to ifb
`$tc qdisc del dev $DEV ingress`;
`$tc qdisc del dev $DEV root`;
`$tc qdisc add dev $DEV handle ffff: ingress`;
`$tc filter add dev $DEV parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev $ifb_name`;
`$tc qdisc add dev $DEV root handle 2: prio`;
`$tc filter add dev $DEV parent 2: protocol ip u32 match u32 0 0 action mirred egress redirect dev $ifb_name`;
}
else
{
if(exists $params{'speed_out'} && exists $params{'burst_out'})
{
$speed_out = $params{'speed_out'};
$burst_out = $params{'burst_out'};

`$tc qdisc del dev $DEV root`;
`$tc qdisc add dev $DEV root handle 1 htb default 30 r2q 100`;
`$tc class add dev $DEV parent 1: classid 1:2 htb rate $speed_out`;
`$tc class add dev $DEV parent 1:2 classid 1:10 htb rate $speed_out`;
`$tc qdisc add dev $DEV parent 1:10 handle 10 sfq`;
`$tc filter add dev $DEV parent 1:0 protocol ip prio 100 u32 match ip dst $REMOTE_IP/32 classid 1:10`;
}
if(exists $params{'speed_in'} && exists $params{'burst_in'})
{
$speed_in = $params{'speed_in'};
$burst_in = $params{'burst_in'};

`$tc qdisc del dev $DEV ingress`;
`$tc qdisc add dev $DEV handle ffff: ingress`;
`$tc filter add dev $DEV parent ffff: protocol ip prio 50 u32 match ip src 0.0.0.0/0 police rate $speed_in buffer 10k drop flowid :1`;
}
}

Шейпинг на ifbX был организован также при помощи скрипта. Выкладывать его не вижу смысла - для всех страждущих имеется htb.init, выполняющий по сути сходные функции.
Только вот ждала засада в CentOS на тему редиректа трафика на другое устройство. Правило честно добавлялось, но работать - не работало. Да и где-то упоминалось в интернете, что с 2.6.18 ifb работает не очень хорошо. А как известно редхат плотным партизанским отрядом засел на 2.6.18 и ни пяди не сдает. С одной стороны правильно - а с другой - могли бы организовать contrib. Итого пришлось собрать ванильное 2.6.22.19 и обновить iproute2 - и вот только тогда пришло счастье работы редиректа.

Проблема сброса пользователя с линии по ответу от радиус была решена патчем на radius, написанным специально для данной цели:
diff -r -U 3 ppp-2.4.4/pppd/plugins/radius/buildreq.c ppp-2.4.4_xxx/pppd/plugins/radius/buildreq.c
--- ppp-2.4.4/pppd/plugins/radius/buildreq.c 2004-11-14 09:26:26.000000000 +0200
+++ ppp-2.4.4_xxx/pppd/plugins/radius/buildreq.c 2008-04-20 13:16:47.276938599 +0300
@@ -288,7 +288,7 @@

int rc_acct_using_server(SERVER *acctserver,
UINT4 client_port,
- VALUE_PAIR *send)
+ VALUE_PAIR *send, VALUE_PAIR **received)
{
SEND_DATA data;
VALUE_PAIR *adt_vp;
@@ -341,8 +341,11 @@

result = rc_send_server (&data, msg, NULL);
}
+
+ // Return acct reply
+ *received = data.receive_pairs;

- rc_avpair_free(data.receive_pairs);
+// rc_avpair_free(data.receive_pairs);

return result;
}
@@ -357,12 +360,12 @@
* filled in by this function, the rest has to be supplied.
*/

-int rc_acct(UINT4 client_port, VALUE_PAIR *send)
+int rc_acct(UINT4 client_port, VALUE_PAIR *send, VALUE_PAIR **received)
{
SERVER *acctserver = rc_conf_srv("acctserver");
if (!acctserver) return (ERROR_RC);

- return rc_acct_using_server(acctserver, client_port, send);
+ return rc_acct_using_server(acctserver, client_port, send, received);
}

/*
diff -r -U 3 ppp-2.4.4/pppd/plugins/radius/radius.c ppp-2.4.4_shulik/pppd/plugins/radius/radius.c
--- ppp-2.4.4/pppd/plugins/radius/radius.c 2006-05-22 03:01:40.000000000 +0300
+++ ppp-2.4.4_shulik/pppd/plugins/radius/radius.c 2008-04-21 17:51:00.997770390 +0300
@@ -706,6 +706,34 @@
return 0;
}

+/**********************************************************************
+* %FUNCTION: radius_check_acct_reply
+* %ARGUMENTS:
+* vp -- received value-pairs
+* %RETURNS:
+* >= 0 on success; -1 on failure
+* %DESCRIPTION:
+* Parses attributes sent by RADIUS server and sets them in pppd.
+***********************************************************************/
+static int
+radius_check_acct_reply(VALUE_PAIR *vp)
+{
+ while (vp) {
+ if (vp->vendorcode == VENDOR_MPD) {
+ switch (vp->attribute) {
+ case PW_MPD_DROP_USER:
+ if (vp->lvalue == 1) {
+ lcp_close(ifunit, "Radius mpd-drop...");
+ }
+ break;
+ }
+ }
+ vp = vp->next;
+ }
+
+ return 0;
+}
+
#ifdef MPPE
/**********************************************************************
* %FUNCTION: radius_setmppekeys
@@ -845,6 +873,7 @@
UINT4 av_type;
int result;
VALUE_PAIR *send = NULL;
+ VALUE_PAIR *received = NULL;
ipcp_options *ho = &ipcp_hisoptions[0];
u_int32_t hisaddr;

@@ -897,12 +926,14 @@

if (rstate.acctserver) {
result = rc_acct_using_server(rstate.acctserver,
- rstate.client_port, send);
+ rstate.client_port, send, &received);
} else {
- result = rc_acct(rstate.client_port, send);
+ result = rc_acct(rstate.client_port, send, &received);
}

rc_avpair_free(send);
+ if(received)
+ rc_avpair_free(received);

if (result != OK_RC) {
/* RADIUS server could be down so make this a warning */
@@ -931,6 +962,7 @@
{
UINT4 av_type;
VALUE_PAIR *send = NULL;
+ VALUE_PAIR *received = NULL;
ipcp_options *ho = &ipcp_hisoptions[0];
u_int32_t hisaddr;
int result;
@@ -1049,9 +1081,9 @@

if (rstate.acctserver) {
result = rc_acct_using_server(rstate.acctserver,
- rstate.client_port, send);
+ rstate.client_port, send, &received);
} else {
- result = rc_acct(rstate.client_port, send);
+ result = rc_acct(rstate.client_port, send, &received);
}

if (result != OK_RC) {
@@ -1059,6 +1091,8 @@
syslog(LOG_WARNING,
"Accounting STOP failed for %s", rstate.user);
}
+ if(received)
+ rc_avpair_free(received);
rc_avpair_free(send);
}

@@ -1076,6 +1110,7 @@
{
UINT4 av_type;
VALUE_PAIR *send = NULL;
+ VALUE_PAIR *received = NULL;
ipcp_options *ho = &ipcp_hisoptions[0];
u_int32_t hisaddr;
int result;
@@ -1146,9 +1181,9 @@

if (rstate.acctserver) {
result = rc_acct_using_server(rstate.acctserver,
- rstate.client_port, send);
+ rstate.client_port, send, &received);
} else {
- result = rc_acct(rstate.client_port, send);
+ result = rc_acct(rstate.client_port, send, &received);
}

if (result != OK_RC) {
@@ -1156,8 +1191,19 @@
syslog(LOG_WARNING,
"Interim accounting failed for %s", rstate.user);
}
- rc_avpair_free(send);
+
+
+ if (result == OK_RC) {
+ if (radius_check_acct_reply(received) < 0) {
+ result = ERROR_RC;
+ }
+ }

+ /* free value pairs */
+ if(received)
+ rc_avpair_free(received);
+ rc_avpair_free(send);
+
/* Schedule another one */
TIMEOUT(radius_acct_interim, NULL, rstate.acct_interim_interval);
}
diff -r -U 3 ppp-2.4.4/pppd/plugins/radius/radiusclient.h ppp-2.4.4_xxx/pppd/plugins/radius/radiusclient.h
--- ppp-2.4.4/pppd/plugins/radius/radiusclient.h 2004-11-14 09:26:26.000000000 +0200
+++ ppp-2.4.4_xxx/pppd/plugins/radius/radiusclient.h 2008-04-20 13:31:32.004230642 +0300
@@ -153,6 +153,8 @@
#define PW_MS_MPPE_SEND_KEY 16 /* string */
#define PW_MS_MPPE_RECV_KEY 17 /* string */

+#define PW_MPD_DROP_USER 154 /* integer */
+
/* Accounting */

#define PW_ACCT_STATUS_TYPE 40 /* integer */
@@ -292,6 +294,7 @@
/* Vendor codes */
#define VENDOR_NONE (-1)
#define VENDOR_MICROSOFT 311
+#define VENDOR_MPD 12341

/* Server data structures */

@@ -402,8 +405,8 @@
int rc_auth_using_server __P((SERVER *, UINT4, VALUE_PAIR *, VALUE_PAIR **,
char *, REQUEST_INFO *));
int rc_auth_proxy __P((VALUE_PAIR *, VALUE_PAIR **, char *));
-int rc_acct __P((UINT4, VALUE_PAIR *));
-int rc_acct_using_server __P((SERVER *, UINT4, VALUE_PAIR *));
+int rc_acct __P((UINT4, VALUE_PAIR *, VALUE_PAIR **));
+int rc_acct_using_server __P((SERVER *, UINT4, VALUE_PAIR *, VALUE_PAIR **));
int rc_acct_proxy __P((VALUE_PAIR *));
int rc_check __P((char *, unsigned short, char *));

Последняя проблема со сбором статистика решалась при помощи fprobe-ulog. Не вижу смысла детально описывать процесс ее инсталляции.

UPD 16.04.2012: Для статистики целесообразнее использовать ipt_NETFLOW (http://sourceforge.net/projects/ipt-netflow/).
Установка достаточно не сложная:
mkdir ~/netflow
cd ~/netflow
wget -O ipt_netflow.tgz http://sourceforge.net/projects/ipt-netflow/files/latest/download
tar -xzvf ipt_netflow.tgz
cd ipt_netflow-1.7.1/
./configure
make all install
depmod

Указываем настройки для модуля. У меня так (с указанием адреса своего коллектора):
# grep ipt_NETFLOW /etc/modprobe.d/options.conf
options ipt_NETFLOW destination=x.x.x.x:20001 hashsize=2097152
В качестве коллектора используется flow-tools. Присутствует в репозитории Ubuntu.
Подгружаем модуль и смотрим, что получилось:
# dmesg | tail -n 5
[1195554.463965] ipt_netflow version 1.7.1 (2097152 buckets)
[1195554.482354] netflow: registered: /proc/net/stat/ipt_netflow
[1195554.482496] netflow: registered: sysctl net.netflow
[1195554.482560] netflow: added destination x.x.x.x:20001
[1195554.482564] ipt_netflow loaded.
Статистику видно тут:
# cat /proc/net/stat/ipt_netflow
Flows: active 0 (peak 0 reached 0d0h16m ago), mem 0K
Hash: size 2097152 (mem 16384K), metric 1.0, 1.0, 1.0, 1.0. MemTraf: 0 pkt, 0 K (pdu 0, 0).
Timeout: active 1800, inactive 15. Maxflows 2000000
Rate: 0 bits/sec, 0 packets/sec; Avg 1 min: 0 bps, 0 pps; 5 min: 0 bps, 0 pps
cpu#  stat: , sock: , traffic: , drop:
Total stat:      0      0      0,    0    0    0    0, sock:      0 0 0, 0 K, traffic: 0, 0 MB, drop: 0, 0 K
cpu0  stat:      0      0      0,    0    0    0    0, sock:      0 0 0, 0 K, traffic: 0, 0 MB, drop: 0, 0 K
cpu1  stat:      0      0      0,    0    0    0    0, sock:      0 0 0, 0 K, traffic: 0, 0 MB, drop: 0, 0 K
cpu2  stat:      0      0      0,    0    0    0    0, sock:      0 0 0, 0 K, traffic: 0, 0 MB, drop: 0, 0 K
cpu3  stat:      0      0      0,    0    0    0    0, sock:      0 0 0, 0 K, traffic: 0, 0 MB, drop: 0, 0 K
sock0: x.x.x.x:20001, sndbuf 126976, filled 1, peak 0; err: sndbuf reached 0, other 0
Осталось загнать в модуль трафик:
$iptables -A FORWARD -o eth0 -j NETFLOW
$iptables -A FORWARD -i eth0 -j NETFLOW
$iptables -A OUTPUT  -o eth0 -j NETFLOW
$iptables -A INPUT   -i eth0 -j NETFLOW

4 комментария:

Анонимный комментирует...

Ув. Alexander патчик явно неполный :(

Alexander комментирует...

Да, действительно патчик почему-то оказался оборван. Перезалил - теперь вроде бы все влезло....

Анонимный комментирует...

а где патчик то?

Alexander комментирует...

Текстом внутри сообщения.