/* SPDX-License-Identifier: LGPL-2.1+ */

#include <stdio.h>
#include <stdlib.h>

#include "alloc-util.h"
#include "bpf-firewall.h"
#include "extract-word.h"
#include "hostname-util.h"
#include "ip-address-access.h"
#include "parse-util.h"
#include "string-util.h"

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

        IPAddressAccessItem **list = data;
        const char *p;
        int r;

        assert(list);

        if (isempty(rvalue)) {
                *list = ip_address_access_free_all(*list);
                return 0;
        }

        p = rvalue;

        for (;;) {
                _cleanup_free_ IPAddressAccessItem *a = NULL;
                _cleanup_free_ char *word = NULL;

                r = extract_first_word(&p, &word, NULL, 0);
                if (r == 0)
                        break;
                if (r == -ENOMEM)
                        return log_oom();
                if (r < 0) {
                        log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
                        break;
                }

                a = new0(IPAddressAccessItem, 1);
                if (!a)
                        return log_oom();

                if (streq(word, "any")) {
                        /* "any" is a shortcut for 0.0.0.0/0 and ::/0 */

                        a->family = AF_INET;
                        LIST_APPEND(items, *list, a);

                        a = new0(IPAddressAccessItem, 1);
                        if (!a)
                                return log_oom();

                        a->family = AF_INET6;

                } else if (is_localhost(word)) {
                        /* "localhost" is a shortcut for 127.0.0.0/8 and ::1/128 */

                        a->family = AF_INET;
                        a->address.in.s_addr = htobe32(0x7f000000);
                        a->prefixlen = 8;
                        LIST_APPEND(items, *list, a);

                        a = new0(IPAddressAccessItem, 1);
                        if (!a)
                                return log_oom();

                        a->family = AF_INET6;
                        a->address.in6 = (struct in6_addr) IN6ADDR_LOOPBACK_INIT;
                        a->prefixlen = 128;

                } else if (streq(word, "link-local")) {

                        /* "link-local" is a shortcut for 169.254.0.0/16 and fe80::/64 */

                        a->family = AF_INET;
                        a->address.in.s_addr = htobe32((UINT32_C(169) << 24 | UINT32_C(254) << 16));
                        a->prefixlen = 16;
                        LIST_APPEND(items, *list, a);

                        a = new0(IPAddressAccessItem, 1);
                        if (!a)
                                return log_oom();

                        a->family = AF_INET6;
                        a->address.in6 = (struct in6_addr) {
                                .s6_addr32[0] = htobe32(0xfe800000)
                        };
                        a->prefixlen = 64;

                } else if (streq(word, "multicast")) {

                        /* "multicast" is a shortcut for 224.0.0.0/4 and ff00::/8 */

                        a->family = AF_INET;
                        a->address.in.s_addr = htobe32((UINT32_C(224) << 24));
                        a->prefixlen = 4;
                        LIST_APPEND(items, *list, a);

                        a = new0(IPAddressAccessItem, 1);
                        if (!a)
                                return log_oom();

                        a->family = AF_INET6;
                        a->address.in6 = (struct in6_addr) {
                                .s6_addr32[0] = htobe32(0xff000000)
                        };
                        a->prefixlen = 8;

                } else {
                        r = in_addr_prefix_from_string_auto(word, &a->family, &a->address, &a->prefixlen);
                        if (r < 0) {
                                log_syntax(unit, LOG_WARNING, filename, line, r, "Address prefix is invalid, ignoring assignment: %s", word);
                                return 0;
                        }
                }

                LIST_APPEND(items, *list, a);
                a = NULL;
        }

        *list = ip_address_access_reduce(*list);

        if (*list) {
                r = bpf_firewall_supported();
                if (r < 0)
                        return r;
                if (r == BPF_FIREWALL_UNSUPPORTED) {
                        static bool warned = false;

                        log_full(warned ? LOG_DEBUG : LOG_WARNING,
                                 "File %s:%u configures an IP firewall (%s=%s), but the local system does not support BPF/cgroup based firewalling.\n"
                                 "Proceeding WITHOUT firewalling in effect! (This warning is only shown for the first loaded unit using IP firewalling.)", filename, line, lvalue, rvalue);

                        warned = true;
                }
        }

        return 0;
}

IPAddressAccessItem* ip_address_access_free_all(IPAddressAccessItem *first) {
        IPAddressAccessItem *next, *p = first;

        while (p) {
                next = p->items_next;
                free(p);

                p = next;
        }

        return NULL;
}

IPAddressAccessItem* ip_address_access_reduce(IPAddressAccessItem *first) {
        IPAddressAccessItem *a, *b, *tmp;
        int r;

        /* Drops all entries from the list that are covered by another entry in full, thus removing all redundant
         * entries. */

        LIST_FOREACH_SAFE(items, a, tmp, first) {

                /* Drop irrelevant bits */
                (void) in_addr_mask(a->family, &a->address, a->prefixlen);

                LIST_FOREACH(items, b, first) {

                        if (a == b)
                                continue;

                        if (a->family != b->family)
                                continue;

                        if (b->prefixlen > a->prefixlen)
                                continue;

                        r = in_addr_prefix_covers(b->family,
                                                  &b->address,
                                                  b->prefixlen,
                                                  &a->address);
                        if (r > 0) {
                                /* b covers a fully, then let's drop a */
                                LIST_REMOVE(items, first, a);
                                free(a);
                                break;
                        }
                }
        }

        return first;
}
