/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <getopt.h>
#include <netinet/icmp6.h>

#include "sd-ndisc-protocol.h"
#include "sd-netlink.h"

#include "alloc-util.h"
#include "build.h"
#include "ether-addr-util.h"
#include "fd-util.h"
#include "hexdecoct.h"
#include "icmp6-util.h"
#include "in-addr-util.h"
#include "main-func.h"
#include "ndisc-option.h"
#include "netlink-util.h"
#include "network-common.h"
#include "parse-util.h"
#include "set.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"

static int arg_ifindex = 0;
static int arg_icmp6_type = 0;
static union in_addr_union arg_dest = IN_ADDR_NULL;
static uint8_t arg_hop_limit = 0;
static uint8_t arg_ra_flags = 0;
static uint8_t arg_preference = false;
static usec_t arg_lifetime = 0;
static usec_t arg_reachable = 0;
static usec_t arg_retransmit = 0;
static uint32_t arg_na_flags = 0;
static union in_addr_union arg_target_address = IN_ADDR_NULL;
static union in_addr_union arg_redirect_destination = IN_ADDR_NULL;
static bool arg_set_source_mac = false;
static struct ether_addr arg_source_mac = {};
static bool arg_set_target_mac = false;
static struct ether_addr arg_target_mac = {};
static struct ip6_hdr *arg_redirected_header = NULL;
static bool arg_set_mtu = false;
static uint32_t arg_mtu = 0;

STATIC_DESTRUCTOR_REGISTER(arg_redirected_header, freep);

static int parse_icmp6_type(const char *str) {
        if (STR_IN_SET(str, "router-solicit", "rs", "RS"))
                return ND_ROUTER_SOLICIT;
        if (STR_IN_SET(str, "router-advertisement", "ra", "RA"))
                return ND_ROUTER_ADVERT;
        if (STR_IN_SET(str, "neighbor-solicit", "ns", "NS"))
                return ND_NEIGHBOR_SOLICIT;
        if (STR_IN_SET(str, "neighbor-advertisement", "na", "NA"))
                return ND_NEIGHBOR_ADVERT;
        if (STR_IN_SET(str, "redirect", "rd", "RD"))
                return ND_REDIRECT;
        return -EINVAL;
}

static int parse_preference(const char *str) {
        if (streq(str, "low"))
                return SD_NDISC_PREFERENCE_LOW;
        if (streq(str, "medium"))
                return SD_NDISC_PREFERENCE_MEDIUM;
        if (streq(str, "high"))
                return SD_NDISC_PREFERENCE_HIGH;
        if (streq(str, "reserved"))
                return SD_NDISC_PREFERENCE_RESERVED;
        return -EINVAL;
}

