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

#include <linux/if_arp.h>
#include <linux/if_macsec.h>

#include "sd-netlink.h"

#include "alloc-util.h"
#include "conf-parser.h"
#include "fileio.h"
#include "hashmap.h"
#include "hexdecoct.h"
#include "macsec.h"
#include "memory-util.h"
#include "netlink-util.h"
#include "networkd-manager.h"
#include "parse-helpers.h"
#include "parse-util.h"
#include "string-util.h"
#include "unaligned.h"

#define SECURITY_ASSOCIATION_NULL               \
        (SecurityAssociation) {                 \
                .activate = -1,                 \
                .use_for_encoding = -1,         \
        }

static void security_association_clear(SecurityAssociation *sa) {
        if (!sa)
                return;

        explicit_bzero_safe(sa->key, sa->key_len);
        free(sa->key);
        free(sa->key_file);
}

static ReceiveAssociation* macsec_receive_association_free(ReceiveAssociation *c) {
        if (!c)
                return NULL;

        if (c->macsec && c->section)
                ordered_hashmap_remove(c->macsec->receive_associations_by_section, c->section);

        config_section_free(c->section);
        security_association_clear(&c->sa);

        return mfree(c);
}

DEFINE_SECTION_CLEANUP_FUNCTIONS(ReceiveAssociation, macsec_receive_association_free);

DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
                receive_association_hash_ops_by_section,
                ConfigSection, config_section_hash_func, config_section_compare_func,
                ReceiveAssociation, macsec_receive_association_free);

static int macsec_receive_association_new_static(MACsec *s, const char *filename, unsigned section_line, ReceiveAssociation **ret) {
        _cleanup_(config_section_freep) ConfigSection *n = NULL;
        _cleanup_(macsec_receive_association_freep) ReceiveAssociation *c = NULL;
        int r;

        assert(s);
        assert(ret);
        assert(filename);
        assert(section_line > 0);

        r = config_section_new(filename, section_line, &n);
        if (r < 0)
                return r;

        c = ordered_hashmap_get(s->receive_associations_by_section, n);
        if (c) {
                *ret = TAKE_PTR(c);
                return 0;
        }

        c = new(ReceiveAssociation, 1);
        if (!c)
                return -ENOMEM;

        *c = (ReceiveAssociation) {
                .macsec = s,
                .section = TAKE_PTR(n),
                .sa = SECURITY_ASSOCIATION_NULL,
        };

        r = ordered_hashmap_ensure_put(&s->receive_associations_by_section, &receive_association_hash_ops_by_section, c->section, c);
        if (r < 0)
                return r;

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

static ReceiveChannel* macsec_receive_channel_free(ReceiveChannel *c) {
        if (!c)
                return NULL;

        if (c->macsec) {
                if (c->sci.as_uint64 > 0)
                        ordered_hashmap_remove_value(c->macsec->receive_channels, &c->sci.as_uint64, c);

                if (c->section)
                        ordered_hashmap_remove(c->macsec->receive_channels_by_section, c->section);
        }

        config_section_free(c->section);

        return mfree(c);
}

DEFINE_SECTION_CLEANUP_FUNCTIONS(ReceiveChannel, macsec_receive_channel_free);

DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
                receive_channel_hash_ops,
                uint64_t, uint64_hash_func, uint64_compare_func,
                ReceiveChannel, macsec_receive_channel_free);

DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
                receive_channel_hash_ops_by_section,
                ConfigSection, config_section_hash_func, config_section_compare_func,
                ReceiveChannel, macsec_receive_channel_free);

static int macsec_receive_channel_new(MACsec *s, uint64_t sci, ReceiveChannel **ret) {
        ReceiveChannel *c;

        assert(s);

        c = new(ReceiveChannel, 1);
        if (!c)
                return -ENOMEM;

        *c = (ReceiveChannel) {
                .macsec = s,
                .sci.as_uint64 = sci,
        };

        *ret = c;
        return 0;
}

