summaryrefslogtreecommitdiff
path: root/fw/src/rules.c
diff options
context:
space:
mode:
Diffstat (limited to 'fw/src/rules.c')
-rw-r--r--fw/src/rules.c440
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";
+}