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

#include "sd-dhcp-client.h"
#include "sd-ipv4acd.h"

#include "networkd-address.h"
#include "networkd-dhcp4.h"
#include "networkd-ipv4acd.h"
#include "networkd-link.h"
#include "networkd-manager.h"

static int static_address_on_stop(Link *link, Address *address) {
        int r;

        assert(link);
        assert(address);

        if (address_get(link, address, NULL) < 0)
                return 0;

        log_link_debug(link, "Removing address "IPV4_ADDRESS_FMT_STR", as the ACD client is stopped.",
                       IPV4_ADDRESS_FMT_VAL(address->in_addr.in));

        r = address_remove(address, link);
        if (r < 0)
                return log_link_warning_errno(link, r, "Failed to remove address "IPV4_ADDRESS_FMT_STR": %m",
                                              IPV4_ADDRESS_FMT_VAL(address->in_addr.in));

        return 0;
}

static int static_address_on_conflict(Link *link, Address *address) {
        int r;

        assert(link);
        assert(address);

        if (address_get(link, address, NULL) < 0) {
                log_link_warning(link, "Cannot configure requested address "IPV4_ADDRESS_FMT_STR", as an address conflict was detected.",
                                 IPV4_ADDRESS_FMT_VAL(address->in_addr.in));
                return 0;
        }

        log_link_warning(link, "Dropping address "IPV4_ADDRESS_FMT_STR", as an address conflict was detected.",
                         IPV4_ADDRESS_FMT_VAL(address->in_addr.in));

        r = address_remove(address, link);
        if (r < 0)
                return log_link_warning_errno(link, r, "Failed to remove address "IPV4_ADDRESS_FMT_STR": %m",
                                              IPV4_ADDRESS_FMT_VAL(address->in_addr.in));

        return 0;
}

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

        assert(link);
        assert(link->dhcp_client);

        r = sd_dhcp_client_send_decline(link->dhcp_client);
        if (r < 0)
                log_link_warning_errno(link, r, "Failed to send DHCP DECLINE, ignoring: %m");

        if (!link->dhcp_lease)
                /* Unlikely, but during probing the address, the lease may be lost. */
                return 0;

        log_link_warning(link, "Dropping DHCPv4 lease, as an address conflict was detected.");
        r = dhcp4_lease_lost(link);
        if (r < 0)
                return log_link_warning_errno(link, r, "Failed to drop DHCPv4 lease: %m");

        /* It is not necessary to call address_remove() here, as dhcp4_lease_lost() removes it. */
        return 0;
}

static void on_acd(sd_ipv4acd *acd, int event, void *userdata, bool is_static) {
        Address *address = userdata;
        Link *link;
        int r;

        assert(acd);
        assert(address);
        assert(address->acd == acd);
        assert(address->link);
        assert(address->family == AF_INET);

        link = address->link;

        switch (event) {
        case SD_IPV4ACD_EVENT_STOP:
                if (is_static) {
                        r = static_address_on_stop(link, address);
                        if (r < 0)
                                link_enter_failed(link);
                }

                /* We have nothing to do for DHCPv4 lease here, as the dhcp client is already stopped
                 * when stopping the ipv4acd client. See link_stop_engines(). */
                break;

        case SD_IPV4ACD_EVENT_BIND:
                log_link_debug(link, "Successfully claimed address "IPV4_ADDRESS_FMT_STR,
                               IPV4_ADDRESS_FMT_VAL(address->in_addr.in));

                address->acd_announced = true;
                break;

        case SD_IPV4ACD_EVENT_CONFLICT:
                if (is_static)
                        r = static_address_on_conflict(link, address);
                else
                        r = dhcp4_address_on_conflict(link);
                if (r < 0)
                        link_enter_failed(link);
                break;

        default:
                assert_not_reached("Invalid IPv4ACD event.");
        }
}

static void static_address_on_acd(sd_ipv4acd *acd, int event, void *userdata) {
        on_acd(acd, event, userdata, true);
}

