/* * 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 #include #include #include #include #include "rules.h" Rule *rules; void (*rulechanged)(void); static Lock rulelock; 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, Rule **head, Rule **tail) { 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(*head == nil) *head = r; else (*tail)->next = r; *tail = r; } /* * Parse without installing. Returns the new list, or nil with *err set. * An empty file is a valid rule set: it denies everything. */ /* * Build the new list in locals. Earlier this borrowed the globals and * put them back afterwards, which meant that for the length of a reload * the relay procs - which take rulelock, a lock this never held - were * walking a list that was first empty and then half built. Every packet * in that window was judged against a partial rule set, and a parse * failure freed nodes a relay might still have been holding. */ Rule* parserules(char *file, char **err) { Rule *head, *tail; Ndbtuple *t; Ndb *db; int nr; head = tail = nil; rulefile = file; parseerr = nil; if((db = ndbopen(file)) == nil){ static char eb[128]; snprint(eb, sizeof eb, "%s: %r", file); *err = eb; return nil; } parsing = 1; if(setjmp(parsejmp) == 0){ for(nr = 1; (t = ndbparse(db)) != nil; nr++){ addrule(t, nr, &head, &tail); ndbfree(t); } } parsing = 0; ndbclose(db); if(parseerr != nil){ freerules(head); *err = parseerr; return nil; } *err = nil; return head; } 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. * * Allocated to fit, not written into 64K and clipped. A rule set that * outgrew the buffer used to come back short, and since prepend, append * and delete all work by formatting the set out, editing the text and * parsing it again, editing one rule past the limit deleted every rule * after it. Silently. */ static long rulesize(Rule *r) { long n; /* * "allow=out" and the fixed part of every attribute, an address * and a mask at their longest, two ports, log=yes, the tabs and * the newline. Only the protocol is unbounded, and it is ndb's * word rather than anything we choose. */ n = 160; if(r->proto != nil) n += strlen(r->proto); return n; } char* rulestext(void) { char *buf, *p, *e; long sz; Rule *r; lock(&rulelock); sz = 1; for(r = rules; r != nil; r = r->next) sz += rulesize(r); if((buf = malloc(sz)) == nil){ unlock(&rulelock); return nil; } p = buf; e = buf + sz; 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); USED(p); return 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 rulestext so that what "rules" prints stays * a rule set that can be written straight back. */ char* hitstext(void) { char *buf, *p, *e; long sz; Rule *r; lock(&rulelock); sz = 1; for(r = rules; r != nil; r = r->next) sz += 64 + (r->proto != nil ? strlen(r->proto) : 0); if((buf = malloc(sz)) == nil){ unlock(&rulelock); return nil; } p = buf; e = buf + sz; 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); USED(p); return buf; } int matchrule(Match *m) { uchar net[IPaddrlen], rnet[IPaddrlen]; Rule *r; m->nr = 0; m->log = 0; lock(&rulelock); for(r = rules; r != nil; r = r->next){ if(r->verb != Vany && r->verb != m->verb) continue; if(r->proto != nil && (m->proto == nil || strcmp(r->proto, m->proto) != 0)) continue; if(r->port >= 0 && r->port != m->port) continue; if(r->lport >= 0 && r->lport != m->lport) continue; if(!r->anyip){ if(m->anyip) /* a wildcard request cannot match a specific rule */ continue; maskip(m->ip, r->mask, net); maskip(r->ip, r->mask, rnet); if(ipcmp(net, rnet) != 0) continue; } m->nr = r->nr; m->log = r->log; if(m->count) r->hits++; if(r->allow){ unlock(&rulelock); m->err[0] = '\0'; return 1; } unlock(&rulelock); snprint(m->err, sizeof m->err, "denied by rule %d", r->nr); return 0; } unlock(&rulelock); snprint(m->err, sizeof m->err, "denied, no rule matched"); return 0; }