1 /*
2  * virfirewalld.c: support for firewalld (https://firewalld.org)
3  *
4  * Copyright (C) 2019 Red Hat, Inc.
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library.  If not, see
18  * <http://www.gnu.org/licenses/>.
19  */
20 
21 #include <config.h>
22 
23 #include <stdarg.h>
24 
25 #include "virfirewall.h"
26 #include "virfirewalld.h"
27 #define LIBVIRT_VIRFIREWALLDPRIV_H_ALLOW
28 #include "virfirewalldpriv.h"
29 #include "viralloc.h"
30 #include "virerror.h"
31 #include "virutil.h"
32 #include "virlog.h"
33 #include "virgdbus.h"
34 #include "virenum.h"
35 
36 #define VIR_FROM_THIS VIR_FROM_FIREWALLD
37 
38 VIR_LOG_INIT("util.firewalld");
39 
40 /* used to convert virFirewallLayer enum values to strings
41  * understood by the firewalld.direct "passthrough" method
42  */
43 VIR_ENUM_DECL(virFirewallLayerFirewallD);
44 VIR_ENUM_IMPL(virFirewallLayerFirewallD,
45               VIR_FIREWALL_LAYER_LAST,
46               "eb",
47               "ipv4",
48               "ipv6",
49               );
50 
51 
52 VIR_ENUM_DECL(virFirewallDBackend);
53 VIR_ENUM_IMPL(virFirewallDBackend,
54               VIR_FIREWALLD_BACKEND_LAST,
55               "",
56               "iptables",
57               "nftables",
58               );
59 
60 
61 /**
62  * virFirewallDIsRegistered:
63  *
64  * Returns 0 if service is registered, -1 on fatal error, or -2 if service is not registered
65  */
66 int
virFirewallDIsRegistered(void)67 virFirewallDIsRegistered(void)
68 {
69     return virGDBusIsServiceRegistered(VIR_FIREWALL_FIREWALLD_SERVICE);
70 }
71 
72 /**
73  * virFirewallDGetVersion:
74  * @version: pointer to location to save version in the form of:
75  *           1000000 * major + 1000 * minor + micro
76  *
77  * queries the firewalld version property from dbus, and converts it
78  * from a string into a number.
79  *
80  * Returns 0 if version was successfully retrieved, or -1 on error
81  */
82 int
virFirewallDGetVersion(unsigned long * version)83 virFirewallDGetVersion(unsigned long *version)
84 {
85     GDBusConnection *sysbus = virGDBusGetSystemBus();
86     g_autoptr(GVariant) message = NULL;
87     g_autoptr(GVariant) reply = NULL;
88     g_autoptr(GVariant) gvar = NULL;
89     char *versionStr;
90 
91     if (!sysbus)
92         return -1;
93 
94     message = g_variant_new("(ss)", "org.fedoraproject.FirewallD1", "version");
95 
96     if (virGDBusCallMethod(sysbus,
97                            &reply,
98                            G_VARIANT_TYPE("(v)"),
99                            NULL,
100                            VIR_FIREWALL_FIREWALLD_SERVICE,
101                            "/org/fedoraproject/FirewallD1",
102                            "org.freedesktop.DBus.Properties",
103                            "Get",
104                            message) < 0)
105         return -1;
106 
107     g_variant_get(reply, "(v)", &gvar);
108     g_variant_get(gvar, "&s", &versionStr);
109 
110     if (virParseVersionString(versionStr, version, false) < 0) {
111         virReportError(VIR_ERR_INTERNAL_ERROR,
112                        _("Failed to parse firewalld version '%s'"),
113                        versionStr);
114         return -1;
115     }
116 
117     VIR_DEBUG("FirewallD version: %s - %lu", versionStr, *version);
118 
119     return 0;
120 }
121 
122 /**
123  * virFirewallDGetBackend:
124  *
125  * Returns virVirewallDBackendType value representing which packet
126  * filtering backend is currently in use by firewalld, or -1 on error.
127  */
128 int
virFirewallDGetBackend(void)129 virFirewallDGetBackend(void)
130 {
131     GDBusConnection *sysbus = virGDBusGetSystemBus();
132     g_autoptr(GVariant) message = NULL;
133     g_autoptr(GVariant) reply = NULL;
134     g_autoptr(GVariant) gvar = NULL;
135     g_autoptr(virError) error = NULL;
136     char *backendStr = NULL;
137     int backend = -1;
138 
139     if (!sysbus)
140         return -1;
141 
142     error = g_new0(virError, 1);
143 
144     message = g_variant_new("(ss)",
145                             "org.fedoraproject.FirewallD1.config",
146                             "FirewallBackend");
147 
148     if (virGDBusCallMethod(sysbus,
149                            &reply,
150                            G_VARIANT_TYPE("(v)"),
151                            error,
152                            VIR_FIREWALL_FIREWALLD_SERVICE,
153                            "/org/fedoraproject/FirewallD1/config",
154                            "org.freedesktop.DBus.Properties",
155                            "Get",
156                            message) < 0)
157         return -1;
158 
159     if (error->level == VIR_ERR_ERROR) {
160         /* we don't want to log any error in the case that
161          * FirewallBackend isn't implemented in this firewalld, since
162          * that just means that it is an old version, and only has an
163          * iptables backend.
164          */
165         VIR_DEBUG("Failed to get FirewallBackend setting, assuming 'iptables'");
166         return VIR_FIREWALLD_BACKEND_IPTABLES;
167     }
168 
169     g_variant_get(reply, "(v)", &gvar);
170     g_variant_get(gvar, "&s", &backendStr);
171 
172     VIR_DEBUG("FirewallD backend: %s", backendStr);
173 
174     if ((backend = virFirewallDBackendTypeFromString(backendStr)) < 0) {
175         virReportError(VIR_ERR_INTERNAL_ERROR,
176                        _("Unrecognized firewalld backend type: %s"),
177                        backendStr);
178         return -1;
179     }
180 
181     return backend;
182 }
183 
184 
185 /**
186  * virFirewallDGetZones:
187  * @zones: array of char *, each entry is a null-terminated zone name
188  * @nzones: number of entries in @zones
189  *
190  * Get the number of currently active firewalld zones, and their names
191  * in an array of null-terminated strings. The memory pointed to by
192  * @zones will belong to the caller, and must be freed.
193  *
194  * Returns 0 on success, -1 (and failure logged) on error
195  */
196 int
virFirewallDGetZones(char *** zones,size_t * nzones)197 virFirewallDGetZones(char ***zones, size_t *nzones)
198 {
199     GDBusConnection *sysbus = virGDBusGetSystemBus();
200     g_autoptr(GVariant) reply = NULL;
201     g_autoptr(GVariant) array = NULL;
202 
203     *nzones = 0;
204     *zones = NULL;
205 
206     if (!sysbus)
207         return -1;
208 
209     if (virGDBusCallMethod(sysbus,
210                            &reply,
211                            G_VARIANT_TYPE("(as)"),
212                            NULL,
213                            VIR_FIREWALL_FIREWALLD_SERVICE,
214                            "/org/fedoraproject/FirewallD1",
215                            "org.fedoraproject.FirewallD1.zone",
216                            "getZones",
217                            NULL) < 0)
218         return -1;
219 
220     g_variant_get(reply, "(@as)", &array);
221     *zones = g_variant_dup_strv(array, nzones);
222 
223     return 0;
224 }
225 
226 
227 /**
228  * virFirewallDZoneExists:
229  * @match: name of zone to look for
230  *
231  * Returns true if the requested zone exists, or false if it doesn't exist
232  */
233 bool
virFirewallDZoneExists(const char * match)234 virFirewallDZoneExists(const char *match)
235 {
236     size_t nzones = 0, i;
237     char **zones = NULL;
238     bool result = false;
239 
240     if (virFirewallDGetZones(&zones, &nzones) < 0)
241         goto cleanup;
242 
243     for (i = 0; i < nzones; i++) {
244         if (STREQ_NULLABLE(zones[i], match))
245             result = true;
246     }
247 
248  cleanup:
249     VIR_DEBUG("Requested zone '%s' %s exist",
250               match, result ? "does" : "doesn't");
251     for (i = 0; i < nzones; i++)
252        VIR_FREE(zones[i]);
253     VIR_FREE(zones);
254     return result;
255 }
256 
257 
258 /**
259  * virFirewallDApplyRule:
260  * @layer:        which layer to apply the rule to
261  * @args:         list of args to send to this layer's passthrough command.
262  * @argsLen:      number of items in @args
263  * @ignoreErrors: true to suppress logging of errors and return success
264  *                false to log errors and return actual status
265  * @output:       output of the direct passthrough command, if it was successful
266  */
267 int
virFirewallDApplyRule(virFirewallLayer layer,char ** args,size_t argsLen,bool ignoreErrors,char ** output)268 virFirewallDApplyRule(virFirewallLayer layer,
269                       char **args, size_t argsLen,
270                       bool ignoreErrors,
271                       char **output)
272 {
273     const char *ipv = virFirewallLayerFirewallDTypeToString(layer);
274     GDBusConnection *sysbus = virGDBusGetSystemBus();
275     g_autoptr(GVariant) message = NULL;
276     g_autoptr(GVariant) reply = NULL;
277     g_autoptr(virError) error = NULL;
278 
279     if (!sysbus)
280         return -1;
281 
282     memset(&error, 0, sizeof(error));
283 
284     if (!ipv) {
285         virReportError(VIR_ERR_INTERNAL_ERROR,
286                        _("Unknown firewall layer %d"),
287                        layer);
288         return -1;
289     }
290 
291     error = g_new0(virError, 1);
292 
293     message = g_variant_new("(s@as)",
294                             ipv,
295                             g_variant_new_strv((const char * const*)args, argsLen));
296 
297     if (virGDBusCallMethod(sysbus,
298                            &reply,
299                            G_VARIANT_TYPE("(s)"),
300                            error,
301                            VIR_FIREWALL_FIREWALLD_SERVICE,
302                            "/org/fedoraproject/FirewallD1",
303                            "org.fedoraproject.FirewallD1.direct",
304                            "passthrough",
305                            message) < 0)
306         return -1;
307 
308     if (error->level == VIR_ERR_ERROR) {
309         /*
310          * As of firewalld-0.3.9.3-1.fc20.noarch the name and
311          * message fields in the error look like
312          *
313          *    name="org.freedesktop.DBus.Python.dbus.exceptions.DBusException"
314          * message="COMMAND_FAILED: '/sbin/iptables --table filter --delete
315          *          INPUT --in-interface virbr0 --protocol udp --destination-port 53
316          *          --jump ACCEPT' failed: iptables: Bad rule (does a matching rule
317          *          exist in that chain?)."
318          *
319          * We'd like to only ignore DBus errors precisely related to the failure
320          * of iptables/ebtables commands. A well designed DBus interface would
321          * return specific named exceptions not the top level generic python dbus
322          * exception name. With this current scheme our only option is todo a
323          * sub-string match for 'COMMAND_FAILED' on the message. eg like
324          *
325          * if (ignoreErrors &&
326          *     STREQ(error.name,
327          *           "org.freedesktop.DBus.Python.dbus.exceptions.DBusException") &&
328          *     STRPREFIX(error.message, "COMMAND_FAILED"))
329          *    ...
330          *
331          * But this risks our error detecting code being broken if firewalld changes
332          * ever alter the message string, so we're avoiding doing that.
333          */
334         if (ignoreErrors) {
335             VIR_DEBUG("Ignoring error '%s': '%s'",
336                       error->str1, error->message);
337         } else {
338             virReportErrorObject(error);
339             return -1;
340         }
341     } else {
342         g_variant_get(reply, "(s)", output);
343     }
344 
345     return 0;
346 }
347 
348 
349 int
virFirewallDInterfaceSetZone(const char * iface,const char * zone)350 virFirewallDInterfaceSetZone(const char *iface,
351                              const char *zone)
352 {
353     GDBusConnection *sysbus = virGDBusGetSystemBus();
354     g_autoptr(GVariant) message = NULL;
355 
356     if (!sysbus)
357         return -1;
358 
359     message = g_variant_new("(ss)", zone, iface);
360 
361     return virGDBusCallMethod(sysbus,
362                              NULL,
363                              NULL,
364                              NULL,
365                              VIR_FIREWALL_FIREWALLD_SERVICE,
366                              "/org/fedoraproject/FirewallD1",
367                              "org.fedoraproject.FirewallD1.zone",
368                              "changeZoneOfInterface",
369                              message);
370 }
371