/* SPDX-License-Identifier: LGPL-2.1-or-later */
/***
  Copyright © 2014 Intel Corporation. All rights reserved.
***/

#include <netinet/icmp6.h>

#include "sd-ndisc.h"

#include "alloc-util.h"
#include "dns-domain.h"
#include "hostname-util.h"
#include "memory-util.h"
#include "missing_network.h"
#include "ndisc-internal.h"
#include "ndisc-router.h"
#include "strv.h"

DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_ndisc_router, sd_ndisc_router, mfree);

sd_ndisc_router *ndisc_router_new(size_t raw_size) {
        sd_ndisc_router *rt;

        if (raw_size > SIZE_MAX - ALIGN(sizeof(sd_ndisc_router)))
                return NULL;

        rt = malloc0(ALIGN(sizeof(sd_ndisc_router)) + raw_size);
        if (!rt)
                return NULL;

        rt->raw_size = raw_size;
        rt->n_ref = 1;

        return rt;
}

int sd_ndisc_router_get_address(sd_ndisc_router *rt, struct in6_addr *ret_addr) {
        assert_return(rt, -EINVAL);
        assert_return(ret_addr, -EINVAL);

        if (in6_addr_is_null(&rt->address))
                return -ENODATA;

        *ret_addr = rt->address;
        return 0;
}

int sd_ndisc_router_get_timestamp(sd_ndisc_router *rt, clockid_t clock, uint64_t *ret) {
        assert_return(rt, -EINVAL);
        assert_return(TRIPLE_TIMESTAMP_HAS_CLOCK(clock), -EOPNOTSUPP);
        assert_return(clock_supported(clock), -EOPNOTSUPP);
        assert_return(ret, -EINVAL);

        if (!triple_timestamp_is_set(&rt->timestamp))
                return -ENODATA;

        *ret = triple_timestamp_by_clock(&rt->timestamp, clock);
        return 0;
}

int sd_ndisc_router_get_raw(sd_ndisc_router *rt, const void **ret, size_t *size) {
        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);
        assert_return(size, -EINVAL);

        *ret = NDISC_ROUTER_RAW(rt);
        *size = rt->raw_size;

        return 0;
}

