pf: initial SCTP support

Basic state tracking for SCTP. This means we scan through the packet to
identify the different chunks (so we can identify state changes).

MFC after:	3 weeks
Sponsored by:	Orange Business Services
Differential Revision:	https://reviews.freebsd.org/D40862
This commit is contained in:
Kristof Provost 2023-04-27 10:58:02 +02:00
parent 0bd4a6837c
commit 010ee43f56
4 changed files with 362 additions and 0 deletions

View File

@ -41,6 +41,7 @@ __FBSDID("$FreeBSD$");
#include <net/if.h>
#define TCPSTATES
#include <netinet/tcp_fsm.h>
#include <netinet/sctp.h>
#include <net/pfvar.h>
#include <arpa/inet.h>
#include <netdb.h>
@ -206,6 +207,36 @@ print_seq(struct pfctl_state_peer *p)
p->seqhi - p->seqlo);
}
static const char *
sctp_state_name(int state)
{
switch (state) {
case SCTP_CLOSED:
return ("CLOSED");
case SCTP_BOUND:
return ("BOUND");
case SCTP_LISTEN:
return ("LISTEN");
case SCTP_COOKIE_WAIT:
return ("COOKIE_WAIT");
case SCTP_COOKIE_ECHOED:
return ("COOKIE_ECHOED");
case SCTP_ESTABLISHED:
return ("ESTABLISHED");
case SCTP_SHUTDOWN_SENT:
return ("SHUTDOWN_SENT");
case SCTP_SHUTDOWN_RECEIVED:
return ("SHUTDOWN_RECEIVED");
case SCTP_SHUTDOWN_ACK_SENT:
return ("SHUTDOWN_ACK_SENT");
case SCTP_SHUTDOWN_PENDING:
return ("SHUTDOWN_PENDING");
default:
return ("?");
}
}
void
print_state(struct pfctl_state *s, int opts)
{
@ -300,6 +331,9 @@ print_state(struct pfctl_state *s, int opts)
const char *states[] = PFUDPS_NAMES;
printf(" %s:%s\n", states[src->state], states[dst->state]);
} else if (proto == IPPROTO_SCTP) {
printf(" %s:%s\n", sctp_state_name(src->state),
sctp_state_name(dst->state));
#ifndef INET6
} else if (proto != IPPROTO_ICMP && src->state < PFOTHERS_NSTATES &&
dst->state < PFOTHERS_NSTATES) {

View File

@ -60,6 +60,7 @@
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/sctp.h>
#include <netinet/ip_icmp.h>
#include <netinet/icmp6.h>
#endif
@ -1541,6 +1542,7 @@ struct pf_pdesc {
union pf_headers {
struct tcphdr tcp;
struct udphdr udp;
struct sctphdr sctp;
struct icmp icmp;
#ifdef INET6
struct icmp6_hdr icmp6;
@ -1570,6 +1572,15 @@ struct pf_pdesc {
u_int8_t dir; /* direction */
u_int8_t sidx; /* key index for source */
u_int8_t didx; /* key index for destination */
#define PFDESC_SCTP_INIT 0x0001
#define PFDESC_SCTP_INIT_ACK 0x0002
#define PFDESC_SCTP_COOKIE 0x0004
#define PFDESC_SCTP_ABORT 0x0008
#define PFDESC_SCTP_SHUTDOWN 0x0010
#define PFDESC_SCTP_SHUTDOWN_COMPLETE 0x0020
#define PFDESC_SCTP_DATA 0x0040
#define PFDESC_SCTP_OTHER 0x0080
u_int16_t sctp_flags;
};
#endif
@ -2270,6 +2281,8 @@ int pf_normalize_tcp_init(struct mbuf *, int, struct pf_pdesc *,
int pf_normalize_tcp_stateful(struct mbuf *, int, struct pf_pdesc *,
u_short *, struct tcphdr *, struct pf_kstate *,
struct pf_state_peer *, struct pf_state_peer *, int *);
int pf_normalize_sctp(int, struct pfi_kkif *, struct mbuf *, int,
int, void *, struct pf_pdesc *);
u_int32_t
pf_state_expires(const struct pf_kstate *);
void pf_purge_expired_fragments(void);

View File

@ -309,6 +309,9 @@ static int pf_test_state_udp(struct pf_kstate **,
static int pf_test_state_icmp(struct pf_kstate **,
struct pfi_kkif *, struct mbuf *, int,
void *, struct pf_pdesc *, u_short *);
static int pf_test_state_sctp(struct pf_kstate **,
struct pfi_kkif *, struct mbuf *, int,
void *, struct pf_pdesc *, u_short *);
static int pf_test_state_other(struct pf_kstate **,
struct pfi_kkif *, struct mbuf *, struct pf_pdesc *);
static u_int16_t pf_calc_mss(struct pf_addr *, sa_family_t,
@ -4244,6 +4247,11 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm, struct pfi_kkif *kif,
dport = pd->hdr.udp.uh_dport;
hdrlen = sizeof(pd->hdr.udp);
break;
case IPPROTO_SCTP:
sport = pd->hdr.sctp.src_port;
dport = pd->hdr.sctp.dest_port;
hdrlen = sizeof(pd->hdr.sctp);
break;
#ifdef INET
case IPPROTO_ICMP:
if (pd->af != AF_INET)
@ -4702,6 +4710,11 @@ pf_create_state(struct pf_krule *r, struct pf_krule *nr, struct pf_krule *a,
pf_set_protostate(s, PF_PEER_DST, PFUDPS_NO_TRAFFIC);
s->timeout = PFTM_UDP_FIRST_PACKET;
break;
case IPPROTO_SCTP:
pf_set_protostate(s, PF_PEER_SRC, SCTP_COOKIE_WAIT);
pf_set_protostate(s, PF_PEER_DST, SCTP_CLOSED);
s->timeout = PFTM_TCP_FIRST_PACKET;
break;
case IPPROTO_ICMP:
#ifdef INET6
case IPPROTO_ICMPV6:
@ -5669,6 +5682,66 @@ pf_test_state_udp(struct pf_kstate **state, struct pfi_kkif *kif,
return (PF_PASS);
}
static int
pf_test_state_sctp(struct pf_kstate **state, struct pfi_kkif *kif,
struct mbuf *m, int off, void *h, struct pf_pdesc *pd, u_short *reason)
{
struct pf_state_key_cmp key;
struct pf_state_peer *src; //, *dst;
struct sctphdr *sh = &pd->hdr.sctp;
u_int8_t psrc; //, pdst;
bzero(&key, sizeof(key));
key.af = pd->af;
key.proto = IPPROTO_SCTP;
if (pd->dir == PF_IN) { /* wire side, straight */
PF_ACPY(&key.addr[0], pd->src, key.af);
PF_ACPY(&key.addr[1], pd->dst, key.af);
key.port[0] = sh->src_port;
key.port[1] = sh->dest_port;
} else { /* stack side, reverse */
PF_ACPY(&key.addr[1], pd->src, key.af);
PF_ACPY(&key.addr[0], pd->dst, key.af);
key.port[1] = sh->src_port;
key.port[0] = sh->dest_port;
}
STATE_LOOKUP(kif, &key, *state, pd);
if (pd->dir == (*state)->direction) {
src = &(*state)->src;
psrc = PF_PEER_SRC;
} else {
src = &(*state)->dst;
psrc = PF_PEER_DST;
}
/* Track state. */
if (pd->sctp_flags & PFDESC_SCTP_INIT) {
if (src->state < SCTP_COOKIE_WAIT) {
pf_set_protostate(*state, psrc, SCTP_COOKIE_WAIT);
(*state)->timeout = PFTM_TCP_OPENING;
}
}
if (pd->sctp_flags & PFDESC_SCTP_COOKIE) {
if (src->state < SCTP_ESTABLISHED) {
pf_set_protostate(*state, psrc, SCTP_ESTABLISHED);
(*state)->timeout = PFTM_TCP_ESTABLISHED;
}
}
if (pd->sctp_flags & (PFDESC_SCTP_SHUTDOWN | PFDESC_SCTP_ABORT |
PFDESC_SCTP_SHUTDOWN_COMPLETE)) {
if (src->state < SCTP_SHUTDOWN_PENDING) {
pf_set_protostate(*state, psrc, SCTP_SHUTDOWN_PENDING);
(*state)->timeout = PFTM_TCP_CLOSING;
}
}
(*state)->expire = time_uptime;
return (PF_PASS);
}
static int
pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
struct mbuf *m, int off, void *h, struct pf_pdesc *pd, u_short *reason)
@ -7365,6 +7438,37 @@ pf_test(int dir, int pflags, struct ifnet *ifp, struct mbuf **m0,
break;
}
case IPPROTO_SCTP: {
if (!pf_pull_hdr(m, off, &pd.hdr.sctp, sizeof(pd.hdr.sctp),
&action, &reason, AF_INET)) {
if (action != PF_PASS)
pd.act.log |= PF_LOG_FORCE;
goto done;
}
pd.sport = &pd.hdr.sctp.src_port;
pd.dport = &pd.hdr.sctp.dest_port;
if (pd.hdr.sctp.src_port == 0 || pd.hdr.sctp.dest_port == 0) {
action = PF_DROP;
REASON_SET(&reason, PFRES_SHORT);
goto done;
}
action = pf_normalize_sctp(dir, kif, m, 0, off, h, &pd);
if (action == PF_DROP)
goto done;
action = pf_test_state_sctp(&s, kif, m, off, h, &pd,
&reason);
if (action == PF_PASS) {
if (V_pfsync_update_state_ptr != NULL)
V_pfsync_update_state_ptr(s);
r = s->rule.ptr;
a = s->anchor.ptr;
} else {
action = pf_test_rule(&r, &s, kif, m, off,
&pd, &a, &ruleset, inp);
}
break;
}
case IPPROTO_ICMP: {
if (!pf_pull_hdr(m, off, &pd.hdr.icmp, ICMP_MINLEN,
&action, &reason, AF_INET)) {
@ -7882,6 +7986,37 @@ pf_test6(int dir, int pflags, struct ifnet *ifp, struct mbuf **m0, struct inpcb
break;
}
case IPPROTO_SCTP: {
if (!pf_pull_hdr(m, off, &pd.hdr.sctp, sizeof(pd.hdr.sctp),
&action, &reason, AF_INET6)) {
if (action != PF_PASS)
pd.act.log |= PF_LOG_FORCE;
goto done;
}
pd.sport = &pd.hdr.sctp.src_port;
pd.dport = &pd.hdr.sctp.dest_port;
if (pd.hdr.sctp.src_port == 0 || pd.hdr.sctp.dest_port == 0) {
action = PF_DROP;
REASON_SET(&reason, PFRES_SHORT);
goto done;
}
action = pf_normalize_sctp(dir, kif, m, 0, off, h, &pd);
if (action == PF_DROP)
goto done;
action = pf_test_state_sctp(&s, kif, m, off, h, &pd,
&reason);
if (action == PF_PASS) {
if (V_pfsync_update_state_ptr != NULL)
V_pfsync_update_state_ptr(s);
r = s->rule.ptr;
a = s->anchor.ptr;
} else {
action = pf_test_rule(&r, &s, kif, m, off,
&pd, &a, &ruleset, inp);
}
break;
}
case IPPROTO_ICMP: {
action = PF_DROP;
DPFPRINTF(PF_DEBUG_MISC,

View File

@ -56,6 +56,8 @@ __FBSDID("$FreeBSD$");
#include <netinet/tcp.h>
#include <netinet/tcp_fsm.h>
#include <netinet/tcp_seq.h>
#include <netinet/sctp_constants.h>
#include <netinet/sctp_header.h>
#ifdef INET6
#include <netinet/ip6.h>
@ -2016,6 +2018,184 @@ pf_normalize_mss(struct mbuf *m, int off, struct pf_pdesc *pd)
return (0);
}
static int
pf_scan_sctp(struct mbuf *m, int ipoff, int off, struct pf_pdesc *pd)
{
struct sctp_chunkhdr ch = { };
int chunk_off = sizeof(struct sctphdr);
int chunk_start;
while (off + chunk_off < pd->tot_len) {
if (!pf_pull_hdr(m, off + chunk_off, &ch, sizeof(ch), NULL,
NULL, pd->af))
return (PF_DROP);
/* Length includes the header, this must be at least 4. */
if (ntohs(ch.chunk_length) < 4)
return (PF_DROP);
chunk_start = chunk_off;
chunk_off += roundup(ntohs(ch.chunk_length), 4);
switch (ch.chunk_type) {
case SCTP_INITIATION: {
struct sctp_init_chunk init;
if (!pf_pull_hdr(m, off + chunk_start, &init,
sizeof(init), NULL, NULL, pd->af))
return (PF_DROP);
/*
* RFC 9620, Section 3.3.2, "The Initiate Tag is allowed to have
* any value except 0."
*/
if (init.init.initiate_tag == 0)
return (PF_DROP);
if (init.init.num_inbound_streams == 0)
return (PF_DROP);
if (init.init.num_outbound_streams == 0)
return (PF_DROP);
if (ntohl(init.init.a_rwnd) < SCTP_MIN_RWND)
return (PF_DROP);
/*
* RFC 9260, Section 3.1, INIT chunks MUST have zero
* verification tag.
*/
if (pd->hdr.sctp.v_tag != 0)
return (PF_DROP);
pd->sctp_flags |= PFDESC_SCTP_INIT;
break;
}
case SCTP_INITIATION_ACK:
pd->sctp_flags |= PFDESC_SCTP_INIT_ACK;
break;
case SCTP_ABORT_ASSOCIATION:
pd->sctp_flags |= PFDESC_SCTP_ABORT;
break;
case SCTP_SHUTDOWN:
case SCTP_SHUTDOWN_ACK:
pd->sctp_flags |= PFDESC_SCTP_SHUTDOWN;
break;
case SCTP_SHUTDOWN_COMPLETE:
pd->sctp_flags |= PFDESC_SCTP_SHUTDOWN_COMPLETE;
break;
case SCTP_COOKIE_ECHO:
case SCTP_COOKIE_ACK:
pd->sctp_flags |= PFDESC_SCTP_COOKIE;
break;
case SCTP_DATA:
pd->sctp_flags |= PFDESC_SCTP_DATA;
break;
default:
pd->sctp_flags |= PFDESC_SCTP_OTHER;
break;
}
}
/* Validate chunk lengths vs. packet length. */
if (off + chunk_off != pd->tot_len)
return (PF_DROP);
/*
* INIT, INIT_ACK or SHUTDOWN_COMPLETE chunks must always be the only
* one in a packet.
*/
if ((pd->sctp_flags & PFDESC_SCTP_INIT) &&
(pd->sctp_flags & ~PFDESC_SCTP_INIT))
return (PF_DROP);
if ((pd->sctp_flags & PFDESC_SCTP_INIT_ACK) &&
(pd->sctp_flags & ~PFDESC_SCTP_INIT_ACK))
return (PF_DROP);
if ((pd->sctp_flags & PFDESC_SCTP_SHUTDOWN_COMPLETE) &&
(pd->sctp_flags & ~PFDESC_SCTP_SHUTDOWN_COMPLETE))
return (PF_DROP);
return (PF_PASS);
}
int
pf_normalize_sctp(int dir, struct pfi_kkif *kif, struct mbuf *m, int ipoff,
int off, void *h, struct pf_pdesc *pd)
{
struct pf_krule *r, *rm = NULL;
struct sctphdr *sh = &pd->hdr.sctp;
u_short reason;
sa_family_t af = pd->af;
int srs;
PF_RULES_RASSERT();
/* Unconditionally scan the SCTP packet, because we need to look for
* things like shutdown and asconf chunks. */
if (pf_scan_sctp(m, ipoff, off, pd) != PF_PASS)
goto sctp_drop;
r = TAILQ_FIRST(pf_main_ruleset.rules[PF_RULESET_SCRUB].active.ptr);
/* Check if there any scrub rules. Lack of scrub rules means enforced
* packet normalization operation just like in OpenBSD. */
srs = (r != NULL);
while (r != NULL) {
pf_counter_u64_add(&r->evaluations, 1);
if (pfi_kkif_match(r->kif, kif) == r->ifnot)
r = r->skip[PF_SKIP_IFP].ptr;
else if (r->direction && r->direction != dir)
r = r->skip[PF_SKIP_DIR].ptr;
else if (r->af && r->af != af)
r = r->skip[PF_SKIP_AF].ptr;
else if (r->proto && r->proto != pd->proto)
r = r->skip[PF_SKIP_PROTO].ptr;
else if (PF_MISMATCHAW(&r->src.addr, pd->src, af,
r->src.neg, kif, M_GETFIB(m)))
r = r->skip[PF_SKIP_SRC_ADDR].ptr;
else if (r->src.port_op && !pf_match_port(r->src.port_op,
r->src.port[0], r->src.port[1], sh->src_port))
r = r->skip[PF_SKIP_SRC_PORT].ptr;
else if (PF_MISMATCHAW(&r->dst.addr, pd->dst, af,
r->dst.neg, NULL, M_GETFIB(m)))
r = r->skip[PF_SKIP_DST_ADDR].ptr;
else if (r->dst.port_op && !pf_match_port(r->dst.port_op,
r->dst.port[0], r->dst.port[1], sh->dest_port))
r = r->skip[PF_SKIP_DST_PORT].ptr;
else {
rm = r;
break;
}
}
if (srs) {
/* With scrub rules present SCTP normalization happens only
* if one of rules has matched and it's not a "no scrub" rule */
if (rm == NULL || rm->action == PF_NOSCRUB)
return (PF_PASS);
pf_counter_u64_critical_enter();
pf_counter_u64_add_protected(&r->packets[dir == PF_OUT], 1);
pf_counter_u64_add_protected(&r->bytes[dir == PF_OUT], pd->tot_len);
pf_counter_u64_critical_exit();
}
/* Verify we're a multiple of 4 bytes long */
if ((pd->tot_len - off - sizeof(struct sctphdr)) % 4)
goto sctp_drop;
/* INIT chunk needs to be the only chunk */
if (pd->sctp_flags & PFDESC_SCTP_INIT)
if (pd->sctp_flags & ~PFDESC_SCTP_INIT)
goto sctp_drop;
return (PF_PASS);
sctp_drop:
REASON_SET(&reason, PFRES_NORM);
if (rm != NULL && r->log)
PFLOG_PACKET(kif, m, AF_INET, reason, r, NULL, NULL, pd,
1);
return (PF_DROP);
}
#ifdef INET
void
pf_scrub_ip(struct mbuf **m0, struct pf_pdesc *pd)