static void dhcp4_address_on_acd(sd_ipv4acd *acd, int event, void *userdata) {
        on_acd(acd, event, userdata, false);
}

static int ipv4acd_check_mac(sd_ipv4acd *acd, const struct ether_addr *mac, void *userdata) {
        Manager *m = userdata;
        struct hw_addr_data hw_addr;

        assert(m);
        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 ipv4acd_configure(Link *link, const Address *a) {
        _cleanup_(address_freep) Address *address = NULL;
        int r;

        assert(link);
        assert(a);
        assert(a->family == AF_INET);

        log_link_debug(link, "Configuring IPv4ACD for address "IPV4_ADDRESS_FMT_STR,
                       IPV4_ADDRESS_FMT_VAL(a->in_addr.in));

        r = address_dup(a, &address);
        if (r < 0)
                return r;

        r = set_ensure_put(&link->addresses_ipv4acd, &address_hash_ops, address);
        if (r < 0)
                return r;
        if (r == 0)
                return -EEXIST;
        address->link = link;

        r = sd_ipv4acd_new(&address->acd);
        if (r < 0)
                return r;

        r = sd_ipv4acd_attach_event(address->acd, link->manager->event, 0);
        if (r < 0)
                return r;

        r = sd_ipv4acd_set_ifindex(address->acd, link->ifindex);
        if (r < 0)
                return r;

        r = sd_ipv4acd_set_mac(address->acd, &link->hw_addr.ether);
        if (r < 0)
                return r;

        r = sd_ipv4acd_set_address(address->acd, &address->in_addr.in);
        if (r < 0)
                return r;

        r = sd_ipv4acd_set_callback(address->acd,
                                    address->is_static ? static_address_on_acd : dhcp4_address_on_acd,
                                    address);
        if (r < 0)
                return r;

        r = sd_ipv4acd_set_check_mac_callback(address->acd, ipv4acd_check_mac, link->manager);
        if (r < 0)
                return r;

        if (link_has_carrier(link)) {
                r = sd_ipv4acd_start(address->acd, true);
                if (r < 0)
                        return r;
        }

        TAKE_PTR(address);
        return 0;
}

int ipv4acd_address_is_ready_to_configure(Link *link, const Address *address) {
        Address *acd_address;
        int r;

        acd_address = set_get(link->addresses_ipv4acd, address);
        if (!acd_address) {
                r = ipv4acd_configure(link, address);
                if (r < 0)
                        return log_link_warning_errno(link, r, "Failed to configure IPv4ACD client: %m");

                return false;
        }

        if (!acd_address->acd_announced)
                return false;

        r = set_ensure_put(&link->addresses, &address_hash_ops, acd_address);
        if (r < 0)
                return log_oom();
        if (r == 0)
                return log_link_warning_errno(link, SYNTHETIC_ERRNO(EEXIST), "Address already exists.");

        acd_address->flags |= IFA_F_TENTATIVE;
        return true;
}

int ipv4acd_update_mac(Link *link) {
        Address *address;
        int k, r = 0;

        assert(link);

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

        SET_FOREACH(address, link->addresses_ipv4acd) {
                assert(address->acd);

                k = sd_ipv4acd_set_mac(address->acd, &link->hw_addr.ether);
                if (k < 0)
                        r = k;
        }
        if (r < 0)
                link_enter_failed(link);

        return r;
}

int ipv4acd_start(Link *link) {
        Address *address;
        int r;

        assert(link);

        SET_FOREACH(address, link->addresses_ipv4acd) {
                if (sd_ipv4acd_is_running(address->acd))
                        continue;

                r = sd_ipv4acd_start(address->acd, true);
                if (r < 0)
                        return r;
        }

        return 0;
}

int ipv4acd_stop(Link *link) {
        Address *address;
        int k, r = 0;

        assert(link);

        SET_FOREACH(address, link->addresses_ipv4acd) {
                k = sd_ipv4acd_stop(address->acd);
                if (k < 0)
                        r = k;
        }

        return r;
}