int ndisc_router_parse(sd_ndisc *nd, sd_ndisc_router *rt) {
        struct nd_router_advert *a;
        const uint8_t *p;
        bool has_mtu = false, has_flag_extension = false;
        size_t left;

        assert(rt);

        if (rt->raw_size < sizeof(struct nd_router_advert))
                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                       "Too small to be a router advertisement, ignoring.");

        /* Router advertisement packets are neatly aligned to 64-bit boundaries, hence we can access them directly */
        a = NDISC_ROUTER_RAW(rt);

        if (a->nd_ra_type != ND_ROUTER_ADVERT)
                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                       "Received ND packet that is not a router advertisement, ignoring.");

        if (a->nd_ra_code != 0)
                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                       "Received ND packet with wrong RA code, ignoring.");

        rt->hop_limit = a->nd_ra_curhoplimit;
        rt->flags = a->nd_ra_flags_reserved; /* the first 8 bits */
        rt->lifetime = be16toh(a->nd_ra_router_lifetime);

        rt->preference = (rt->flags >> 3) & 3;
        if (!IN_SET(rt->preference, SD_NDISC_PREFERENCE_LOW, SD_NDISC_PREFERENCE_HIGH))
                rt->preference = SD_NDISC_PREFERENCE_MEDIUM;

        p = (const uint8_t*) NDISC_ROUTER_RAW(rt) + sizeof(struct nd_router_advert);
        left = rt->raw_size - sizeof(struct nd_router_advert);

        for (;;) {
                uint8_t type;
                size_t length;

                if (left == 0)
                        break;

                if (left < 2)
                        return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                               "Option lacks header, ignoring datagram.");

                type = p[0];
                length = p[1] * 8;

                if (length == 0)
                        return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                               "Zero-length option, ignoring datagram.");
                if (left < length)
                        return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                               "Option truncated, ignoring datagram.");

                switch (type) {

                case SD_NDISC_OPTION_PREFIX_INFORMATION:

                        if (length != 4*8)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                                       "Prefix option of invalid size, ignoring datagram.");

                        if (p[2] > 128)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                                       "Bad prefix length, ignoring datagram.");

                        break;

                case SD_NDISC_OPTION_MTU: {
                        uint32_t m;

                        if (has_mtu) {
                                log_ndisc(nd, "MTU option specified twice, ignoring.");
                                break;
                        }

                        if (length != 8)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                                       "MTU option of invalid size, ignoring datagram.");

                        m = be32toh(*(uint32_t*) (p + 4));
                        if (m >= IPV6_MIN_MTU) /* ignore invalidly small MTUs */
                                rt->mtu = m;

                        has_mtu = true;
                        break;
                }

                case SD_NDISC_OPTION_ROUTE_INFORMATION:
                        if (length < 1*8 || length > 3*8)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                                       "Route information option of invalid size, ignoring datagram.");

                        if (p[2] > 128)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                                       "Bad route prefix length, ignoring datagram.");

                        break;

                case SD_NDISC_OPTION_RDNSS:
                        if (length < 3*8 || (length % (2*8)) != 1*8)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG), "RDNSS option has invalid size.");

                        break;

                case SD_NDISC_OPTION_FLAGS_EXTENSION:

                        if (has_flag_extension) {
                                log_ndisc(nd, "Flags extension option specified twice, ignoring.");
                                break;
                        }

                        if (length < 1*8)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                                       "Flags extension option has invalid size.");

                        /* Add in the additional flags bits */
                        rt->flags |=
                                ((uint64_t) p[2] << 8) |
                                ((uint64_t) p[3] << 16) |
                                ((uint64_t) p[4] << 24) |
                                ((uint64_t) p[5] << 32) |
                                ((uint64_t) p[6] << 40) |
                                ((uint64_t) p[7] << 48);

                        has_flag_extension = true;
                        break;

                case SD_NDISC_OPTION_DNSSL:
                        if (length < 2*8)
                                return log_ndisc_errno(nd, SYNTHETIC_ERRNO(EBADMSG),
                                                       "DNSSL option has invalid size.");

                        break;
                }

                p += length, left -= length;
        }

        rt->rindex = sizeof(struct nd_router_advert);
        return 0;
}

int sd_ndisc_router_get_hop_limit(sd_ndisc_router *rt, uint8_t *ret) {
        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        *ret = rt->hop_limit;
        return 0;
}

int sd_ndisc_router_get_flags(sd_ndisc_router *rt, uint64_t *ret_flags) {
        assert_return(rt, -EINVAL);
        assert_return(ret_flags, -EINVAL);

        *ret_flags = rt->flags;
        return 0;
}

int sd_ndisc_router_get_lifetime(sd_ndisc_router *rt, uint16_t *ret_lifetime) {
        assert_return(rt, -EINVAL);
        assert_return(ret_lifetime, -EINVAL);

        *ret_lifetime = rt->lifetime;
        return 0;
}

int sd_ndisc_router_get_preference(sd_ndisc_router *rt, unsigned *ret) {
        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        *ret = rt->preference;
        return 0;
}

int sd_ndisc_router_get_mtu(sd_ndisc_router *rt, uint32_t *ret) {
        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        if (rt->mtu <= 0)
                return -ENODATA;

        *ret = rt->mtu;
        return 0;
}

int sd_ndisc_router_option_rewind(sd_ndisc_router *rt) {
        assert_return(rt, -EINVAL);

        assert(rt->raw_size >= sizeof(struct nd_router_advert));
        rt->rindex = sizeof(struct nd_router_advert);

        return rt->rindex < rt->raw_size;
}

int sd_ndisc_router_option_next(sd_ndisc_router *rt) {
        size_t length;

        assert_return(rt, -EINVAL);

        if (rt->rindex == rt->raw_size) /* EOF */
                return -ESPIPE;

        if (rt->rindex + 2 > rt->raw_size) /* Truncated message */
                return -EBADMSG;

        length = NDISC_ROUTER_OPTION_LENGTH(rt);
        if (rt->rindex + length > rt->raw_size)
                return -EBADMSG;

        rt->rindex += length;
        return rt->rindex < rt->raw_size;
}