static int macsec_receive_channel_new_static(MACsec *s, const char *filename, unsigned section_line, ReceiveChannel **ret) {
        _cleanup_(config_section_freep) ConfigSection *n = NULL;
        _cleanup_(macsec_receive_channel_freep) ReceiveChannel *c = NULL;
        int r;

        assert(s);
        assert(ret);
        assert(filename);
        assert(section_line > 0);

        r = config_section_new(filename, section_line, &n);
        if (r < 0)
                return r;

        c = ordered_hashmap_get(s->receive_channels_by_section, n);
        if (c) {
                *ret = TAKE_PTR(c);
                return 0;
        }

        r = macsec_receive_channel_new(s, 0, &c);
        if (r < 0)
                return r;

        c->section = TAKE_PTR(n);

        r = ordered_hashmap_ensure_put(&s->receive_channels_by_section, &receive_channel_hash_ops_by_section, c->section, c);
        if (r < 0)
                return r;

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

static TransmitAssociation* macsec_transmit_association_free(TransmitAssociation *a) {
        if (!a)
                return NULL;

        if (a->macsec && a->section)
                ordered_hashmap_remove(a->macsec->transmit_associations_by_section, a->section);

        config_section_free(a->section);
        security_association_clear(&a->sa);

        return mfree(a);
}

DEFINE_SECTION_CLEANUP_FUNCTIONS(TransmitAssociation, macsec_transmit_association_free);

DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
                transmit_association_hash_ops_by_section,
                ConfigSection, config_section_hash_func, config_section_compare_func,
                TransmitAssociation, macsec_transmit_association_free);

static int macsec_transmit_association_new_static(MACsec *s, const char *filename, unsigned section_line, TransmitAssociation **ret) {
        _cleanup_(config_section_freep) ConfigSection *n = NULL;
        _cleanup_(macsec_transmit_association_freep) TransmitAssociation *a = NULL;
        int r;

        assert(s);
        assert(ret);
        assert(filename);
        assert(section_line > 0);

        r = config_section_new(filename, section_line, &n);
        if (r < 0)
                return r;

        a = ordered_hashmap_get(s->transmit_associations_by_section, n);
        if (a) {
                *ret = TAKE_PTR(a);
                return 0;
        }

        a = new(TransmitAssociation, 1);
        if (!a)
                return -ENOMEM;

        *a = (TransmitAssociation) {
                .macsec = s,
                .section = TAKE_PTR(n),
                .sa = SECURITY_ASSOCIATION_NULL,
        };

        r = ordered_hashmap_ensure_put(&s->transmit_associations_by_section, &transmit_association_hash_ops_by_section, a->section, a);
        if (r < 0)
                return r;

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

static int netdev_macsec_create_message(NetDev *netdev, int command, sd_netlink_message **ret) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        assert(netdev);
        assert(netdev->ifindex > 0);
        assert(netdev->manager);

        r = sd_genl_message_new(netdev->manager->genl, MACSEC_GENL_NAME, command, &m);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u32(m, MACSEC_ATTR_IFINDEX, netdev->ifindex);
        if (r < 0)
                return r;

        *ret = TAKE_PTR(m);

        return 0;
}

static int netdev_macsec_fill_message_sci(NetDev *netdev, MACsecSCI *sci, sd_netlink_message *m) {
        int r;

        assert(netdev);
        assert(m);
        assert(sci);

        r = sd_netlink_message_open_container(m, MACSEC_ATTR_RXSC_CONFIG);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u64(m, MACSEC_RXSC_ATTR_SCI, sci->as_uint64);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(m);
        if (r < 0)
                return r;

        return 0;
}

