/* * virfirewall.c: integration with firewalls * * Copyright (C) 2013 Red Hat, Inc. * * This library 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. * * This library 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 this library. If not, see * . * * Authors: * Daniel P. Berrange */ #include #define __VIR_FIREWALL_PRIV_H_ALLOW__ #include #include "viralloc.h" #include "virfirewallpriv.h" #include "virerror.h" #include "virutil.h" #include "virstring.h" #include "vircommand.h" #include "virlog.h" #include "virdbus.h" #include "virfile.h" #include "virthread.h" #define VIR_FROM_THIS VIR_FROM_FIREWALL VIR_LOG_INIT("util.firewall"); typedef struct _virFirewallGroup virFirewallGroup; typedef virFirewallGroup *virFirewallGroupPtr; VIR_ENUM_DECL(virFirewallLayerCommand) VIR_ENUM_IMPL(virFirewallLayerCommand, VIR_FIREWALL_LAYER_LAST, EBTABLES_PATH, IPTABLES_PATH, IP6TABLES_PATH); VIR_ENUM_DECL(virFirewallLayerFirewallD) VIR_ENUM_IMPL(virFirewallLayerFirewallD, VIR_FIREWALL_LAYER_LAST, "eb", "ipv4", "ipv6") struct _virFirewallRule { virFirewallLayer layer; virFirewallQueryCallback queryCB; void *queryOpaque; bool ignoreErrors; size_t argsAlloc; size_t argsLen; char **args; }; struct _virFirewallGroup { unsigned int actionFlags; unsigned int rollbackFlags; size_t naction; virFirewallRulePtr *action; size_t nrollback; virFirewallRulePtr *rollback; bool addingRollback; }; struct _virFirewall { int err; size_t ngroups; virFirewallGroupPtr *groups; size_t currentGroup; }; static virFirewallBackend currentBackend = VIR_FIREWALL_BACKEND_AUTOMATIC; static virMutex ruleLock = VIR_MUTEX_INITIALIZER; static int virFirewallValidateBackend(virFirewallBackend backend); static int virFirewallOnceInit(void) { return virFirewallValidateBackend(currentBackend); } VIR_ONCE_GLOBAL_INIT(virFirewall) static bool iptablesUseLock; static bool ip6tablesUseLock; static bool ebtablesUseLock; static void virFirewallCheckUpdateLock(bool *lockflag, const char *const*args) { virCommandPtr cmd = virCommandNewArgs(args); if (virCommandRun(cmd, NULL) < 0) { VIR_INFO("locking not supported by %s", args[0]); } else { VIR_INFO("using locking for %s", args[0]); *lockflag = true; } virCommandFree(cmd); } static void virFirewallCheckUpdateLocking(void) { const char *iptablesArgs[] = { IPTABLES_PATH, "-w", "-L", "-n", NULL, }; const char *ip6tablesArgs[] = { IP6TABLES_PATH, "-w", "-L", "-n", NULL, }; const char *ebtablesArgs[] = { EBTABLES_PATH, "--concurrent", "-L", NULL, }; virFirewallCheckUpdateLock(&iptablesUseLock, iptablesArgs); virFirewallCheckUpdateLock(&ip6tablesUseLock, ip6tablesArgs); virFirewallCheckUpdateLock(&ebtablesUseLock, ebtablesArgs); } static int virFirewallValidateBackend(virFirewallBackend backend) { VIR_DEBUG("Validating backend %d", backend); #if WITH_DBUS if (backend == VIR_FIREWALL_BACKEND_AUTOMATIC || backend == VIR_FIREWALL_BACKEND_FIREWALLD) { int rv = virDBusIsServiceRegistered(VIR_FIREWALL_FIREWALLD_SERVICE); VIR_DEBUG("Firewalld is registered ? %d", rv); if (rv < 0) { if (rv == -2) { if (backend == VIR_FIREWALL_BACKEND_FIREWALLD) { virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("firewalld firewall backend requested, but service is not running")); return -1; } else { VIR_DEBUG("firewalld service not running, trying direct backend"); backend = VIR_FIREWALL_BACKEND_DIRECT; } } else { return -1; } } else { VIR_DEBUG("firewalld service running, using firewalld backend"); backend = VIR_FIREWALL_BACKEND_FIREWALLD; } } #else if (backend == VIR_FIREWALL_BACKEND_AUTOMATIC) { VIR_DEBUG("DBus support disabled, trying direct backend"); backend = VIR_FIREWALL_BACKEND_DIRECT; } else if (backend == VIR_FIREWALL_BACKEND_FIREWALLD) { virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("firewalld firewall backend requested, but DBus support disabled")); return -1; } #endif if (backend == VIR_FIREWALL_BACKEND_DIRECT) { const char *commands[] = { IPTABLES_PATH, IP6TABLES_PATH, EBTABLES_PATH }; size_t i; for (i = 0; i < ARRAY_CARDINALITY(commands); i++) { if (!virFileIsExecutable(commands[i])) { virReportSystemError(errno, _("direct firewall backend requested, but %s is not available"), commands[i]); return -1; } } VIR_DEBUG("found iptables/ip6tables/ebtables, using direct backend"); } currentBackend = backend; virFirewallCheckUpdateLocking(); return 0; } int virFirewallSetBackend(virFirewallBackend backend) { currentBackend = backend; if (virFirewallInitialize() < 0) return -1; return virFirewallValidateBackend(backend); } static virFirewallGroupPtr virFirewallGroupNew(void) { virFirewallGroupPtr group; if (VIR_ALLOC(group) < 0) return NULL; return group; } /** * virFirewallNew: * * Creates a new firewall ruleset for changing rules * of @layer. This should be followed by a call to * virFirewallStartTransaction before adding * any rules * * Returns the new firewall ruleset */ virFirewallPtr virFirewallNew(void) { virFirewallPtr firewall; if (virFirewallInitialize() < 0) return NULL; if (VIR_ALLOC(firewall) < 0) return NULL; return firewall; } static void virFirewallRuleFree(virFirewallRulePtr rule) { size_t i; if (!rule) return; for (i = 0; i < rule->argsLen; i++) VIR_FREE(rule->args[i]); VIR_FREE(rule->args); VIR_FREE(rule); } static void virFirewallGroupFree(virFirewallGroupPtr group) { size_t i; if (!group) return; for (i = 0; i < group->naction; i++) virFirewallRuleFree(group->action[i]); VIR_FREE(group->action); for (i = 0; i < group->nrollback; i++) virFirewallRuleFree(group->rollback[i]); VIR_FREE(group->rollback); VIR_FREE(group); } /** * virFirewallFree: * * Release all memory associated with the firewall * ruleset */ void virFirewallFree(virFirewallPtr firewall) { size_t i; if (!firewall) return; for (i = 0; i < firewall->ngroups; i++) virFirewallGroupFree(firewall->groups[i]); VIR_FREE(firewall->groups); VIR_FREE(firewall); } #define VIR_FIREWALL_RETURN_IF_ERROR(firewall) \ do { \ if (!firewall || firewall->err) \ return; \ } while (0) #define VIR_FIREWALL_RULE_RETURN_IF_ERROR(firewall, rule)\ do { \ if (!firewall || firewall->err || !rule) \ return; \ } while (0) #define VIR_FIREWALL_RETURN_NULL_IF_ERROR(firewall) \ do { \ if (!firewall || firewall->err) \ return NULL; \ } while (0) #define ADD_ARG(rule, str) \ do { \ if (VIR_RESIZE_N(rule->args, \ rule->argsAlloc, \ rule->argsLen, 1) < 0) \ goto no_memory; \ \ if (VIR_STRDUP(rule->args[rule->argsLen++], str) < 0) \ goto no_memory; \ } while (0) static virFirewallRulePtr virFirewallAddRuleFullV(virFirewallPtr firewall, virFirewallLayer layer, bool ignoreErrors, virFirewallQueryCallback cb, void *opaque, va_list args) { virFirewallGroupPtr group; virFirewallRulePtr rule; char *str; VIR_FIREWALL_RETURN_NULL_IF_ERROR(firewall); if (firewall->ngroups == 0) { firewall->err = EINVAL; return NULL; } group = firewall->groups[firewall->currentGroup]; if (VIR_ALLOC(rule) < 0) goto no_memory; rule->layer = layer; rule->queryCB = cb; rule->queryOpaque = opaque; rule->ignoreErrors = ignoreErrors; switch (rule->layer) { case VIR_FIREWALL_LAYER_ETHERNET: if (ebtablesUseLock) ADD_ARG(rule, "--concurrent"); break; case VIR_FIREWALL_LAYER_IPV4: if (iptablesUseLock) ADD_ARG(rule, "-w"); break; case VIR_FIREWALL_LAYER_IPV6: if (ip6tablesUseLock) ADD_ARG(rule, "-w"); break; case VIR_FIREWALL_LAYER_LAST: break; } while ((str = va_arg(args, char *)) != NULL) { ADD_ARG(rule, str); } if (group->addingRollback) { if (VIR_APPEND_ELEMENT_COPY(group->rollback, group->nrollback, rule) < 0) goto no_memory; } else { if (VIR_APPEND_ELEMENT_COPY(group->action, group->naction, rule) < 0) goto no_memory; } return rule; no_memory: firewall->err = ENOMEM; virFirewallRuleFree(rule); return NULL; } /** * virFirewallAddRule: * @firewall: firewall ruleset to add to * @layer: the firewall layer to change * @...: NULL terminated list of strings for the rule * * Add any type of rule to the firewall ruleset. * * Returns the new rule */ virFirewallRulePtr virFirewallAddRule(virFirewallPtr firewall, virFirewallLayer layer, ...) { virFirewallRulePtr rule; va_list args; va_start(args, layer); rule = virFirewallAddRuleFullV(firewall, layer, false, NULL, NULL, args); va_end(args); return rule; } /** * virFirewallAddRuleFull: * @firewall: firewall ruleset to add to * @layer: the firewall layer to change * @ignoreErrors: true to ignore failure of the command * @cb: callback to invoke with result of query * @opaque: data passed into @cb * @...: NULL terminated list of strings for the rule * * Add any type of rule to the firewall ruleset. Any output * generated by the addition will be fed into the query * callback @cb. This callback is permitted to create new * rules by invoking the virFirewallAddRule method, but * is not permitted to start new transactions. * * If @ignoreErrors is set to TRUE, then any failure of * the command is ignored. If it is set to FALSE, then * the behaviour upon failure is determined by the flags * set when the transaction was started. * * Returns the new rule */ virFirewallRulePtr virFirewallAddRuleFull(virFirewallPtr firewall, virFirewallLayer layer, bool ignoreErrors, virFirewallQueryCallback cb, void *opaque, ...) { virFirewallRulePtr rule; va_list args; va_start(args, opaque); rule = virFirewallAddRuleFullV(firewall, layer, ignoreErrors, cb, opaque, args); va_end(args); return rule; } /** * virFirewallRemoveRule: * @firewall: firewall ruleset to remove from * @rule: the rule to remove * * Remove a rule from the current transaction */ void virFirewallRemoveRule(virFirewallPtr firewall, virFirewallRulePtr rule) { size_t i; virFirewallGroupPtr group; /* Explicitly not checking firewall->err too, * because if rule was partially created * before hitting error we must still remove * it to avoid leaking 'rule' */ if (!firewall) return; if (firewall->ngroups == 0) return; group = firewall->groups[firewall->currentGroup]; if (group->addingRollback) { for (i = 0; i < group->nrollback; i++) { if (group->rollback[i] == rule) { VIR_DELETE_ELEMENT(group->rollback, i, group->nrollback); virFirewallRuleFree(rule); break; } } } else { for (i = 0; i < group->naction; i++) { if (group->action[i] == rule) { VIR_DELETE_ELEMENT(group->action, i, group->naction); virFirewallRuleFree(rule); return; } } } } void virFirewallRuleAddArg(virFirewallPtr firewall, virFirewallRulePtr rule, const char *arg) { VIR_FIREWALL_RULE_RETURN_IF_ERROR(firewall, rule); ADD_ARG(rule, arg); return; no_memory: firewall->err = ENOMEM; } void virFirewallRuleAddArgFormat(virFirewallPtr firewall, virFirewallRulePtr rule, const char *fmt, ...) { char *arg; va_list list; VIR_FIREWALL_RULE_RETURN_IF_ERROR(firewall, rule); va_start(list, fmt); if (virVasprintf(&arg, fmt, list) < 0) goto no_memory; ADD_ARG(rule, arg); va_end(list); VIR_FREE(arg); return; no_memory: firewall->err = ENOMEM; va_end(list); VIR_FREE(arg); } void virFirewallRuleAddArgSet(virFirewallPtr firewall, virFirewallRulePtr rule, const char *const *args) { VIR_FIREWALL_RULE_RETURN_IF_ERROR(firewall, rule); while (*args) { ADD_ARG(rule, *args); args++; } return; no_memory: firewall->err = ENOMEM; } void virFirewallRuleAddArgList(virFirewallPtr firewall, virFirewallRulePtr rule, ...) { va_list list; const char *str; VIR_FIREWALL_RULE_RETURN_IF_ERROR(firewall, rule); va_start(list, rule); while ((str = va_arg(list, char *)) != NULL) { ADD_ARG(rule, str); } va_end(list); return; no_memory: firewall->err = ENOMEM; va_end(list); } size_t virFirewallRuleGetArgCount(virFirewallRulePtr rule) { if (!rule) return 0; return rule->argsLen; } /** * virFirewallStartTransaction: * @firewall: the firewall ruleset * @flags: bitset of virFirewallTransactionFlags * * Start a new transaction with associated rollback * block. * * Should be followed by calls to add various rules to * the transaction. Then virFirwallStartRollback should * be used to provide rules to rollback upon transaction * failure */ void virFirewallStartTransaction(virFirewallPtr firewall, unsigned int flags) { virFirewallGroupPtr group; VIR_FIREWALL_RETURN_IF_ERROR(firewall); if (!(group = virFirewallGroupNew())) { firewall->err = ENOMEM; return; } group->actionFlags = flags; if (VIR_EXPAND_N(firewall->groups, firewall->ngroups, 1) < 0) { firewall->err = ENOMEM; virFirewallGroupFree(group); return; } firewall->groups[firewall->ngroups - 1] = group; firewall->currentGroup = firewall->ngroups - 1; } /** * virFirewallBeginRollback: * @firewall: the firewall ruleset * @flags: bitset of virFirewallRollbackFlags * * Mark the beginning of a set of rules able to rollback * changes in this and all earlier transactions. * * Should be followed by calls to add various rules needed * to rollback state. Then virFirewallStartTransaction * should be used to indicate the beginning of the next * transactional ruleset. */ void virFirewallStartRollback(virFirewallPtr firewall, unsigned int flags) { virFirewallGroupPtr group; VIR_FIREWALL_RETURN_IF_ERROR(firewall); if (firewall->ngroups == 0) { firewall->err = EINVAL; return; } group = firewall->groups[firewall->ngroups-1]; group->rollbackFlags = flags; group->addingRollback = true; } static char * virFirewallRuleToString(virFirewallRulePtr rule) { const char *bin = virFirewallLayerCommandTypeToString(rule->layer); virBuffer buf = VIR_BUFFER_INITIALIZER; size_t i; virBufferAdd(&buf, bin, -1); for (i = 0; i < rule->argsLen; i++) { virBufferAddLit(&buf, " "); virBufferAdd(&buf, rule->args[i], -1); } return virBufferContentAndReset(&buf); } static int virFirewallApplyRuleDirect(virFirewallRulePtr rule, bool ignoreErrors, char **output) { size_t i; const char *bin = virFirewallLayerCommandTypeToString(rule->layer); virCommandPtr cmd = NULL; int status; int ret = -1; char *error = NULL; if (!bin) { virReportError(VIR_ERR_INTERNAL_ERROR, _("Unknown firewall layer %d"), rule->layer); goto cleanup; } cmd = virCommandNewArgList(bin, NULL); for (i = 0; i < rule->argsLen; i++) virCommandAddArg(cmd, rule->args[i]); virCommandSetOutputBuffer(cmd, output); virCommandSetErrorBuffer(cmd, &error); if (virCommandRun(cmd, &status) < 0) goto cleanup; if (status != 0) { if (ignoreErrors) { VIR_DEBUG("Ignoring error running command"); } else { char *args = virCommandToString(cmd); virReportError(VIR_ERR_INTERNAL_ERROR, _("Failed to apply firewall rules %s: %s"), NULLSTR(args), NULLSTR(error)); VIR_FREE(args); VIR_FREE(*output); goto cleanup; } } ret = 0; cleanup: VIR_FREE(error); virCommandFree(cmd); return ret; } #ifdef WITH_DBUS static int virFirewallApplyRuleFirewallD(virFirewallRulePtr rule, bool ignoreErrors, char **output) { const char *ipv = virFirewallLayerFirewallDTypeToString(rule->layer); DBusConnection *sysbus = virDBusGetSystemBus(); DBusMessage *reply = NULL; DBusError error; int ret = -1; if (!sysbus) return -1; dbus_error_init(&error); if (!ipv) { virReportError(VIR_ERR_INTERNAL_ERROR, _("Unknown firewall layer %d"), rule->layer); goto cleanup; } if (virDBusCallMethod(sysbus, &reply, &error, VIR_FIREWALL_FIREWALLD_SERVICE, "/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.direct", "passthrough", "sa&s", ipv, (int)rule->argsLen, rule->args) < 0) goto cleanup; if (dbus_error_is_set(&error)) { /* * As of firewalld-0.3.9.3-1.fc20.noarch the name and * message fields in the error look like * * name="org.freedesktop.DBus.Python.dbus.exceptions.DBusException" * message="COMMAND_FAILED: '/sbin/iptables --table filter --delete * INPUT --in-interface virbr0 --protocol udp --destination-port 53 * --jump ACCEPT' failed: iptables: Bad rule (does a matching rule * exist in that chain?)." * * We'd like to only ignore DBus errors precisely related to the failure * of iptables/ebtables commands. A well designed DBus interface would * return specific named exceptions not the top level generic python dbus * exception name. With this current scheme our only option is todo a * sub-string match for 'COMMAND_FAILED' on the message. eg like * * if (ignoreErrors && * STREQ(error.name, * "org.freedesktop.DBus.Python.dbus.exceptions.DBusException") && * STRPREFIX(error.message, "COMMAND_FAILED")) * ... * * But this risks our error detecting code being broken if firewalld changes * ever alter the message string, so we're avoiding doing that. */ if (ignoreErrors) { VIR_DEBUG("Ignoring error '%s': '%s'", error.name, error.message); } else { virReportError(VIR_ERR_INTERNAL_ERROR, _("Unable to apply rule '%s'"), error.message); goto cleanup; } } else { if (virDBusMessageRead(reply, "s", output) < 0) goto cleanup; } ret = 0; cleanup: dbus_error_free(&error); if (reply) dbus_message_unref(reply); return ret; } #endif static int virFirewallApplyRule(virFirewallPtr firewall, virFirewallRulePtr rule, bool ignoreErrors) { char *output = NULL; char **lines = NULL; int ret = -1; char *str = virFirewallRuleToString(rule); VIR_INFO("Applying rule '%s'", NULLSTR(str)); VIR_FREE(str); if (rule->ignoreErrors) ignoreErrors = rule->ignoreErrors; switch (currentBackend) { case VIR_FIREWALL_BACKEND_DIRECT: if (virFirewallApplyRuleDirect(rule, ignoreErrors, &output) < 0) return -1; break; #if WITH_DBUS case VIR_FIREWALL_BACKEND_FIREWALLD: if (virFirewallApplyRuleFirewallD(rule, ignoreErrors, &output) < 0) return -1; break; #endif default: virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("Unexpected firewall engine backend")); return -1; } if (rule->queryCB && output) { if (!(lines = virStringSplit(output, "\n", -1))) goto cleanup; VIR_DEBUG("Invoking query %p with '%s'", rule->queryCB, output); if (rule->queryCB(firewall, (const char *const *)lines, rule->queryOpaque) < 0) goto cleanup; if (firewall->err == ENOMEM) { virReportOOMError(); goto cleanup; } if (firewall->err) { virReportSystemError(firewall->err, "%s", _("Unable to create rule")); goto cleanup; } } ret = 0; cleanup: virStringFreeList(lines); VIR_FREE(output); return ret; } static int virFirewallApplyGroup(virFirewallPtr firewall, size_t idx) { virFirewallGroupPtr group = firewall->groups[idx]; bool ignoreErrors = (group->actionFlags & VIR_FIREWALL_TRANSACTION_IGNORE_ERRORS); size_t i; VIR_INFO("Starting transaction for firewall=%p group=%p flags=%x", firewall, group, group->actionFlags); firewall->currentGroup = idx; group->addingRollback = false; for (i = 0; i < group->naction; i++) { if (virFirewallApplyRule(firewall, group->action[i], ignoreErrors) < 0) return -1; } return 0; } static void virFirewallRollbackGroup(virFirewallPtr firewall, size_t idx) { virFirewallGroupPtr group = firewall->groups[idx]; size_t i; VIR_INFO("Starting rollback for group %p", group); firewall->currentGroup = idx; group->addingRollback = true; for (i = 0; i < group->nrollback; i++) { ignore_value(virFirewallApplyRule(firewall, group->rollback[i], true)); } } int virFirewallApply(virFirewallPtr firewall) { size_t i, j; int ret = -1; virMutexLock(&ruleLock); if (!firewall || firewall->err == ENOMEM) { virReportOOMError(); goto cleanup; } if (firewall->err) { virReportSystemError(firewall->err, "%s", _("Unable to create rule")); goto cleanup; } VIR_DEBUG("Applying groups for %p", firewall); for (i = 0; i < firewall->ngroups; i++) { if (virFirewallApplyGroup(firewall, i) < 0) { VIR_DEBUG("Rolling back groups upto %zu for %p", i, firewall); size_t first = i; virErrorPtr saved_error = virSaveLastError(); /* * Look at any inheritance markers to figure out * what the first rollback group we need to apply is */ for (j = 0; j < i; j++) { VIR_DEBUG("Checking inheritance of group %zu", i - j); if (firewall->groups[i - j]->rollbackFlags & VIR_FIREWALL_ROLLBACK_INHERIT_PREVIOUS) first = (i - j) - 1; } /* * Now apply all rollback groups in order */ for (j = first; j <= i; j++) { VIR_DEBUG("Rolling back group %zu", j); virFirewallRollbackGroup(firewall, j); } virSetError(saved_error); virFreeError(saved_error); VIR_DEBUG("Done rolling back groups for %p", firewall); goto cleanup; } } VIR_DEBUG("Done applying groups for %p", firewall); ret = 0; cleanup: virMutexUnlock(&ruleLock); return ret; }