int sd_ndisc_router_option_get_type(sd_ndisc_router *rt, uint8_t *ret) {
        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        if (rt->rindex == rt->raw_size) /* EOF */
                return -ESPIPE;

        if (rt->rindex + 2 > rt->raw_size) /* Truncated message */
                return -EBADMSG;

        *ret = NDISC_ROUTER_OPTION_TYPE(rt);
        return 0;
}

int sd_ndisc_router_option_is_type(sd_ndisc_router *rt, uint8_t type) {
        uint8_t k;
        int r;

        assert_return(rt, -EINVAL);

        r = sd_ndisc_router_option_get_type(rt, &k);
        if (r < 0)
                return r;

        return type == k;
}

int sd_ndisc_router_option_get_raw(sd_ndisc_router *rt, const void **ret, size_t *size) {
        size_t length;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);
        assert_return(size, -EINVAL);

        /* Note that this returns the full option, including the option header */

        if (rt->rindex + 2 > rt->raw_size)
                return -EBADMSG;

        length = NDISC_ROUTER_OPTION_LENGTH(rt);
        if (rt->rindex + length > rt->raw_size)
                return -EBADMSG;

        *ret = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex;
        *size = length;

        return 0;
}

static int get_prefix_info(sd_ndisc_router *rt, struct nd_opt_prefix_info **ret) {
        struct nd_opt_prefix_info *ri;
        size_t length;
        int r;

        assert(rt);
        assert(ret);

        r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_PREFIX_INFORMATION);
        if (r < 0)
                return r;
        if (r == 0)
                return -EMEDIUMTYPE;

        length = NDISC_ROUTER_OPTION_LENGTH(rt);
        if (length != sizeof(struct nd_opt_prefix_info))
                return -EBADMSG;

        ri = (struct nd_opt_prefix_info*) ((uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex);
        if (ri->nd_opt_pi_prefix_len > 128)
                return -EBADMSG;

        *ret = ri;
        return 0;
}

int sd_ndisc_router_prefix_get_valid_lifetime(sd_ndisc_router *rt, uint32_t *ret) {
        struct nd_opt_prefix_info *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_prefix_info(rt, &ri);
        if (r < 0)
                return r;

        *ret = be32toh(ri->nd_opt_pi_valid_time);
        return 0;
}

int sd_ndisc_router_prefix_get_preferred_lifetime(sd_ndisc_router *rt, uint32_t *ret) {
        struct nd_opt_prefix_info *pi;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_prefix_info(rt, &pi);
        if (r < 0)
                return r;

        *ret = be32toh(pi->nd_opt_pi_preferred_time);
        return 0;
}

int sd_ndisc_router_prefix_get_flags(sd_ndisc_router *rt, uint8_t *ret) {
        struct nd_opt_prefix_info *pi;
        uint8_t flags;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_prefix_info(rt, &pi);
        if (r < 0)
                return r;

        flags = pi->nd_opt_pi_flags_reserved;

        if ((flags & ND_OPT_PI_FLAG_AUTO) && (pi->nd_opt_pi_prefix_len != 64)) {
                log_ndisc(NULL, "Invalid prefix length, ignoring prefix for stateless autoconfiguration.");
                flags &= ~ND_OPT_PI_FLAG_AUTO;
        }

        *ret = flags;
        return 0;
}

int sd_ndisc_router_prefix_get_address(sd_ndisc_router *rt, struct in6_addr *ret_addr) {
        struct nd_opt_prefix_info *pi;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret_addr, -EINVAL);

        r = get_prefix_info(rt, &pi);
        if (r < 0)
                return r;

        *ret_addr = pi->nd_opt_pi_prefix;
        return 0;
}