static int netdev_macsec_fill_message_sa(NetDev *netdev, SecurityAssociation *a, sd_netlink_message *m) {
        int r;

        assert(netdev);
        assert(a);
        assert(m);

        r = sd_netlink_message_open_container(m, MACSEC_ATTR_SA_CONFIG);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_AN, a->association_number);
        if (r < 0)
                return r;

        if (a->packet_number > 0) {
                r = sd_netlink_message_append_u32(m, MACSEC_SA_ATTR_PN, a->packet_number);
                if (r < 0)
                        return r;
        }

        if (a->key_len > 0) {
                r = sd_netlink_message_append_data(m, MACSEC_SA_ATTR_KEYID, a->key_id, MACSEC_KEYID_LEN);
                if (r < 0)
                        return r;

                r = sd_netlink_message_append_data(m, MACSEC_SA_ATTR_KEY, a->key, a->key_len);
                if (r < 0)
                        return r;
        }

        if (a->activate >= 0) {
                r = sd_netlink_message_append_u8(m, MACSEC_SA_ATTR_ACTIVE, a->activate);
                if (r < 0)
                        return r;
        }

        r = sd_netlink_message_close_container(m);
        if (r < 0)
                return r;

        return 0;
}

static int macsec_receive_association_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
        int r;

        assert(netdev);
        assert(netdev->state != _NETDEV_STATE_INVALID);

        r = sd_netlink_message_get_errno(m);
        if (r == -EEXIST)
                log_netdev_info(netdev,
                                "MACsec receive secure association exists, using it without changing parameters");
        else if (r < 0) {
                log_netdev_warning_errno(netdev, r,
                                         "Failed to add receive secure association: %m");
                netdev_enter_failed(netdev);

                return 1;
        }

        log_netdev_debug(netdev, "Receive secure association is configured");

        return 1;
}

static int netdev_macsec_configure_receive_association(NetDev *netdev, ReceiveAssociation *a) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        assert(netdev);
        assert(a);

        if (!netdev_is_managed(netdev))
                return 0; /* Already detached, due to e.g. reloading .netdev files. */

        r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_RXSA, &m);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");

        r = netdev_macsec_fill_message_sa(netdev, &a->sa, m);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");

        r = netdev_macsec_fill_message_sci(netdev, &a->sci, m);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");

        r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_receive_association_handler,
                               netdev_destroy_callback, netdev);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to configure receive secure association: %m");

        netdev_ref(netdev);

        return 0;
}

static int macsec_receive_channel_handler(sd_netlink *rtnl, sd_netlink_message *m, ReceiveChannel *c) {
        assert(c);
        assert(c->macsec);

        NetDev *netdev = ASSERT_PTR(NETDEV(c->macsec));
        int r;

        assert(netdev->state != _NETDEV_STATE_INVALID);

        r = sd_netlink_message_get_errno(m);
        if (r == -EEXIST)
                log_netdev_debug(netdev,
                                 "MACsec receive channel exists, using it without changing parameters");
        else if (r < 0) {
                log_netdev_warning_errno(netdev, r,
                                         "Failed to add receive secure channel: %m");
                netdev_enter_failed(netdev);

                return 1;
        }

        log_netdev_debug(netdev, "Receive channel is configured");

        for (unsigned i = 0; i < c->n_rxsa; i++) {
                r = netdev_macsec_configure_receive_association(netdev, c->rxsa[i]);
                if (r < 0) {
                        log_netdev_warning_errno(netdev, r,
                                                 "Failed to configure receive security association: %m");
                        netdev_enter_failed(netdev);
                        return 1;
                }
        }

        return 1;
}

static void receive_channel_destroy_callback(ReceiveChannel *c) {
        assert(c);
        assert(c->macsec);

        netdev_unref(NETDEV(c->macsec));
}

static int netdev_macsec_configure_receive_channel(NetDev *netdev, ReceiveChannel *c) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        assert(netdev);
        assert(c);

        if (!netdev_is_managed(netdev))
                return 0; /* Already detached, due to e.g. reloading .netdev files. */

        r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_RXSC, &m);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");

        r = netdev_macsec_fill_message_sci(netdev, &c->sci, m);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");

        r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_receive_channel_handler,
                               receive_channel_destroy_callback, c);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to configure receive channel: %m");

        netdev_ref(netdev);

        return 0;
}

