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

#include <linux/rtnetlink.h>
#include <netinet/in.h>

#include "sd-event.h"
#include "sd-ipv4ll.h"

#include "errno-util.h"
#include "netif-util.h"
#include "networkd-address.h"
#include "networkd-ipv4acd.h"
#include "networkd-ipv4ll.h"
#include "networkd-link.h"
#include "networkd-manager.h"
#include "parse-util.h"
#include "set.h"
#include "string-util.h"

bool link_ipv4ll_enabled(Link *link) {
        assert(link);

        if (!link_ipv4acd_supported(link))
                return false;

        if (!link->network)
                return false;

        if (link->network->bond)
                return false;

        return link->network->link_local & ADDRESS_FAMILY_IPV4;
}

static int address_new_from_ipv4ll(Link *link, Address **ret) {
        _cleanup_(address_unrefp) Address *address = NULL;
        struct in_addr addr;
        int r;

        assert(link);
        assert(link->ipv4ll);
        assert(ret);

        r = sd_ipv4ll_get_address(link->ipv4ll, &addr);
        if (r < 0)
                return r;

        r = address_new(&address);
        if (r < 0)
                return -ENOMEM;

        address->source = NETWORK_CONFIG_SOURCE_IPV4LL;
        address->family = AF_INET;
        address->in_addr.in = addr;
        address->prefixlen = 16;
        address->scope = RT_SCOPE_LINK;
        address->route_metric = IPV4LL_ROUTE_METRIC;

        *ret = TAKE_PTR(address);
        return 0;
}

static int ipv4ll_address_lost(Link *link) {
        _cleanup_(address_unrefp) Address *address = NULL;
        int r;

        assert(link);

        link->ipv4ll_address_configured = false;

        r = address_new_from_ipv4ll(link, &address);
        if (r == -ENOENT)
                return 0;
        if (r < 0)
                return r;

        log_link_debug(link, "IPv4 link-local release "IPV4_ADDRESS_FMT_STR,
                       IPV4_ADDRESS_FMT_VAL(address->in_addr.in));

        return address_remove_and_cancel(address, link);
}

static int ipv4ll_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Request *req, Link *link, Address *address) {
        int r;

        assert(link);
        assert(!link->ipv4ll_address_configured);
        assert(address);

        r = address_configure_handler_internal(m, link, address);
        if (r <= 0)
                return r;

        link->ipv4ll_address_configured = true;
        link_check_ready(link);

        return 1;
}

static int ipv4ll_address_claimed(sd_ipv4ll *ll, Link *link) {
        _cleanup_(address_unrefp) Address *address = NULL;
        int r;

        assert(ll);
        assert(link);

        link->ipv4ll_address_configured = false;

        r = address_new_from_ipv4ll(link, &address);
        if (r == -ENOENT)
                return 0;
        if (r < 0)
                return r;

        log_link_debug(link, "IPv4 link-local claim "IPV4_ADDRESS_FMT_STR,
                       IPV4_ADDRESS_FMT_VAL(address->in_addr.in));

        r = link_request_stacked_netdevs(link, NETDEV_LOCAL_ADDRESS_IPV4LL);
        if (r < 0)
                return r;

        return link_request_address(link, address, NULL, ipv4ll_address_handler, NULL);
}

static void ipv4ll_handler(sd_ipv4ll *ll, int event, void *userdata) {
        Link *link = ASSERT_PTR(userdata);
        int r;

        assert(link->network);

        if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
                return;

        switch (event) {
                case SD_IPV4LL_EVENT_STOP:
                        r = ipv4ll_address_lost(link);
                        if (r < 0) {
                                link_enter_failed(link);
                                return;
                        }
                        break;
                case SD_IPV4LL_EVENT_CONFLICT:
                        r = ipv4ll_address_lost(link);
                        if (r < 0) {
                                link_enter_failed(link);
                                return;
                        }

                        r = sd_ipv4ll_restart(ll);
                        if (r < 0) {
                                log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m");
                                link_enter_failed(link);
                        }
                        break;
                case SD_IPV4LL_EVENT_BIND:
                        r = ipv4ll_address_claimed(ll, link);
                        if (r < 0) {
                                log_link_error(link, "Failed to configure ipv4ll address: %m");
                                link_enter_failed(link);
                                return;
                        }
                        break;
                default:
                        log_link_warning(link, "IPv4 link-local unknown event: %d", event);
        }
}

static int ipv4ll_check_mac(sd_ipv4ll *ll, const struct ether_addr *mac, void *userdata) {
        Manager *m = ASSERT_PTR(userdata);
        struct hw_addr_data hw_addr;

        assert(mac);

        hw_addr = (struct hw_addr_data) {
                .length = ETH_ALEN,
                .ether = *mac,
        };

        return link_get_by_hw_addr(m, &hw_addr, NULL) >= 0;
}

static int ipv4ll_set_address(Link *link) {
        int r;

        assert(link);
        assert(link->network);
        assert(link->ipv4ll);

        /* 1. Use already assigned address. */
        Address *a;
        SET_FOREACH(a, link->addresses) {
                if (a->source != NETWORK_CONFIG_SOURCE_IPV4LL)
                        continue;

                assert(a->family == AF_INET);
                return sd_ipv4ll_set_address(link->ipv4ll, &a->in_addr.in);
        }

        /* 2. If no address is assigned yet, use explicitly configured address. */
        if (in4_addr_is_set(&link->network->ipv4ll_start_address))
                return sd_ipv4ll_set_address(link->ipv4ll, &link->network->ipv4ll_start_address);

        /* 3. If KeepConfiguration=dynamic, use a foreign IPv4LL address. */
        if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DYNAMIC))
                return 0;

        SET_FOREACH(a, link->addresses) {
                if (a->source != NETWORK_CONFIG_SOURCE_FOREIGN)
                        continue;
                if (a->family != AF_INET)
                        continue;
                if (!in4_addr_is_link_local_dynamic(&a->in_addr.in))
                        continue;

                r = sd_ipv4ll_set_address(link->ipv4ll, &a->in_addr.in);
                if (r < 0)
                        return r;

                /* Make sure the address is not removed by link_drop_unmanaged_addresses(). */
                a->source = NETWORK_CONFIG_SOURCE_IPV4LL;
                return 0;
        }

        return 0;
}

