squid, pf и таблицы адресов

Программирование
Появилась задача — использовать в сквиде список адресов не из фиксированного файла, а прямиком из таблиц firewall (pf).
Напомню, в сквиде можно объявить список адресов, например вот так:
acl dst_free dst "/etc/firewall/net.free"

ну и в файле /etc/firewall/net.free прописать наши адреса, например:
10.0.0.0/8
62.76.176.0/20
78.132.128.0/17
82.179.144.0/20
83.234.112.0/24
91.202.20.0/22
91.211.28.0/22
93.186.96.0/20
172.16.0.0/12
192.168.135.12/30
192.168.140.0/24
193.33.62.0/23
193.34.12.0/22
193.203.60.0/22
195.19.96.0/19
213.135.128.0/19

Таким образом, после запуска сквид в acl dst_free будут находиться эти адреса.
А у нас задача — использовать адреса, которые сейчас актуальные в таблице pf.
Для примера, на текущий момент в таблице net_free находятся такие адреса:
[16:36 dk@mira ~]> pfctl -t net_free -T show
   10.0.0.0/8
   62.76.176.0/20
   78.132.128.0/17
   82.179.144.0/20
   83.234.112.0/24
   91.202.20.0/22
   91.211.28.0/22
   93.186.96.0/20
   172.16.0.0/12
   192.168.135.12/30
   192.168.140.0/24
   193.33.62.0/23
   193.34.12.0/22
   193.203.60.0/22
   195.19.96.0/19
   213.135.128.0/19


Эта таблица в pf постоянно меняется, и вот как-то надо ее подружить со сквидом.

В сквиде есть особый тип acl — external_acl_type. Он применяется для использования внешний программ для составления acl.
Первая мысль — написать perl-скрипт, который будет дергать pfctl и узнавать на соответствие адреса таблице. Через pfctl это делается так:
[16:39 dk@mira ~]> pfctl -t net_free -T test mail.ru
0/4 addresses match.

Т.е. не один из адресов mail.ru не находится в таблице net_free (0 совпадений из 4).
Или так:
[16:39 dk@mira ~]> pfctl -t net_free -T test tmb.ru
1/1 addresses match.

Адрес tmb.ru принадлежит таблице net_free (1 совпадение из 1).

Но, чуток подумал, решил так не делать. Все таки этот скрипт будет дергаться довольно часто.
Итак, прочитав man pf, решил написать определение принадлежности адреса в таблице на Си.

Переводить man по pf нет ни времени ни желания, да в Интернет найти не составит никакого труда. Поэтому, элементарные вещи объясняться не будут.

Как все это дело работает:
Сквид умеет вызывать внешние acl с различными параметрами. Мне понадобятся такие параметры, как имя таблицы и ip-адрес. Первое разочарование — сквид не умеет передавать ip-адрес, а только хост (это может быть как dns-имя так и ip-адрес).
На выходе из программы должно последовать либо «OK» (этот хост принадлежит указанной таблице), либо «ERR» (хост не принадлежит указанной таблице).
Собственно все.

Исходя из того, что сквид не передает ip-адрес напрямую — будем запрашивать dns-сервер, для определения такового:
// Определяем адрес хоста
    hEnt = gethostbyname(host);
    if (!hEnt || hEnt->h_length < 1) return 0;

После чего приводим адрес к виду, понятному pf:
// Заполняем структуру pfr_addr
    bzero(&pfAddr, sizeof(pfAddr));
    switch(hEnt->h_addrtype){
        case AF_INET:
            pfAddr.pfra_ip4addr.s_addr = * (unsigned long *) hEnt->h_addr_list[0];
            break;
        case AF_INET6:
            memcpy(&pfAddr.pfra_ip6addr, hEnt->h_addr_list[0], 16);
            break;
        default:
            close(pf);
            return 0;
    }
    pfAddr.pfra_af = hEnt->h_addrtype;
    pfAddr.pfra_net = 32;

Затем составляем запрос и отсылаем его pf:
// Заполняем структуру io
    bzero(&io, sizeof(io));
    io.pfrio_table.pfrt_anchor[0] = '\0';
    strcpy(io.pfrio_table.pfrt_name, table);
    io.pfrio_buffer = &pfAddr;
    io.pfrio_size = 1;
    io.pfrio_esize = sizeof(struct pfr_addr);
    // Ищем адрес в таблице PF
    if (ioctl(pf, DIOCRTSTADDRS, &io) < 0) {
        close(pf);
        return 0;
    }
    else return io.pfrio_nmatch;

В результате вернется либо «0» — ничего не найдено, либо положительное число — что-то было найдено.

Это и есть вся логика программы, не густо :)
Полный исходный текст:
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/pfvar.h>
#include <netdb.h>


char dev[PATH_MAX];

