diff options
Diffstat (limited to 'fw/src/rules.c')
| -rw-r--r-- | fw/src/rules.c | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/fw/src/rules.c b/fw/src/rules.c new file mode 100644 index 0000000..c0df82d --- /dev/null +++ b/fw/src/rules.c @@ -0,0 +1,440 @@ +/* + * The shared rule engine. + * + * Rules are an ndb file: one entry is one rule, matched top to bottom, + * first match wins, no match denies. ndbparse hands entries back in + * file order, which is what keeps this a list rather than a lookup. + * Firewall matching is a solved interface and being different about it + * would be cost for its own sake. + * + * allow=out proto=tcp port=443 + * deny=in ip=1.1.1.1 + * allow=out ip=10.0.2.0/24 + * + * connect and announce are accepted as spellings of out and in, since + * that is what they mean at the ctl layer. + * + * ip and port always mean the *peer* - the far end of the traffic, + * whichever direction it is going - so one rule reads the same whether + * it is enforced against a connect string or against a packet header. + * lport is the local side, and is only meaningful at the packet layer. + */ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include <ndb.h> +#include <ip.h> +#include "rules.h" + +Rule *rules; +void (*rulechanged)(void); + +static Lock rulelock; +static Rule *lastrule; +static char *rulefile; +static char *parseerr; +static jmp_buf parsejmp; +static int parsing; + +/* + * A bad rule typed at a ctl file must fail the write, not the firewall, + * so parsing longjmps out instead of calling sysfatal when it is being + * driven from there. + */ +static void +rulefail(char *fmt, ...) +{ + static char buf[256]; + va_list arg; + + va_start(arg, fmt); + vsnprint(buf, sizeof buf, fmt, arg); + va_end(arg); + if(parsing){ + parseerr = buf; + longjmp(parsejmp, 1); + } + sysfatal("%s", buf); +} + +static struct { + char *name; + int num; +} protos[] = { + { "icmp", 1 }, + { "igmp", 2 }, + { "tcp", 6 }, + { "udp", 17 }, + { "gre", 47 }, + { "icmpv6", 58 }, + { "il", 40 }, + { nil, 0 }, +}; + +int +protoname2num(char *name) +{ + int i; + + for(i = 0; protos[i].name != nil; i++) + if(strcmp(protos[i].name, name) == 0) + return protos[i].num; + return -1; +} + +char* +protonum2name(int num) +{ + static char buf[16]; + int i; + + for(i = 0; protos[i].name != nil; i++) + if(protos[i].num == num) + return protos[i].name; + snprint(buf, sizeof buf, "%d", num); + return buf; +} + +void* +emalloc(ulong n) +{ + void *p; + + if((p = mallocz(n, 1)) == nil) + sysfatal("out of memory"); + return p; +} + +char* +estrdup(char *s) +{ + char *p; + + if((p = strdup(s)) == nil) + sysfatal("out of memory"); + return p; +} + +static int +verbof(char *s) +{ + if(*s == '\0' || strcmp(s, "*") == 0) + return Vany; + if(strcmp(s, "out") == 0 || strcmp(s, "connect") == 0) + return Vout; + if(strcmp(s, "in") == 0 || strcmp(s, "announce") == 0) + return Vin; + return -1; +} + +/* + * An attribute we do not recognise is fatal rather than ignored. In a + * lookup database ignoring it would be the friendly thing; here, + * quietly dropping "prot=tcp" would leave a rule matching every + * protocol instead of one, and a typo that fails open is not something + * a firewall gets to do. + */ +static void +addrule(Ndbtuple *t, int nr) +{ + char *ip, *mask, *p, abuf[64]; + Rule *r; + int n, act; + + act = -1; + ip = mask = nil; + r = emalloc(sizeof *r); + r->nr = nr; + r->verb = Vany; + r->port = -1; + r->lport = -1; + r->anyip = 1; + + for(; t != nil; t = t->entry){ + if(strcmp(t->attr, "allow") == 0 || strcmp(t->attr, "deny") == 0){ + if(act >= 0) + rulefail("%s: rule %d: two actions in one rule", rulefile, nr); + act = strcmp(t->attr, "allow") == 0; + if((r->verb = verbof(t->val)) < 0) + rulefail("%s: rule %d: %s: want in, out or *", + rulefile, nr, t->val); + }else if(strcmp(t->attr, "proto") == 0){ + if(strcmp(t->val, "*") != 0) + r->proto = estrdup(t->val); + }else if(strcmp(t->attr, "port") == 0){ + if(strcmp(t->val, "*") != 0) + r->port = atoi(t->val); + }else if(strcmp(t->attr, "lport") == 0){ + if(strcmp(t->val, "*") != 0) + r->lport = atoi(t->val); + }else if(strcmp(t->attr, "log") == 0){ + if(strcmp(t->val, "no") == 0 || strcmp(t->val, "0") == 0) + r->log = 0; + else + r->log = 1; + }else if(strcmp(t->attr, "ip") == 0) + ip = t->val; + else if(strcmp(t->attr, "ipmask") == 0) + mask = t->val; + else + rulefail("%s: rule %d: %s: unknown attribute", rulefile, nr, t->attr); + } + if(act < 0) + rulefail("%s: rule %d: needs allow= or deny=", rulefile, nr); + r->allow = act; + + if(ip != nil && strcmp(ip, "*") != 0){ + /* + * ip=10.0.2.0/24 is taken as well as ip=10.0.2.0 ipmask=/24. + * parseipmask tells a prefix length from a dotted mask by the + * leading slash, so the slash has to survive the split. + */ + if((p = strchr(ip, '/')) != nil){ + if(mask != nil) + rulefail("%s: rule %d: mask given twice", rulefile, nr); + n = p - ip; + if(n >= sizeof abuf) + rulefail("%s: rule %d: address too long", rulefile, nr); + memmove(abuf, ip, n); + abuf[n] = '\0'; + mask = p; + ip = abuf; + } + if(parseipandmask(r->ip, r->mask, ip, mask) == -1) + rulefail("%s: rule %d: %s: unparseable address or mask", + rulefile, nr, ip); + r->anyip = 0; + } + + if(lastrule == nil) + rules = r; + else + lastrule->next = r; + lastrule = r; +} + +/* + * Parse without installing. Returns the new list, or nil with *err set. + * An empty file is a valid rule set: it denies everything. + */ +Rule* +parserules(char *file, char **err) +{ + Rule *new, *save, *savelast; + Ndbtuple *t; + Ndb *db; + int nr; + + save = rules; + savelast = lastrule; + rules = lastrule = nil; + rulefile = file; + parseerr = nil; + + if((db = ndbopen(file)) == nil){ + static char eb[128]; + + snprint(eb, sizeof eb, "%s: %r", file); + *err = eb; + rules = save; + lastrule = savelast; + return nil; + } + parsing = 1; + if(setjmp(parsejmp) == 0){ + for(nr = 1; (t = ndbparse(db)) != nil; nr++){ + addrule(t, nr); + ndbfree(t); + } + } + parsing = 0; + ndbclose(db); + + new = rules; + rules = save; + lastrule = savelast; + if(parseerr != nil){ + freerules(new); + *err = parseerr; + return nil; + } + *err = nil; + return new; +} + +void +freerules(Rule *r) +{ + Rule *next; + + for(; r != nil; r = next){ + next = r->next; + free(r->proto); + free(r); + } +} + +void +installrules(Rule *new) +{ + Rule *old; + + lock(&rulelock); + old = rules; + rules = new; + unlock(&rulelock); + freerules(old); + if(rulechanged != nil) + (*rulechanged)(); +} + +void +readrules(char *file) +{ + Rule *new; + char *err; + + if((new = parserules(file, &err)) == nil && err != nil) + sysfatal("%s", err); + installrules(new); +} + +/* + * The current set, written back out as ndb. What comes out here must + * parse back in unchanged; it is what gets persisted. + */ +long +fmtrules(char *buf, long nbuf) +{ + char *p, *e; + Rule *r; + + p = buf; + e = buf + nbuf; + lock(&rulelock); + for(r = rules; r != nil; r = r->next){ + p = seprint(p, e, "%s=%s", r->allow ? "allow" : "deny", + r->verb == Vin ? "in" : r->verb == Vout ? "out" : "*"); + if(r->proto != nil) + p = seprint(p, e, "\tproto=%s", r->proto); + if(!r->anyip) + p = seprint(p, e, "\tip=%I\tipmask=%M", r->ip, r->mask); + if(r->port >= 0) + p = seprint(p, e, "\tport=%d", r->port); + if(r->lport >= 0) + p = seprint(p, e, "\tlport=%d", r->lport); + if(r->log) + p = seprint(p, e, "\tlog=yes"); + p = seprint(p, e, "\n"); + } + unlock(&rulelock); + return p - buf; +} + +/* + * A firewall that was told to log and silently cannot is worse than one + * that never logged: you would believe you had an audit trail. syslog + * does not create its file, so say so plainly at startup rather than + * dropping the lines on the floor. + */ +void +checklogging(void) +{ + Rule *r; + int fd; + + for(r = rules; r != nil; r = r->next) + if(r->log) + break; + if(r == nil) + return; + if((fd = open("/sys/log/fw", OWRITE)) < 0){ + fprint(2, "fw: rules ask for logging, but /sys/log/fw cannot be " + "written: %r\n"); + fprint(2, "fw: make it once with: touch /sys/log/fw; chmod +a /sys/log/fw\n"); + fprint(2, "fw: filtering anyway, but nothing will be logged\n"); + return; + } + close(fd); +} + +void +dumprules(void) +{ + Rule *r; + + for(r = rules; r != nil; r = r->next) + fprint(2, "rule %d: %s %s proto %s port %d lport %d anyip %d ip %I mask %I\n", + r->nr, r->allow ? "allow" : "deny", + r->verb == Vin ? "in" : r->verb == Vout ? "out" : "*", + r->proto != nil ? r->proto : "*", r->port, r->lport, + r->anyip, r->ip, r->mask); +} + +/* + * The rules with a count of how often each has decided something. A + * rule that has never fired is either dead or protecting you from + * something that has not happened yet, and it is worth being able to + * tell which. Kept out of fmtrules so that what "rules" prints stays + * a rule set that can be written straight back. + */ +long +fmthits(char *buf, long nbuf) +{ + char *p, *e; + Rule *r; + + p = buf; + e = buf + nbuf; + lock(&rulelock); + for(r = rules; r != nil; r = r->next) + p = seprint(p, e, "%-8ld %s=%s%s%s\n", r->hits, + r->allow ? "allow" : "deny", + r->verb == Vin ? "in" : r->verb == Vout ? "out" : "*", + r->proto != nil ? "\tproto=" : "", + r->proto != nil ? r->proto : ""); + unlock(&rulelock); + return p - buf; +} + +char* +matchrule(int verb, char *proto, uchar *ip, int anyip, int port, int lport, Rule **rp) +{ + uchar net[IPaddrlen], rnet[IPaddrlen]; + static char err[128]; + Rule *r; + + if(rp != nil) + *rp = nil; + lock(&rulelock); + for(r = rules; r != nil; r = r->next){ + if(r->verb != Vany && r->verb != verb) + continue; + if(r->proto != nil && (proto == nil || strcmp(r->proto, proto) != 0)) + continue; + if(r->port >= 0 && r->port != port) + continue; + if(r->lport >= 0 && r->lport != lport) + continue; + if(!r->anyip){ + if(anyip) /* a wildcard request cannot match a specific rule */ + continue; + maskip(ip, r->mask, net); + maskip(r->ip, r->mask, rnet); + if(ipcmp(net, rnet) != 0) + continue; + } + if(rp != nil) + *rp = r; + r->hits++; + if(r->allow){ + unlock(&rulelock); + return nil; + } + snprint(err, sizeof err, "denied by rule %d", r->nr); + unlock(&rulelock); + return err; + } + unlock(&rulelock); + return "denied, no rule matched"; +} |
