From: Lennart Poettering Date: Tue, 13 Jan 2015 12:44:30 +0000 (+0100) Subject: shared: add minimal firewall manipulation helpers for establishing NAT rules, using... X-Git-Tag: v219~537 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=elogind.git;a=commitdiff_plain;h=76917807eb50ccde58901e8bec7ed3d408d1cc22 shared: add minimal firewall manipulation helpers for establishing NAT rules, using libiptc --- diff --git a/.gitignore b/.gitignore index 2394e7c20..caef8310f 100644 --- a/.gitignore +++ b/.gitignore @@ -182,6 +182,7 @@ /test-fdset /test-fileio /test-fstab-util +/test-fw-util /test-hashmap /test-hostname /test-icmp6-rs diff --git a/Makefile.am b/Makefile.am index 7f9dc2608..205dafafc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -982,6 +982,24 @@ libsystemd_label_la_LIBADD = \ # ----------------------------------------------------------------------------- +if HAVE_LIBIPTC +noinst_LTLIBRARIES += \ + libsystemd-fw.la + +libsystemd_fw_la_SOURCES = \ + src/shared/fw-util.h \ + src/shared/fw-util.c + +libsystemd_fw_la_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBIPTC_CFLAGS) + +libsystemd_fw_la_LIBADD = \ + $(LIBIPTC_LIBS) +endif + +# ----------------------------------------------------------------------------- + if ENABLE_LDCONFIG dist_systemunit_DATA += \ units/ldconfig.service @@ -1334,7 +1352,8 @@ manual_tests += \ test-watchdog \ test-log \ test-ipcrm \ - test-btrfs + test-btrfs \ + test-fw-util if HAVE_KMOD manual_tests += \ @@ -1815,6 +1834,18 @@ test_btrfs_LDADD = \ libsystemd-label.la \ libsystemd-shared.la +test_fw_util_SOURCES = \ + src/test/test-fw-util.c + +test_fw_util_CFLAGS = \ + $(AM_CFLAGS) \ + $(LIBIPTC_CFLAGS) + +test_fw_util_LDADD = \ + libsystemd-fw.la \ + libsystemd-shared.la \ + $(LIBIPTC_LIBS) + test_rtnl_manual_SOURCES = \ src/test/test-rtnl-manual.c diff --git a/configure.ac b/configure.ac index ddc604b75..5057f8e99 100644 --- a/configure.ac +++ b/configure.ac @@ -869,6 +869,21 @@ if test "x$enable_libidn" != "xno"; then fi AM_CONDITIONAL(HAVE_LIBIDN, [test "$have_libidn" = "yes"]) +# ------------------------------------------------------------------------------ +have_libiptc=no +AC_ARG_ENABLE(libiptc, AS_HELP_STRING([--disable-libiptc], [Disable optional LIBIPTC support])) +if test "x$enable_libiptc" != "xno"; then + PKG_CHECK_MODULES(LIBIPTC, [libiptc], + [AC_DEFINE(HAVE_LIBIPTC, 1, [Define if libiptc is available]) + have_libiptc=yes + M4_DEFINES="$M4_DEFINES -DHAVE_LIBIPTC"], + [have_libiptc=no]) + if test "x$have_libiptc" = "xno" -a "x$enable_libiptc" = "xyes"; then + AC_MSG_ERROR([*** libiptc support requested but libraries not found]) + fi +fi +AM_CONDITIONAL(HAVE_LIBIPTC, [test "$have_libiptc" = "yes"]) + # ------------------------------------------------------------------------------ have_binfmt=no AC_ARG_ENABLE(binfmt, AS_HELP_STRING([--disable-binfmt], [disable binfmt tool])) @@ -1405,6 +1420,7 @@ AC_MSG_RESULT([ GNUTLS: ${have_gnutls} libcurl: ${have_libcurl} libidn: ${have_libidn} + libiptc: ${have_libiptc} ELFUTILS: ${have_elfutils} binfmt: ${have_binfmt} vconsole: ${have_vconsole} diff --git a/src/shared/fw-util.c b/src/shared/fw-util.c new file mode 100644 index 000000000..ceb1ae508 --- /dev/null +++ b/src/shared/fw-util.c @@ -0,0 +1,344 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2015 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" +#include "fw-util.h" + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct xtc_handle*, iptc_free); + +static int entry_fill_basics( + struct ipt_entry *entry, + int protocol, + const char *in_interface, + const union in_addr_union *source, + unsigned source_prefixlen, + const char *out_interface, + const union in_addr_union *destination, + unsigned destination_prefixlen) { + + assert(entry); + + if (out_interface && strlen(out_interface) >= IFNAMSIZ) + return -EINVAL; + + if (in_interface && strlen(in_interface) >= IFNAMSIZ) + return -EINVAL; + + entry->ip.proto = protocol; + + if (in_interface) { + strcpy(entry->ip.iniface, in_interface); + memset(entry->ip.iniface_mask, 0xFF, strlen(in_interface)+1); + } + if (source) { + entry->ip.src = source->in; + in_addr_prefixlen_to_netmask(&entry->ip.smsk, source_prefixlen); + } + + if (out_interface) { + strcpy(entry->ip.outiface, out_interface); + memset(entry->ip.outiface_mask, 0xFF, strlen(out_interface)+1); + } + if (destination) { + entry->ip.dst = destination->in; + in_addr_prefixlen_to_netmask(&entry->ip.dmsk, destination_prefixlen); + } + + return 0; +} + +int fw_add_masquerade( + bool add, + int af, + int protocol, + const union in_addr_union *source, + unsigned source_prefixlen, + const char *out_interface, + const union in_addr_union *destination, + unsigned destination_prefixlen) { + + _cleanup_(iptc_freep) struct xtc_handle *h = NULL; + struct ipt_entry *entry, *mask; + struct ipt_entry_target *t; + size_t sz; + struct nf_nat_ipv4_multi_range_compat *mr; + int r; + + if (af != AF_INET) + return -ENOTSUP; + + if (protocol != 0 && protocol != IPPROTO_TCP && protocol != IPPROTO_UDP) + return -ENOTSUP; + + h = iptc_init("nat"); + if (!h) + return -errno; + + sz = XT_ALIGN(sizeof(struct ipt_entry)) + + XT_ALIGN(sizeof(struct ipt_entry_target)) + + XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)); + + /* Put together the entry we want to add or remove */ + entry = alloca0(sz); + entry->next_offset = sz; + entry->target_offset = XT_ALIGN(sizeof(struct ipt_entry)); + r = entry_fill_basics(entry, protocol, NULL, source, source_prefixlen, out_interface, destination, destination_prefixlen); + if (r < 0) + return r; + + /* Fill in target part */ + t = ipt_get_target(entry); + t->u.target_size = + XT_ALIGN(sizeof(struct ipt_entry_target)) + + XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)); + strncpy(t->u.user.name, "MASQUERADE", sizeof(t->u.user.name)); + mr = (struct nf_nat_ipv4_multi_range_compat*) t->data; + mr->rangesize = 1; + + /* Create a search mask entry */ + mask = alloca(sz); + memset(mask, 0xFF, sz); + + if (add) { + if (iptc_check_entry("POSTROUTING", entry, (unsigned char*) mask, h)) + return 0; + if (errno != ENOENT) /* if other error than not existing yet, fail */ + return -errno; + + if (!iptc_insert_entry("POSTROUTING", entry, 0, h)) + return -errno; + } else { + if (!iptc_delete_entry("POSTROUTING", entry, (unsigned char*) mask, h)) { + if (errno == ENOENT) /* if it's already gone, all is good! */ + return 0; + + return -errno; + } + } + + if (!iptc_commit(h)) + return -errno; + + return 0; +} + +int fw_add_local_dnat( + bool add, + int af, + int protocol, + const char *in_interface, + const union in_addr_union *source, + unsigned source_prefixlen, + const union in_addr_union *destination, + unsigned destination_prefixlen, + uint16_t local_port, + const union in_addr_union *remote, + uint16_t remote_port, + const union in_addr_union *previous_remote) { + + + _cleanup_(iptc_freep) struct xtc_handle *h = NULL; + struct ipt_entry *entry, *mask; + struct ipt_entry_target *t; + struct ipt_entry_match *m; + struct xt_addrtype_info_v1 *at; + struct nf_nat_ipv4_multi_range_compat *mr; + size_t sz, msz; + int r; + + assert(add || !previous_remote); + + if (af != AF_INET) + return -ENOTSUP; + + if (protocol != IPPROTO_TCP && protocol != IPPROTO_UDP) + return -ENOTSUP; + + if (local_port <= 0) + return -EINVAL; + + if (remote_port <= 0) + return -EINVAL; + + h = iptc_init("nat"); + if (!h) + return -errno; + + sz = XT_ALIGN(sizeof(struct ipt_entry)) + + XT_ALIGN(sizeof(struct ipt_entry_match)) + + XT_ALIGN(sizeof(struct xt_addrtype_info_v1)) + + XT_ALIGN(sizeof(struct ipt_entry_target)) + + XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)); + + if (protocol == IPPROTO_TCP) + msz = XT_ALIGN(sizeof(struct ipt_entry_match)) + + XT_ALIGN(sizeof(struct xt_tcp)); + else + msz = XT_ALIGN(sizeof(struct ipt_entry_match)) + + XT_ALIGN(sizeof(struct xt_udp)); + + sz += msz; + + /* Fill in basic part */ + entry = alloca0(sz); + entry->next_offset = sz; + entry->target_offset = + XT_ALIGN(sizeof(struct ipt_entry)) + + XT_ALIGN(sizeof(struct ipt_entry_match)) + + XT_ALIGN(sizeof(struct xt_addrtype_info_v1)) + + msz; + r = entry_fill_basics(entry, protocol, in_interface, source, source_prefixlen, NULL, destination, destination_prefixlen); + if (r < 0) + return r; + + /* Fill in first match */ + m = (struct ipt_entry_match*) ((uint8_t*) entry + XT_ALIGN(sizeof(struct ipt_entry))); + m->u.match_size = msz; + if (protocol == IPPROTO_TCP) { + struct xt_tcp *tcp; + + strncpy(m->u.user.name, "tcp", sizeof(m->u.user.name)); + tcp = (struct xt_tcp*) m->data; + tcp->dpts[0] = tcp->dpts[1] = local_port; + tcp->spts[0] = 0; + tcp->spts[1] = 0xFFFF; + + } else { + struct xt_udp *udp; + + strncpy(m->u.user.name, "udp", sizeof(m->u.user.name)); + udp = (struct xt_udp*) m->data; + udp->dpts[0] = udp->dpts[1] = local_port; + udp->spts[0] = 0; + udp->spts[1] = 0xFFFF; + } + + /* Fill in second match */ + m = (struct ipt_entry_match*) ((uint8_t*) entry + XT_ALIGN(sizeof(struct ipt_entry)) + msz); + m->u.match_size = + XT_ALIGN(sizeof(struct ipt_entry_match)) + + XT_ALIGN(sizeof(struct xt_addrtype_info_v1)); + strncpy(m->u.user.name, "addrtype", sizeof(m->u.user.name)); + m->u.user.revision = 1; + at = (struct xt_addrtype_info_v1*) m->data; + at->dest = XT_ADDRTYPE_LOCAL; + + /* Fill in target part */ + t = ipt_get_target(entry); + t->u.target_size = + XT_ALIGN(sizeof(struct ipt_entry_target)) + + XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)); + strncpy(t->u.user.name, "DNAT", sizeof(t->u.user.name)); + mr = (struct nf_nat_ipv4_multi_range_compat*) t->data; + mr->rangesize = 1; + mr->range[0].flags = NF_NAT_RANGE_PROTO_SPECIFIED|NF_NAT_RANGE_MAP_IPS; + mr->range[0].min_ip = mr->range[0].max_ip = remote->in.s_addr; + if (protocol == IPPROTO_TCP) + mr->range[0].min.tcp.port = mr->range[0].max.tcp.port = htons(remote_port); + else + mr->range[0].min.udp.port = mr->range[0].max.udp.port = htons(remote_port); + + mask = alloca0(sz); + memset(mask, 0xFF, sz); + + if (add) { + /* Add the PREROUTING rule, if it is missing so far */ + if (!iptc_check_entry("PREROUTING", entry, (unsigned char*) mask, h)) { + if (errno != ENOENT) + return -EINVAL; + + if (!iptc_insert_entry("PREROUTING", entry, 0, h)) + return -errno; + } + + /* If a previous remote is set, remove its entry */ + if (previous_remote && previous_remote->in.s_addr != remote->in.s_addr) { + mr->range[0].min_ip = mr->range[0].max_ip = previous_remote->in.s_addr; + + if (!iptc_delete_entry("PREROUTING", entry, (unsigned char*) mask, h)) { + if (errno != ENOENT) + return -errno; + } + + mr->range[0].min_ip = mr->range[0].max_ip = remote->in.s_addr; + } + + /* Add the OUTPUT rule, if it is missing so far */ + if (!in_interface) { + + /* Don't apply onto loopback addresses */ + if (!destination) { + entry->ip.dst.s_addr = htobe32(0x7F000000); + entry->ip.dmsk.s_addr = htobe32(0xFF000000); + entry->ip.invflags = IPT_INV_DSTIP; + } + + if (!iptc_check_entry("OUTPUT", entry, (unsigned char*) mask, h)) { + if (errno != ENOENT) + return -errno; + + if (!iptc_insert_entry("OUTPUT", entry, 0, h)) + return -errno; + } + + /* If a previous remote is set, remove its entry */ + if (previous_remote && previous_remote->in.s_addr != remote->in.s_addr) { + mr->range[0].min_ip = mr->range[0].max_ip = previous_remote->in.s_addr; + + if (!iptc_delete_entry("OUTPUT", entry, (unsigned char*) mask, h)) { + if (errno != ENOENT) + return -errno; + } + } + } + } else { + if (!iptc_delete_entry("PREROUTING", entry, (unsigned char*) mask, h)) { + if (errno != ENOENT) + return -errno; + } + + if (!in_interface) { + if (!destination) { + entry->ip.dst.s_addr = htobe32(0x7F000000); + entry->ip.dmsk.s_addr = htobe32(0xFF000000); + entry->ip.invflags = IPT_INV_DSTIP; + } + + if (!iptc_delete_entry("OUTPUT", entry, (unsigned char*) mask, h)) { + if (errno != ENOENT) + return -errno; + } + } + } + + if (!iptc_commit(h)) + return -errno; + + return 0; +} diff --git a/src/shared/fw-util.h b/src/shared/fw-util.h new file mode 100644 index 000000000..58b4c2024 --- /dev/null +++ b/src/shared/fw-util.h @@ -0,0 +1,82 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +#pragma once + +/*** + This file is part of systemd. + + Copyright 2015 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include "in-addr-util.h" + +#ifdef HAVE_LIBIPTC + +int fw_add_masquerade( + bool add, + int af, + int protocol, + const union in_addr_union *source, + unsigned source_prefixlen, + const char *out_interface, + const union in_addr_union *destination, + unsigned destination_prefixlen); + +int fw_add_local_dnat( + bool add, + int af, + int protocol, + const char *in_interface, + const union in_addr_union *source, + unsigned source_prefixlen, + const union in_addr_union *destination, + unsigned destination_prefixlen, + uint16_t local_port, + const union in_addr_union *remote, + uint16_t remote_port, + const union in_addr_union *previous_remote); + +#else + +static inline int fw_add_masquerade( + bool add, + int af, + int protocol, + const union in_addr_union *source, + unsigned source_prefixlen, + const char *out_interface, + const union in_addr_union *destination, + unsigned destination_prefixlen) { + return -ENOTSUP; +} + +static inline int fw_add_local_dnat( + bool add, + int af, + int protocol, + const char *in_interface, + const union in_addr_union *source, + unsigned source_prefixlen, + const union in_addr_union *destination, + unsigned destination_prefixlen, + uint16_t local_port, + const union in_addr_union *remote, + uint16_t remote_port, + const union in_addr_union *previous_remote) { + return -ENOSTUP; +} + +#endif diff --git a/src/shared/in-addr-util.c b/src/shared/in-addr-util.c index 9dc9ec82b..b02e7516c 100644 --- a/src/shared/in-addr-util.c +++ b/src/shared/in-addr-util.c @@ -243,12 +243,25 @@ int in_addr_from_string_auto(const char *s, int *family, union in_addr_union *re return -EINVAL; } -unsigned in_addr_netmask_to_prefixlen(const struct in_addr *addr) { +unsigned char in_addr_netmask_to_prefixlen(const struct in_addr *addr) { assert(addr); return 32 - u32ctz(be32toh(addr->s_addr)); } +struct in_addr* in_addr_prefixlen_to_netmask(struct in_addr *addr, unsigned char prefixlen) { + assert(addr); + assert(prefixlen <= 32); + + /* Shifting beyond 32 is not defined, handle this specially. */ + if (prefixlen == 0) + addr->s_addr = 0; + else + addr->s_addr = htobe32((0xffffffff << (32 - prefixlen)) & 0xffffffff); + + return addr; +} + int in_addr_default_prefixlen(const struct in_addr *addr, unsigned char *prefixlen) { uint8_t msb_octet = *(uint8_t*) addr; @@ -284,9 +297,6 @@ int in_addr_default_subnet_mask(const struct in_addr *addr, struct in_addr *mask if (r < 0) return r; - assert(prefixlen > 0 && prefixlen < 32); - - mask->s_addr = htobe32((0xffffffff << (32 - prefixlen)) & 0xffffffff); - + in_addr_prefixlen_to_netmask(mask, prefixlen); return 0; } diff --git a/src/shared/in-addr-util.h b/src/shared/in-addr-util.h index 8da030ceb..4cf4418b4 100644 --- a/src/shared/in-addr-util.h +++ b/src/shared/in-addr-util.h @@ -39,7 +39,8 @@ int in_addr_prefix_next(int family, union in_addr_union *u, unsigned prefixlen); int in_addr_to_string(int family, const union in_addr_union *u, char **ret); int in_addr_from_string(int family, const char *s, union in_addr_union *ret); int in_addr_from_string_auto(const char *s, int *family, union in_addr_union *ret); -unsigned in_addr_netmask_to_prefixlen(const struct in_addr *addr); +unsigned char in_addr_netmask_to_prefixlen(const struct in_addr *addr); +struct in_addr* in_addr_prefixlen_to_netmask(struct in_addr *addr, unsigned char prefixlen); int in_addr_default_prefixlen(const struct in_addr *addr, unsigned char *prefixlen); int in_addr_default_subnet_mask(const struct in_addr *addr, struct in_addr *mask); diff --git a/src/test/test-fw-util.c b/src/test/test-fw-util.c new file mode 100644 index 000000000..ab891aa0c --- /dev/null +++ b/src/test/test-fw-util.c @@ -0,0 +1,60 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2015 Lennart Poettering + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include "log.h" +#include "fw-util.h" + +#define MAKE_IN_ADDR_UNION(a,b,c,d) (union in_addr_union) { .in.s_addr = htobe32((uint32_t) (a) << 24 | (uint32_t) (b) << 16 | (uint32_t) (c) << 8 | (uint32_t) (d))} + +int main(int argc, char *argv[]) { + int r; + log_set_max_level(LOG_DEBUG); + + r = fw_add_masquerade(true, AF_INET, 0, NULL, 0, "foobar", NULL, 0); + if (r < 0) + log_error_errno(r, "Failed to modify firewall: %m"); + + r = fw_add_masquerade(true, AF_INET, 0, NULL, 0, "foobar", NULL, 0); + if (r < 0) + log_error_errno(r, "Failed to modify firewall: %m"); + + r = fw_add_masquerade(false, AF_INET, 0, NULL, 0, "foobar", NULL, 0); + if (r < 0) + log_error_errno(r, "Failed to modify firewall: %m"); + + r = fw_add_local_dnat(true, AF_INET, IPPROTO_TCP, NULL, NULL, 0, NULL, 0, 4711, &MAKE_IN_ADDR_UNION(1, 2, 3, 4), 815, NULL); + if (r < 0) + log_error_errno(r, "Failed to modify firewall: %m"); + + r = fw_add_local_dnat(true, AF_INET, IPPROTO_TCP, NULL, NULL, 0, NULL, 0, 4711, &MAKE_IN_ADDR_UNION(1, 2, 3, 4), 815, NULL); + if (r < 0) + log_error_errno(r, "Failed to modify firewall: %m"); + + r = fw_add_local_dnat(true, AF_INET, IPPROTO_TCP, NULL, NULL, 0, NULL, 0, 4711, &MAKE_IN_ADDR_UNION(1, 2, 3, 5), 815, &MAKE_IN_ADDR_UNION(1, 2, 3, 4)); + if (r < 0) + log_error_errno(r, "Failed to modify firewall: %m"); + + r = fw_add_local_dnat(false, AF_INET, IPPROTO_TCP, NULL, NULL, 0, NULL, 0, 4711, &MAKE_IN_ADDR_UNION(1, 2, 3, 5), 815, NULL); + if (r < 0) + log_error_errno(r, "Failed to modify firewall: %m"); + + return 0; +}