/* * Copyright (c) 2026 Ishan Agrawal * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "sysdecode.h" #include "support.h" /* * Decodes a buffer as a Netlink message stream. * * Returns true if the data was successfully decoded as Netlink. * Returns false if the data is malformed, allowing the caller * to fallback to a standard hex/string dump. */ static struct name_table *family_table = NULL; static size_t num_family = 0; typedef void decode_attr_f(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args); struct nlattr_decoder { uint16_t type; /* Attribute type */ const char *attr_name; /* Attribute name*/ decode_attr_f *cb; /* decoder function to call */ const void *args; /* nested decoder */ }; struct nlattr_decoder_set { const struct nlattr_decoder *decoders; /* PFNL CMD Decoders */ size_t count; /*Attribute Count*/ }; struct pfnl_cmd_decoder { int cmd_num; /* PFNL CMD */ const struct nlattr_decoder_set *ds; /* PFNL CMD Decoder set */ }; #define NL_DECLARE_ATTR_DECODER(_name, _np) \ static const struct nlattr_decoder_set _name = { \ .decoders = &((_np)[0]), \ .count = nitems(_np), \ } static void nl_decode_attrs_raw(FILE *fp, const struct nlattr *nla_head, size_t len, const struct nlattr_decoder *ps, size_t pslen); static void sysdecode_netlink_pf_constructor(void) __attribute__ ((__constructor__)); static void nlattr_decode_in6_addr(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args) { (void)args; struct in6_addr target; if (NLA_DATA_LEN(attr) < (int)sizeof(target)) return; memcpy(&target, NLA_DATA_CONST(attr), sizeof(struct in6_addr)); fprintf(fp, "%s=", attr_name); char buf[INET6_ADDRSTRLEN]; if (inet_ntop(AF_INET6, &target, buf, sizeof(buf)) != NULL) fprintf(fp, "%s", buf); } static void nlattr_decode_uint64(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args __unused) { uint64_t target; if (NLA_DATA_LEN(attr) < (int)sizeof(target)) return; memcpy(&target, NLA_DATA_CONST(attr), sizeof(uint64_t)); fprintf(fp, "%s=%ju", attr_name, target); } static void nlattr_decode_uint32(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args) { (void)args; uint32_t target; if (NLA_DATA_LEN(attr) < (int)sizeof(target)) return; memcpy(&target, NLA_DATA_CONST(attr), sizeof(uint32_t)); fprintf(fp, "%s=%u", attr_name, target); } static void nlattr_decode_uint16(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args __unused) { uint16_t target; if (NLA_DATA_LEN(attr) < (int)sizeof(target)) return; memcpy(&target, NLA_DATA_CONST(attr), sizeof(uint16_t)); fprintf(fp, "%s=%u", attr_name, target); } static void nlattr_decode_uint8(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args) { (void)args; uint8_t target; if (NLA_DATA_LEN(attr) < (int)sizeof(target)) return; memcpy(&target, NLA_DATA_CONST(attr), sizeof(uint8_t)); fprintf(fp, "%s=%u", attr_name, target); } static void nlattr_decode_bool(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args __unused) { bool target; if (NLA_DATA_LEN(attr) < (int)sizeof(target)) return; memcpy(&target, NLA_DATA_CONST(attr), sizeof(bool)); fprintf(fp, "%s=", attr_name); if (target) fprintf(fp, "TRUE"); else fprintf(fp, "FALSE"); } static void nlattr_decode_string(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args) { (void)args; if (NLA_DATA_LEN(attr) == 0) return; const char *target = (const char *)NLA_DATA_CONST(attr); fprintf(fp, "%s=%s", attr_name, target); } static void nlattr_decode_nested(FILE *fp, const struct nlattr *attr, const char *attr_name, const void *args) { const struct nlattr_decoder_set *set = args; if (set == NULL) return; fprintf(fp, "%s=", attr_name); nl_decode_attrs_raw(fp, NLA_DATA_CONST(attr), NLA_DATA_LEN(attr), set->decoders, set->count); } static const struct nlattr_decoder * search_decoders(const struct nlattr_decoder *ps, size_t pslen, int key) { size_t left_i = 0, right_i = pslen - 1; if (pslen == 0) return (NULL); if (key < ps[0].type || key > ps[pslen - 1].type) return (NULL); while (left_i + 1 < right_i) { size_t mid_i = (left_i + right_i) / 2; if (key < ps[mid_i].type) right_i = mid_i; else if (key > ps[mid_i].type) left_i = mid_i + 1; else return (&ps[mid_i]); } if (ps[left_i].type == key) return (&ps[left_i]); else if (ps[right_i].type == key) return (&ps[right_i]); return (NULL); } static const struct pfnl_cmd_decoder * search_cmd_decoders(const struct pfnl_cmd_decoder *ps, size_t pslen, int key) { size_t left_i = 0, right_i = pslen - 1; if (pslen == 0) return (NULL); if (key < ps[0].cmd_num || key > ps[pslen - 1].cmd_num) return (NULL); while (left_i + 1 < right_i) { size_t mid_i = (left_i + right_i) / 2; if (key < ps[mid_i].cmd_num) right_i = mid_i; else if (key > ps[mid_i].cmd_num) left_i = mid_i + 1; else return (&ps[mid_i]); } if (ps[left_i].cmd_num == key) return (&ps[left_i]); else if (ps[right_i].cmd_num == key) return (&ps[right_i]); return (NULL); } static void nl_decode_attrs_raw(FILE *fp, const struct nlattr *nla_head, size_t len, const struct nlattr_decoder *ps, size_t pslen) { const struct nlattr_decoder *s; const struct nlattr *nla; bool first = true; fprintf(fp, "{"); NLA_FOREACH_CONST(nla, nla_head, len) { s = search_decoders(ps, pslen, nla->nla_type & NLA_TYPE_MASK); if (s != NULL && s->cb != NULL) { if (!first) fprintf(fp, ","); s->cb(fp, nla, s->attr_name, s->args); } first = false; } fprintf(fp, "}"); } static const struct nlattr_decoder nla_d_getrules[] = { { .type = PF_GR_ANCHOR, .attr_name = "anchor", .cb = nlattr_decode_string }, { .type = PF_GR_ACTION, .attr_name = "action", .cb = nlattr_decode_uint8 }, { .type = PF_GR_NR, .attr_name = "nr", .cb = nlattr_decode_uint32 }, { .type = PF_GR_TICKET, .attr_name = "ticket", .cb = nlattr_decode_uint32 }, { .type = PF_GR_CLEAR, .attr_name = "clear", .cb = nlattr_decode_uint8 }, }; NL_DECLARE_ATTR_DECODER(getrules_decoder, nla_d_getrules); static const struct nlattr_decoder nla_d_set_limit[] = { { .type = PF_LI_INDEX, .attr_name = "index", .cb = nlattr_decode_uint32 }, { .type = PF_LI_LIMIT, .attr_name = "limit", .cb = nlattr_decode_uint32 }, }; NL_DECLARE_ATTR_DECODER(set_limit_decoder, nla_d_set_limit); static const struct nlattr_decoder nla_d_addr_wrap[] = { { .type = PF_AT_ADDR, .attr_name = "addr", .cb = nlattr_decode_in6_addr }, { .type = PF_AT_MASK, .attr_name = "mask", .cb = nlattr_decode_in6_addr }, { .type = PF_AT_IFNAME, .attr_name = "ifname", .cb = nlattr_decode_string }, { .type = PF_AT_TABLENAME, .attr_name = "tablename", .cb = nlattr_decode_string }, { .type = PF_AT_TYPE, .attr_name = "type", .cb = nlattr_decode_uint8 }, { .type = PF_AT_IFLAGS, .attr_name = "iflags", .cb = nlattr_decode_uint8 }, }; NL_DECLARE_ATTR_DECODER(addr_wrap_decoder, nla_d_addr_wrap); static const struct nlattr_decoder nla_d_pool_addr[] = { { .type = PF_PA_ADDR, .attr_name = "addr", .cb = nlattr_decode_nested, .args = &addr_wrap_decoder}, { .type = PF_PA_IFNAME, .attr_name = "ifname", .cb = nlattr_decode_string }, }; NL_DECLARE_ATTR_DECODER(pool_addr_decoder, nla_d_pool_addr); static const struct nlattr_decoder nla_d_add_addr[] = { { .type = PF_AA_ACTION, .attr_name = "action", .cb = nlattr_decode_uint32 }, { .type = PF_AA_TICKET, .attr_name = "ticket", .cb = nlattr_decode_uint32 }, { .type = PF_AA_NR, .attr_name = "nr", .cb = nlattr_decode_uint32 }, { .type = PF_AA_R_NUM, .attr_name = "r_num", .cb = nlattr_decode_uint32 }, { .type = PF_AA_R_ACTION, .attr_name = "r_action", .cb = nlattr_decode_uint8 }, { .type = PF_AA_R_LAST, .attr_name = "r_last", .cb = nlattr_decode_uint8 }, { .type = PF_AA_AF, .attr_name = "af", .cb = nlattr_decode_uint8 }, { .type = PF_AA_ANCHOR, .attr_name = "anchor", .cb = nlattr_decode_string }, { .type = PF_AA_ADDR, .attr_name = "addr", .cb = nlattr_decode_nested , .args = &pool_addr_decoder}, { .type = PF_AA_WHICH, .attr_name = "which", .cb = nlattr_decode_uint32 }, }; NL_DECLARE_ATTR_DECODER(addr_decoder, nla_d_add_addr); static const struct nlattr_decoder nla_d_ruleaddr[] = { { .type = PF_RAT_ADDR, .attr_name = "addr", .cb = nlattr_decode_nested, .args = &addr_wrap_decoder }, { .type = PF_RAT_SRC_PORT, .attr_name = "src_port", .cb = nlattr_decode_uint16 }, { .type = PF_RAT_DST_PORT, .attr_name = "dst_port", .cb = nlattr_decode_uint16 }, { .type = PF_RAT_NEG, .attr_name = "neg", .cb = nlattr_decode_uint8 }, { .type = PF_RAT_OP, .attr_name = "op", .cb = nlattr_decode_uint8 }, }; NL_DECLARE_ATTR_DECODER(rule_addr_decoder, nla_d_ruleaddr); static const struct nlattr_decoder nla_d_clear_states[] = { { .type = PF_CS_CMP_ID, .attr_name = "cmp_id", .cb = nlattr_decode_uint64 }, { .type = PF_CS_CMP_CREATORID, .attr_name = "cmp_creatorid", .cb = nlattr_decode_uint32 }, { .type = PF_CS_CMP_DIR, .attr_name = "cmp_dir", .cb = nlattr_decode_uint8 }, { .type = PF_CS_AF, .attr_name = "af", .cb = nlattr_decode_uint8 }, { .type = PF_CS_PROTO, .attr_name = "proto", .cb = nlattr_decode_uint8 }, { .type = PF_CS_SRC, .attr_name = "src", .cb = nlattr_decode_nested, .args = &rule_addr_decoder }, { .type = PF_CS_DST, .attr_name = "dst", .cb = nlattr_decode_nested, .args = &rule_addr_decoder }, { .type = PF_CS_RT_ADDR, .attr_name = "rt_addr", .cb = nlattr_decode_nested, .args = &rule_addr_decoder }, { .type = PF_CS_IFNAME, .attr_name = "ifname", .cb = nlattr_decode_string }, { .type = PF_CS_LABEL, .attr_name = "label", .cb = nlattr_decode_string }, { .type = PF_CS_KILL_MATCH, .attr_name = "kill_match", .cb = nlattr_decode_bool }, { .type = PF_CS_NAT, .attr_name = "nat", .cb = nlattr_decode_bool }, }; NL_DECLARE_ATTR_DECODER(killclear_states_decoder, nla_d_clear_states); static const struct nlattr_decoder nla_d_ruleset[] = { { .type = PF_RS_PATH, .attr_name = "path", .cb = nlattr_decode_string }, { .type = PF_RS_NR, .attr_name = "nr", .cb = nlattr_decode_uint32 }, }; NL_DECLARE_ATTR_DECODER(ruleset_decoder, nla_d_ruleset); static inline void nl_verify_decoders(const struct nlattr_decoder_set **decoder, size_t count) { for (size_t i = 0; i < count; i++) { const struct nlattr_decoder_set *p = decoder[i]; for (size_t j = 1; j < p->count; j++) { assert(p->decoders[j].type > p->decoders[j-1].type); } } } #define NL_VERIFY_DECODERS(_p) nl_verify_decoders((_p), nitems(_p)) static const struct nlattr_decoder_set *all_decoders[] = { &getrules_decoder, &set_limit_decoder, &addr_wrap_decoder, &pool_addr_decoder, &addr_decoder, &rule_addr_decoder, &killclear_states_decoder, &ruleset_decoder, }; static const struct pfnl_cmd_decoder cmd_decoder[] = { { .cmd_num = PFNL_CMD_GETRULES, .ds = &getrules_decoder }, { .cmd_num = PFNL_CMD_KILLSTATES, .ds = &killclear_states_decoder }, { .cmd_num = PFNL_CMD_GET_LIMIT, .ds = &set_limit_decoder }, { .cmd_num = PFNL_CMD_GET_ADDRS, .ds = &addr_decoder }, { .cmd_num = PFNL_CMD_GET_ADDR, .ds = &addr_decoder }, { .cmd_num = PFNL_CMD_GET_RULESETS, .ds = &ruleset_decoder }, { .cmd_num = PFNL_CMD_GET_RULESET, .ds = &ruleset_decoder }, }; static inline void pfnl_verify_cmd_decoders(const struct pfnl_cmd_decoder *cmds, size_t count) { int num = cmds[0].cmd_num; for (size_t i = 1; i < count; i++) { const struct pfnl_cmd_decoder *p = &cmds[i]; assert(p->cmd_num > num); num = p->cmd_num; } } static void sysdecode_netlink_pf(FILE *fp, const struct genlmsghdr *genl, size_t nlm_len) { uint8_t cmd = genl->cmd; const char *cmd_name = sysdecode_pfnl_cmd(cmd); if (cmd_name != NULL) fprintf(fp, "cmd=%s", cmd_name); else fprintf(fp, "cmd=%u", cmd); const struct nlattr *nla = (const struct nlattr *)(const void *) ((const char *)genl + sizeof(struct genlmsghdr)); const struct pfnl_cmd_decoder *d; d = search_cmd_decoders(cmd_decoder, nitems(cmd_decoder), cmd); if (d != NULL) { nl_decode_attrs_raw(fp, nla, nlm_len, d->ds->decoders, d->ds->count); } } bool sysdecode_netlink(FILE *fp, const void *buf, size_t len, int protocol) { const struct nlmsghdr *nl = buf; size_t remaining = len; bool first = true; /* Basic sanity check: Buffer must be at least one header size. */ if (remaining < sizeof(struct nlmsghdr)) return (false); /* * Protocol Sanity Check: * The first message length must be valid (>= header) and fit * inside the provided buffer snapshot. */ if (nl->nlmsg_len < sizeof(struct nlmsghdr) || nl->nlmsg_len > remaining) return (false); if (family_table == NULL) { family_table = malloc((num_family + 1) * sizeof(struct name_table)); family_table[num_family] = (struct name_table){0, NULL}; } fprintf(fp, "netlink{"); while (remaining >= sizeof(struct nlmsghdr)) { if (!first) fprintf(fp, ","); /* Safety check for current message. */ if (nl->nlmsg_len < sizeof(struct nlmsghdr) || nl->nlmsg_len > remaining) { fprintf(fp, ""); break; } fprintf(fp, "flags="); sysdecode_nlm_flag(fp, nl->nlmsg_flags); fprintf(fp, ",seq=%u,pid=%u", nl->nlmsg_seq, nl->nlmsg_pid); fprintf(fp, ",len=%u,type=", nl->nlmsg_len); /* Decode Standard Message Types. */ switch (nl->nlmsg_type) { case NLMSG_NOOP: fprintf(fp, "NLMSG_NOOP"); break; case NLMSG_ERROR: fprintf(fp, "NLMSG_ERROR"); break; case NLMSG_DONE: fprintf(fp, "NLMSG_DONE"); break; case NLMSG_OVERRUN: fprintf(fp, "NLMSG_OVERRUN"); break; case GENL_ID_CTRL: if (protocol != NETLINK_GENERIC) break; fprintf(fp, "GENL_ID_CTRL"); const struct genlmsghdr *genl = (const struct genlmsghdr *)(const void *) ((const char *)nl + sizeof(struct nlmsghdr)); uint16_t family_id = 0; const char *family_name = NULL; fprintf(fp, ",genl={cmd=%u," "ver=%u,reserve=%u", genl->cmd, genl->version, genl->reserved); size_t cur_len = (sizeof(struct nlmsghdr) + sizeof(struct genlmsghdr)); size_t nla_len = nl->nlmsg_len - cur_len; const struct nlattr *nla; const struct nlattr *nla_head = (const struct nlattr *) (const void *)((const char *)nl + cur_len); NLA_FOREACH_CONST(nla, nla_head, nla_len) { switch (nla->nla_type) { case CTRL_ATTR_FAMILY_ID: memcpy(&family_id, NLA_DATA_CONST(nla), sizeof(family_id)); fprintf(fp, ",family_id=%u", family_id); break; case CTRL_ATTR_FAMILY_NAME: family_name = ((const char *)NLA_DATA_CONST(nla)); fprintf(fp, ",family_name=%s", family_name); break; default: break; } } if (family_name && family_id && !lookup_value(family_table, family_id)) { num_family++; family_table = realloc(family_table, (num_family + 1) * sizeof(struct name_table)); family_table[num_family - 1].val = family_id; family_table[num_family - 1].str = strdup(family_name); family_table[num_family] = (struct name_table){0, NULL}; } fprintf(fp, "}"); break; default: fprintf(fp, "%u", nl->nlmsg_type); break; } const char *family = lookup_value(family_table, nl->nlmsg_type); if (family != NULL && protocol == NETLINK_GENERIC) { fprintf(fp, ",%s={", family); if (strcmp(family, "pfctl") == 0) { const struct genlmsghdr *genl = (const struct genlmsghdr *)(const void *) ((const char *)nl + sizeof(struct nlmsghdr)); const size_t nlm_len = nl->nlmsg_len - (sizeof(struct nlmsghdr) + sizeof(struct genlmsghdr)); sysdecode_netlink_pf(fp, genl, nlm_len); } fprintf(fp, "}"); } /* Handle Alignment (Netlink messages are 4-byte aligned). */ size_t aligned_len = NLMSG_ALIGN(nl->nlmsg_len); if (aligned_len > remaining) remaining = 0; else remaining -= aligned_len; nl = (const struct nlmsghdr *)(const void *)((const char *)nl + aligned_len); first = false; } fprintf(fp, "}"); return (true); } static void sysdecode_netlink_pf_constructor(void) { NL_VERIFY_DECODERS(all_decoders); pfnl_verify_cmd_decoders(cmd_decoder, nitems(cmd_decoder)); }