static int parse_argv(int argc, char *argv[]) {
        enum {
                ARG_VERSION = 0x100,
                ARG_RA_HOP_LIMIT,
                ARG_RA_MANAGED,
                ARG_RA_OTHER,
                ARG_RA_HOME_AGENT,
                ARG_RA_PREFERENCE,
                ARG_RA_LIFETIME,
                ARG_RA_REACHABLE,
                ARG_RA_RETRANSMIT,
                ARG_NA_ROUTER,
                ARG_NA_SOLICITED,
                ARG_NA_OVERRIDE,
                ARG_TARGET_ADDRESS,
                ARG_REDIRECT_DESTINATION,
                ARG_OPTION_SOURCE_LL,
                ARG_OPTION_TARGET_LL,
                ARG_OPTION_REDIRECTED_HEADER,
                ARG_OPTION_MTU,
        };

        static const struct option options[] = {
                { "version",              no_argument,       NULL, ARG_VERSION                  },
                { "interface",            required_argument, NULL, 'i'                          },
                { "type",                 required_argument, NULL, 't'                          },
                { "dest",                 required_argument, NULL, 'd'                          },
                /* For Router Advertisement */
                { "hop-limit",            required_argument, NULL, ARG_RA_HOP_LIMIT             },
                { "managed",              required_argument, NULL, ARG_RA_MANAGED               },
                { "other",                required_argument, NULL, ARG_RA_OTHER                 },
                { "home-agent",           required_argument, NULL, ARG_RA_HOME_AGENT            },
                { "preference",           required_argument, NULL, ARG_RA_PREFERENCE            },
                { "lifetime",             required_argument, NULL, ARG_RA_LIFETIME              },
                { "reachable-time",       required_argument, NULL, ARG_RA_REACHABLE             },
                { "retransmit-timer",     required_argument, NULL, ARG_RA_RETRANSMIT            },
                /* For Neighbor Advertisement */
                { "is-router",            required_argument, NULL, ARG_NA_ROUTER                },
                { "is-solicited",         required_argument, NULL, ARG_NA_SOLICITED             },
                { "is-override",          required_argument, NULL, ARG_NA_OVERRIDE              },
                /* For Neighbor Solicit, Neighbor Advertisement, and Redirect */
                { "target-address",       required_argument, NULL, ARG_TARGET_ADDRESS           },
                /* For Redirect */
                { "redirect-destination", required_argument, NULL, ARG_REDIRECT_DESTINATION     },
                /* Options */
                { "source-ll-address",    required_argument, NULL, ARG_OPTION_SOURCE_LL         },
                { "target-ll-address",    required_argument, NULL, ARG_OPTION_TARGET_LL         },
                { "redirected-header",    required_argument, NULL, ARG_OPTION_REDIRECTED_HEADER },
                { "mtu",                  required_argument, NULL, ARG_OPTION_MTU               },
                {}
        };

        _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
        int c, r;

        assert(argc >= 0);
        assert(argv);

        while ((c = getopt_long(argc, argv, "i:t:d:", options, NULL)) >= 0) {

                switch (c) {

                case ARG_VERSION:
                        return version();

                case 'i':
                        r = rtnl_resolve_interface_or_warn(&rtnl, optarg);
                        if (r < 0)
                                return r;
                        arg_ifindex = r;
                        break;

                case 't':
                        r = parse_icmp6_type(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse message type: %m");
                        arg_icmp6_type = r;
                        break;

                case 'd':
                        r = in_addr_from_string(AF_INET6, optarg, &arg_dest);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse destination address: %m");
                        if (!in6_addr_is_link_local(&arg_dest.in6))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                       "The destination address %s is not a link-local address.", optarg);
                        break;

                case ARG_RA_HOP_LIMIT:
                        r = safe_atou8(optarg, &arg_hop_limit);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse hop limit: %m");
                        break;

                case ARG_RA_MANAGED:
                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse managed flag: %m");
                        SET_FLAG(arg_ra_flags, ND_RA_FLAG_MANAGED, r);
                        break;

                case ARG_RA_OTHER:
                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse other flag: %m");
                        SET_FLAG(arg_ra_flags, ND_RA_FLAG_OTHER, r);
                        break;

                case ARG_RA_HOME_AGENT:
                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse home-agent flag: %m");
                        SET_FLAG(arg_ra_flags, ND_RA_FLAG_HOME_AGENT, r);
                        break;

                case ARG_RA_PREFERENCE:
                        r = parse_preference(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse preference: %m");
                        arg_preference = r;
                        break;

                case ARG_RA_LIFETIME:
                        r = parse_sec(optarg, &arg_lifetime);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse lifetime: %m");
                        break;

                case ARG_RA_REACHABLE:
                        r = parse_sec(optarg, &arg_reachable);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse reachable time: %m");
                        break;

                case ARG_RA_RETRANSMIT:
                        r = parse_sec(optarg, &arg_retransmit);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse retransmit timer: %m");
                        break;

                case ARG_NA_ROUTER:
                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse is-router flag: %m");
                        SET_FLAG(arg_na_flags, ND_NA_FLAG_ROUTER, r);
                        break;

                case ARG_NA_SOLICITED:
                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse is-solicited flag: %m");
                        SET_FLAG(arg_na_flags, ND_NA_FLAG_SOLICITED, r);
                        break;

                case ARG_NA_OVERRIDE:
                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse is-override flag: %m");
                        SET_FLAG(arg_na_flags, ND_NA_FLAG_OVERRIDE, r);
                        break;

                case ARG_TARGET_ADDRESS:
                        r = in_addr_from_string(AF_INET6, optarg, &arg_target_address);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse target address: %m");
                        break;

                case ARG_REDIRECT_DESTINATION:
                        r = in_addr_from_string(AF_INET6, optarg, &arg_redirect_destination);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse destination address: %m");
                        break;

                case ARG_OPTION_SOURCE_LL:
                        r = parse_boolean(optarg);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse source LL address option: %m");
                        arg_set_source_mac = r;
                        break;

                case ARG_OPTION_TARGET_LL:
                        r = parse_ether_addr(optarg, &arg_target_mac);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse target LL address option: %m");
                        arg_set_target_mac = true;
                        break;

                case ARG_OPTION_REDIRECTED_HEADER: {
                        _cleanup_free_ void *p = NULL;
                        size_t len;

                        r = unbase64mem(optarg, &p, &len);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse redirected header: %m");

                        if (len < sizeof(struct ip6_hdr))
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid redirected header.");

                        arg_redirected_header = TAKE_PTR(p);
                        break;
                }
                case ARG_OPTION_MTU:
                        r = safe_atou32(optarg, &arg_mtu);
                        if (r < 0)
                                return log_error_errno(r, "Failed to parse MTU: %m");
                        arg_set_mtu = true;
                        break;

                case '?':
                        return -EINVAL;

                default:
                        assert_not_reached();
                }
        }

        if (arg_ifindex <= 0)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--interface/-i option is mandatory.");

        if (arg_icmp6_type <= 0)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--type/-t option is mandatory.");

        if (in6_addr_is_null(&arg_dest.in6)) {
                if (IN_SET(arg_icmp6_type, ND_ROUTER_ADVERT, ND_NEIGHBOR_ADVERT, ND_REDIRECT))
                        arg_dest.in6 = IN6_ADDR_ALL_NODES_MULTICAST;
                else
                        arg_dest.in6 = IN6_ADDR_ALL_ROUTERS_MULTICAST;
        }

        if (arg_set_source_mac) {
                struct hw_addr_data hw_addr;

                r = rtnl_get_link_hw_addr(&rtnl, arg_ifindex, &hw_addr);
                if (r < 0)
                        return log_error_errno(r, "Failed to get the source link-layer address: %m");

                if (hw_addr.length != sizeof(struct ether_addr))
                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
                                               "Unsupported hardware address length %zu.",
                                               hw_addr.length);

                arg_source_mac = hw_addr.ether;
        }

        return 1;
}

