root/net/netfilter/nfnetlink_osf.c
// SPDX-License-Identifier: GPL-2.0-only
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kernel.h>

#include <linux/capability.h>
#include <linux/if.h>
#include <linux/inetdevice.h>
#include <linux/ip.h>
#include <linux/list.h>
#include <linux/rculist.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/tcp.h>

#include <net/ip.h>
#include <net/tcp.h>

#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/x_tables.h>
#include <net/netfilter/nf_log.h>
#include <linux/netfilter/nfnetlink_osf.h>

/*
 * Indexed by dont-fragment bit.
 * It is the only constant value in the fingerprint.
 */
struct list_head nf_osf_fingers[2];
EXPORT_SYMBOL_GPL(nf_osf_fingers);

static inline int nf_osf_ttl(const struct sk_buff *skb,
                             int ttl_check, unsigned char f_ttl)
{
        struct in_device *in_dev = __in_dev_get_rcu(skb->dev);
        const struct iphdr *ip = ip_hdr(skb);
        const struct in_ifaddr *ifa;
        int ret = 0;

        if (ttl_check == NF_OSF_TTL_TRUE)
                return ip->ttl == f_ttl;
        if (ttl_check == NF_OSF_TTL_NOCHECK)
                return 1;
        else if (ip->ttl <= f_ttl)
                return 1;

        in_dev_for_each_ifa_rcu(ifa, in_dev) {
                if (inet_ifa_match(ip->saddr, ifa)) {
                        ret = (ip->ttl == f_ttl);
                        break;
                }
        }

        return ret;
}

struct nf_osf_hdr_ctx {
        bool                    df;
        u16                     window;
        u16                     totlen;
        const unsigned char     *optp;
        unsigned int            optsize;
};

static bool nf_osf_match_one(const struct sk_buff *skb,
                             const struct nf_osf_user_finger *f,
                             int ttl_check,
                             struct nf_osf_hdr_ctx *ctx)
{
        const __u8 *optpinit = ctx->optp;
        unsigned int check_WSS = 0;
        int fmatch = FMATCH_WRONG;
        int foptsize, optnum;
        u16 mss = 0;

        if (ctx->totlen != f->ss || !nf_osf_ttl(skb, ttl_check, f->ttl))
                return false;

        /*
         * Should not happen if userspace parser was written correctly.
         */
        if (f->wss.wc >= OSF_WSS_MAX)
                return false;

        /* Check options */

        foptsize = 0;
        for (optnum = 0; optnum < f->opt_num; ++optnum)
                foptsize += f->opt[optnum].length;

        if (foptsize > MAX_IPOPTLEN ||
            ctx->optsize > MAX_IPOPTLEN ||
            ctx->optsize != foptsize)
                return false;

        check_WSS = f->wss.wc;

        for (optnum = 0; optnum < f->opt_num; ++optnum) {
                if (f->opt[optnum].kind == *ctx->optp) {
                        __u32 len = f->opt[optnum].length;
                        const __u8 *optend = ctx->optp + len;

                        fmatch = FMATCH_OK;

                        switch (*ctx->optp) {
                        case OSFOPT_MSS:
                                mss = ctx->optp[3];
                                mss <<= 8;
                                mss |= ctx->optp[2];

                                mss = ntohs((__force __be16)mss);
                                break;
                        case OSFOPT_TS:
                                break;
                        }

                        ctx->optp = optend;
                } else
                        fmatch = FMATCH_OPT_WRONG;

                if (fmatch != FMATCH_OK)
                        break;
        }

        if (fmatch != FMATCH_OPT_WRONG) {
                fmatch = FMATCH_WRONG;

                switch (check_WSS) {
                case OSF_WSS_PLAIN:
                        if (f->wss.val == 0 || ctx->window == f->wss.val)
                                fmatch = FMATCH_OK;
                        break;
                case OSF_WSS_MSS:
                        /*
                         * Some smart modems decrease mangle MSS to
                         * SMART_MSS_2, so we check standard, decreased
                         * and the one provided in the fingerprint MSS
                         * values.
                         */
#define SMART_MSS_1     1460
#define SMART_MSS_2     1448
                        if (ctx->window == f->wss.val * mss ||
                            ctx->window == f->wss.val * SMART_MSS_1 ||
                            ctx->window == f->wss.val * SMART_MSS_2)
                                fmatch = FMATCH_OK;
                        break;
                case OSF_WSS_MTU:
                        if (ctx->window == f->wss.val * (mss + 40) ||
                            ctx->window == f->wss.val * (SMART_MSS_1 + 40) ||
                            ctx->window == f->wss.val * (SMART_MSS_2 + 40))
                                fmatch = FMATCH_OK;
                        break;
                case OSF_WSS_MODULO:
                        if ((ctx->window % f->wss.val) == 0)
                                fmatch = FMATCH_OK;
                        break;
                }
        }

        if (fmatch != FMATCH_OK)
                ctx->optp = optpinit;

        return fmatch == FMATCH_OK;
}

