Articoli Manifesto Tools Links Canali Libri Contatti ?
OpenBSD / Kernel

PF: la vita segreta di uno pseudo device

Abstract
In questo breve documento vedremo cosa succede quando gestiamo il Packet Filter di OpenBSD attraverso il tool pfctl. Ovvero prenderemo a pretesto il comportamento di pfctl per dare uno sguardo all'implementazione dei comandi ioctl e degli pseudo-device all'interno del kernel di OpenBSD.
Data di stesura: 29/12/2003
Data di pubblicazione: 23/02/2004
Ultima modifica: 04/04/2006
di Gianluigi Spagnuolo Discuti sul forum   Stampa

Introduzione

Poiché il packet filter PF è implementato nel kernel, viene utilizzata uno pseudo-device, "/dev/pf", per permettere ai processi in zona utente di controllarlo. Tale controllo è svolto attraverso una interfaccia ioctl(2). I sorgenti presenti nel documento sono tratti dal kernel della versione 3.4-RELEASE di OpenBSD.

Device in OpenBSD

A livello del kernel ogni device ha un insieme di funzioni che vengono chiamate quando un programma utente vi accede.
Per permettere l'utilizzo di alcune macro il nome di tali funzioni è composto dal nome del device seguito da quello della funzione vera e propria, ad esempio nel caso di pf avremo pfopen, pfclose e così via.

Ovviamente ogni device supporterà solo le funzioni che necessitano al proprio utilizzo, in ogni caso esiste un set di funzioni che non mancano quasi mai, ovvero attach, open, close e ioctl.
Di seguito vedremo in dettaglio queste funzioni e vedremo come sono implementate nel kernel di OpenBSD. Per pf il file che le contiene è "/sys/net/pf_ioctl.c".

Attach

Questa funzione è chiamata solo quando il kernel è avviato, viene usata in genere per inizializzare le varie strutture necessarie al device.
La funzione attach per pf è
void pfattach(int num)
l'unico parametro passato è un intero che rappresenta il numero di device che il driver dovrà gestire, nel caso di pf è fissato a 1.
Come detto prima tale funzione si occupa dell'inizializzazione di alcuni parametri ed in particolare inizializza i timeout, definisce il comportamento di default del firewall
pf_default_rule.action = PF_PASS
e avvia le varie componenti di pf
pfr_initialize()
pf_osfp_initialize()
pf_normalize_init()

Open e Close

La funzione pfopen è chiamata quando un programma a livello utente esegue una open(2) sul device.

È definita nel seguente modo:

int pfopen(dev_t dev, int flags, int fmt, struct proc *p)
{
  if (minor(dev) >= 1)
    return (ENXIO);
  return (0);
}
"dev" indica il numero del device, dev_t è definito in /sys/sys/types.h:
typedef int32_t dev_t;
"flags" e "fmt" rappresentano le modalità di apertura indicate nella open(2).
"proc" è un puntatore alla struttura proc del processo che ha chiamato la open.

La funzione pfclose ovviamente chiude un device aperto precedentemente. I parametri sono gli stessi della funzione pfopen:

int pfclose(dev_t dev, int flags, int fmt, struct proc *p)
{
  if (minor(dev) >= 1)
    return (ENXIO);
  return (0);
}
La funzione minor() restituisce il device minor number, se tale valore è maggiore o uguale a 1 viene restituito l'errore ENXIO (No such device or address).
minor() fa coppia con major() ed è definito in types.h nel seguente modo:
#define major(x)        ((int32_t)(((u_int32_t)(x) >> 8) & 0xff))
#define minor(x)        ((int32_t)((x) & 0xff) | (((x) & 0xffff0000) >> 8))

Ioctl

Le ioctl su pf sono implementate dalla funzione pfioctl:
int pfioctl(dev_t dev, u_long cmd, caddr_t addr,
            int flags, struct proc *p)
