From 38f289588d7f17f86a20a3372390257e3214155b Mon Sep 17 00:00:00 2001 From: Calvin Morrison Date: Tue, 18 Aug 2026 22:23:00 -0400 Subject: fw: a fragmented datagram crosses Only the first fragment of a datagram carries the transport header, so every later one matched no port, and a rule set written in ports -- which is every rule set in fwrules(6) -- denied it. Measured, 3000 bytes of UDP over a 1500 mtu, against a rule permitting the port: passed 1 dropped 2 The first fragment crossed and the receiver waited for the rest until it gave up. The alternative this replaced was worse: reading ports out of a later fragment lets one whose payload bytes happen to look like an open connection through, which is a firewall evasion older than most firewalls. So the first fragment decides and the rest of the train inherits. The train is what the receiving stack reassembles on -- protocol, addresses, identification -- and lasts about as long as that stack will hold the pieces. A train whose head we never saw is judged on its addresses alone, and so is normally denied: it is either an attack or the tail of a datagram we already refused. Same measurement after: passed 3 dropped 0 and one rule decision for the datagram rather than one per fragment. IPv6 fragments live in an extension header, which fw does not walk, so none of this reaches them; that stays in BUGS. Four checks, three of which fail against the previous fw.c. The fourth -- the far stack's own InDatagrams -- is read as a difference across the exchange, not a total: an IP stack outlives the run that made it, and reading the total made the check pass on a build that had dropped two thirds of the datagram. Co-Authored-By: Claude Opus 5 --- fw/src/fw.c | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 9 deletions(-) (limited to 'fw/src') 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; } -- cgit v1.2.3