static const struct tcphdr *nf_osf_hdr_ctx_init(struct nf_osf_hdr_ctx *ctx,
                                                const struct sk_buff *skb,
                                                const struct iphdr *ip,
                                                unsigned char *opts,
                                                struct tcphdr *_tcph)
{
        const struct tcphdr *tcp;

        tcp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(struct tcphdr), _tcph);
        if (!tcp)
                return NULL;

        if (!tcp->syn)
                return NULL;

        ctx->totlen = ntohs(ip->tot_len);
        ctx->df = ntohs(ip->frag_off) & IP_DF;
        ctx->window = ntohs(tcp->window);

        if (tcp->doff * 4 > sizeof(struct tcphdr)) {
                ctx->optsize = tcp->doff * 4 - sizeof(struct tcphdr);

                ctx->optp = skb_header_pointer(skb, ip_hdrlen(skb) +
                                sizeof(struct tcphdr), ctx->optsize, opts);
                if (!ctx->optp)
                        return NULL;
        }

        return tcp;
}

bool
nf_osf_match(const struct sk_buff *skb, u_int8_t family,
             int hooknum, struct net_device *in, struct net_device *out,
             const struct nf_osf_info *info, struct net *net,
             const struct list_head *nf_osf_fingers)
{
        const struct iphdr *ip = ip_hdr(skb);
        const struct nf_osf_user_finger *f;
        unsigned char opts[MAX_IPOPTLEN];
        const struct nf_osf_finger *kf;
        int fcount = 0, ttl_check;
        int fmatch = FMATCH_WRONG;
        struct nf_osf_hdr_ctx ctx;
        const struct tcphdr *tcp;
        struct tcphdr _tcph;

        memset(&ctx, 0, sizeof(ctx));

        tcp = nf_osf_hdr_ctx_init(&ctx, skb, ip, opts, &_tcph);
        if (!tcp)
                return false;

        ttl_check = (info->flags & NF_OSF_TTL) ? info->ttl : 0;

        list_for_each_entry_rcu(kf, &nf_osf_fingers[ctx.df], finger_entry) {

                f = &kf->finger;

                if (!(info->flags & NF_OSF_LOG) && strcmp(info->genre, f->genre))
                        continue;

                if (!nf_osf_match_one(skb, f, ttl_check, &ctx))
                        continue;

                fmatch = FMATCH_OK;

                fcount++;

                if (info->flags & NF_OSF_LOG)
                        nf_log_packet(net, family, hooknum, skb,
                                      in, out, NULL,
                                      "%s [%s:%s] : %pI4:%d -> %pI4:%d hops=%d\n",
                                      f->genre, f->version, f->subtype,
                                      &ip->saddr, ntohs(tcp->source),
                                      &ip->daddr, ntohs(tcp->dest),
                                      f->ttl - ip->ttl);

                if ((info->flags & NF_OSF_LOG) &&
                    info->loglevel == NF_OSF_LOGLEVEL_FIRST)
                        break;
        }

        if (!fcount && (info->flags & NF_OSF_LOG))
                nf_log_packet(net, family, hooknum, skb, in, out, NULL,
                              "Remote OS is not known: %pI4:%u -> %pI4:%u\n",
                              &ip->saddr, ntohs(tcp->source),
                              &ip->daddr, ntohs(tcp->dest));

        if (fcount)
                fmatch = FMATCH_OK;

        return fmatch == FMATCH_OK;
}
EXPORT_SYMBOL_GPL(nf_osf_match);

bool nf_osf_find(const struct sk_buff *skb,
                 const struct list_head *nf_osf_fingers,
                 const int ttl_check, struct nf_osf_data *data)
{
        const struct iphdr *ip = ip_hdr(skb);
        const struct nf_osf_user_finger *f;
        unsigned char opts[MAX_IPOPTLEN];
        const struct nf_osf_finger *kf;
        struct nf_osf_hdr_ctx ctx;
        const struct tcphdr *tcp;
        struct tcphdr _tcph;
        bool found = false;

        memset(&ctx, 0, sizeof(ctx));

        tcp = nf_osf_hdr_ctx_init(&ctx, skb, ip, opts, &_tcph);
        if (!tcp)
                return false;

        list_for_each_entry_rcu(kf, &nf_osf_fingers[ctx.df], finger_entry) {
                f = &kf->finger;
                if (!nf_osf_match_one(skb, f, ttl_check, &ctx))
                        continue;

                data->genre = f->genre;
                data->version = f->version;
                found = true;
                break;
        }

        return found;
}
EXPORT_SYMBOL_GPL(nf_osf_find);

static const struct nla_policy nfnl_osf_policy[OSF_ATTR_MAX + 1] = {
        [OSF_ATTR_FINGER]       = { .len = sizeof(struct nf_osf_user_finger) },
};