int sd_ndisc_router_prefix_get_prefixlen(sd_ndisc_router *rt, unsigned *ret) {
        struct nd_opt_prefix_info *pi;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_prefix_info(rt, &pi);
        if (r < 0)
                return r;

        if (pi->nd_opt_pi_prefix_len > 128)
                return -EBADMSG;

        *ret = pi->nd_opt_pi_prefix_len;
        return 0;
}

static int get_route_info(sd_ndisc_router *rt, uint8_t **ret) {
        uint8_t *ri;
        size_t length;
        int r;

        assert(rt);
        assert(ret);

        r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_ROUTE_INFORMATION);
        if (r < 0)
                return r;
        if (r == 0)
                return -EMEDIUMTYPE;

        length = NDISC_ROUTER_OPTION_LENGTH(rt);
        if (length < 1*8 || length > 3*8)
                return -EBADMSG;

        ri = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex;

        if (ri[2] > 128)
                return -EBADMSG;

        *ret = ri;
        return 0;
}

int sd_ndisc_router_route_get_lifetime(sd_ndisc_router *rt, uint32_t *ret) {
        uint8_t *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_route_info(rt, &ri);
        if (r < 0)
                return r;

        *ret = be32toh(*(uint32_t*) (ri + 4));
        return 0;
}

int sd_ndisc_router_route_get_address(sd_ndisc_router *rt, struct in6_addr *ret_addr) {
        uint8_t *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret_addr, -EINVAL);

        r = get_route_info(rt, &ri);
        if (r < 0)
                return r;

        zero(*ret_addr);
        memcpy(ret_addr, ri + 8, NDISC_ROUTER_OPTION_LENGTH(rt) - 8);

        return 0;
}

int sd_ndisc_router_route_get_prefixlen(sd_ndisc_router *rt, unsigned *ret) {
        uint8_t *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_route_info(rt, &ri);
        if (r < 0)
                return r;

        *ret = ri[2];
        return 0;
}

int sd_ndisc_router_route_get_preference(sd_ndisc_router *rt, unsigned *ret) {
        uint8_t *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_route_info(rt, &ri);
        if (r < 0)
                return r;

        *ret = (ri[3] >> 3) & 3;
        if (!IN_SET(*ret, SD_NDISC_PREFERENCE_LOW, SD_NDISC_PREFERENCE_HIGH))
                *ret = SD_NDISC_PREFERENCE_MEDIUM;

        return 0;
}

static int get_rdnss_info(sd_ndisc_router *rt, uint8_t **ret) {
        size_t length;
        int r;

        assert(rt);
        assert(ret);

        r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_RDNSS);
        if (r < 0)
                return r;
        if (r == 0)
                return -EMEDIUMTYPE;

        length = NDISC_ROUTER_OPTION_LENGTH(rt);
        if (length < 3*8 || (length % (2*8)) != 1*8)
                return -EBADMSG;

        *ret = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex;
        return 0;
}

int sd_ndisc_router_rdnss_get_addresses(sd_ndisc_router *rt, const struct in6_addr **ret) {
        uint8_t *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_rdnss_info(rt, &ri);
        if (r < 0)
                return r;

        *ret = (const struct in6_addr*) (ri + 8);
        return (NDISC_ROUTER_OPTION_LENGTH(rt) - 8) / 16;
}

int sd_ndisc_router_rdnss_get_lifetime(sd_ndisc_router *rt, uint32_t *ret) {
        uint8_t *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_rdnss_info(rt, &ri);
        if (r < 0)
                return r;

        *ret = be32toh(*(uint32_t*) (ri + 4));
        return 0;
}

static int get_dnssl_info(sd_ndisc_router *rt, uint8_t **ret) {
        size_t length;
        int r;

        assert(rt);
        assert(ret);

        r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_DNSSL);
        if (r < 0)
                return r;
        if (r == 0)
                return -EMEDIUMTYPE;

        length = NDISC_ROUTER_OPTION_LENGTH(rt);
        if (length < 2*8)
                return -EBADMSG;

        *ret = (uint8_t*) NDISC_ROUTER_RAW(rt) + rt->rindex;
        return 0;
}