static int macsec_transmit_association_handler(sd_netlink *rtnl, sd_netlink_message *m, NetDev *netdev) {
        int r;

        assert(netdev);
        assert(netdev->state != _NETDEV_STATE_INVALID);

        r = sd_netlink_message_get_errno(m);
        if (r == -EEXIST)
                log_netdev_info(netdev,
                                "MACsec transmit secure association exists, using it without changing parameters");
        else if (r < 0) {
                log_netdev_warning_errno(netdev, r,
                                         "Failed to add transmit secure association: %m");
                netdev_enter_failed(netdev);

                return 1;
        }

        log_netdev_debug(netdev, "Transmit secure association is configured");

        return 1;
}

static int netdev_macsec_configure_transmit_association(NetDev *netdev, TransmitAssociation *a) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL;
        int r;

        assert(netdev);
        assert(a);

        if (!netdev_is_managed(netdev))
                return 0; /* Already detached, due to e.g. reloading .netdev files. */

        r = netdev_macsec_create_message(netdev, MACSEC_CMD_ADD_TXSA, &m);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to create netlink message: %m");

        r = netdev_macsec_fill_message_sa(netdev, &a->sa, m);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to fill netlink message: %m");

        r = netlink_call_async(netdev->manager->genl, NULL, m, macsec_transmit_association_handler,
                               netdev_destroy_callback, netdev);
        if (r < 0)
                return log_netdev_error_errno(netdev, r, "Failed to configure transmit secure association: %m");

        netdev_ref(netdev);

        return 0;
}

static int netdev_macsec_configure(NetDev *netdev, Link *link) {
        MACsec *s = MACSEC(netdev);
        TransmitAssociation *a;
        ReceiveChannel *c;
        int r;

        ORDERED_HASHMAP_FOREACH(a, s->transmit_associations_by_section) {
                r = netdev_macsec_configure_transmit_association(netdev, a);
                if (r < 0)
                        return r;
        }

        ORDERED_HASHMAP_FOREACH(c, s->receive_channels) {
                r = netdev_macsec_configure_receive_channel(netdev, c);
                if (r < 0)
                        return r;
        }

        return 0;
}

static int netdev_macsec_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
        assert(m);

        MACsec *v = MACSEC(netdev);
        int r;

        if (v->encrypt >= 0) {
                r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCRYPT, v->encrypt);
                if (r < 0)
                        return r;
        }

        r = sd_netlink_message_append_u8(m, IFLA_MACSEC_ENCODING_SA, v->encoding_an);
        if (r < 0)
                return r;

        /* The properties below cannot be updated, and the kernel refuses the whole request if one of the
         * following attributes is set for an existing interface. */
        if (netdev->ifindex > 0)
                return 0;

        if (v->port > 0) {
                r = sd_netlink_message_append_u16(m, IFLA_MACSEC_PORT, v->port);
                if (r < 0)
                        return r;
        }

        /* Currently not supported by networkd, but IFLA_MACSEC_CIPHER_SUITE, IFLA_MACSEC_ICV_LEN, and
         * IFLA_MACSEC_SCI can neither set for an existing interface. */

        return 0;
}

int config_parse_macsec_port(
                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) {

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        MACsec *s = ASSERT_PTR(userdata);
        _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
        _cleanup_(macsec_receive_channel_free_or_set_invalidp) ReceiveChannel *c = NULL;
        uint16_t port;
        void *dest;
        int r;

        /* This parses port used to make Secure Channel Identifier (SCI) */

        if (streq(section, "MACsec"))
                dest = &s->port;
        else if (streq(section, "MACsecReceiveChannel")) {
                r = macsec_receive_channel_new_static(s, filename, section_line, &c);
                if (r < 0)
                        return log_oom();

                dest = &c->sci.port;
        } else {
                assert(streq(section, "MACsecReceiveAssociation"));

                r = macsec_receive_association_new_static(s, filename, section_line, &b);
                if (r < 0)
                        return log_oom();

                dest = &b->sci.port;
        }

        r = parse_ip_port(rvalue, &port);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse port '%s' for secure channel identifier. Ignoring assignment: %m",
                           rvalue);
                return 0;
        }

        unaligned_write_be16(dest, port);

        TAKE_PTR(b);
        TAKE_PTR(c);

        return 0;
}