Dove "dev", "flags" e "proc" hanno lo stesso significato visto in pfopen.
"cmd" è il comando da eseguire, i comandi sono definiti nel file pfvar.h, ma su questo ci torneremo dopo.
"addr" è un puntatore ai parametri passati dal programma utente e varia da comando a comando, ad esempio il comando DIOCBEGINRULES ne fa un casting alla struttura pfioc_rule
struct pfioc_rule       *pr = (struct pfioc_rule *)addr;
il tipo caddr_t è definito in types.h:
typedef char *          caddr_t;
A livello utente il prototipo di una generica ioctl(2) è il seguente
int ioctl(int d, unsigned long request, ...)
dove "d" è il descrittore di un file, e "request" è il comando che si vuole eseguire, è possibile inserire anche un terzo argomento "arg" che contiene informazioni aggiuntive necessarie all'esecuzione della richiesta.

Per vedere come funziona l'interfaccia ioctl verso pf partiamo un po' più da lontano e precisamente da pfctl.

Vediamo cosa accade quando attiviamo il packet filter con il comando:

# pfctl -e
Viene settato il flag PF_OPT_ENABLE di opts e viene chiamata la funzione pfctl_enable:
if (opts & PF_OPT_ENABLE)
  if (pfctl_enable(dev, opts))
    error = 1;
dev è un file descriptor, restituito da una open(2), che fa riferimento a "/dev/pf" mentre opts contiene le opzioni impostate. Come abbiamo visto prima chiamando la open(2) su /dev/pf viene scomodata la funzione pfopen.

La funzione pfctl_enable, sempre all'interno del file pfctl.c, è la seguente:

int
pfctl_enable(int dev, int opts)
{
  if (ioctl(dev, DIOCSTART)) {
    if (errno == EEXIST)
      errx(1, "pf already enabled");
    else
      err(1, "DIOCSTART");
  }
  if ((opts & PF_OPT_QUIET) == 0)
    fprintf(stderr, "pf enabled\n");

  if (altqsupport && ioctl(dev, DIOCSTARTALTQ))
    if (errno != EEXIST)
      err(1, "DIOCSTARTALTQ");

  return (0);
}
La parte importante di questa funzione è la chiamata a ioctl con /dev/pf come file descriptor e DIOCSTART come comando di controllo (o richiesta).
Per il resto si può notare che se il flag PF_OPT_QUIET è impostato, eseguendo pfctl con l'opzione -q, non viene stampata la stringa "pf enabled", in quanto con tale opzione vengono stampati sullo standard error solo i messaggi d'errore e di warning.
L'ultimo "if" invece riguarda il sistema ALTQ, che non tratteremo in questo documento.

Andiamo ora a vedere come sono implementate le ioctl per lo pseudo-device pf. Tutto quello che ci serve si trova nella directory /sys/net dei sorgenti del kernel.

Vediamo innanzitutto, nel file pfvar.h, come è definito DIOCSTART:

#define DIOCSTART       _IO  ('D',  1)
_IO(g,n) è una macro definita in "ioccom.h" per facilitare la definizione dei comandi ioctl, "g" rappresenta il "magic number" del device ed "n" è il numero ordinale del comando all'interno del gruppo.
Oltre a _IO sono definite anche _IOW, _IOR e _IOWR, ogni macro corrisponde ad una possibile direzione per il trasferimento dei dati.
In particolare abbiamo che _IO corrisponde alla direzione IOC_VOID che si usa quando non ci sono dati da trasferire, invece _IOW corrisponde a IOC_IN da usare quando i dati devono essere scritti sul device, e così via.
_IOW, _IOR e _IOWR prendono anche un terzo argomento "t" che rappresenta la struttura dati coinvolta nel comando, in realtà quello che interessa al kernel è la dimensione di tali dati.

Vediamo ora, nel file pf_ioctl.c, come è implementato il comando DIOCSTART. Come spesso accade i comandi sono gestiti attraverso uno switch su "cmd" che è il comando passato a ioctl come secondo parametro.