int ipv4ll_configure(Link *link) {
        uint64_t seed;
        int r;

        assert(link);
        assert(link->network);

        if (!link_ipv4ll_enabled(link))
                return 0;

        if (link->ipv4ll)
                return 0;

        r = sd_ipv4ll_new(&link->ipv4ll);
        if (r < 0)
                return r;

        r = sd_ipv4ll_attach_event(link->ipv4ll, link->manager->event, 0);
        if (r < 0)
                return r;

        if (link->dev &&
            net_get_unique_predictable_data(link->dev, true, &seed) >= 0) {
                r = sd_ipv4ll_set_address_seed(link->ipv4ll, seed);
                if (r < 0)
                        return r;
        }

        r = ipv4ll_set_address(link);
        if (r < 0)
                return r;

        r = sd_ipv4ll_set_mac(link->ipv4ll, &link->hw_addr.ether);
        if (r < 0)
                return r;

        r = sd_ipv4ll_set_timeout(link->ipv4ll, link->network->ipv4_dad_timeout_usec);
        if (r < 0)
                return r;

        r = sd_ipv4ll_set_ifindex(link->ipv4ll, link->ifindex);
        if (r < 0)
                return r;

        r = sd_ipv4ll_set_callback(link->ipv4ll, ipv4ll_handler, link);
        if (r < 0)
                return r;

        return sd_ipv4ll_set_check_mac_callback(link->ipv4ll, ipv4ll_check_mac, link->manager);
}

int ipv4ll_start(Link *link) {
        int r;

        assert(link);

        if (!link->ipv4ll)
                return 0;

        if (sd_ipv4ll_is_running(link->ipv4ll))
                return 0;

        if (!link_has_carrier(link))
                return 0;

        /* On exit, we cannot and should not start sd-ipv4ll. */
        r = sd_event_get_state(link->manager->event);
        if (r < 0)
                return r;
        if (r == SD_EVENT_FINISHED)
                return 0;

        r = sd_ipv4ll_start(link->ipv4ll);
        if (r < 0)
                return r;

        return 1; /* started */
}

int link_drop_ipv4ll_config(Link *link, Network *network) {
        int ret = 0;

        assert(link);
        assert(link->network);

        if (link->network == network)
                return 0; /* .network file is unchanged. It is not necessary to reconfigure the client. */

        if (!link_ipv4ll_enabled(link)) {
                /* The client is disabled. Stop if it is running, and drop the address. */
                ret = sd_ipv4ll_stop(link->ipv4ll);

                /* Also, explicitly drop the address for the case that this is called on start up.
                 * See also comments in link_drop_dhcp4_config(). */
                Address *a;
                SET_FOREACH(a, link->addresses) {
                        if (a->source != NETWORK_CONFIG_SOURCE_IPV4LL)
                                continue;

                        assert(a->family == AF_INET);
                        RET_GATHER(ret, address_remove_and_cancel(a, link));
                }
        }

        link->ipv4ll = sd_ipv4ll_unref(link->ipv4ll);
        return ret;
}

int ipv4ll_update_mac(Link *link) {
        assert(link);

        if (link->hw_addr.length != ETH_ALEN)
                return 0;
        if (ether_addr_is_null(&link->hw_addr.ether))
                return 0;
        if (!link->ipv4ll)
                return 0;

        return sd_ipv4ll_set_mac(link->ipv4ll, &link->hw_addr.ether);
}

int config_parse_ipv4ll(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        AddressFamily *link_local = ASSERT_PTR(data);
        int r;

        assert(filename);
        assert(lvalue);
        assert(rvalue);

        /* Note that this is mostly like
         * config_parse_address_family(), except that it
         * applies only to IPv4 */

        r = parse_boolean(rvalue);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse %s=%s, ignoring assignment. "
                           "Note that the setting %s= is deprecated, please use LinkLocalAddressing= instead.",
                           lvalue, rvalue, lvalue);
                return 0;
        }

        SET_FLAG(*link_local, ADDRESS_FAMILY_IPV4, r);

        log_syntax(unit, LOG_WARNING, filename, line, 0,
                   "%s=%s is deprecated, please use LinkLocalAddressing=%s instead.",
                   lvalue, rvalue, address_family_to_string(*link_local));

        return 0;
}

int config_parse_ipv4ll_address(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        union in_addr_union a;
        struct in_addr *ipv4ll_address = ASSERT_PTR(data);
        int r;

        assert(filename);
        assert(lvalue);
        assert(rvalue);

        if (isempty(rvalue)) {
                *ipv4ll_address = (struct in_addr) {};
                return 0;
        }

        r = in_addr_from_string(AF_INET, rvalue, &a);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse %s=, ignoring assignment: %s", lvalue, rvalue);
                return 0;
        }
        if (!in4_addr_is_link_local_dynamic(&a.in)) {
                log_syntax(unit, LOG_WARNING, filename, line, 0,
                           "Specified address cannot be used as an IPv4 link local address, ignoring assignment: %s",
                           rvalue);
                return 0;
        }

        *ipv4ll_address = a.in;
        return 0;
}
