summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fw/src/fw.c157
-rwxr-xr-xfw/test/fwtest.rc50
2 files changed, 197 insertions, 10 deletions
diff --git a/fw/src/fw.c b/fw/src/fw.c
index 294046d..1fb88bd 100644
--- a/fw/src/fw.c
+++ b/fw/src/fw.c
@@ -48,10 +48,18 @@ struct Pkt
uchar dst[IPaddrlen];
int sport;
int dport;
- int frag; /* a later fragment: no ports in it */
+ int frag; /* Fragno, Fragfirst or Fraglater */
+ int id; /* which datagram, if it is fragmented */
int verb;
};
+enum
+{
+ Fragno, /* a whole datagram */
+ Fragfirst, /* the piece with the transport header */
+ Fraglater, /* a piece without one */
+};
+
static int debug;
static int stateless;
static int nallow, ndeny;
@@ -221,6 +229,122 @@ revalidate(void)
return n;
}
+/*
+ * Fragment trains.
+ *
+ * Only the first fragment of a datagram carries the transport header,
+ * so every later one matches no port, and a rule set written in ports -
+ * which is every rule set worth writing - denies it. The first
+ * fragment crosses and the receiver waits for the rest until it gives
+ * up. Reading ports out of a later fragment, which is what this did
+ * before that, is worse: a fragment whose payload bytes happen to look
+ * like an open connection is let through.
+ *
+ * So the first fragment decides and the rest of the train inherits.
+ * The train is what the receiving stack will reassemble on - protocol,
+ * addresses and identification - and it lasts about as long as that
+ * stack will hold the pieces. A train we never saw the head of is
+ * judged on its addresses alone, and so is normally denied, which is
+ * the right way round: it is either an attack or a datagram whose
+ * first fragment we already refused.
+ *
+ * IPv6 fragments are carried in an extension header, which fw does not
+ * walk, so none of this reaches them.
+ */
+enum { Nfrag = 61, Fragtime = 30 };
+
+typedef struct Frag Frag;
+struct Frag
+{
+ int proto;
+ int id;
+ uchar src[IPaddrlen];
+ uchar dst[IPaddrlen];
+ long last;
+ Frag *next;
+};
+
+static Frag *fragtab[Nfrag];
+static Lock fraglock;
+static int fragsweep;
+
+static uint
+fraghash(int proto, uchar *src, uchar *dst, int id)
+{
+ uint h;
+ int i;
+
+ h = proto*31 + id;
+ for(i = 0; i < IPaddrlen; i++)
+ h = h*33 + src[i]*3 + dst[i];
+ return h % Nfrag;
+}
+
+static void
+reapfrags(int i, long now)
+{
+ Frag *f, **pp;
+
+ for(pp = &fragtab[i]; (f = *pp) != nil; ){
+ if(now - f->last > Fragtime){
+ *pp = f->next;
+ free(f);
+ }else
+ pp = &f->next;
+ }
+}
+
+/* is this a later piece of a datagram whose head we let through? */
+static int
+fragseen(Pkt *p)
+{
+ Frag *f;
+ long now;
+ uint h;
+ int r;
+
+ r = 0;
+ now = time(0);
+ h = fraghash(p->proto, p->src, p->dst, p->id);
+ lock(&fraglock);
+ for(f = fragtab[h]; f != nil; f = f->next)
+ if(f->proto == p->proto && f->id == p->id
+ && ipcmp(f->src, p->src) == 0 && ipcmp(f->dst, p->dst) == 0){
+ if(now - f->last <= Fragtime){
+ f->last = now;
+ r = 1;
+ }
+ break;
+ }
+ unlock(&fraglock);
+ return r;
+}
+
+static void
+fragadd(Pkt *p)
+{
+ Frag *f;
+ long now;
+ uint h;
+
+ now = time(0);
+ h = fraghash(p->proto, p->src, p->dst, p->id);
+ lock(&fraglock);
+ reapfrags(h, now);
+ fragsweep = (fragsweep + 1) % Nfrag;
+ reapfrags(fragsweep, now);
+
+ f = emalloc(sizeof *f);
+ f->proto = p->proto;
+ f->id = p->id;
+ ipmove(f->src, p->src);
+ ipmove(f->dst, p->dst);
+ f->last = now;
+ f->next = fragtab[h];
+ fragtab[h] = f;
+ unlock(&fraglock);
+}
+
static void
flowadd(Pkt *p)
{
@@ -359,14 +483,16 @@ parsepkt(uchar *b, int n, Pkt *p)
v4tov6(p->src, b + 12);
v4tov6(p->dst, b + 16);
/*
- * Only the first fragment carries the transport header.
- * Reading one out of a later fragment gives payload bytes
- * as ports, which both loses the traffic - it matches no
- * flow - and lets a crafted fragment whose bytes happen to
- * match an open flow through.
+ * Which piece of a datagram this is. A later one has no
+ * transport header, so reading ports out of it gives
+ * payload bytes; the first one of a train has to be
+ * remembered so the rest can inherit its verdict.
*/
+ p->id = nhgets(b + 4);
if((nhgets(b + 6) & 0x1FFF) != 0)
- p->frag = 1;
+ p->frag = Fraglater;
+ else if((b[6] & 0x20) != 0)
+ p->frag = Fragfirst;
t = b + hl;
n -= hl;
break;
@@ -383,7 +509,7 @@ parsepkt(uchar *b, int n, Pkt *p)
return;
}
- if(!p->frag && (p->proto == 6 || p->proto == 17) && n >= 4){
+ if(p->frag != Fraglater && (p->proto == 6 || p->proto == 17) && n >= 4){
p->sport = nhgets(t);
p->dport = nhgets(t + 2);
}
@@ -427,6 +553,16 @@ permitted(uchar *buf, int n, int verb, Pkt *p)
fprint(2, "pass %s %s %I!%d -> %I!%d (state)\n",
verb == Vout ? "out" : "in", protonum2name(p->proto),
p->src, p->sport, p->dst, p->dport);
+ if(p->frag == Fragfirst)
+ fragadd(p);
+ return 1;
+ }
+ if(p->frag == Fraglater && fragseen(p)){
+ nallow++;
+ if(debug)
+ fprint(2, "pass %s %s %I -> %I id %d (fragment)\n",
+ verb == Vout ? "out" : "in", protonum2name(p->proto),
+ p->src, p->dst, p->id);
return 1;
}
if(!matchrule(&m)){
@@ -442,6 +578,8 @@ permitted(uchar *buf, int n, int verb, Pkt *p)
return 0;
}
nallow++;
+ if(p->frag == Fragfirst)
+ fragadd(p);
if(m.log)
syslog(0, "fw", "pass %s %s %I!%d -> %I!%d",
verb == Vout ? "out" : "in", protonum2name(p->proto),
@@ -450,7 +588,8 @@ permitted(uchar *buf, int n, int verb, Pkt *p)
fprint(2, "pass %s %s %I!%d -> %I!%d\n",
verb == Vout ? "out" : "in", protonum2name(p->proto),
p->src, p->sport, p->dst, p->dport);
- flowadd(p);
+ if(p->frag != Fraglater) /* it has no ports to key a flow on */
+ flowadd(p);
return 1;
}
diff --git a/fw/test/fwtest.rc b/fw/test/fwtest.rc
index d4222a8..8ae3f42 100755
--- a/fw/test/fwtest.rc
+++ b/fw/test/fwtest.rc
@@ -46,7 +46,7 @@ mkdir -p $mtpt
# The stacks the packet checks build for themselves. Fixed numbers are
# fine because they are unbound again at the end of each block, and the
# last check proves it.
-stacks=(20 21 22 23)
+stacks=(20 21 22 23 24 25)
# A port of our own. Conversations outlive the run that made them - a
# devip Conv is never freed - so a fixed port makes one run's leftovers
@@ -396,6 +396,54 @@ echo '== packets, between two stacks'
stopfw $nA $nB
}
+echo '== a fragmented datagram crosses'
+# Only the first fragment carries the transport header, so the rest
+# match no port and a rule set written in ports denied them: the first
+# fragment crossed and the receiver waited for the others until it gave
+# up. 3000 bytes over a 1500 mtu is three fragments.
+@{
+ rfork n
+ echo 'allow=in proto=udp lport='^$port > $tmp/frag.ndb
+ bind -a '#I24' $nA
+ bind -a '#I25' $nB
+ $fw -m $mtpt $tmp/frag.ndb $nA^'!'^10.9.9.1^'!'^/24 $nB^'!'^10.9.9.2^'!'^/24 >[2]/dev/null &
+ sleep 3
+
+ dd -if /dev/zero -of $tmp/big -bs 3000 -count 1 >[2]/dev/null
+ # a difference, not a total: an IP stack outlives the run that made
+ # it, and so does its count of datagrams
+ before=`{awk '/^InDatagrams/ {print $2}' $nB/udp/stats}
+ @{
+ conv=`{cat /fd/0}
+ echo -n 'announce 10.9.9.2!'^$port >[1=0]
+ sleep 12
+ } <>[0] $nB/udp/clone &
+ sleep 2
+ @{
+ conv=`{cat /fd/0}
+ echo -n 'connect 10.9.9.2!'^$port >[1=0]
+ dd -if $tmp/big -of $nA/udp/$conv/data -bs 3000 -count 1 >[2]/dev/null
+ } <>[0] $nA/udp/clone
+ sleep 3
+
+ r=`{awk '/^passed/ {print $2}' $mtpt/stats}
+ check 'every fragment of it is passed' 3 $"r
+ r=`{awk '/^dropped/ {print $2}' $mtpt/stats}
+ check 'and none of them dropped' 0 $"r
+
+ # one decision, not three: the first fragment consults the rules
+ # and the rest of the train inherits what it decided
+ r=`{awk '/allow=in/ {print $1}' $mtpt/stats}
+ check 'the rules are consulted once for the datagram' 1 $"r
+
+ # the far stack's own count, which does not depend on us reading it
+ after=`{awk '/^InDatagrams/ {print $2}' $nB/udp/stats}
+ r=`{echo $before $after | awk '{print $2 - $1}'}
+ check 'and the far side reassembles it' 1 $"r
+
+ stopfw $nA $nB
+}
+
echo '== fwstart'
if(test -f $lib/fwstart)
@{