int pf_test_host(char *host, char *table)
{
    struct hostent* hEnt;
    struct pfr_addr pfAddr;
    struct pfioc_table io;
    int pf;
    // pf
    pf = open(dev, O_RDWR);
    if (pf < 0) return 0;
    // Определяем адрес хоста
    hEnt = gethostbyname(host);
    if (!hEnt || hEnt->h_length < 1) return 0;
    // Заполняем структуру pfr_addr
    bzero(&pfAddr, sizeof(pfAddr));
    switch(hEnt->h_addrtype){
        case AF_INET:
            pfAddr.pfra_ip4addr.s_addr = * (unsigned long *) hEnt->h_addr_list[0];
            break;
        case AF_INET6:
            memcpy(&pfAddr.pfra_ip6addr, hEnt->h_addr_list[0], 16);
            break;
        default:
            close(pf);
            return 0;
    }
    pfAddr.pfra_af = hEnt->h_addrtype;
    pfAddr.pfra_net = 32;
    // Заполняем структуру io
    bzero(&io, sizeof(io));
    io.pfrio_table.pfrt_anchor[0] = '\0';
    strcpy(io.pfrio_table.pfrt_name, table);
    io.pfrio_buffer = &pfAddr;
    io.pfrio_size = 1;
    io.pfrio_esize = sizeof(struct pfr_addr);
    // Ищем адрес в таблице PF
    if (ioctl(pf, DIOCRTSTADDRS, &io) < 0) {
        close(pf);
        return 0;
    }
    else return io.pfrio_nmatch;
}

void usage()
{
    puts("\
Usage: squid_pf [-d <pf device>] [-h]\n\
Options:\n\
\t-d <device name> - pf device (default: /dev/pf);\n\
\t-h               - show help.\n\
");
}

int main(int argc, char *argv[])
{
    char* opts = "d:h";
    char buf[1024];
    int opt;
    // Разбираем параметры командной строки
    strncpy(dev, "/dev/pf", sizeof(dev));
    while ((opt = getopt(argc, argv, opts)) != -1){
        switch(opt){
            // путь к устройству pf
            case 'd':
                strncpy(dev, optarg, sizeof(dev));
                break;
            // показать справку
            case 'h':
                usage();
                return 0;
        }
    }
    //
    setvbuf (stdout, NULL, _IOLBF, 0);
    // Зацикливаемся
    while (fgets(buf, sizeof(buf), stdin)){
        char *cp = strchr(buf, '\n');
        char *host;
        char *table;
        if (cp == NULL){
            while (fgets(buf, sizeof(buf), stdin))
                if (strchr(buf, '\n') != NULL) break;
            fprintf(stderr, "ERROR: message too long");
            printf("ERR\n");
        }
        else {
            // Ставим вместо '\n' конец строки
            *cp = '\0';
            // Выбираем хост и имя таблицы
            host = strtok(buf, " ");
            table = strtok(NULL, " ");
            if (!host || !table) printf("ERR\n");
            else printf(pf_test_host(host, table) ? "OK\n" : "ERR\n");
        }
    }
}


Компилируем, запускаем и проверяем:
[16:54 dk@mira ~/squid_pf]> ./squid_pf
10.2.2.10 net_free
OK
ya.ru net_free
ERR
lenta.ru net_free
ERR
tmb.ru net_free
OK
lanta-net.ru
ERR
lanta-net.ru net_free
OK
mks-tambov.ru net_free
OK
oss-tambov.ru net_free
OK

Все работает, как и задумывалось.

Теперь добавляем ее в сквид, для этого в конфиг вносим следующие строки:
external_acl_type squid_pf ttl=60               %DST /usr/local/libexec/squid/squid_pf
acl     dst_free                external        squid_pf net_free

Параметр %DST — это хост, который сквид будет передавать программе.
net_free — это имя таблицы.
В итоге строка программе будет передаваться в таком виде "%DST net_free", вместо %DST — имя хоста, например «ya.ru net_free»

Все это дело будет использоваться для ограничения скорости в сквиде, механизм ограничения скорости называется delay pools. Вносим изменения в конфиг:
# Delay Pools
# -----------------------------------------------------------------------------
delay_pools 1
delay_class 1 1
delay_parameters 1 65536/65536
delay_access 1 allow !dst_free
delay_access 1 deny all


Пробуем ..., и ничего не получается :) Политика dst_free просто не вызывается в delay_access. После недолгого поиска в Интернет — определяется суть проблема. В сквид политики делятся на 2 типа, slow и fast. fast — сквид может определить результат политики без лишних телодвижений (например, ip-адрес клиента, порт, протокол, адрес запроса и т.д. и т.п.). slow — сквиду придется все-таки произвести лишние телодвижения (например вызвать внешнюю программу, обработать ответ от удаленного сервера и прочее).
И разумеется external_acl_type попадается под тип slow.

Но на всякую проблему находится и решение. Если в процессе запроса политика хотя раз рассчитывалась — она автоматически становится fast. Этим фактом и воспользуемся, для чего добавим следующие строчки в конфиг:
http_access allow dst_free
http_access allow !dst_free


Перезапускаем очередной раз сквид (squid -k reconfigure) и проводим тестирование


Все работает.

Итоги:
Важно понять — что в данном случае и pf и прокси-сервер оперируют одинаковыми таблицами адресов. И внеся новые адреса в таблицы — изменения коснутся и как pf так и squid.

1 комментарий

avatar
Забавно, возможно пригодится :)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.