Changeset View
Changeset View
Standalone View
Standalone View
sys/netgraph/ng_antispoof.c
Property | Old Value | New Value |
---|---|---|
svn:eol-style | null | native \ No newline at end of property |
svn:keywords | null | FreeBSD=%H \ No newline at end of property |
svn:mime-type | null | text/plain \ No newline at end of property |
/*- | |||||
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD | |||||
* | |||||
* Copyright (c) 2020 Markus Stoff <markus@stoffdv.at> | |||||
* All rights reserved. | |||||
* | |||||
* Redistribution and use in source and binary forms, with or without | |||||
* modification, are permitted provided that the following conditions | |||||
* are met: | |||||
* 1. Redistributions of source code must retain the above copyright | |||||
* notice, this list of conditions and the following disclaimer. | |||||
* 2. Redistributions in binary form must reproduce the above copyright | |||||
* notice, this list of conditions and the following disclaimer in the | |||||
* documentation and/or other materials provided with the distribution. | |||||
* | |||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE | |||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |||||
* SUCH DAMAGE. | |||||
* | |||||
* $FreeBSD$ | |||||
*/ | |||||
/* | |||||
* This node prevents spoofing on the filter hook. | |||||
* It has 3 hooks: filter, downstream and nomatch. Packets entering or leaving | |||||
* the filter hook ar only passed if the upstream address is matched by a filter | |||||
* rule. Traffic flows between the downstream and the upstream hook, unless a | |||||
* packet is blocked, which is diverted to the nomatch hook (if connected). | |||||
*/ | |||||
#include <sys/param.h> | |||||
#include <sys/errno.h> | |||||
#include <sys/kernel.h> | |||||
#include <sys/malloc.h> | |||||
#include <sys/mbuf.h> | |||||
#include <sys/socket.h> | |||||
#include <sys/syslog.h> | |||||
/* clang-format off */ | |||||
#include <net/ethernet.h> | |||||
#include <netinet/in.h> | |||||
#include <netinet/if_ether.h> | |||||
#include <netinet/ip.h> | |||||
#include <netinet/ip6.h> | |||||
#include <netgraph/ng_message.h> | |||||
#include <netgraph/netgraph.h> | |||||
#include <netgraph/ng_antispoof.h> | |||||
#include <netgraph/ng_parse.h> | |||||
/* clang-format on */ | |||||
/* | |||||
* Remove following includes when moving parsing functions to netgraph/parse.c | |||||
*/ | |||||
#include <sys/ctype.h> | |||||
#include <machine/stdarg.h> | |||||
/* struct and enum prototypes */ | |||||
struct packet_info; | |||||
struct packet_info_conv; | |||||
struct ng_antispoof_private; | |||||
struct ng_antispoof_filter; | |||||
enum as_flow_direction; | |||||
/* Function prototypes */ | |||||
static int ng_parse_append( | |||||
char **cbufp, size_t *cbuflenp, const char *fmt, ...); | |||||
static int ng_parse_append_type(const struct ng_parse_type *type, | |||||
const u_char *data, char **cbuf, size_t *cbuflenp); | |||||
static ng_parse_t ng_ipaddr_cidr_parse; | |||||
static ng_unparse_t ng_ipaddr_cidr_unparse; | |||||
static ng_getDefault_t ng_ipaddr_cidr_getDefault; | |||||
static ng_getAlign_t ng_ipaddr_cidr_getAlign; | |||||
static ng_getDefault_t ng_ipmask_getDefault; | |||||
static ng_parse_t ng_ip6addr_cidr_parse; | |||||
static ng_unparse_t ng_ip6addr_cidr_unparse; | |||||
static ng_getDefault_t ng_ip6addr_cidr_getDefault; | |||||
static ng_getAlign_t ng_ip6addr_cidr_getAlign; | |||||
static ng_parse_t ng_enaddr_prefix_parse; | |||||
static ng_unparse_t ng_enaddr_prefix_unparse; | |||||
static ng_getDefault_t ng_enaddr_prefix_getDefault; | |||||
static ng_getAlign_t ng_enaddr_prefix_getAlign; | |||||
static bool is_prefix(const void *data, size_t len); | |||||
static uint8_t get_prefix_length(const void *data, size_t len); | |||||
static ng_parse_t ng_antispoof_filter_parse; | |||||
static ng_unparse_t ng_antispoof_filter_unparse; | |||||
static ng_getAlign_t ng_antispoof_filter_getAlign; | |||||
static inline int mkipmask(struct in_addr *addr, uint8_t n_sigbits); | |||||
static inline int mkipv6mask(struct in6_addr *addr, uint8_t n_sigbits); | |||||
static inline void mkipv6mask_or_default128( | |||||
struct in6_addr *addr, uint8_t n_sigbits); | |||||
static inline int mkethernetmask(u_char *addr, uint8_t n_sigbits); | |||||
static inline void mkethernetmask_or_default48(u_char *addr, uint8_t n_sigbits); | |||||
static ng_parse_array_getLength_t ng_antispoof_getTableLength; | |||||
/* Netgraph methods */ | |||||
static ng_constructor_t ng_antispoof_constructor; | |||||
static ng_newhook_t ng_antispoof_newhook; | |||||
static ng_rcvmsg_t ng_antispoof_rcvmsg; | |||||
static ng_rcvdata_t ng_antispoof_rcvdata; | |||||
static ng_close_t ng_antispoof_close; | |||||
static ng_shutdown_t ng_antispoof_shutdown; | |||||
static ng_disconnect_t ng_antispoof_disconnect; | |||||
/* Other internal functions */ | |||||
static int ng_antispoof_getstats(struct ng_antispoof_private *privdata, | |||||
const struct ng_mesg *request, struct ng_mesg **response); | |||||
static void ng_antispoof_clearstats(struct ng_antispoof_private *privdata); | |||||
static uint16_t ng_antispoof_setconfig_find( | |||||
struct ng_antispoof_private * privdata, | |||||
const struct ng_antispoof_filter *filter); | |||||
static void ng_antispoof_setconfig_insert(struct ng_antispoof_private *privdata, | |||||
uint16_t pos, const struct ng_antispoof_filter *filter); | |||||
static void ng_antispoof_setconfig_remove( | |||||
struct ng_antispoof_private *privdata, uint16_t pos); | |||||
static void ng_antispoof_setconfig_mkfilter_ip( | |||||
struct ng_antispoof_filter * new_filter, | |||||
const struct ng_antispoof_rule_ip *rule); | |||||
static void ng_antispoof_setconfig_mkfilter_ipv6( | |||||
struct ng_antispoof_filter * new_filter, | |||||
const struct ng_antispoof_rule_ipv6 *rule); | |||||
static void ng_antispoof_setconfig_addrule_ip( | |||||
struct ng_antispoof_private * privdata, | |||||
const struct ng_antispoof_rule_ip *rule); | |||||
static void ng_antispoof_setconfig_addrule_ipv6( | |||||
struct ng_antispoof_private * privdata, | |||||
const struct ng_antispoof_rule_ipv6 *rule); | |||||
static void ng_antispoof_setconfig_delrule_ip( | |||||
struct ng_antispoof_private * privdata, | |||||
const struct ng_antispoof_rule_ip *rule); | |||||
static void ng_antispoof_setconfig_delrule_ipv6( | |||||
struct ng_antispoof_private * privdata, | |||||
const struct ng_antispoof_rule_ipv6 *rule); | |||||
static int ng_antispoof_rcvmsg_impl(struct ng_antispoof_private *privdata, | |||||
const struct ng_mesg *request, struct ng_mesg **response); | |||||
static enum as_error pullup(struct mbuf **m, size_t len); | |||||
static inline enum as_error ng_antispoof_skip_vlan( | |||||
struct mbuf **m, struct packet_info *pkt_info, size_t *offset); | |||||
static inline enum as_error ng_antispoof_collect_layer2(struct mbuf **m, | |||||
struct packet_info *pkt_info, enum as_flow_direction flow, size_t *offset); | |||||
static inline enum as_error ng_antispoof_collect_arp(struct mbuf **m, | |||||
struct packet_info *pkt_info, enum as_flow_direction flow, size_t *offset); | |||||
static inline enum as_error ng_antispoof_collect_ip(struct mbuf **m, | |||||
struct packet_info *pkt_info, enum as_flow_direction flow, size_t *offset); | |||||
static inline enum as_error ng_antispoof_collect_ipv6(struct mbuf **m, | |||||
struct packet_info *pkt_info, enum as_flow_direction flow, size_t *offset); | |||||
static inline enum as_error ng_antispoof_collect_layer3(struct mbuf **m, | |||||
struct packet_info *pkt_info, enum as_flow_direction flow, size_t *offset); | |||||
static enum as_error ng_antispoof_collect_data( | |||||
struct mbuf **m, struct packet_info *pkt_info, enum as_flow_direction flow); | |||||
static enum as_error ng_antispoof_packet_matches( | |||||
const struct packet_info_conv * pkt_info, | |||||
const struct ng_antispoof_filter *filters, uint16_t begin, uint16_t end); | |||||
static const char *ng_antispoof_nodename(node_p node); | |||||
/* | |||||
* Compute alignment of type T | |||||
* | |||||
* Examples: | |||||
* x = ALIGNMENT_OF(int8_t); // x = 1 | |||||
* x = ALIGNMENT_OF(int32_t); // x = 4 | |||||
* x = ALIGNMENT_OF(struct in6_addr); // x = 4 | |||||
*/ | |||||
/* clang-format off */ | |||||
#define ALIGNMENT_OF(T) ((size_t)&((struct { char x; T y; } *)0)->y) | |||||
/* clang-format on */ | |||||
/* | |||||
* Compute offset of member M in struct S | |||||
* | |||||
* Examples: | |||||
* struct demo { | |||||
* int8_t x; | |||||
* int8_t y; | |||||
* int32_t z; | |||||
* }; | |||||
* x = OFFSET(demo, x); // x = 0 | |||||
* x = OFFSET(demo, y); // y = 1 | |||||
* x = OFFSET(demo, z); // x = 4 | |||||
*/ | |||||
#define OFFSET(S, M) ((size_t) & ((S *)0)->M) | |||||
/* | |||||
* How many fields of type F are required to cover type T in memory? | |||||
* | |||||
* Examples: | |||||
* x = NFIELDS(char, char); // x = 1 | |||||
* x = NFIELDS(int64_t, int64_t); // x = 1 | |||||
* x = NFIELDS(char, int64_t); // x = 1 | |||||
* x = NFIELDS(int64_t, char); // x = 8 | |||||
* x = NFIELDS(char[7], int32_t); // x = 2 | |||||
*/ | |||||
#define NFIELDS(T, F) ((sizeof(T) - 1) / sizeof(F) + 1) | |||||
/* | |||||
* Immutable VLAN header struct (IEEE 802.1Q and 802.1ad) | |||||
*/ | |||||
struct ng_antispoof_vlanhdr { | |||||
/* | |||||
* TCI (Tag Control Information) format: | |||||
* | |||||
* uint16_t pcp : 3; // IEEE P802.1p Priority Code Point | |||||
* uint16_t dei : 1; // Drop Eligible Indicator (former CF) | |||||
* uint16_t vid : 12; // VLAN ID | |||||
* | |||||
* How to access individual TCI components: | |||||
* | |||||
* #include <net/ethernet.h> | |||||
* uint16_t pcp = EVL_PRIOFTAG (ntohs(tag)); | |||||
* uint16_t dei = EVL_CFIOFTAG (ntohs(tag)); | |||||
* uint16_t vid = EVL_VLANOFTAG (noths(tag)); | |||||
* | |||||
*/ | |||||
const uint16_t tag; /* TCI (Tag Control Information) */ | |||||
const uint16_t proto; /* TPID (Tag Protocoll Identifier) */ | |||||
}; | |||||
/* | |||||
* Packet information required for filtering | |||||
*/ | |||||
struct packet_info { | |||||
uint16_t ether_type; /* protocol (ETHERTYPE_IP or ETHERTYPE_IPV6) */ | |||||
u_char en_addr[ETHER_ADDR_LEN]; /* ethernet address */ | |||||
// uint16_t qinq_tag; /* IEEE 802.1ad vlan tag (QinQ) */ | |||||
// uint16_t vlan_tag; /* IEEE 802.1Q vlan tag */ | |||||
union { | |||||
struct in_addr ip_addr; /* IPv4 address */ | |||||
struct in6_addr ip6_addr; /* IPv6 address */ | |||||
}; | |||||
}; | |||||
/* | |||||
* Size of uintmax_t array that completely covers a packet_info struct. | |||||
*/ | |||||
#define FILTER_RAW_SZ (NFIELDS(struct packet_info, uintmax_t)) | |||||
/* | |||||
* Convert packet_info into an array of integers for efficient filtering | |||||
*/ | |||||
struct packet_info_conv { | |||||
union { | |||||
struct packet_info pinfo; | |||||
uintmax_t raw[FILTER_RAW_SZ]; | |||||
}; | |||||
}; | |||||
/* | |||||
* Antispoof filter consisting of address and network mask | |||||
*/ | |||||
struct ng_antispoof_filter { | |||||
struct packet_info_conv addr; /* address to filter */ | |||||
struct packet_info_conv mask; /* mask to apply */ | |||||
}; | |||||
/* Parse type for struct ng_antispoof_rule_ip */ | |||||
static const struct ng_parse_struct_field ng_antispoof_rule_ip_type_fields[] = | |||||
NG_ANTISPOOF_RULE_IP_TYPE_INFO; | |||||
static const struct ng_parse_type ng_antispoof_rule_ip_type = { | |||||
&ng_parse_struct_type, &ng_antispoof_rule_ip_type_fields | |||||
}; | |||||
/* Parse type for struct ng_antispoof_rule_ipv6 */ | |||||
static const struct ng_parse_struct_field ng_antispoof_rule_ipv6_type_fields[] = | |||||
NG_ANTISPOOF_RULE_IPV6_TYPE_INFO; | |||||
static const struct ng_parse_type ng_antispoof_rule_ipv6_type = { | |||||
&ng_parse_struct_type, &ng_antispoof_rule_ipv6_type_fields | |||||
}; | |||||
/* | |||||
* Append to a fixed length string buffer. | |||||
*/ | |||||
static int | |||||
ng_parse_append(char **cbufp, size_t *cbuflenp, const char *fmt, ...) | |||||
{ | |||||
va_list args; | |||||
va_start(args, fmt); | |||||
int len = vsnprintf(*cbufp, *cbuflenp, fmt, args); | |||||
va_end(args); | |||||
if (len >= *cbuflenp) | |||||
return (ERANGE); | |||||
*cbufp += len; | |||||
*cbuflenp -= len; | |||||
return (0); | |||||
} | |||||
/* | |||||
* Unparse type to a fixed length string buffer. | |||||
*/ | |||||
static int | |||||
ng_parse_append_type(const struct ng_parse_type *type, const u_char *data, | |||||
char **cbuf, size_t *cbuflenp) | |||||
{ | |||||
/* Protect against overflow when converting to int calling ng_unparse */ | |||||
if (*cbuflenp > INT_MAX) | |||||
return (ERANGE); | |||||
int error = ng_unparse(type, data, *cbuf, *cbuflenp); | |||||
if (error == 0) { | |||||
size_t len = strnlen(*cbuf, *cbuflenp); | |||||
*cbuf += len; | |||||
*cbuflenp -= len; | |||||
} | |||||
return (error); | |||||
} | |||||
/************************************************************************ | |||||
IP PREFIX TYPE | |||||
************************************************************************/ | |||||
/* | |||||
* Parse an IP address prefix in CIDR notation | |||||
* | |||||
* Format: IP [ '/' LENGTH ] | |||||
* | |||||
* If provided, LENGTH must be in the range 0-32. | |||||
* If LENGTH is omitted, it is set to UINT8_MAX to indicate an unset value. | |||||
* | |||||
* Examples: | |||||
* 1.2.3.4 // addr=1.2.3.4, length=UINT_MAX | |||||
* 1.2.3.4/32 // addr=1.2.3.4, length=32 | |||||
* 192.168.1.0/24 // addr=192.168.1.0, length=24 | |||||
* 10.0.0.0/8 // addr=10.0.0.0, length=8 | |||||
*/ | |||||
static int | |||||
ng_ipaddr_cidr_parse(const struct ng_parse_type *type, const char *s, int *off, | |||||
const u_char *const start, u_char *const buf, int *buflen) | |||||
{ | |||||
struct ng_antispoof_cidr_ip *cidr = (struct ng_antispoof_cidr_ip *)buf; | |||||
/* Make sure the buffer is big enough to hold the result */ | |||||
if (*buflen < sizeof(*cidr)) | |||||
return (ERANGE); | |||||
/* Parse IP address */ | |||||
int len = sizeof(cidr->addr); | |||||
int error = ng_parse( | |||||
&ng_parse_ipaddr_type, s, off, (u_char *)&cidr->addr, &len); | |||||
if (error != 0) | |||||
return (error); | |||||
/* Parse prefix length (number of most significant bits set to 1) */ | |||||
if (*(s + *off) == '/') { | |||||
++(*off); | |||||
len = sizeof(cidr->length); | |||||
error = ng_parse(&ng_parse_uint8_type, s, off, | |||||
(u_char *)&cidr->length, &len); | |||||
if (error != 0) | |||||
return (error); | |||||
if (cidr->length > 32) | |||||
return (EINVAL); | |||||
} else { | |||||
/* Set to high value to indicate an unset value */ | |||||
cidr->length = UINT8_MAX; | |||||
} | |||||
/* Report amount of data written to buffer */ | |||||
*buflen = sizeof(*cidr); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Unparse prefix in its canonical form | |||||
* | |||||
* The following normalizations are implemented: | |||||
* - if the prefix length is greater than 32, LENGTH is omitted | |||||
*/ | |||||
static int | |||||
ng_ipaddr_cidr_unparse(const struct ng_parse_type *type, const u_char *data, | |||||
int *off, char *cbuf, int cbuflen) | |||||
{ | |||||
if (cbuflen < 0) | |||||
return (ERANGE); | |||||
size_t buflen = cbuflen; | |||||
const struct ng_antispoof_cidr_ip *const cidr = | |||||
(const struct ng_antispoof_cidr_ip *)(data + *off); | |||||
int error = ng_parse_append_type( | |||||
&ng_parse_ipaddr_type, (const u_char *)&cidr->addr, &cbuf, &buflen); | |||||
if (error != 0) | |||||
return (error); | |||||
if (cidr->length <= 32) { | |||||
error = ng_parse_append(&cbuf, &buflen, "/%d", cidr->length); | |||||
if (error != 0) | |||||
return (error); | |||||
} | |||||
/* Increase offset by the amount of data read */ | |||||
*off += sizeof(*cidr); | |||||
return (0); | |||||
} | |||||
static int | |||||
ng_ipaddr_cidr_getDefault(const struct ng_parse_type *type, | |||||
const u_char *const start, u_char *buf, int *buflen) | |||||
{ | |||||
int error = 0; | |||||
/* | |||||
* Zero out memory so ng_unparse_composite can check default values via | |||||
* bcmp(). | |||||
*/ | |||||
struct ng_antispoof_cidr_ip cidr; | |||||
bzero(&cidr, sizeof(cidr)); | |||||
cidr.addr = (struct in_addr) { 0 }; | |||||
cidr.length = UINT8_MAX; | |||||
if (*buflen >= sizeof(cidr)) { | |||||
bcopy(&cidr, buf, sizeof(cidr)); | |||||
*buflen = sizeof(cidr); | |||||
} else | |||||
error = ERANGE; | |||||
return (error); | |||||
} | |||||
static int | |||||
ng_ipaddr_cidr_getAlign(const struct ng_parse_type *type) | |||||
{ | |||||
return (ALIGNMENT_OF(struct ng_antispoof_cidr_ip)); | |||||
} | |||||
/* clang-format off */ | |||||
const struct ng_parse_type ng_parse_ipaddr_cidr_type = { | |||||
NULL, | |||||
NULL, | |||||
NULL, | |||||
ng_ipaddr_cidr_parse, | |||||
ng_ipaddr_cidr_unparse, | |||||
ng_ipaddr_cidr_getDefault, | |||||
ng_ipaddr_cidr_getAlign | |||||
}; | |||||
/* clang-format on */ | |||||
/************************************************************************ | |||||
IP MASK TYPE | |||||
************************************************************************/ | |||||
static int | |||||
ng_ipmask_getDefault(const struct ng_parse_type *type, | |||||
const u_char *const start, u_char *buf, int *buflen) | |||||
{ | |||||
struct in_addr mask = { 0xffffffff }; | |||||
int error = 0; | |||||
if (*buflen >= sizeof(mask)) { | |||||
bcopy(&mask, buf, sizeof(mask)); | |||||
*buflen = sizeof(mask); | |||||
} else | |||||
error = ERANGE; | |||||
return (error); | |||||
} | |||||
/* | |||||
* IPv4 network mask | |||||
* | |||||
* This is a specialization of an IPv4 address that provides a different default | |||||
* value (255.255.255.255 instead of 0.0.0.0). | |||||
*/ | |||||
/* clang-format off */ | |||||
const struct ng_parse_type ng_parse_ipmask_type = { | |||||
&ng_parse_ipaddr_type, | |||||
NULL, | |||||
NULL, | |||||
NULL, | |||||
NULL, | |||||
ng_ipmask_getDefault, | |||||
NULL | |||||
}; | |||||
/* clang-format on */ | |||||
/************************************************************************ | |||||
IPv6 PREFIX TYPE | |||||
************************************************************************/ | |||||
/* | |||||
* Parse an IPv6 address prefix in CIDR notation | |||||
* | |||||
* Format: IPv6 [ '/' LENGTH ] | |||||
* | |||||
* If provided, LENGTH must be in the range 0-128. | |||||
* If LENGTH is omitted, it is set to UINT8_MAX to indicate an unset value. | |||||
* | |||||
* Examples: | |||||
* ::1 // addr=::1, length=UINT_MAX | |||||
* ::1/128 // addr=::1, length=128 | |||||
* ff02::/16 // addr=ff02::, length=16 | |||||
*/ | |||||
static int | |||||
ng_ip6addr_cidr_parse(const struct ng_parse_type *type, const char *s, int *off, | |||||
const u_char *const start, u_char *const buf, int *buflen) | |||||
{ | |||||
struct ng_antispoof_cidr_ipv6 *cidr = | |||||
(struct ng_antispoof_cidr_ipv6 *)buf; | |||||
/* Make sure the buffer is big enough to hold the result */ | |||||
if (*buflen < sizeof(*cidr)) | |||||
return (ERANGE); | |||||
int len = sizeof(cidr->addr); | |||||
int error = ng_parse( | |||||
&ng_parse_ip6addr_type, s, off, (u_char *)&cidr->addr, &len); | |||||
if (error != 0) | |||||
return (error); | |||||
/* Parse addr length (number of most significant bits) */ | |||||
if (*(s + *off) == '/') { | |||||
++(*off); | |||||
len = sizeof(cidr->length); | |||||
error = ng_parse(&ng_parse_uint8_type, s, off, | |||||
(u_char *)&cidr->length, &len); | |||||
if (error != 0) | |||||
return (error); | |||||
if (cidr->length > 128) | |||||
return (EINVAL); | |||||
} else { | |||||
/* Set to high value to indicate an unset value */ | |||||
cidr->length = UINT8_MAX; | |||||
} | |||||
/* Report amount of data written to buffer */ | |||||
*buflen = sizeof(*cidr); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Unparse prefix in its canonical form | |||||
* | |||||
* The following normalizations are implemented: | |||||
* - if the prefix length is greater than 128, LENGTH is omitted | |||||
*/ | |||||
static int | |||||
ng_ip6addr_cidr_unparse(const struct ng_parse_type *type, const u_char *data, | |||||
int *off, char *cbuf, int cbuflen) | |||||
{ | |||||
if (cbuflen < 0) | |||||
return (ERANGE); | |||||
size_t buflen = cbuflen; | |||||
const struct ng_antispoof_cidr_ipv6 *const cidr = | |||||
(const struct ng_antispoof_cidr_ipv6 *)(data + *off); | |||||
int error = ng_parse_append_type(&ng_parse_ip6addr_type, | |||||
(const u_char *)&cidr->addr, &cbuf, &buflen); | |||||
if (error != 0) | |||||
return (error); | |||||
if (cidr->length <= 128) { | |||||
error = ng_parse_append(&cbuf, &buflen, "/%d", cidr->length); | |||||
if (error != 0) | |||||
return (error); | |||||
} | |||||
/* Increase offset by the amount of data read */ | |||||
*off += sizeof(*cidr); | |||||
return (0); | |||||
} | |||||
static int | |||||
ng_ip6addr_cidr_getDefault(const struct ng_parse_type *type, | |||||
const u_char *const start, u_char *buf, int *buflen) | |||||
{ | |||||
int error = 0; | |||||
/* | |||||
* Zero out memory so ng_unparse_composite can check default values via | |||||
* bcmp(). | |||||
*/ | |||||
struct ng_antispoof_cidr_ipv6 cidr; | |||||
bzero(&cidr, sizeof(cidr)); | |||||
cidr.addr = (struct in6_addr)IN6ADDR_ANY_INIT; | |||||
cidr.length = UINT8_MAX; | |||||
if (*buflen >= sizeof(cidr)) { | |||||
bcopy(&cidr, buf, sizeof(cidr)); | |||||
*buflen = sizeof(cidr); | |||||
} else | |||||
error = ERANGE; | |||||
return (error); | |||||
} | |||||
static int | |||||
ng_ip6addr_cidr_getAlign(const struct ng_parse_type *type) | |||||
{ | |||||
return (ALIGNMENT_OF(struct ng_antispoof_cidr_ipv6)); | |||||
} | |||||
/* clang-format off */ | |||||
const struct ng_parse_type ng_parse_ip6addr_cidr_type = { | |||||
NULL, | |||||
NULL, | |||||
NULL, | |||||
ng_ip6addr_cidr_parse, | |||||
ng_ip6addr_cidr_unparse, | |||||
ng_ip6addr_cidr_getDefault, | |||||
ng_ip6addr_cidr_getAlign | |||||
}; | |||||
/* clang-format on */ | |||||
/************************************************************************ | |||||
ETHERNET PREFIX TYPE | |||||
************************************************************************/ | |||||
/* | |||||
* Parse an ethernet address prefix in a CIDR-like notation | |||||
* | |||||
* Format: ETHER [ '/' LENGTH ] | |||||
* | |||||
* If provided, LENGTH must be in the range 0-48. | |||||
* If LENGTH is omitted, it is set to UINT8_MAX to indicate an unset value. | |||||
* | |||||
* Examples: | |||||
* 0a:00:00:00:00:01 // addr=0a:00:00:00:00:01, length=UINT_MAX | |||||
* 0a:00:00:00:00:01/48 // addr=0a:00:00:00:00:01, length=48 | |||||
* 33:33:00:00:00:00/16 // addr=33:33:00:00:00:00, length=16 | |||||
*/ | |||||
static int | |||||
ng_enaddr_prefix_parse(const struct ng_parse_type *type, const char *s, | |||||
int *off, const u_char *const start, u_char *const buf, int *buflen) | |||||
{ | |||||
struct ng_antispoof_ether_prefix *prefix = | |||||
(struct ng_antispoof_ether_prefix *)buf; | |||||
/* Make sure the buffer is big enough to hold the result */ | |||||
if (*buflen < sizeof(*prefix)) | |||||
return (ERANGE); | |||||
int len = sizeof(prefix->addr); | |||||
int error = ng_parse( | |||||
&ng_parse_enaddr_type, s, off, (u_char *)&prefix->addr, &len); | |||||
if (error != 0) | |||||
return (error); | |||||
/* Parse prefix length (number of most significant bits) */ | |||||
if (*(s + *off) == '/') { | |||||
++(*off); | |||||
len = sizeof(prefix->length); | |||||
error = ng_parse(&ng_parse_uint8_type, s, off, | |||||
(u_char *)&prefix->length, &len); | |||||
if (error != 0) | |||||
return (error); | |||||
if (prefix->length > 48) | |||||
return (EINVAL); | |||||
} else { | |||||
/* Set to high value to indicate an unset value */ | |||||
prefix->length = UINT8_MAX; | |||||
} | |||||
/* Report amount of data written to buffer */ | |||||
*buflen = sizeof(*prefix); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Unparse prefix in its canonical form | |||||
* | |||||
* The following normalizations are implemented: | |||||
* - if the prefix length is greater than 48, LENGTH is omitted | |||||
*/ | |||||
static int | |||||
ng_enaddr_prefix_unparse(const struct ng_parse_type *type, const u_char *data, | |||||
int *off, char *cbuf, int cbuflen) | |||||
{ | |||||
if (cbuflen < 0) | |||||
return (ERANGE); | |||||
size_t buflen = cbuflen; | |||||
const struct ng_antispoof_ether_prefix *const prefix = | |||||
(const struct ng_antispoof_ether_prefix *)(data + *off); | |||||
int error = ng_parse_append_type(&ng_parse_enaddr_type, | |||||
(const u_char *)&prefix->addr, &cbuf, &buflen); | |||||
if (error != 0) | |||||
return (error); | |||||
if (prefix->length <= 48) { | |||||
error = ng_parse_append(&cbuf, &buflen, "/%d", prefix->length); | |||||
if (error != 0) | |||||
return (error); | |||||
} | |||||
/* Increase offset by the amount of data read */ | |||||
*off += sizeof(*prefix); | |||||
return (0); | |||||
} | |||||
static int | |||||
ng_enaddr_prefix_getDefault(const struct ng_parse_type *type, | |||||
const u_char *const start, u_char *buf, int *buflen) | |||||
{ | |||||
struct ng_antispoof_ether_prefix prefix = { { 0 }, UINT8_MAX }; | |||||
int error = 0; | |||||
if (*buflen >= sizeof(prefix)) { | |||||
bcopy(&prefix, buf, sizeof(prefix)); | |||||
*buflen = sizeof(prefix); | |||||
} else | |||||
error = ERANGE; | |||||
return (error); | |||||
} | |||||
static int | |||||
ng_enaddr_prefix_getAlign(const struct ng_parse_type *type) | |||||
{ | |||||
return (ALIGNMENT_OF(struct ng_antispoof_ether_prefix)); | |||||
} | |||||
/* clang-format off */ | |||||
const struct ng_parse_type ng_parse_enaddr_prefix_type = { | |||||
NULL, | |||||
NULL, | |||||
NULL, | |||||
ng_enaddr_prefix_parse, | |||||
ng_enaddr_prefix_unparse, | |||||
ng_enaddr_prefix_getDefault, | |||||
ng_enaddr_prefix_getAlign | |||||
}; | |||||
/* clang-format on */ | |||||
/************************************************************************ | |||||
ANTISPOOF FILTER TYPE | |||||
************************************************************************/ | |||||
/* | |||||
* Is given mask a prefix? | |||||
* | |||||
* Returns true if most significant bits are all 1 and least significant bits | |||||
* are all 0. | |||||
* | |||||
* Examples: | |||||
* 1111 0000 // true, prefix of length 4 | |||||
* 0000 0000 // true, prefix of length 0 | |||||
* 1111 1111 // true, prefix of length 8 | |||||
* 0000 1111 // false | |||||
* 1100 1100 // false | |||||
*/ | |||||
static bool | |||||
is_prefix(const void *data, size_t len) | |||||
{ | |||||
const u_char *_data = data; | |||||
bool set_left_most_bits_parsed = false; | |||||
for (const u_char *end = _data + len; _data < end; ++_data) { | |||||
u_char c = 1 << (CHAR_BIT - 1); | |||||
for (uint_fast8_t i = 0; i < CHAR_BIT; ++i, c >>= 1) { | |||||
if ((*_data & c) == 0) | |||||
set_left_most_bits_parsed = true; | |||||
else if (set_left_most_bits_parsed) | |||||
return (false); | |||||
} | |||||
} | |||||
return (true); | |||||
} | |||||
/* | |||||
* Length of prefix in given mask. | |||||
* | |||||
* Precondition: is_prefix(data, len) == true | |||||
* | |||||
* Examples: | |||||
* 1111 0000 // length=4 | |||||
* 0000 0000 // length=0 | |||||
* 1111 1111 // length=8 | |||||
*/ | |||||
static uint8_t | |||||
get_prefix_length(const void *data, size_t len) | |||||
{ | |||||
const u_char *_data = data; | |||||
uint8_t prefix_length = 0; | |||||
for (const u_char *end = _data + len; _data < end; ++_data) { | |||||
u_char c = 1 << (CHAR_BIT - 1); | |||||
for (uint_fast8_t i = 0; i < CHAR_BIT; ++i, c >>= 1) { | |||||
if ((*_data & c) == 0) | |||||
return (prefix_length); | |||||
++prefix_length; | |||||
} | |||||
} | |||||
return (prefix_length); | |||||
} | |||||
/* Parsing filters into a data structure is not supported */ | |||||
static int | |||||
ng_antispoof_filter_parse(const struct ng_parse_type *type, const char *s, | |||||
int *off, const u_char *const start, u_char *const buf, int *buflen) | |||||
{ | |||||
/* Unparse-only data structure */ | |||||
return (EOPNOTSUPP); | |||||
} | |||||
/* | |||||
* Unparse filter rule in its canonical form. | |||||
* | |||||
* The following normalizations are implemented: | |||||
* - IP rules are provided in CIDR notation if possible (ip_addr=1.2.3.4/24) | |||||
* - If the IP mask is not a prefix, the IP address along with the mask is | |||||
* provided (ip_addr=1.2.3.4 ip_mask=255.255.0.255) | |||||
* - The length part of a prefix (ethernet, IP and IPv6) is omitted if it is | |||||
* greater or equal the maximum length of the prefix | |||||
* | |||||
* Examples: | |||||
* 192.168.1.0/255.255.255.0 -> 192.168.1.0/24 | |||||
* 192.168.0.1/255.255.0.255 -> 192.168.0.1/255.255.0.255 | |||||
* 192.168.1.1/32 -> 192.168.1.1 | |||||
* ::1/128 -> ::1 | |||||
* 0a:00:00:00:00:01/48 -> 0a:00:00:00:00:01 | |||||
* | |||||
* { ether=0a:00:00:00:00:01 ip_addr=192.168.1.0/24 } | |||||
* { ether=0a:00:00:00:00:01 ip_addr=192.168.0.1 ip_mask=255.255.0.255 } | |||||
* { ether=0a:00:00:00:00:01 ip_addr=192.168.1.1 } | |||||
* { ether=0a:00:00:00:00:01 ip6_addr=::1 } | |||||
*/ | |||||
static int | |||||
ng_antispoof_filter_unparse(const struct ng_parse_type *type, | |||||
const u_char *data, int *off, char *cbuf, int cbuflen) | |||||
{ | |||||
if (cbuflen < 0) | |||||
return (ERANGE); | |||||
size_t buflen = cbuflen; | |||||
int error = 0; | |||||
const struct ng_antispoof_filter *const filter = | |||||
(const struct ng_antispoof_filter *)(data + *off); | |||||
switch (ntohs(filter->addr.pinfo.ether_type)) { | |||||
case ETHERTYPE_IP: { | |||||
/* | |||||
* Zero out memory so ng_unparse_composite can check default | |||||
* values via bcmp(). | |||||
*/ | |||||
struct ng_antispoof_rule_ip rule; | |||||
bzero(&rule, sizeof(rule)); | |||||
bcopy(filter->addr.pinfo.en_addr, rule.ether.addr, | |||||
sizeof(rule.ether.addr)); | |||||
rule.ether.length = | |||||
get_prefix_length(filter->mask.pinfo.en_addr, | |||||
sizeof(filter->mask.pinfo.en_addr)); | |||||
/* Omit maximum prefix length */ | |||||
if (rule.ether.length >= 48) | |||||
rule.ether.length = UINT8_MAX; | |||||
bcopy(&filter->addr.pinfo.ip_addr, &rule.ip_addr.addr, | |||||
sizeof(rule.ip_addr.addr)); | |||||
if (is_prefix(&filter->mask.pinfo.ip_addr, | |||||
sizeof(filter->mask.pinfo.ip_addr))) { | |||||
rule.ip_addr.length = | |||||
get_prefix_length(&filter->mask.pinfo.ip_addr, | |||||
sizeof(filter->mask.pinfo.ip_addr)); | |||||
/* Omit maximum prefix length */ | |||||
if (rule.ip_addr.length >= 32) | |||||
rule.ip_addr.length = UINT8_MAX; | |||||
/* Omit ip_mask (set to default) */ | |||||
int len = sizeof(rule.ip_mask); | |||||
error = ng_parse_getDefault(&ng_parse_ipmask_type, | |||||
(u_char *)&rule.ip_mask, &len); | |||||
} else { | |||||
/* Omit prefix length */ | |||||
rule.ip_addr.length = UINT8_MAX; | |||||
bcopy(&filter->mask.pinfo.ip_addr, &rule.ip_mask, | |||||
sizeof(rule.ip_mask)); | |||||
} | |||||
error = ng_parse_append_type(&ng_antispoof_rule_ip_type, | |||||
(const u_char *)&rule, &cbuf, &buflen); | |||||
break; | |||||
} | |||||
case ETHERTYPE_IPV6: { | |||||
/* | |||||
* Zero out memory so ng_unparse_composite can check default | |||||
* values via bcmp(). | |||||
*/ | |||||
struct ng_antispoof_rule_ipv6 rule; | |||||
bzero(&rule, sizeof(rule)); | |||||
bcopy(filter->addr.pinfo.en_addr, rule.ether.addr, | |||||
sizeof(rule.ether.addr)); | |||||
rule.ether.length = | |||||
get_prefix_length(filter->mask.pinfo.en_addr, | |||||
sizeof(filter->mask.pinfo.en_addr)); | |||||
/* Omit maximum prefix length */ | |||||
if (rule.ether.length >= 48) | |||||
rule.ether.length = UINT8_MAX; | |||||
rule.ip6_addr.addr = filter->addr.pinfo.ip6_addr; | |||||
rule.ip6_addr.length = | |||||
get_prefix_length(&filter->mask.pinfo.ip6_addr, | |||||
sizeof(filter->mask.pinfo.ip6_addr)); | |||||
/* Omit maximum prefix length */ | |||||
if (rule.ip6_addr.length >= 128) | |||||
rule.ip6_addr.length = UINT8_MAX; | |||||
error = ng_parse_append_type(&ng_antispoof_rule_ipv6_type, | |||||
(const u_char *)&rule, &cbuf, &buflen); | |||||
break; | |||||
} | |||||
default: { | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
} | |||||
/* Increase offset by the amount of data read */ | |||||
if (error == 0) | |||||
*off += sizeof(*filter); | |||||
return (error); | |||||
} | |||||
static int | |||||
ng_antispoof_filter_getAlign(const struct ng_parse_type *type) | |||||
{ | |||||
return (ALIGNMENT_OF(struct ng_antispoof_filter)); | |||||
} | |||||
/* | |||||
* Antispoof Filter | |||||
* | |||||
* Unparses a filter rule. | |||||
*/ | |||||
/* clang-format off */ | |||||
const struct ng_parse_type ng_parse_antispoof_filter_type = { | |||||
NULL, | |||||
NULL, | |||||
NULL, | |||||
&ng_antispoof_filter_parse, | |||||
&ng_antispoof_filter_unparse, | |||||
NULL, | |||||
&ng_antispoof_filter_getAlign | |||||
}; | |||||
/* clang-format on */ | |||||
/************************************************************************ | |||||
ANTISPOOF NODE IMPLEMENTATION | |||||
************************************************************************/ | |||||
#ifdef NG_SEPARATE_MALLOC | |||||
static MALLOC_DEFINE( | |||||
M_NETGRAPH_ANTISPOOF, "netgraph_antispoof", "netgraph antispoof node"); | |||||
#else | |||||
#define M_NETGRAPH_ANTISPOOF M_NETGRAPH | |||||
#endif | |||||
/* | |||||
* Generate mask for IP prefix | |||||
* | |||||
* Set the n_sigbits most significant bits in addr and return 0. | |||||
* If n_sigbits > 32, addr is lef unchanged and ERANGE is returned. | |||||
* If addr is NULL, EINVAL is returned. | |||||
*/ | |||||
static inline int | |||||
mkipmask(struct in_addr *addr, uint8_t n_sigbits) | |||||
{ | |||||
if (addr == NULL) | |||||
return (EINVAL); | |||||
if (n_sigbits > 32) | |||||
return (ERANGE); | |||||
/* clang-format off */ | |||||
addr->s_addr = n_sigbits >= 32 | |||||
? 0xffffffff | |||||
: htonl(~((uint32_t)~0 >> n_sigbits)); | |||||
/* clang-format on */ | |||||
return (0); | |||||
} | |||||
/* | |||||
* Generate mask for IPv6 prefix | |||||
* | |||||
* Set the n_sigbits most significant bits in addr and return 0. | |||||
* If n_sigbits > 128, addr is lef unchanged and ERANGE is returned. | |||||
* If addr is NULL, EINVAL is returned. | |||||
*/ | |||||
static inline int | |||||
mkipv6mask(struct in6_addr *addr, uint8_t n_sigbits) | |||||
{ | |||||
if (addr == NULL) | |||||
return (EINVAL); | |||||
if (n_sigbits > 128) | |||||
return (ERANGE); | |||||
/* | |||||
* Use f to iterate through the four 32-bit fields of the IPv6 address. | |||||
* Decrement n_sigbits by 32 (the bits processed) with each iteration. | |||||
* As sigbit is <= 128 bits, we remain within addr's bounds. | |||||
*/ | |||||
*addr = (struct in6_addr)IN6MASK0; | |||||
uint32_t *f = addr->__u6_addr.__u6_addr32; | |||||
for (; n_sigbits >= 32; n_sigbits -= 32) { | |||||
*(f++) = 0xffffffff; | |||||
} | |||||
/* Make sure we remain within the bounds of addr */ | |||||
if (n_sigbits > 0) | |||||
*f = htonl(~((uint32_t)~0 >> n_sigbits)); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Generate mask for IPv6 prefix (default to /128 if n_sigbits > 128) | |||||
*/ | |||||
static inline void | |||||
mkipv6mask_or_default128(struct in6_addr *addr, uint8_t n_sigbits) | |||||
{ | |||||
mkipv6mask(addr, n_sigbits < 128 ? n_sigbits : 128); | |||||
} | |||||
/* | |||||
* Generate mask for ethernet prefix | |||||
* | |||||
* Set the n_sigbits most significant bits in addr and return 0. | |||||
* If n_sigbits > 48, addr is lef unchanged and ERANGE is returned. | |||||
* If addr is NULL, EINVAL is returned. | |||||
*/ | |||||
static inline int | |||||
mkethernetmask(u_char *addr, uint8_t n_sigbits) | |||||
{ | |||||
if (addr == NULL) | |||||
return (EINVAL); | |||||
if (n_sigbits > 48) | |||||
return (ERANGE); | |||||
/* | |||||
* Use f to iterate through the four 8-bit fields of the ethernet | |||||
* address. Decrement n_sigbits by 8 (the bits processed) with each | |||||
* iteration. As sigbit is <= 48 bits, we remain within addr's bounds. | |||||
*/ | |||||
bzero(addr, ETHER_ADDR_LEN); | |||||
u_char *f = addr; | |||||
for (; n_sigbits >= 8; n_sigbits -= 8) { | |||||
*(f++) = 0xff; | |||||
} | |||||
/* Make sure we remain within the bounds of addr */ | |||||
if (n_sigbits > 0) | |||||
*f = ~((u_char)~0 >> n_sigbits); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Generate mask for ethernet prefix (default to /48 if n_sigbits > 48) | |||||
*/ | |||||
static inline void | |||||
mkethernetmask_or_default48(u_char *addr, uint8_t n_sigbits) | |||||
{ | |||||
mkethernetmask(addr, n_sigbits < 48 ? n_sigbits : 48); | |||||
} | |||||
/* Error constants */ | |||||
enum as_error { | |||||
AS_NOERROR = 0, | |||||
AS_E_PKT_TO_SMALL, /* Packet smaller than reguested m_pullup size */ | |||||
AS_E_ENOBUFS, /* m_pullup was unable to allocate new memory */ | |||||
AS_E_UNSUPPORTED_ET, /* Ethertype not supported */ | |||||
AS_E_UNSUPPORTED_ARP, /* ARP type not supported */ | |||||
AS_E_NOMATCH, /* Packet didn't match any rule */ | |||||
AS_E_MATCH, /* Packet matched a rule */ | |||||
AS_E_MALFORMED_VLAN_STACK, | |||||
}; | |||||
/* Flow direction */ | |||||
enum as_flow_direction { | |||||
AS_FLOW_TO_FILTER = 0, /* Packet flows towards filter hook */ | |||||
AS_FLOW_FROM_FILTER, /* Packet arrived on filter hook */ | |||||
AS_NOFLOW, /* Packet is immediately discarded */ | |||||
}; | |||||
/* | |||||
* Per hook info | |||||
* | |||||
* Each hook keeps track of its 'stats', carries a reference to itself ('hook') | |||||
* and is aware of its 'flow_direction' (to decide if in- or out-filters must be | |||||
* applied). The possible output hooks are prewired ('dest' and 'nomatch'). | |||||
*/ | |||||
/* clang-format off */ | |||||
struct hookinfo { | |||||
struct ng_antispoof_hookstat stats; | |||||
hook_p hook; | |||||
struct hookinfo * dest; | |||||
struct hookinfo * nomatch; | |||||
enum as_flow_direction flow_direction; | |||||
}; | |||||
/* | |||||
* Per node info | |||||
* | |||||
* 'filters' contains 'filter_count' elements. The 'in_filter_count' left most | |||||
* (starting at index zero) elements are only validated for inbound traffic | |||||
* (originating at the 'downstream' hook). The 'out_filter_count' right most (at | |||||
* the end of the array) elements are only validated for outbound traffic | |||||
* (originating at the 'filter' hook). Packets that did not match any filter | |||||
* rule are diverted to the 'nomatch' hook. | |||||
*/ | |||||
struct ng_antispoof_private { | |||||
struct hookinfo downstream; | |||||
struct hookinfo filter; | |||||
struct hookinfo nomatch; | |||||
struct ng_antispoof_filter *filters; /* filter array */ | |||||
uint16_t filter_count; /* size of filter array */ | |||||
uint16_t in_filter_count; /* number of inbound only filters */ | |||||
uint16_t out_filter_count; /* number of outbound only filters */ | |||||
struct ng_antispoof_config conf; /* node configuration */ | |||||
}; | |||||
struct ng_antispoof_filters_ary { | |||||
uint16_t n; | |||||
struct ng_antispoof_filter filters[]; | |||||
}; | |||||
/* clang-format on */ | |||||
/* Keep this in sync with the above structure definition */ | |||||
/* clang-format off */ | |||||
#define NG_ANTISPOOF_FILTERS_ARY_INFO(harytype) { \ | |||||
{ "n", &ng_parse_uint16_type }, \ | |||||
{ "filters", (harytype) }, \ | |||||
{ \ | |||||
NULL \ | |||||
} \ | |||||
} | |||||
/* clang-format on */ | |||||
/* | |||||
* How to determine the length of the table returned by NGM_ANTISPOOF_GET_RULES | |||||
*/ | |||||
static int | |||||
ng_antispoof_getTableLength( | |||||
const struct ng_parse_type *type, const u_char *start, const u_char *buf) | |||||
{ | |||||
const struct ng_antispoof_filters_ary *const table = | |||||
(const struct ng_antispoof_filters_ary *)(buf - | |||||
OFFSET(struct ng_antispoof_filters_ary, filters)); | |||||
return (table->n); | |||||
} | |||||
/* Parse type for struct ng_antispoof_config */ | |||||
static const struct ng_parse_struct_field ng_antispoof_config_type_fields[] = | |||||
NG_ANTISPOOF_CONFIG_TYPE_INFO; | |||||
static const struct ng_parse_type ng_antispoof_config_type = { | |||||
&ng_parse_struct_type, &ng_antispoof_config_type_fields | |||||
}; | |||||
/* Parse type for struct ng_antispoof_hookstat */ | |||||
static const struct ng_parse_struct_field ng_antispoof_hookstat_type_fields[] = | |||||
NG_ANTISPOOF_HOOKSTAT_INFO; | |||||
static const struct ng_parse_type ng_antispoof_hookstat_type = { | |||||
&ng_parse_struct_type, &ng_antispoof_hookstat_type_fields | |||||
}; | |||||
/* Parse type for struct ng_antispoof_stats */ | |||||
static const struct ng_parse_struct_field ng_antispoof_stats_type_fields[] = | |||||
NG_ANTISPOOF_STATS_INFO(&ng_antispoof_hookstat_type); | |||||
static const struct ng_parse_type ng_antispoof_stats_type = { | |||||
&ng_parse_struct_type, &ng_antispoof_stats_type_fields | |||||
}; | |||||
/* Parse type for struct ng_antispoof_filters_ary */ | |||||
static const struct ng_parse_array_info ng_antispoof_hary_type_info = { | |||||
&ng_parse_antispoof_filter_type, ng_antispoof_getTableLength | |||||
}; | |||||
static const struct ng_parse_type ng_antispoof_hary_type = { | |||||
&ng_parse_array_type, &ng_antispoof_hary_type_info | |||||
}; | |||||
static const struct ng_parse_struct_field ng_antispoof_host_ary_type_fields[] = | |||||
NG_ANTISPOOF_FILTERS_ARY_INFO(&ng_antispoof_hary_type); | |||||
static const struct ng_parse_type ng_antispoof_host_ary_type = { | |||||
&ng_parse_struct_type, &ng_antispoof_host_ary_type_fields | |||||
}; | |||||
/* List of commands and how to convert arguments to/from ASCII */ | |||||
static const struct ng_cmdlist ng_antispoof_cmds[] = { | |||||
/* clang-format off */ | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_SET_CONFIG, | |||||
"setconfig", | |||||
&ng_antispoof_config_type, | |||||
NULL | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_GET_CONFIG, | |||||
"getconfig", | |||||
NULL, | |||||
&ng_antispoof_config_type | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_ADD_INET, | |||||
"addinet", | |||||
&ng_antispoof_rule_ip_type, | |||||
NULL | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_ADD_INET6, | |||||
"addinet6", | |||||
&ng_antispoof_rule_ipv6_type, | |||||
NULL | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_DEL_INET, | |||||
"delinet", | |||||
&ng_antispoof_rule_ip_type, | |||||
NULL | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_DEL_INET6, | |||||
"delinet6", | |||||
&ng_antispoof_rule_ipv6_type, | |||||
NULL | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_GET_RULES, | |||||
"getrules", | |||||
NULL, | |||||
&ng_antispoof_host_ary_type | |||||
//&ng_antispoof_rules_type | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_RESET, | |||||
"reset", | |||||
NULL, | |||||
NULL | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_GET_STATS, | |||||
"getstats", | |||||
NULL, | |||||
&ng_antispoof_stats_type | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_CLR_STATS, | |||||
"clrstats", | |||||
NULL, | |||||
NULL | |||||
}, | |||||
{ | |||||
NGM_ANTISPOOF_COOKIE, | |||||
NGM_ANTISPOOF_GETCLR_STATS, | |||||
"getclrstats", | |||||
NULL, | |||||
&ng_antispoof_stats_type | |||||
}, | |||||
{ 0 } | |||||
/* clang-format on */ | |||||
}; | |||||
/* Netgraph type descriptor */ | |||||
static struct ng_type ng_antispoof_typestruct = { | |||||
/* clang-format off */ | |||||
.version = NG_ABI_VERSION, | |||||
.name = NG_ANTISPOOF_NODE_TYPE, | |||||
.constructor = ng_antispoof_constructor, | |||||
.rcvmsg = ng_antispoof_rcvmsg, | |||||
.close = ng_antispoof_close, | |||||
.shutdown = ng_antispoof_shutdown, | |||||
.newhook = ng_antispoof_newhook, | |||||
.rcvdata = ng_antispoof_rcvdata, | |||||
.disconnect = ng_antispoof_disconnect, | |||||
.cmdlist = ng_antispoof_cmds, | |||||
/* clang-format on */ | |||||
}; | |||||
NETGRAPH_INIT(antispoof, &ng_antispoof_typestruct); | |||||
/* | |||||
* Node constructor | |||||
*/ | |||||
static int | |||||
ng_antispoof_constructor(node_p node) | |||||
{ | |||||
struct ng_antispoof_private *const privdata = | |||||
malloc(sizeof(*privdata), M_NETGRAPH_ANTISPOOF, M_WAITOK | M_ZERO); | |||||
privdata->filter.flow_direction = AS_FLOW_FROM_FILTER; | |||||
privdata->downstream.flow_direction = AS_FLOW_TO_FILTER; | |||||
privdata->nomatch.flow_direction = AS_NOFLOW; | |||||
NG_NODE_SET_PRIVATE(node, privdata); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Add a hook | |||||
*/ | |||||
static int | |||||
ng_antispoof_newhook(node_p node, hook_p hook, const char *name) | |||||
{ | |||||
struct ng_antispoof_private *const privdata = NG_NODE_PRIVATE(node); | |||||
struct hookinfo * hinfo; | |||||
/* Precalculate internal paths. */ | |||||
if (strcmp(name, NG_ANTISPOOF_HOOK_FILTER) == 0) { | |||||
hinfo = &privdata->filter; | |||||
privdata->downstream.dest = hinfo; | |||||
} else if (strcmp(name, NG_ANTISPOOF_HOOK_DOWNSTREAM) == 0) { | |||||
hinfo = &privdata->downstream; | |||||
privdata->filter.dest = hinfo; | |||||
} else if (strcmp(name, NG_ANTISPOOF_HOOK_NOMATCH) == 0) { | |||||
hinfo = &privdata->nomatch; | |||||
privdata->filter.nomatch = hinfo; | |||||
privdata->downstream.nomatch = hinfo; | |||||
privdata->nomatch.nomatch = NULL; | |||||
} else | |||||
return (EINVAL); | |||||
hinfo->hook = hook; | |||||
bzero(&hinfo->stats, sizeof(hinfo->stats)); | |||||
NG_HOOK_SET_PRIVATE(hook, hinfo); | |||||
return (0); | |||||
} | |||||
static inline int | |||||
ng_antispoof_getstats(struct ng_antispoof_private *privdata, | |||||
const struct ng_mesg *request, struct ng_mesg **response) | |||||
{ | |||||
int error = 0; | |||||
NG_MKRESPONSE( | |||||
*response, request, sizeof(struct ng_antispoof_stats), M_NOWAIT); | |||||
if (*response != NULL) { | |||||
struct ng_antispoof_stats *const stats = | |||||
(struct ng_antispoof_stats *)(*response)->data; | |||||
bcopy(&privdata->filter.stats, &stats->filter, | |||||
sizeof(stats->filter)); | |||||
bcopy(&privdata->downstream.stats, &stats->downstream, | |||||
sizeof(stats->downstream)); | |||||
bcopy(&privdata->nomatch.stats, &stats->nomatch, | |||||
sizeof(stats->nomatch)); | |||||
} else | |||||
error = ENOMEM; | |||||
return (error); | |||||
} | |||||
static inline void | |||||
ng_antispoof_clearstats(struct ng_antispoof_private *privdata) | |||||
{ | |||||
bzero(&privdata->filter.stats, sizeof(privdata->filter.stats)); | |||||
bzero(&privdata->downstream.stats, sizeof(privdata->downstream.stats)); | |||||
bzero(&privdata->nomatch.stats, sizeof(privdata->nomatch.stats)); | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static uint16_t | |||||
ng_antispoof_setconfig_find(struct ng_antispoof_private *privdata, | |||||
const struct ng_antispoof_filter * filter) | |||||
{ | |||||
for (uint_fast16_t i = 0; i < privdata->filter_count; ++i) { | |||||
if (memcmp(filter, &privdata->filters[i], sizeof(*filter)) == 0) | |||||
return (i); | |||||
} | |||||
return (UINT16_MAX); | |||||
} | |||||
/* TODO: Prevent uint16_t overflow (range check on privdata->filter_count) */ | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_insert(struct ng_antispoof_private *privdata, | |||||
uint16_t pos, const struct ng_antispoof_filter *filter) | |||||
{ | |||||
pos = pos <= privdata->filter_count ? pos : privdata->filter_count; | |||||
struct ng_antispoof_filter *filters = mallocarray( | |||||
1 + privdata->filter_count, sizeof(struct ng_antispoof_filter), | |||||
M_NETGRAPH_ANTISPOOF, M_WAITOK | M_ZERO); | |||||
if (privdata->filters != NULL) { | |||||
bcopy(privdata->filters, filters, | |||||
pos * sizeof(struct ng_antispoof_filter)); | |||||
bcopy(privdata->filters + pos, filters + pos + 1, | |||||
(privdata->filter_count - pos) * | |||||
sizeof(struct ng_antispoof_filter)); | |||||
free(privdata->filters, M_NETGRAPH_ANTISPOOF); | |||||
} | |||||
filters[pos] = *filter; | |||||
privdata->filters = filters; | |||||
++privdata->filter_count; | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_remove( | |||||
struct ng_antispoof_private *privdata, uint16_t pos) | |||||
{ | |||||
if ((privdata->filters != NULL) && (pos < privdata->filter_count)) { | |||||
size_t tpos = | |||||
pos + 1; /* Index of first element after removed one */ | |||||
bcopy(privdata->filters + tpos, privdata->filters + pos, | |||||
(privdata->filter_count - tpos) * | |||||
sizeof(*privdata->filters)); | |||||
if (pos < privdata->in_filter_count) | |||||
--privdata->in_filter_count; | |||||
else if (pos >= | |||||
privdata->filter_count - privdata->out_filter_count) | |||||
--privdata->out_filter_count; | |||||
--privdata->filter_count; | |||||
} | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_mkfilter_ip(struct ng_antispoof_filter *new_filter, | |||||
const struct ng_antispoof_rule_ip * rule) | |||||
{ | |||||
if (new_filter == NULL) | |||||
return; | |||||
if (rule == NULL) | |||||
return; | |||||
bzero(new_filter, sizeof(*new_filter)); | |||||
struct packet_info *filter = &new_filter->addr.pinfo; | |||||
struct packet_info *mask = &new_filter->mask.pinfo; | |||||
/* Filter IPv4 traffic */ | |||||
filter->ether_type = htons(ETHERTYPE_IP); | |||||
mask->ether_type = 0xffff; | |||||
/* Ethernet address to match */ | |||||
bcopy(&rule->ether.addr, filter->en_addr, sizeof(filter->en_addr)); | |||||
mkethernetmask_or_default48(mask->en_addr, rule->ether.length); | |||||
/* IP address to match */ | |||||
filter->ip_addr = rule->ip_addr.addr; | |||||
mask->ip_addr = rule->ip_mask; | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_mkfilter_ipv6(struct ng_antispoof_filter *new_filter, | |||||
const struct ng_antispoof_rule_ipv6 * rule) | |||||
{ | |||||
if (new_filter == NULL) | |||||
return; | |||||
if (rule == NULL) | |||||
return; | |||||
bzero(new_filter, sizeof(*new_filter)); | |||||
struct packet_info *filter = &new_filter->addr.pinfo; | |||||
struct packet_info *mask = &new_filter->mask.pinfo; | |||||
/* Filter IPv6 traffic */ | |||||
filter->ether_type = htons(ETHERTYPE_IPV6); | |||||
mask->ether_type = 0xffff; | |||||
/* Ethernet address to match */ | |||||
bcopy(&rule->ether.addr, filter->en_addr, sizeof(filter->en_addr)); | |||||
mkethernetmask_or_default48(mask->en_addr, rule->ether.length); | |||||
/* IPv6 address to match */ | |||||
filter->ip6_addr = rule->ip6_addr.addr; | |||||
mkipv6mask_or_default128(&mask->ip6_addr, rule->ip6_addr.length); | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_addrule_ip(struct ng_antispoof_private *privdata, | |||||
const struct ng_antispoof_rule_ip * rule) | |||||
{ | |||||
struct ng_antispoof_filter filter; | |||||
ng_antispoof_setconfig_mkfilter_ip(&filter, rule); | |||||
/* Insert rule */ | |||||
uint16_t slot = privdata->filter_count - privdata->out_filter_count; | |||||
ng_antispoof_setconfig_insert(privdata, slot, &filter); | |||||
/* Create rule to accept incoming traffic on ether ff:ff:ff:ff:ff:ff */ | |||||
mkethernetmask_or_default48(filter.addr.pinfo.en_addr, 48); | |||||
mkethernetmask_or_default48(filter.mask.pinfo.en_addr, 48); | |||||
/* Insert rule */ | |||||
slot = privdata->in_filter_count; | |||||
ng_antispoof_setconfig_insert(privdata, slot, &filter); | |||||
++privdata->in_filter_count; | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_addrule_ipv6(struct ng_antispoof_private *privdata, | |||||
const struct ng_antispoof_rule_ipv6 * rule) | |||||
{ | |||||
struct ng_antispoof_filter filter; | |||||
ng_antispoof_setconfig_mkfilter_ipv6(&filter, rule); | |||||
/* Insert rule */ | |||||
uint16_t slot = privdata->filter_count - privdata->out_filter_count; | |||||
ng_antispoof_setconfig_insert(privdata, slot, &filter); | |||||
/* Create rule to accept incoming traffic on ether 33:33:00:00:00:00/16 | |||||
*/ | |||||
filter.addr.pinfo.en_addr[0] = 0x33; | |||||
filter.addr.pinfo.en_addr[1] = 0x33; | |||||
filter.addr.pinfo.en_addr[2] = 0; | |||||
filter.addr.pinfo.en_addr[3] = 0; | |||||
filter.addr.pinfo.en_addr[4] = 0; | |||||
filter.addr.pinfo.en_addr[5] = 0; | |||||
mkethernetmask_or_default48(filter.mask.pinfo.en_addr, 16); | |||||
/* Insert rule */ | |||||
slot = privdata->in_filter_count; | |||||
ng_antispoof_setconfig_insert(privdata, slot, &filter); | |||||
++privdata->in_filter_count; | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_delrule_ip(struct ng_antispoof_private *privdata, | |||||
const struct ng_antispoof_rule_ip * rule) | |||||
{ | |||||
struct ng_antispoof_filter filter; | |||||
ng_antispoof_setconfig_mkfilter_ip(&filter, rule); | |||||
/* Remove rule */ | |||||
uint16_t slot; | |||||
while ((slot = ng_antispoof_setconfig_find(privdata, &filter)) != | |||||
UINT16_MAX) | |||||
ng_antispoof_setconfig_remove(privdata, slot); | |||||
/* Remove rule to accept incoming traffic on ether ff:ff:ff:ff:ff:ff */ | |||||
mkethernetmask_or_default48(filter.addr.pinfo.en_addr, 48); | |||||
mkethernetmask_or_default48(filter.mask.pinfo.en_addr, 48); | |||||
/* Remove rule */ | |||||
while ((slot = ng_antispoof_setconfig_find(privdata, &filter)) != | |||||
UINT16_MAX) | |||||
ng_antispoof_setconfig_remove(privdata, slot); | |||||
} | |||||
/* Don't inline - not in the hot path */ | |||||
static void | |||||
ng_antispoof_setconfig_delrule_ipv6(struct ng_antispoof_private *privdata, | |||||
const struct ng_antispoof_rule_ipv6 * rule) | |||||
{ | |||||
struct ng_antispoof_filter filter; | |||||
ng_antispoof_setconfig_mkfilter_ipv6(&filter, rule); | |||||
/* Remove rule */ | |||||
uint16_t slot; | |||||
while ((slot = ng_antispoof_setconfig_find(privdata, &filter)) != | |||||
UINT16_MAX) | |||||
ng_antispoof_setconfig_remove(privdata, slot); | |||||
/* Remove rule to accept incoming traffic on ether 33:33:00:00:00:00/16 | |||||
*/ | |||||
filter.addr.pinfo.en_addr[0] = 0x33; | |||||
filter.addr.pinfo.en_addr[1] = 0x33; | |||||
filter.addr.pinfo.en_addr[2] = 0; | |||||
filter.addr.pinfo.en_addr[3] = 0; | |||||
filter.addr.pinfo.en_addr[4] = 0; | |||||
filter.addr.pinfo.en_addr[5] = 0; | |||||
mkethernetmask_or_default48(filter.mask.pinfo.en_addr, 16); | |||||
/* Remove rule */ | |||||
while ((slot = ng_antispoof_setconfig_find(privdata, &filter)) != | |||||
UINT16_MAX) | |||||
ng_antispoof_setconfig_remove(privdata, slot); | |||||
} | |||||
static int | |||||
ng_antispoof_rcvmsg_impl(struct ng_antispoof_private *privdata, | |||||
const struct ng_mesg *request, struct ng_mesg **response) | |||||
{ | |||||
int error = 0; | |||||
switch (request->header.cmd) { | |||||
case NGM_ANTISPOOF_GET_CONFIG: { | |||||
NG_MKRESPONSE(*response, request, | |||||
sizeof(struct ng_antispoof_config), M_NOWAIT); | |||||
if (*response == NULL) { | |||||
error = ENOMEM; | |||||
break; | |||||
} | |||||
struct ng_antispoof_config *const conf = | |||||
(struct ng_antispoof_config *)(*response)->data; | |||||
*conf = privdata->conf; /* no sanity checking needed */ | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_SET_CONFIG: { | |||||
if (request->header.arglen == | |||||
sizeof(struct ng_antispoof_config)) | |||||
privdata->conf = | |||||
*(const struct ng_antispoof_config *)request->data; | |||||
else | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_ADD_INET: { | |||||
if (request->header.arglen == | |||||
sizeof(struct ng_antispoof_rule_ip)) { | |||||
struct ng_antispoof_rule_ip rule = | |||||
*(const struct ng_antispoof_rule_ip *)request->data; | |||||
/* | |||||
* If IP address came in prefix notation, ignore subnet | |||||
* mask | |||||
*/ | |||||
if (rule.ip_addr.length <= 32) { | |||||
error = mkipmask( | |||||
&rule.ip_mask, rule.ip_addr.length); | |||||
} | |||||
if (error == 0) { | |||||
ng_antispoof_setconfig_addrule_ip( | |||||
privdata, &rule); | |||||
} | |||||
} else | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_ADD_INET6: { | |||||
if (request->header.arglen == | |||||
sizeof(struct ng_antispoof_rule_ipv6)) { | |||||
const struct ng_antispoof_rule_ipv6 *const rule = | |||||
(const struct ng_antispoof_rule_ipv6 *) | |||||
request->data; | |||||
if (error == 0) { | |||||
ng_antispoof_setconfig_addrule_ipv6( | |||||
privdata, rule); | |||||
} | |||||
} else | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_DEL_INET: { | |||||
if (request->header.arglen == | |||||
sizeof(struct ng_antispoof_rule_ip)) { | |||||
struct ng_antispoof_rule_ip rule = | |||||
*(const struct ng_antispoof_rule_ip *)request->data; | |||||
/* | |||||
* If IP address came in prefix notation, ignore subnet | |||||
* mask | |||||
*/ | |||||
if (rule.ip_addr.length <= 32) { | |||||
error = mkipmask( | |||||
&rule.ip_mask, rule.ip_addr.length); | |||||
} | |||||
if (error == 0) { | |||||
ng_antispoof_setconfig_delrule_ip( | |||||
privdata, &rule); | |||||
} | |||||
} else | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_DEL_INET6: { | |||||
if (request->header.arglen == | |||||
sizeof(struct ng_antispoof_rule_ipv6)) { | |||||
const struct ng_antispoof_rule_ipv6 *const rule = | |||||
(const struct ng_antispoof_rule_ipv6 *) | |||||
request->data; | |||||
if (error == 0) { | |||||
ng_antispoof_setconfig_delrule_ipv6( | |||||
privdata, rule); | |||||
} | |||||
} else | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_GET_RULES: { | |||||
uint16_t num_filters = privdata->filter_count - | |||||
privdata->in_filter_count - privdata->out_filter_count; | |||||
struct ng_antispoof_filters_ary *ary; | |||||
NG_MKRESPONSE(*response, request, | |||||
sizeof(*ary) + (num_filters * sizeof(*ary->filters)), | |||||
M_NOWAIT); | |||||
if (*response != NULL) { | |||||
ary = (struct ng_antispoof_filters_ary *)(*response) | |||||
->data; | |||||
bcopy(privdata->filters + privdata->in_filter_count, | |||||
ary->filters, num_filters * sizeof(*ary->filters)); | |||||
ary->n = num_filters; | |||||
} else | |||||
error = ENOMEM; | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_RESET: { | |||||
free(privdata->filters, M_NETGRAPH_ANTISPOOF); | |||||
privdata->filters = NULL; | |||||
privdata->filter_count = 0; | |||||
privdata->in_filter_count = 0; | |||||
privdata->out_filter_count = 0; | |||||
bzero(&privdata->conf, sizeof(privdata->conf)); | |||||
ng_antispoof_clearstats(privdata); | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_GET_STATS: { | |||||
error = ng_antispoof_getstats(privdata, request, response); | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_CLR_STATS: { | |||||
ng_antispoof_clearstats(privdata); | |||||
break; | |||||
} | |||||
case NGM_ANTISPOOF_GETCLR_STATS: { | |||||
error = ng_antispoof_getstats(privdata, request, response); | |||||
if (error == 0) | |||||
ng_antispoof_clearstats(privdata); | |||||
break; | |||||
} | |||||
default: | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
return (error); | |||||
} | |||||
/* | |||||
* Receive a control request | |||||
*/ | |||||
static int | |||||
ng_antispoof_rcvmsg(node_p node, item_p item, hook_p lasthook) | |||||
{ | |||||
struct ng_antispoof_private *const privdata = NG_NODE_PRIVATE(node); | |||||
struct ng_mesg * response = NULL; | |||||
int error = 0; | |||||
struct ng_mesg *request; | |||||
NGI_GET_MSG(item, request); | |||||
switch (request->header.typecookie) { | |||||
case NGM_ANTISPOOF_COOKIE: | |||||
error = ng_antispoof_rcvmsg_impl(privdata, request, &response); | |||||
break; | |||||
case NGM_FLOW_COOKIE: | |||||
if (lasthook == privdata->downstream.hook || | |||||
lasthook == privdata->filter.hook) { | |||||
const struct hookinfo *const hinfo = | |||||
NG_HOOK_PRIVATE(lasthook); | |||||
if (hinfo && hinfo->dest) { | |||||
NGI_MSG(item) = request; | |||||
NG_FWD_ITEM_HOOK( | |||||
error, item, hinfo->dest->hook); | |||||
return (error); | |||||
} | |||||
} | |||||
break; | |||||
default: | |||||
error = EINVAL; | |||||
break; | |||||
} | |||||
NG_RESPOND_MSG(error, node, item, response); | |||||
NG_FREE_MSG(request); | |||||
return (error); | |||||
} | |||||
/* | |||||
* Puts the first 'len' byte of the mbuf '*m' in contiguous memory and lets '*m' | |||||
* point to this memory. | |||||
* | |||||
* If 'len' is greater than the size of the packet, the pullup is not attempted | |||||
* and AS_E_PKT_TO_SMALL is returned. If m_pullup was unable to allocate memory, | |||||
* the mbuf is freed and AS_E_ENOBUFS is returned. Otherwise returns AS_NOERROR. | |||||
*/ | |||||
static enum as_error | |||||
pullup(struct mbuf **m, size_t len) | |||||
{ | |||||
enum as_error error = AS_NOERROR; | |||||
/* Sanity check packet and pull up header */ | |||||
if ((*m)->m_pkthdr.len < len) | |||||
error = AS_E_PKT_TO_SMALL; | |||||
else if ((*m)->m_len < len && (*m = m_pullup(*m, len)) == NULL) | |||||
error = AS_E_ENOBUFS; | |||||
return (error); | |||||
} | |||||
/* | |||||
* Skips past an optional well-formed VLAN stack. | |||||
* | |||||
* Skips any number of IEEE 802.1ad (QinQ) headers followed by a single 802.1Q | |||||
* header. If a stack of QinQ headers is not terminated by a 802.1Q header, the | |||||
* frame is malformed and AS_E_MALFORMED_VLAN_STACK is returned. | |||||
*/ | |||||
static inline enum as_error | |||||
ng_antispoof_skip_vlan( | |||||
struct mbuf **m, struct packet_info *pkt_info, size_t *offset) | |||||
{ | |||||
typedef struct ng_antispoof_vlanhdr IEEE802_1Q; | |||||
bool isMultiTagged = false; | |||||
enum as_error error = AS_NOERROR; | |||||
/* htons() on a constant value will be optimized away by the compiler */ | |||||
while (pkt_info->ether_type == htons(ETHERTYPE_QINQ)) { | |||||
isMultiTagged = true; | |||||
error = pullup(m, *offset + sizeof(IEEE802_1Q)); | |||||
if (error == AS_NOERROR) { | |||||
const IEEE802_1Q *const evl = | |||||
(IEEE802_1Q *)(mtod(*m, u_char *) + *offset); | |||||
*offset += sizeof(IEEE802_1Q); | |||||
pkt_info->ether_type = evl->proto; | |||||
} else | |||||
return (error); | |||||
} | |||||
/* htons() on a constant value will be optimized away by the compiler */ | |||||
if (pkt_info->ether_type == htons(ETHERTYPE_VLAN)) { | |||||
error = pullup(m, *offset + sizeof(IEEE802_1Q)); | |||||
if (error == AS_NOERROR) { | |||||
const IEEE802_1Q *const evl = | |||||
(IEEE802_1Q *)(mtod(*m, u_char *) + *offset); | |||||
*offset += sizeof(IEEE802_1Q); | |||||
pkt_info->ether_type = evl->proto; | |||||
} | |||||
} else if (isMultiTagged) | |||||
/* In a multi-tagged frame, the last tag MUST be IEEE 802.1Q */ | |||||
error = AS_E_MALFORMED_VLAN_STACK; | |||||
return (error); | |||||
} | |||||
/* | |||||
* Collects source and destination ethernet addresses and skips the optional | |||||
* VLAN stack. | |||||
*/ | |||||
static inline enum as_error | |||||
ng_antispoof_collect_layer2(struct mbuf **m, struct packet_info *pkt_info, | |||||
enum as_flow_direction flow, size_t *offset) | |||||
{ | |||||
enum as_error error = pullup(m, *offset + ETHER_HDR_LEN); | |||||
if (error == AS_NOERROR) { | |||||
const struct ether_header *const eh = | |||||
mtod(*m, struct ether_header *); | |||||
*offset += ETHER_HDR_LEN; | |||||
pkt_info->ether_type = eh->ether_type; | |||||
bcopy((flow == AS_FLOW_FROM_FILTER) ? eh->ether_shost : | |||||
eh->ether_dhost, | |||||
pkt_info->en_addr, ETHER_ADDR_LEN); | |||||
error = ng_antispoof_skip_vlan(m, pkt_info, offset); | |||||
} | |||||
return (error); | |||||
} | |||||
/* Collects source and destination IP addresses from an ARP frame */ | |||||
static inline enum as_error | |||||
ng_antispoof_collect_arp(struct mbuf **m, struct packet_info *pkt_info, | |||||
enum as_flow_direction flow, size_t *offset) | |||||
{ | |||||
enum as_error error = pullup(m, *offset + sizeof(struct ether_arp)); | |||||
if (error == AS_NOERROR) { | |||||
const struct ether_arp *arp = | |||||
(struct ether_arp *)(mtod(*m, u_char *) + *offset); | |||||
*offset += sizeof(struct ether_arp); | |||||
/* htons() on a constant value will be optimized away by the | |||||
* compiler */ | |||||
if ((arp->ea_hdr.ar_pro == htons(ETHERTYPE_IP)) && | |||||
(arp->ea_hdr.ar_hln == ETHER_ADDR_LEN)) { | |||||
pkt_info->ether_type = arp->ea_hdr.ar_pro; | |||||
bcopy((flow == AS_FLOW_FROM_FILTER) ? arp->arp_spa : | |||||
arp->arp_tpa, | |||||
&pkt_info->ip_addr, sizeof(pkt_info->ip_addr)); | |||||
} else | |||||
error = AS_E_UNSUPPORTED_ARP; | |||||
} | |||||
return (error); | |||||
} | |||||
/* Collects source and destination IP addresses */ | |||||
static inline enum as_error | |||||
ng_antispoof_collect_ip(struct mbuf **m, struct packet_info *pkt_info, | |||||
enum as_flow_direction flow, size_t *offset) | |||||
{ | |||||
int error = pullup(m, *offset + sizeof(struct ip)); | |||||
if (error == AS_NOERROR) { | |||||
const struct ip *const ip = | |||||
(struct ip *)(mtod(*m, u_char *) + *offset); | |||||
*offset += sizeof(struct ether_arp); | |||||
pkt_info->ip_addr = | |||||
(flow == AS_FLOW_FROM_FILTER) ? ip->ip_src : ip->ip_dst; | |||||
} | |||||
return (error); | |||||
} | |||||
/* Collects source and destination IPv6 addresses */ | |||||
static inline enum as_error | |||||
ng_antispoof_collect_ipv6(struct mbuf **m, struct packet_info *pkt_info, | |||||
enum as_flow_direction flow, size_t *offset) | |||||
{ | |||||
int error = pullup(m, *offset + sizeof(struct ip6_hdr)); | |||||
if (error == AS_NOERROR) { | |||||
const struct ip6_hdr *const ipv6 = | |||||
(struct ip6_hdr *)(mtod(*m, u_char *) + *offset); | |||||
*offset += sizeof(struct ether_arp); | |||||
pkt_info->ip6_addr = (flow == AS_FLOW_FROM_FILTER) ? | |||||
ipv6->ip6_src : | |||||
ipv6->ip6_dst; | |||||
} | |||||
return (error); | |||||
} | |||||
/* Collects L3 based information (ARP, IP, IPv6) */ | |||||
static inline enum as_error | |||||
ng_antispoof_collect_layer3(struct mbuf **m, struct packet_info *pkt_info, | |||||
enum as_flow_direction flow, size_t *offset) | |||||
{ | |||||
enum as_error error = AS_E_UNSUPPORTED_ET; | |||||
switch (ntohs(pkt_info->ether_type)) { | |||||
case ETHERTYPE_IP: | |||||
error = ng_antispoof_collect_ip(m, pkt_info, flow, offset); | |||||
break; | |||||
case ETHERTYPE_IPV6: | |||||
error = ng_antispoof_collect_ipv6(m, pkt_info, flow, offset); | |||||
break; | |||||
case ETHERTYPE_ARP: | |||||
error = ng_antispoof_collect_arp(m, pkt_info, flow, offset); | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
return (error); | |||||
} | |||||
static inline enum as_error | |||||
ng_antispoof_collect_data( | |||||
struct mbuf **m, struct packet_info *pkt_info, enum as_flow_direction flow) | |||||
{ | |||||
size_t offset = 0; | |||||
enum as_error error = | |||||
ng_antispoof_collect_layer2(m, pkt_info, flow, &offset); | |||||
if (error == AS_NOERROR) | |||||
error = ng_antispoof_collect_layer3(m, pkt_info, flow, &offset); | |||||
return (error); | |||||
} | |||||
/* | |||||
* Match given packet against ruleset | |||||
* | |||||
* A rule matches if 'filter == packet & mask' is true. | |||||
* | |||||
* 'begin' and 'end' mark the range of rules to be validated. | |||||
* 'begin' is inclusive, 'end' is exclusive (past the last element to be | |||||
* processed). | |||||
* | |||||
* Note: No range checking is performed by this function! | |||||
*/ | |||||
static inline enum as_error | |||||
ng_antispoof_packet_matches(const struct packet_info_conv *pkt_info, | |||||
const struct ng_antispoof_filter *filters, uint16_t begin, uint16_t end) | |||||
{ | |||||
bool is_match = false; | |||||
for (const struct ng_antispoof_filter *filter = filters + begin, | |||||
*filters_end = filters + end; | |||||
filter != filters_end && !is_match; ++filter) { | |||||
is_match = true; | |||||
for (uint_fast8_t e = 0; e < FILTER_RAW_SZ; ++e) { | |||||
is_match = is_match && | |||||
((pkt_info->raw[e] & filter->mask.raw[e]) == | |||||
(filter->addr.raw[e] & filter->mask.raw[e])); | |||||
} | |||||
} | |||||
return (is_match ? AS_E_MATCH : AS_E_NOMATCH); | |||||
} | |||||
/* | |||||
* Receive data on a hook | |||||
* | |||||
* If data comes in the filter link check the source fields in the protocol | |||||
* headers. If they are valid, forward on the downstream link. Divert to | |||||
* nomatch link otherwise. | |||||
* | |||||
* If data comes in the downstream link check the target fields in the protocol | |||||
* headers. If they are valid, forward on the filter link. Divert to nomatch | |||||
* link otherwise. | |||||
* | |||||
* If data comes in the nomatch link drop packet on the floor (don't forward any | |||||
* data from nomatch). | |||||
*/ | |||||
static int | |||||
ng_antispoof_rcvdata(hook_p hook, item_p item) | |||||
{ | |||||
const struct ng_antispoof_private *const privdata = | |||||
NG_NODE_PRIVATE(NG_HOOK_NODE(hook)); | |||||
struct hookinfo *const hinfo = NG_HOOK_PRIVATE(hook); | |||||
int ng_error = 0; | |||||
struct mbuf *m; | |||||
NGI_GET_M(item, m); | |||||
/* Update stats on incoming hook */ | |||||
hinfo->stats.inOctets += m->m_pkthdr.len; | |||||
hinfo->stats.inFrames++; | |||||
/* Traffic entering at the nomatch hook is discarded immediately */ | |||||
if (hinfo->flow_direction == AS_NOFLOW) { | |||||
NG_FREE_ITEM(item); | |||||
NG_FREE_M(m); | |||||
return (ng_error); | |||||
} | |||||
/* Collect information for filtering */ | |||||
struct packet_info_conv pkt_info = { .raw = { 0 } }; | |||||
enum as_error error = ng_antispoof_collect_data( | |||||
&m, &pkt_info.pinfo, hinfo->flow_direction); | |||||
if (error == AS_NOERROR) { | |||||
/* Calculate range of rules to be applied */ | |||||
uint16_t begin = (hinfo->flow_direction == AS_FLOW_TO_FILTER) ? | |||||
0 : | |||||
privdata->in_filter_count; | |||||
uint16_t end = (hinfo->flow_direction == AS_FLOW_FROM_FILTER) ? | |||||
privdata->filter_count : | |||||
privdata->filter_count - privdata->out_filter_count; | |||||
/* Match packet */ | |||||
error = ng_antispoof_packet_matches( | |||||
&pkt_info, privdata->filters, begin, end); | |||||
} | |||||
switch (error) { | |||||
case AS_E_MATCH: { | |||||
/* | |||||
* Deliver frame out destination hook | |||||
* Update stats on outgoing hook | |||||
*/ | |||||
if (hinfo->dest != NULL) { | |||||
hinfo->dest->stats.outOctets += m->m_pkthdr.len; | |||||
hinfo->dest->stats.outFrames++; | |||||
NG_FWD_NEW_DATA(ng_error, item, hinfo->dest->hook, m); | |||||
} | |||||
/* Drop packet if there is no outgoing hook */ | |||||
else { | |||||
NG_FREE_ITEM(item); | |||||
NG_FREE_M(m); | |||||
} | |||||
break; | |||||
} | |||||
case AS_E_PKT_TO_SMALL: { | |||||
/* Update statistics */ | |||||
hinfo->stats.recvRunts++; | |||||
/* FALLTHROUGH */ | |||||
} | |||||
case AS_NOERROR: /* FALLTHROUGH */ | |||||
case AS_E_UNSUPPORTED_ET: /* FALLTHROUGH */ | |||||
case AS_E_UNSUPPORTED_ARP: /* FALLTHROUGH */ | |||||
case AS_E_MALFORMED_VLAN_STACK: /* FALLTHROUGH */ | |||||
case AS_E_NOMATCH: { | |||||
/* | |||||
* Packet did not match any filter | |||||
* Update stats and deliver frame on nomatch hook if attached | |||||
*/ | |||||
if (hinfo->nomatch != NULL) { | |||||
hinfo->nomatch->stats.outOctets += m->m_pkthdr.len; | |||||
hinfo->nomatch->stats.outFrames++; | |||||
NG_FWD_NEW_DATA( | |||||
ng_error, item, hinfo->nomatch->hook, m); | |||||
} else { | |||||
NG_FREE_ITEM(item); | |||||
NG_FREE_M(m); | |||||
} | |||||
break; | |||||
} | |||||
case AS_E_ENOBUFS: { | |||||
/* | |||||
* m_pullup was unable to allocate memory and freed the mbuf | |||||
* Update stats and drop packet | |||||
*/ | |||||
hinfo->stats.memoryFailures++; | |||||
NG_FREE_ITEM(item); | |||||
ng_error = ENOBUFS; | |||||
break; | |||||
} | |||||
default: { | |||||
/* | |||||
* This should never be reached. Log an error and drop | |||||
* everything. | |||||
*/ | |||||
const node_p node = NG_HOOK_NODE(hook); | |||||
log(LOG_ERR, | |||||
"ng_antispoof: %s: %s(%d): Unreachable code. Please file a " | |||||
"bug report at https://bugs.freebsd.org/", | |||||
ng_antispoof_nodename(node), __func__, __LINE__); | |||||
NG_FREE_ITEM(item); | |||||
NG_FREE_M(m); | |||||
ng_error = EDOOFUS; | |||||
break; | |||||
} | |||||
} | |||||
/* We should not be here. Return a programming error. */ | |||||
return (ng_error); | |||||
} | |||||
/* | |||||
* We are going to be shut down soon | |||||
* | |||||
* If we have both a filter and downstream hook, then we probably want to | |||||
* extricate ourselves and leave the two peers still linked to each other. | |||||
* Otherwise we should just shut down as a normal node would. | |||||
* | |||||
* No special treatment for the nomatch hook. Let it die with the node. | |||||
*/ | |||||
static int | |||||
ng_antispoof_close(node_p node) | |||||
{ | |||||
const struct ng_antispoof_private *const privdata = | |||||
NG_NODE_PRIVATE(node); | |||||
if (privdata->downstream.hook && privdata->filter.hook) | |||||
ng_bypass(privdata->downstream.hook, privdata->filter.hook); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Shutdown processing | |||||
*/ | |||||
static int | |||||
ng_antispoof_shutdown(node_p node) | |||||
{ | |||||
struct ng_antispoof_private *const privdata = NG_NODE_PRIVATE(node); | |||||
NG_NODE_SET_PRIVATE(node, NULL); | |||||
free(privdata->filters, M_NETGRAPH_ANTISPOOF); | |||||
free(privdata, M_NETGRAPH_ANTISPOOF); | |||||
NG_NODE_UNREF(node); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Hook disconnection | |||||
*/ | |||||
static int | |||||
ng_antispoof_disconnect(hook_p hook) | |||||
{ | |||||
struct ng_antispoof_private *const privdata = | |||||
NG_NODE_PRIVATE(NG_HOOK_NODE(hook)); | |||||
struct hookinfo *const hinfo = NG_HOOK_PRIVATE(hook); | |||||
KASSERT(hinfo != NULL, ("%s: null info", __func__)); | |||||
hinfo->hook = NULL; | |||||
/* Recalculate internal paths. */ | |||||
if (privdata->downstream.dest == hinfo) | |||||
privdata->downstream.dest = NULL; | |||||
if (privdata->filter.dest == hinfo) | |||||
privdata->filter.dest = NULL; | |||||
if (privdata->downstream.nomatch == hinfo) | |||||
privdata->downstream.nomatch = NULL; | |||||
if (privdata->filter.nomatch == hinfo) | |||||
privdata->filter.nomatch = NULL; | |||||
/* Die when last hook disconnected. */ | |||||
if ((NG_NODE_NUMHOOKS(NG_HOOK_NODE(hook)) == 0) && | |||||
NG_NODE_IS_VALID(NG_HOOK_NODE(hook))) | |||||
ng_rmnode_self(NG_HOOK_NODE(hook)); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Return node's "name", even if it doesn't have one. | |||||
*/ | |||||
static const char * | |||||
ng_antispoof_nodename(node_p node) | |||||
{ | |||||
static char name[NG_NODESIZ]; | |||||
if (NG_NODE_HAS_NAME(node)) | |||||
snprintf(name, sizeof(name), "%s", NG_NODE_NAME(node)); | |||||
else | |||||
snprintf(name, sizeof(name), "[%x]", ng_node2ID(node)); | |||||
return (name); | |||||
} |