int sd_ndisc_router_dnssl_get_domains(sd_ndisc_router *rt, char ***ret) {
        _cleanup_strv_free_ char **l = NULL;
        _cleanup_free_ char *e = NULL;
        size_t n = 0, left;
        uint8_t *ri, *p;
        bool first = true;
        int r;
        unsigned k = 0;

        assert_return(rt, -EINVAL);
        assert_return(ret, -EINVAL);

        r = get_dnssl_info(rt, &ri);
        if (r < 0)
                return r;

        p = ri + 8;
        left = NDISC_ROUTER_OPTION_LENGTH(rt) - 8;

        for (;;) {
                if (left == 0) {

                        if (n > 0) /* Not properly NUL terminated */
                                return -EBADMSG;

                        break;
                }

                if (*p == 0) {
                        /* Found NUL termination */

                        if (n > 0) {
                                _cleanup_free_ char *normalized = NULL;

                                e[n] = 0;
                                r = dns_name_normalize(e, 0, &normalized);
                                if (r < 0)
                                        return r;

                                /* Ignore the root domain name or "localhost" and friends */
                                if (!is_localhost(normalized) &&
                                    !dns_name_is_root(normalized)) {

                                        if (strv_push(&l, normalized) < 0)
                                                return -ENOMEM;

                                        normalized = NULL;
                                        k++;
                                }
                        }

                        n = 0;
                        first = true;
                        p++, left--;
                        continue;
                }

                /* Check for compression (which is not allowed) */
                if (*p > 63)
                        return -EBADMSG;

                if (1U + *p + 1U > left)
                        return -EBADMSG;

                if (!GREEDY_REALLOC(e, n + !first + DNS_LABEL_ESCAPED_MAX + 1U))
                        return -ENOMEM;

                if (first)
                        first = false;
                else
                        e[n++] = '.';

                r = dns_label_escape((char*) p+1, *p, e + n, DNS_LABEL_ESCAPED_MAX);
                if (r < 0)
                        return r;

                n += r;

                left -= 1 + *p;
                p += 1 + *p;
        }

        if (strv_isempty(l)) {
                *ret = NULL;
                return 0;
        }

        *ret = TAKE_PTR(l);

        return k;
}

int sd_ndisc_router_dnssl_get_lifetime(sd_ndisc_router *rt, uint32_t *ret_sec) {
        uint8_t *ri;
        int r;

        assert_return(rt, -EINVAL);
        assert_return(ret_sec, -EINVAL);

        r = get_dnssl_info(rt, &ri);
        if (r < 0)
                return r;

        *ret_sec = be32toh(*(uint32_t*) (ri + 4));
        return 0;
}

int sd_ndisc_router_captive_portal_get_uri(sd_ndisc_router *rt, const char **ret_uri, size_t *ret_size) {
        int r;
        const char *nd_opt_captive_portal;
        size_t length;

        assert_return(rt, -EINVAL);
        assert_return(ret_uri, -EINVAL);

        r = sd_ndisc_router_option_is_type(rt, SD_NDISC_OPTION_CAPTIVE_PORTAL);
        if (r < 0)
                return r;
        if (r == 0)
                return -EMEDIUMTYPE;

        r = sd_ndisc_router_option_get_raw(rt, (void *)&nd_opt_captive_portal, &length);
        if (r < 0)
                return r;

        /* The length field has units of 8 octets */
        assert(length % 8 == 0);
        if (length == 0)
                return -EBADMSG;

        /* Check that the message is not truncated by an embedded NUL.
         * NUL padding to a multiple of 8 is expected. */
        size_t size = strnlen(nd_opt_captive_portal + 2, length - 2);
        if (DIV_ROUND_UP(size + 2, 8) != length / 8)
                return -EBADMSG;

        /* Let's not return an empty buffer */
        if (size == 0) {
                *ret_uri = NULL;
                *ret_size = 0;
                return 0;
        }

        *ret_uri = nd_opt_captive_portal + 2;
        *ret_size = size;

        return 0;
}