int config_parse_macsec_hw_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) {

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        MACsec *s = ASSERT_PTR(userdata);
        _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
        _cleanup_(macsec_receive_channel_free_or_set_invalidp) ReceiveChannel *c = NULL;
        int r;

        if (streq(section, "MACsecReceiveChannel"))
                r = macsec_receive_channel_new_static(s, filename, section_line, &c);
        else
                r = macsec_receive_association_new_static(s, filename, section_line, &b);
        if (r < 0)
                return log_oom();

        r = parse_ether_addr(rvalue, b ? &b->sci.mac : &c->sci.mac);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse MAC address for secure channel identifier. "
                           "Ignoring assignment: %s", rvalue);
                return 0;
        }

        TAKE_PTR(b);
        TAKE_PTR(c);

        return 0;
}

int config_parse_macsec_packet_number(
                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) {

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        MACsec *s = ASSERT_PTR(userdata);
        _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
        _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
        uint32_t val, *dest;
        int r;

        if (streq(section, "MACsecTransmitAssociation"))
                r = macsec_transmit_association_new_static(s, filename, section_line, &a);
        else
                r = macsec_receive_association_new_static(s, filename, section_line, &b);
        if (r < 0)
                return log_oom();

        dest = a ? &a->sa.packet_number : &b->sa.packet_number;

        r = safe_atou32(rvalue, &val);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse packet number. Ignoring assignment: %s", rvalue);
                return 0;
        }
        if (streq(section, "MACsecTransmitAssociation") && val == 0) {
                log_syntax(unit, LOG_WARNING, filename, line, 0,
                           "Invalid packet number. Ignoring assignment: %s", rvalue);
                return 0;
        }

        *dest = val;
        TAKE_PTR(a);
        TAKE_PTR(b);

        return 0;
}

int config_parse_macsec_key(
                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) {

        _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
        _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
        _cleanup_(erase_and_freep) void *p = NULL;
        MACsec *s = userdata;
        SecurityAssociation *dest;
        size_t l;
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        (void) warn_file_is_world_accessible(filename, NULL, unit, line);

        if (streq(section, "MACsecTransmitAssociation"))
                r = macsec_transmit_association_new_static(s, filename, section_line, &a);
        else
                r = macsec_receive_association_new_static(s, filename, section_line, &b);
        if (r < 0)
                return log_oom();

        dest = a ? &a->sa : &b->sa;

        r = unhexmem_full(rvalue, SIZE_MAX, /* secure = */ true, &p, &l);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse key. Ignoring assignment: %m");
                return 0;
        }

        if (l != 16) {
                /* See DEFAULT_SAK_LEN in drivers/net/macsec.c */
                log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid key length (%zu). Ignoring assignment", l);
                return 0;
        }

        explicit_bzero_safe(dest->key, dest->key_len);
        free_and_replace(dest->key, p);
        dest->key_len = l;

        TAKE_PTR(a);
        TAKE_PTR(b);

        return 0;
}

int config_parse_macsec_key_file(
                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) {

        _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
        _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
        _cleanup_free_ char *path = NULL;
        MACsec *s = userdata;
        char **dest;
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        if (streq(section, "MACsecTransmitAssociation"))
                r = macsec_transmit_association_new_static(s, filename, section_line, &a);
        else
                r = macsec_receive_association_new_static(s, filename, section_line, &b);
        if (r < 0)
                return log_oom();

        dest = a ? &a->sa.key_file : &b->sa.key_file;

        if (isempty(rvalue)) {
                *dest = mfree(*dest);
                return 0;
        }

        path = strdup(rvalue);
        if (!path)
                return log_oom();

        if (path_simplify_and_warn(path, PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS, unit, filename, line, lvalue) < 0)
                return 0;

        free_and_replace(*dest, path);
        TAKE_PTR(a);
        TAKE_PTR(b);

        return 0;
}