switch (cmd) {

case DIOCSTART:
  if (pf_status.running)
    error = EEXIST;
  else {
    u_int32_t states = pf_status.states;
    bzero(&pf_status, sizeof(struct pf_status));
    pf_status.running = 1;
    pf_status.states = states;
    pf_status.since = time.tv_sec;
    if (status_ifp != NULL)
      strlcpy(pf_status.ifname,
    status_ifp->if_xname, IFNAMSIZ);
    DPFPRINTF(PF_DEBUG_MISC, ("pf: started\n"));
  }
  break;
...
}
Viene controllato innanzitutto se PF è già in esecuzione, in caso affermativo viene restituito a pfctl l'errore EEXIST, e di conseguenza pfctl stamperà sullo standard error la frase "pf already enabled":
# pfctl -e
pf enabled
# pfctl -e
pfctl: pf already enabled
Vengono poi impostati alcuni campi della struttura pf_status, che come si deduce dal nome contiene alcune informazioni sullo stato di pf, pf_status è definita in pfvar.h:
struct pf_status {
  u_int64_t       counters[PFRES_MAX];
  u_int64_t       fcounters[FCNT_MAX];
  u_int64_t       pcounters[2][2][3];
  u_int64_t       bcounters[2][2];
  u_int32_t       running;
  u_int32_t       states;
  u_int32_t       since;
  u_int32_t       debug;
  char            ifname[IFNAMSIZ];
};
In particolare è possibile notare che viene conservato il valore di states che contiene il numero di entry nella tabella di stato, che è possibile vedere con il comando:
# pfctl -s state
Viene anche posto a 1 il valore di pf_status.running, che sta a indicare che pf è in esecuzione. Viene inizializzato il valore di pf_status.since utilizzato da print_status in pfctl_parser.c per calcolare il tempo di esecuzione.

DPFPRINTF è definito nel seguente modo

#define DPFPRINTF(n, x) if (pf_status.debug >= (n)) printf x
la stringa x viene stampata se il livello di debug (pf_status.debug) è maggiore o uguale al valore n, che nel nostro caso vale PF_DEBUG_MISC.
I possibili livelli di debug, in ordine crescente di informazioni fornite, sono PF_DEBUG_NONE, PF_DEBUG_URGENT, PF_DEBUG_MISC e PF_DEBUG_NOISY.
I livelli di debug sono impostabili attraverso pfctl con l'opzione -x <level>, dove level può assumere i valori none, urgent, misc e loud.

In realtà non vedremo mai questa stringa perché in questa posizione la variabile pf_status.debug è stata azzerata dalla funzione bzero(3) poche righe più sopra.
La variabile debug viene posta a PF_DEBUG_URGENT all'inizio dalla funzione pfattach, chiamata dal kernel durante il boot.
Poi se non si indica un livello di debug a pfctl in fase di avvio con l'opzione -x il valore di debug viene posto a zero, se invece si passa un valore di debug la funzione pfctl_debug, che si occupa di impostare il livello di debug, viene comunque chiamata dopo pfctl_enable.
Quindi in ogni caso alla riga

DPFPRINTF(PF_DEBUG_MISC, ("pf: started\n"));
il valore di pf_status.debug è zero, di conseguenza niente stampa.
Questo comportamento è stato corretto alcuni mesi dopo l'uscita della versione 3.4-RELEASE usata nella stesura di questo documento.
Sono state apportate anche numerose modifiche ben più sostanziali, come ad esempio, rimanendo nell'ambito ioctl, la sostituzione dei comandi DIOCBEGINRULES, DIOCCOMMITRULES, DIOCBEGINALTQS, DIOCCOMMITALTQS, DIOCRINABEGIN e DIOCRINADEFINE con DIOCXBEGIN, DIOCXCOMMIT e DIOCXROLLBACK, è variata anche la struttura pf_status per permettere la gestione di altre funzioni.

Conclusioni

Per concludere una curiosità. Durante questo breve viaggio nel kernel ho trovato due bug... vabbè bug è una parola grossa, in realtà si trattava di "errori di distrazione" della serie "il primo che li legge li trova"!
In ogni modo quello più consistente era già stato corretto, non stavo lavorando sull'ultima versione del file in questione, l'altro invece è stato corretto in meno di 20 minuti dalla comunicazione via email a Daniel Hartmeier!

Informazioni sull'autore

Gianluigi Spagnuolo, studente. Si interessa di programmazione, sicurezza informatica, buddismo zen, reti e sistemi operativi liberi.
È tra i fondatori dell' Italian Ruby User Group e dell'Irpinia Linux User Group e moderatore della mailing list Security4Dummies.
Home page: http://kirash.interfree.it

È possibile consultare l'elenco degli articoli scritti da Gianluigi Spagnuolo.

Altri articoli sul tema OpenBSD / Kernel.

Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.