static int nfnl_osf_add_callback(struct sk_buff *skb,
                                 const struct nfnl_info *info,
                                 const struct nlattr * const osf_attrs[])
{
        struct nf_osf_user_finger *f;
        struct nf_osf_finger *kf = NULL, *sf;
        unsigned int tot_opt_len = 0;
        int err = 0;
        int i;

        if (!capable(CAP_NET_ADMIN))
                return -EPERM;

        if (!osf_attrs[OSF_ATTR_FINGER])
                return -EINVAL;

        if (!(info->nlh->nlmsg_flags & NLM_F_CREATE))
                return -EINVAL;

        f = nla_data(osf_attrs[OSF_ATTR_FINGER]);

        if (f->opt_num > ARRAY_SIZE(f->opt))
                return -EINVAL;

        for (i = 0; i < f->opt_num; i++) {
                if (!f->opt[i].length || f->opt[i].length > MAX_IPOPTLEN)
                        return -EINVAL;
                if (f->opt[i].kind == OSFOPT_MSS && f->opt[i].length < 4)
                        return -EINVAL;

                tot_opt_len += f->opt[i].length;
                if (tot_opt_len > MAX_IPOPTLEN)
                        return -EINVAL;
        }

        if (!memchr(f->genre, 0, MAXGENRELEN) ||
            !memchr(f->subtype, 0, MAXGENRELEN) ||
            !memchr(f->version, 0, MAXGENRELEN))
                return -EINVAL;

        kf = kmalloc_obj(struct nf_osf_finger);
        if (!kf)
                return -ENOMEM;

        memcpy(&kf->finger, f, sizeof(struct nf_osf_user_finger));

        list_for_each_entry(sf, &nf_osf_fingers[!!f->df], finger_entry) {
                if (memcmp(&sf->finger, f, sizeof(struct nf_osf_user_finger)))
                        continue;

                kfree(kf);
                kf = NULL;

                if (info->nlh->nlmsg_flags & NLM_F_EXCL)
                        err = -EEXIST;
                break;
        }

        /*
         * We are protected by nfnl mutex.
         */
        if (kf)
                list_add_tail_rcu(&kf->finger_entry, &nf_osf_fingers[!!f->df]);

        return err;
}

static int nfnl_osf_remove_callback(struct sk_buff *skb,
                                    const struct nfnl_info *info,
                                    const struct nlattr * const osf_attrs[])
{
        struct nf_osf_user_finger *f;
        struct nf_osf_finger *sf;
        int err = -ENOENT;

        if (!capable(CAP_NET_ADMIN))
                return -EPERM;

        if (!osf_attrs[OSF_ATTR_FINGER])
                return -EINVAL;

        f = nla_data(osf_attrs[OSF_ATTR_FINGER]);

        list_for_each_entry(sf, &nf_osf_fingers[!!f->df], finger_entry) {
                if (memcmp(&sf->finger, f, sizeof(struct nf_osf_user_finger)))
                        continue;

                /*
                 * We are protected by nfnl mutex.
                 */
                list_del_rcu(&sf->finger_entry);
                kfree_rcu(sf, rcu_head);

                err = 0;
                break;
        }

        return err;
}

static const struct nfnl_callback nfnl_osf_callbacks[OSF_MSG_MAX] = {
        [OSF_MSG_ADD]   = {
                .call           = nfnl_osf_add_callback,
                .type           = NFNL_CB_MUTEX,
                .attr_count     = OSF_ATTR_MAX,
                .policy         = nfnl_osf_policy,
        },
        [OSF_MSG_REMOVE]        = {
                .call           = nfnl_osf_remove_callback,
                .type           = NFNL_CB_MUTEX,
                .attr_count     = OSF_ATTR_MAX,
                .policy         = nfnl_osf_policy,
        },
};

static const struct nfnetlink_subsystem nfnl_osf_subsys = {
        .name                   = "osf",
        .subsys_id              = NFNL_SUBSYS_OSF,
        .cb_count               = OSF_MSG_MAX,
        .cb                     = nfnl_osf_callbacks,
};

static int __init nfnl_osf_init(void)
{
        int err = -EINVAL;
        int i;

        for (i = 0; i < ARRAY_SIZE(nf_osf_fingers); ++i)
                INIT_LIST_HEAD(&nf_osf_fingers[i]);

        err = nfnetlink_subsys_register(&nfnl_osf_subsys);
        if (err < 0) {
                pr_err("Failed to register OSF nsfnetlink helper (%d)\n", err);
                goto err_out_exit;
        }
        return 0;

err_out_exit:
        return err;
}

static void __exit nfnl_osf_fini(void)
{
        struct nf_osf_finger *f;
        int i;

        nfnetlink_subsys_unregister(&nfnl_osf_subsys);

        rcu_read_lock();
        for (i = 0; i < ARRAY_SIZE(nf_osf_fingers); ++i) {
                list_for_each_entry_rcu(f, &nf_osf_fingers[i], finger_entry) {
                        list_del_rcu(&f->finger_entry);
                        kfree_rcu(f, rcu_head);
                }
        }
        rcu_read_unlock();

        rcu_barrier();
}

module_init(nfnl_osf_init);
module_exit(nfnl_osf_fini);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Passive OS fingerprint matching");
MODULE_ALIAS_NFNL_SUBSYS(NFNL_SUBSYS_OSF);