int config_parse_macsec_key_id(
                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) {

        _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
        _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
        _cleanup_free_ void *p = NULL;
        MACsec *s = userdata;
        uint8_t *dest;
        size_t l;
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        if (streq(section, "MACsecTransmitAssociation"))
                r = macsec_transmit_association_new_static(s, filename, section_line, &a);
        else
                r = macsec_receive_association_new_static(s, filename, section_line, &b);
        if (r < 0)
                return log_oom();

        r = unhexmem(rvalue, &p, &l);
        if (r == -ENOMEM)
                return log_oom();
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse KeyId=%s, ignoring assignment: %m", rvalue);
                return 0;
        }
        if (l > MACSEC_KEYID_LEN) {
                log_syntax(unit, LOG_WARNING, filename, line, 0,
                           "Specified KeyId= is larger then the allowed maximum (%zu > %i), ignoring: %s",
                           l, MACSEC_KEYID_LEN, rvalue);
                return 0;
        }

        dest = a ? a->sa.key_id : b->sa.key_id;
        memcpy_safe(dest, p, l);
        memzero(dest + l, MACSEC_KEYID_LEN - l);

        TAKE_PTR(a);
        TAKE_PTR(b);

        return 0;
}

int config_parse_macsec_sa_activate(
                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) {

        _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
        _cleanup_(macsec_receive_association_free_or_set_invalidp) ReceiveAssociation *b = NULL;
        MACsec *s = userdata;
        int *dest, r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        if (streq(section, "MACsecTransmitAssociation"))
                r = macsec_transmit_association_new_static(s, filename, section_line, &a);
        else
                r = macsec_receive_association_new_static(s, filename, section_line, &b);
        if (r < 0)
                return log_oom();

        dest = a ? &a->sa.activate : &b->sa.activate;

        r = parse_tristate(rvalue, dest);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse activation mode of %s security association. "
                           "Ignoring assignment: %s",
                           streq(section, "MACsecTransmitAssociation") ? "transmit" : "receive",
                           rvalue);
                return 0;
        }

        TAKE_PTR(a);
        TAKE_PTR(b);

        return 0;
}

int config_parse_macsec_use_for_encoding(
                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) {

        _cleanup_(macsec_transmit_association_free_or_set_invalidp) TransmitAssociation *a = NULL;
        MACsec *s = userdata;
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        r = macsec_transmit_association_new_static(s, filename, section_line, &a);
        if (r < 0)
                return log_oom();

        if (isempty(rvalue)) {
                a->sa.use_for_encoding = -1;
                TAKE_PTR(a);
                return 0;
        }

        r = parse_tristate(rvalue, &a->sa.use_for_encoding);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse %s= setting. Ignoring assignment: %s",
                           lvalue, rvalue);
                return 0;
        }

        if (a->sa.use_for_encoding > 0)
                a->sa.activate = true;

        TAKE_PTR(a);

        return 0;
}

static int macsec_read_key_file(NetDev *netdev, SecurityAssociation *sa) {
        _cleanup_(erase_and_freep) uint8_t *key = NULL;
        size_t key_len;
        int r;

        assert(netdev);
        assert(sa);

        if (!sa->key_file)
                return 0;

        r = read_full_file_full(
                        AT_FDCWD, sa->key_file, UINT64_MAX, MACSEC_KEYID_LEN,
                        READ_FULL_FILE_SECURE |
                        READ_FULL_FILE_UNHEX |
                        READ_FULL_FILE_WARN_WORLD_READABLE |
                        READ_FULL_FILE_CONNECT_SOCKET |
                        READ_FULL_FILE_FAIL_WHEN_LARGER,
                        NULL, (char **) &key, &key_len);
        if (r < 0)
                return log_netdev_error_errno(netdev, r,
                                              "Failed to read key from '%s', ignoring: %m",
                                              sa->key_file);

        if (key_len != MACSEC_KEYID_LEN)
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "Invalid key length (%zu bytes), ignoring.", key_len);

        explicit_bzero_safe(sa->key, sa->key_len);
        free_and_replace(sa->key, key);
        sa->key_len = key_len;

        return 0;
}

