Index: sys/modules/netgraph/antispoof/Makefile =================================================================== --- sys/modules/netgraph/antispoof/Makefile +++ sys/modules/netgraph/antispoof/Makefile @@ -0,0 +1,7 @@ +# $FreeBSD$ +# $Whistle: Makefile,v 1.2 1999/01/19 19:39:22 archie Exp $ + +KMOD= ng_antispoof +SRCS= ng_antispoof.c ng_parse.c + +.include Index: sys/netgraph/ng_antispoof.h =================================================================== --- sys/netgraph/ng_antispoof.h +++ sys/netgraph/ng_antispoof.h @@ -0,0 +1,232 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2020 Markus Stoff + * 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$ + */ + +#ifndef _NETGRAPH_NG_ANTISPOOF_H_ +#define _NETGRAPH_NG_ANTISPOOF_H_ + +/* + * IP MASK TYPE + * + * Default value: 255.255.255.255 + * Additional info: None required + */ +extern const struct ng_parse_type ng_parse_ipmask_type; + +/* + * ETHERNET PREFIX TYPE + * + * Default value: 00:00:00:00:00:00/UINT_MAX + * Additional info: None required + */ +extern const struct ng_parse_type ng_parse_enaddr_prefix_type; + +/* + * IP PREFIX TYPE + * + * Default value: 0.0.0.0/UINT_MAX + * Additional info: None required + */ +extern const struct ng_parse_type ng_parse_ipaddr_cidr_type; + +/* + * IP PREFIX TYPE + * + * Default value: ::/UINT_MAX + * Additional info: None required + */ +extern const struct ng_parse_type ng_parse_ip6addr_cidr_type; + +/* + * IP PREFIX TYPE + * + * Default value: 0.0.0.0/UINT_MAX + * Additional info: None required + */ +extern const struct ng_parse_type ng_parse_rules_array_type; + +/* Node type name and magic cookie */ +#define NG_ANTISPOOF_NODE_TYPE "antispoof" +#define NGM_ANTISPOOF_COOKIE 1596357443 + +/* Hook names */ +#define NG_ANTISPOOF_HOOK_FILTER "filter" +#define NG_ANTISPOOF_HOOK_DOWNSTREAM "downstream" +#define NG_ANTISPOOF_HOOK_NOMATCH "nomatch" + +/* + * CIDR structures (RFC 4632, prefix notation) + * + * Pair of IP/IPv6 address ('prefix') and an implied network mask with the + * 'length' most significant bits set to 1. + * + * Textual representation: prefix/length (e.g.: 127.0.0.1/8, ff02::/16) + * + * Examples: + * 192.168.42.0/24 --> (192.168.42.0, 255.255.255.0) + * 127.0.0.0/8 --> (127.0.0.0, 255.0.0.0) + * + * dead:beef::/32 --> (dead:beef::, ffff:ffff::) + * ::1/128 --> (::1, ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) + */ +struct ng_antispoof_ether_prefix { + u_char addr[6]; /* ethernet address */ + uint8_t length; /* number of most significant bits set to 1 */ +}; +struct ng_antispoof_cidr_ip { + struct in_addr addr; /* ip address */ + uint8_t length; /* number of most significant bits set to 1 */ +}; +struct ng_antispoof_cidr_ipv6 { + struct in6_addr addr; /* ipv6 address */ + uint8_t length; /* number of most significant bits set to 1 */ +}; + +/* Node configuration structure */ +struct ng_antispoof_config { + uint8_t debugLevel; /* debug level */ +}; + +/* Keep this in sync with the above structure definition */ +/* clang-format off */ +#define NG_ANTISPOOF_CONFIG_TYPE_INFO \ + { \ + { "debugLevel", &ng_parse_uint8_type }, \ + { \ + NULL \ + } \ + } +/* clang-format on */ + +/* IP Filter rule */ +struct ng_antispoof_rule_ip { + struct ng_antispoof_ether_prefix ether; /* mac in prefix notation */ + struct ng_antispoof_cidr_ip ip_addr; /* ip in prefix notation */ + struct in_addr ip_mask; /* ip network mask */ +}; + +/* Keep this in sync with the above structure definition */ +/* clang-format off */ +#define NG_ANTISPOOF_RULE_IP_TYPE_INFO \ + { \ + { "ether", &ng_parse_enaddr_prefix_type }, \ + { "ip_addr", &ng_parse_ipaddr_cidr_type }, \ + { "ip_mask", &ng_parse_ipmask_type }, \ + { \ + NULL \ + } \ + } +/* clang-format on */ + +/* IPv6 Filter rule */ +struct ng_antispoof_rule_ipv6 { + struct ng_antispoof_ether_prefix ether; /* mac in prefix notation */ + struct ng_antispoof_cidr_ipv6 ip6_addr; /* ipv6 in prefix notation */ +}; + +/* Keep this in sync with the above structure definition */ +/* clang-format off */ +#define NG_ANTISPOOF_RULE_IPV6_TYPE_INFO \ + { \ + { "ether", &ng_parse_enaddr_prefix_type }, \ + { "ip6_addr", &ng_parse_ip6addr_cidr_type }, \ + { \ + NULL \ + } \ + } +/* clang-format on */ + +/* Statistics structure for one hook */ +struct ng_antispoof_hookstat { + u_int64_t inOctets; + u_int64_t inFrames; + u_int64_t outOctets; + u_int64_t outFrames; + u_int64_t spoofedMac; /* pkts rec'd with spoofed source mac */ + u_int64_t spoofedIp; /* pkts rec'd with spoofed source ip */ + u_int64_t spoofedIpv6; /* pkts rec'd with spoofed v6 source ip */ + u_int64_t recvRunts; /* pkts rec'd to small for protocol headers */ + u_int64_t memoryFailures; /* times couldn't get mem or mbuf */ +}; + +/* Keep this in sync with the above structure definition */ +/* clang-format off */ +#define NG_ANTISPOOF_HOOKSTAT_INFO \ + { \ + { "inOctets", &ng_parse_uint64_type }, \ + { "inFrames", &ng_parse_uint64_type }, \ + { "outOctets", &ng_parse_uint64_type }, \ + { "outFrames", &ng_parse_uint64_type }, \ + { "spoofedMac", &ng_parse_uint64_type }, \ + { "spoofedIp", &ng_parse_uint64_type }, \ + { "spoofedIpv6", &ng_parse_uint64_type }, \ + { "recvRunts", &ng_parse_uint64_type }, \ + { "memoryFailures", &ng_parse_uint64_type }, \ + { \ + NULL \ + } \ + } +/* clang-format on */ + +/* Statistics structure returned by NGM_ANTISPOOF_GET_STATS */ +struct ng_antispoof_stats { + struct ng_antispoof_hookstat filter; + struct ng_antispoof_hookstat downstream; + struct ng_antispoof_hookstat nomatch; +}; + +/* Keep this in sync with the above structure definition */ +/* clang-format off */ +#define NG_ANTISPOOF_STATS_INFO(hstype) \ + { \ + { "filter", (hstype) }, \ + { "downstream", (hstype) }, \ + { "nomatch", (hstype) }, \ + { NULL } \ + } +/* clang-format on */ + +/* Netgraph commands */ +/* clang-format off */ +enum { + NGM_ANTISPOOF_SET_CONFIG = 1, /* set node configuration */ + NGM_ANTISPOOF_GET_CONFIG, /* get node configuration */ + NGM_ANTISPOOF_ADD_INET, /* add ip rule */ + NGM_ANTISPOOF_ADD_INET6, /* add ipv6 rule */ + NGM_ANTISPOOF_DEL_INET, /* delete ip rule */ + NGM_ANTISPOOF_DEL_INET6, /* delete ipv6 rule */ + NGM_ANTISPOOF_GET_RULES, /* get rules */ + NGM_ANTISPOOF_RESET, /* reset (forget) all information */ + NGM_ANTISPOOF_GET_STATS, /* get stats */ + NGM_ANTISPOOF_CLR_STATS, /* clear stats */ + NGM_ANTISPOOF_GETCLR_STATS, /* atomically get and clear stats */ +}; +/* clang-format on */ + +#endif /* _NETGRAPH_NG_ANTISPOOF_H_ */ Index: sys/netgraph/ng_antispoof.c =================================================================== --- sys/netgraph/ng_antispoof.c +++ sys/netgraph/ng_antispoof.c @@ -0,0 +1,2241 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2020 Markus Stoff + * 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 +#include +#include +#include +#include +#include +#include + +/* clang-format off */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +/* clang-format on */ + +/* + * Remove following includes when moving parsing functions to netgraph/parse.c + */ +#include + +#include + +/* 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 + * 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); +} Index: sys/netgraph/ng_parse.h =================================================================== --- sys/netgraph/ng_parse.h +++ sys/netgraph/ng_parse.h @@ -442,6 +442,14 @@ extern const struct ng_parse_type ng_parse_ipaddr_type; /* + * IPv6 ADDRESS TYPE + * + * Default value: :: + * Additional info: None required + */ +extern const struct ng_parse_type ng_parse_ip6addr_type; + +/* * ETHERNET ADDRESS TYPE * * Default value: None Index: sys/netgraph/ng_parse.c =================================================================== --- sys/netgraph/ng_parse.c +++ sys/netgraph/ng_parse.c @@ -67,6 +67,16 @@ #define M_NETGRAPH_PARSE M_NETGRAPH #endif +/* + * 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 + */ +#define ALIGNMENT_OF(T) ((size_t)&((struct { char x; T y; } *)0)->y) + /* Compute alignment for primitive integral types */ struct int16_temp { char x; @@ -960,9 +970,11 @@ if ((error = ng_int8_parse(&ng_parse_int8_type, s, off, start, buf + i, buflen)) != 0) return (error); - if (i < 3 && s[*off] != '.') - return (EINVAL); - (*off)++; + if (i < 3) { + if (s[*off] != '.') + return (EINVAL); + (*off)++; + } } *buflen = 4; return (0); @@ -1008,6 +1020,235 @@ }; /************************************************************************ + IPV6 ADDRESS TYPE + ************************************************************************/ + +static inline bool +isipv6char(u_char ch) +{ + return (isxdigit(ch) || ch == ':'); +} + +/* + * Output IPv6 segment (range of 16-bit fields) + * + * start: begin of range (inclusive) + * end: end of range (exclusive) + * + * Example: begin=2 end=5 will output fields 2, 3 and 4. + */ +static int +ng_ip6addr_unparse_segment(char **cbufp, int *cbuflenp, + const struct in6_addr *ip, size_t begin, size_t end) +{ + int error = end <= 8 ? 0 : ERANGE; + for (size_t i = begin; error == 0 && i < end; ++i) { + error = ng_parse_append(cbufp, cbuflenp, "%s%x", + i > begin ? ":" : "", ntohs(ip->__u6_addr.__u6_addr16[i])); + } + return (error); +} + +/* + * Parse IPv6 address (RFC 4291) + * + * This parser supports the following forms of text representation: + * - The preferred form x:x:x:x:x:x:x:x (RFC 4291 2.2 (1)) + * - Compressed zeros: :: (RFC 4291 2.2 (2)) + * + * It does NOT support the following forms: + * - Mixed form with IPv4 representation of the last 32 bits (RFC 4291 2.2 (3)) + * x:x:x:x:x:x:d.d.d.d or ::d.d.d.d + */ +static int +ng_ip6addr_parse(const struct ng_parse_type *type, + const char *s, int *off, const u_char *const start, + u_char *const buf, int *buflen) +{ + struct in6_addr *ip = (struct in6_addr *)buf; + + /* Make sure the buffer is big enough to hold the result */ + if (*buflen < sizeof(*ip)) + return (ERANGE); + + int runpos = -1; /* Position of run of zeros (-1: no run exists) */ + int field = 0; /* Number of parsed fields / index of next field */ + + /* :: (run of zeros) may occur at the beginning of address */ + if (*(s + *off) == ':') + if (*(s + *off) == ':') { + runpos = field; + (*off) += 2; + } + + /* + * As long as there is a hex digit or a colon on the input, + * 1. parse field + * 2. parse field separator or :: + */ + while (isipv6char(*(s + *off))) { + /* Boundary check index of next field */ + if (field >= 8) + return (EINVAL); + + /* Parse field */ + char *eptr = 0; + unsigned long val = strtoul(s + *off, &eptr, 16); + if (val <= 0xffff && eptr != s + *off) { + ip->__u6_addr.__u6_addr16[field] = htons((uint16_t)val); + *off = (eptr - s); + ++field; + } else + return (EINVAL); + + /* Parse field separator (:) or run of zeros (::) */ + if (*(s + *off) == ':') { + if (*(s + *off + 1) == ':') { + /* :: is allowed only once */ + if (runpos == -1) { + runpos = field; + (*off) += 2; + } else + return (EINVAL); + } + /* A field separator must be followed by another field + */ + else if (isxdigit(*(s + *off + 1))) + ++(*off); + else + return (EINVAL); + } + } + + /* If a run of zeros exists, insert it at runpos */ + if (runpos != -1) { + /* There must be room for at least one more field to expand :: + */ + if (field >= 8) + return (EINVAL); + + /* Not sure if memmove_s is always available in kernel space */ + int fields2move = field - runpos; + for (int i = 1; i <= fields2move; ++i) { + ip->__u6_addr.__u6_addr16[8 - i] = + ip->__u6_addr.__u6_addr16[field - i]; + } + /* Zero out run of zeros */ + int runlen = 8 - field; + bzero(&ip->__u6_addr.__u6_addr16[runpos], + runlen * sizeof(ip->__u6_addr.__u6_addr16[runpos])); + } + /* Without ::, the number of parsed fields must be exactly 8 */ + else if (field != 8) + return (EINVAL); + + /* Report amount of data written to buffer */ + *buflen = sizeof(*ip); + + return (0); +} + +/* + * Output address in its canonical form (RFC 5952) + * + * The following normalizations are implemented: + * - no leading zeroes in 16-bit fields + * - :: must not be used to shorten a single 16-bit field + * - :: replaces the longest run + * - :: replaces the first of the longest runs (if multiple) + * - lower case hex digits + * + * The following normalizations are NOT implemented: + * - embedded IPv4 addresses should be printed in mixed form + */ +static int +ng_ip6addr_unparse(const struct ng_parse_type *type, + const u_char *data, int *off, char *cbuf, int cbuflen) +{ + const struct in6_addr *const ip = + (const struct in6_addr *)(data + *off); + int error = 0; + + int r_start = 0; /* Result: start of first longest run */ + int r_len = 0; /* Result: length of first longest run */ + int p_start = 0; /* Start of currently parsed run */ + int p_len = 0; /* Length of currently parsed run */ + + /* Find the first (left most) of the longest runs of zeroes */ + for (int i = 0; i < 8; i++) { + if (ip->__u6_addr.__u6_addr16[i] == 0) { + /* Beginning of a new run? */ + if (p_len == 0) + p_start = i; + ++p_len; + } else { + /* Use of LT makes sure we stick with the left most + * candidate */ + if (r_len < p_len) { + r_start = p_start; + r_len = p_len; + } + p_len = 0; + } + } + /* Use of LT makes sure we stick with the left most candidate */ + if (r_len < p_len) { + r_start = p_start; + r_len = p_len; + } + + /* Don't use :: to shorten a single 16-bit field */ + if (r_len > 1) { + error = + ng_ip6addr_unparse_segment(&cbuf, &cbuflen, ip, 0, r_start); + if (error == 0) + error = ng_parse_append(&cbuf, &cbuflen, "::"); + if (error == 0) + error = ng_ip6addr_unparse_segment( + &cbuf, &cbuflen, ip, r_start + r_len, 8); + } else + error = ng_ip6addr_unparse_segment(&cbuf, &cbuflen, ip, 0, 8); + + /* Increase offset by the amount of data read */ + if (error == 0) + *off += sizeof(*ip); + + return (error); +} + +static int +ng_ip6addr_getDefault(const struct ng_parse_type *type, + const u_char *const start, u_char *buf, int *buflen) +{ + struct in6_addr ip = IN6ADDR_ANY_INIT; + int error = 0; + + if (*buflen >= sizeof(ip)) { + bcopy(&ip, buf, sizeof(ip)); + *buflen = sizeof(ip); + } else + error = ERANGE; + + return (error); +} + +static int +ng_ip6addr_getAlign(const struct ng_parse_type *type) +{ + return (ALIGNMENT_OF(struct in6_addr)); +} + +const struct ng_parse_type ng_parse_ip6addr_type = { + NULL, + NULL, + NULL, + ng_ip6addr_parse, + ng_ip6addr_unparse, + ng_ip6addr_getDefault, + ng_ip6addr_getAlign +}; + +/************************************************************************ ETHERNET ADDRESS TYPE ************************************************************************/