static int send_icmp6(int fd, const struct icmp6_hdr *hdr) {
        _cleanup_set_free_ Set *options = NULL;
        int r;

        assert(fd >= 0);
        assert(hdr);

        if (arg_set_source_mac) {
                r = ndisc_option_add_link_layer_address(&options, 0, SD_NDISC_OPTION_SOURCE_LL_ADDRESS, &arg_source_mac);
                if (r < 0)
                        return r;
        }

        if (arg_set_target_mac) {
                r = ndisc_option_add_link_layer_address(&options, 0, SD_NDISC_OPTION_TARGET_LL_ADDRESS, &arg_target_mac);
                if (r < 0)
                        return r;
        }

        if (arg_redirected_header) {
                r = ndisc_option_add_redirected_header(&options, 0, arg_redirected_header);
                if (r < 0)
                        return r;
        }

        if (arg_set_mtu) {
                r = ndisc_option_add_mtu(&options, 0, arg_mtu);
                if (r < 0)
                        return r;
        }

        return ndisc_send(fd, &arg_dest.in6, hdr, options, now(CLOCK_BOOTTIME));
}

static int send_router_solicit(int fd) {
        struct nd_router_solicit hdr = {
                .nd_rs_type = ND_ROUTER_SOLICIT,
        };

        assert(fd >= 0);

        return send_icmp6(fd, &hdr.nd_rs_hdr);
}

static int send_router_advertisement(int fd) {
        struct nd_router_advert hdr = {
                .nd_ra_type = ND_ROUTER_ADVERT,
                .nd_ra_router_lifetime = usec_to_be16_sec(arg_lifetime),
                .nd_ra_reachable = usec_to_be32_msec(arg_reachable),
                .nd_ra_retransmit = usec_to_be32_msec(arg_retransmit),
        };

        assert(fd >= 0);

        /* The nd_ra_curhoplimit and nd_ra_flags_reserved fields cannot specified with nd_ra_router_lifetime
         * simultaneously in the structured initializer in the above. */
        hdr.nd_ra_curhoplimit = arg_hop_limit;
        hdr.nd_ra_flags_reserved = arg_ra_flags;

        return send_icmp6(fd, &hdr.nd_ra_hdr);
}

static int send_neighbor_solicit(int fd) {
        struct nd_neighbor_solicit hdr = {
                .nd_ns_type = ND_NEIGHBOR_SOLICIT,
                .nd_ns_target = arg_target_address.in6,
        };

        assert(fd >= 0);

        return send_icmp6(fd, &hdr.nd_ns_hdr);
}

static int send_neighbor_advertisement(int fd) {
        struct nd_neighbor_advert hdr = {
                .nd_na_type = ND_NEIGHBOR_ADVERT,
                .nd_na_flags_reserved = arg_na_flags,
                .nd_na_target = arg_target_address.in6,
        };

        assert(fd >= 0);

        return send_icmp6(fd, &hdr.nd_na_hdr);
}

static int send_redirect(int fd) {
        struct nd_redirect hdr = {
                .nd_rd_type = ND_REDIRECT,
                .nd_rd_target = arg_target_address.in6,
                .nd_rd_dst = arg_redirect_destination.in6,
        };

        assert(fd >= 0);

        return send_icmp6(fd, &hdr.nd_rd_hdr);
}

static int run(int argc, char *argv[]) {
        _cleanup_close_ int fd = -EBADF;
        int r;

        log_setup();

        r = parse_argv(argc, argv);
        if (r <= 0)
                return r;

        fd = icmp6_bind(arg_ifindex, /* is_router = */ false);
        if (fd < 0)
                return log_error_errno(fd, "Failed to bind socket to interface: %m");

        switch (arg_icmp6_type) {
        case ND_ROUTER_SOLICIT:
                return send_router_solicit(fd);
        case ND_ROUTER_ADVERT:
                return send_router_advertisement(fd);
        case ND_NEIGHBOR_SOLICIT:
                return send_neighbor_solicit(fd);
        case ND_NEIGHBOR_ADVERT:
                return send_neighbor_advertisement(fd);
        case ND_REDIRECT:
                return send_redirect(fd);
        default:
                assert_not_reached();
        }

        return 0;
}

DEFINE_MAIN_FUNCTION(run);