static int macsec_receive_channel_verify(ReceiveChannel *c) {
        NetDev *netdev;
        int r;

        assert(c);
        assert(c->macsec);

        netdev = NETDEV(c->macsec);

        if (section_is_invalid(c->section))
                return -EINVAL;

        if (ether_addr_is_null(&c->sci.mac))
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "%s: MACsec receive channel without MAC address configured. "
                                              "Ignoring [MACsecReceiveChannel] section from line %u",
                                              c->section->filename, c->section->line);

        if (c->sci.port == 0)
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "%s: MACsec receive channel without port configured. "
                                              "Ignoring [MACsecReceiveChannel] section from line %u",
                                              c->section->filename, c->section->line);

        r = ordered_hashmap_ensure_put(&c->macsec->receive_channels, &receive_channel_hash_ops, &c->sci.as_uint64, c);
        if (r == -ENOMEM)
                return log_oom();
        if (r == -EEXIST)
                return log_netdev_error_errno(netdev, r,
                                              "%s: Multiple [MACsecReceiveChannel] sections have same SCI, "
                                              "Ignoring [MACsecReceiveChannel] section from line %u",
                                              c->section->filename, c->section->line);
        if (r < 0)
                return log_netdev_error_errno(netdev, r,
                                              "%s: Failed to store [MACsecReceiveChannel] section at hashmap, "
                                              "Ignoring [MACsecReceiveChannel] section from line %u",
                                              c->section->filename, c->section->line);
        return 0;
}

static int macsec_transmit_association_verify(TransmitAssociation *t) {
        NetDev *netdev;
        int r;

        assert(t);
        assert(t->macsec);

        netdev = NETDEV(t->macsec);

        if (section_is_invalid(t->section))
                return -EINVAL;

        if (t->sa.packet_number == 0)
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "%s: MACsec transmit secure association without PacketNumber= configured. "
                                              "Ignoring [MACsecTransmitAssociation] section from line %u",
                                              t->section->filename, t->section->line);

        r = macsec_read_key_file(netdev, &t->sa);
        if (r < 0)
                return r;

        if (t->sa.key_len <= 0)
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "%s: MACsec transmit secure association without key configured. "
                                              "Ignoring [MACsecTransmitAssociation] section from line %u",
                                              t->section->filename, t->section->line);

        return 0;
}

static int macsec_receive_association_verify(ReceiveAssociation *a) {
        ReceiveChannel *c;
        NetDev *netdev;
        int r;

        assert(a);
        assert(a->macsec);

        netdev = NETDEV(a->macsec);

        if (section_is_invalid(a->section))
                return -EINVAL;

        r = macsec_read_key_file(netdev, &a->sa);
        if (r < 0)
                return r;

        if (a->sa.key_len <= 0)
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "%s: MACsec receive secure association without key configured. "
                                              "Ignoring [MACsecReceiveAssociation] section from line %u",
                                              a->section->filename, a->section->line);

        if (ether_addr_is_null(&a->sci.mac))
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "%s: MACsec receive secure association without MAC address configured. "
                                              "Ignoring [MACsecReceiveAssociation] section from line %u",
                                              a->section->filename, a->section->line);

        if (a->sci.port == 0)
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
                                              "%s: MACsec receive secure association without port configured. "
                                              "Ignoring [MACsecReceiveAssociation] section from line %u",
                                              a->section->filename, a->section->line);

        c = ordered_hashmap_get(a->macsec->receive_channels, &a->sci.as_uint64);
        if (!c) {
                _cleanup_(macsec_receive_channel_freep) ReceiveChannel *new_channel = NULL;

                r = macsec_receive_channel_new(a->macsec, a->sci.as_uint64, &new_channel);
                if (r < 0)
                        return log_oom();

                r = ordered_hashmap_ensure_put(&a->macsec->receive_channels, &receive_channel_hash_ops, &new_channel->sci.as_uint64, new_channel);
                if (r == -ENOMEM)
                        return log_oom();
                if (r < 0)
                        return log_netdev_error_errno(netdev, r,
                                                      "%s: Failed to store receive channel at hashmap, "
                                                      "Ignoring [MACsecReceiveAssociation] section from line %u",
                                                      a->section->filename, a->section->line);
                c = TAKE_PTR(new_channel);
        }
        if (c->n_rxsa >= MACSEC_MAX_ASSOCIATION_NUMBER)
                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(ERANGE),
                                              "%s: Too many [MACsecReceiveAssociation] sections for the same receive channel, "
                                              "Ignoring [MACsecReceiveAssociation] section from line %u",
                                              a->section->filename, a->section->line);

        a->sa.association_number = c->n_rxsa;
        c->rxsa[c->n_rxsa++] = a;

        return 0;
}

static int netdev_macsec_verify(NetDev *netdev, const char *filename) {
        assert(filename);

        MACsec *v = MACSEC(netdev);
        TransmitAssociation *a;
        ReceiveAssociation *n;
        ReceiveChannel *c;
        uint8_t an, encoding_an;
        bool use_for_encoding;
        int r;

        ORDERED_HASHMAP_FOREACH(c, v->receive_channels_by_section) {
                r = macsec_receive_channel_verify(c);
                if (r < 0)
                        macsec_receive_channel_free(c);
        }

        an = 0;
        use_for_encoding = false;
        encoding_an = 0;
        ORDERED_HASHMAP_FOREACH(a, v->transmit_associations_by_section) {
                r = macsec_transmit_association_verify(a);
                if (r < 0) {
                        macsec_transmit_association_free(a);
                        continue;
                }

                if (an >= MACSEC_MAX_ASSOCIATION_NUMBER) {
                        log_netdev_error(netdev,
                                         "%s: Too many [MACsecTransmitAssociation] sections configured. "
                                         "Ignoring [MACsecTransmitAssociation] section from line %u",
                                         a->section->filename, a->section->line);
                        macsec_transmit_association_free(a);
                        continue;
                }

                a->sa.association_number = an++;

                if (a->sa.use_for_encoding > 0) {
                        if (use_for_encoding) {
                                log_netdev_warning(netdev,
                                                   "%s: Multiple security associations are set to be used for transmit channel."
                                                   "Disabling UseForEncoding= in [MACsecTransmitAssociation] section from line %u",
                                                   a->section->filename, a->section->line);
                                a->sa.use_for_encoding = false;
                        } else {
                                encoding_an = a->sa.association_number;
                                use_for_encoding = true;
                        }
                }
        }

        assert(encoding_an < MACSEC_MAX_ASSOCIATION_NUMBER);
        v->encoding_an = encoding_an;

        ORDERED_HASHMAP_FOREACH(n, v->receive_associations_by_section) {
                r = macsec_receive_association_verify(n);
                if (r < 0)
                        macsec_receive_association_free(n);
        }

        return 0;
}

static void macsec_init(NetDev *netdev) {
        MACsec *v = MACSEC(netdev);

        v->encrypt = -1;
}

static void macsec_done(NetDev *netdev) {
        MACsec *v = MACSEC(netdev);

        ordered_hashmap_free(v->receive_channels);
        ordered_hashmap_free(v->receive_channels_by_section);
        ordered_hashmap_free(v->transmit_associations_by_section);
        ordered_hashmap_free(v->receive_associations_by_section);
}

const NetDevVTable macsec_vtable = {
        .object_size = sizeof(MACsec),
        .init = macsec_init,
        .sections = NETDEV_COMMON_SECTIONS "MACsec\0MACsecReceiveChannel\0MACsecTransmitAssociation\0MACsecReceiveAssociation\0",
        .fill_message_create = netdev_macsec_fill_message_create,
        .post_create = netdev_macsec_configure,
        .done = macsec_done,
        .create_type = NETDEV_CREATE_STACKED,
        .config_verify = netdev_macsec_verify,
        .iftype = ARPHRD_ETHER,
        .generate_mac = true,
};
