* Copyright (C) 2018 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from 'cockpit';
import * as service from 'service';
import { debounce } from 'throttle-debounce';
import * as utils from './utils.js';
const firewall = {
owner: undefined,
installed: true,
enabled: false,
readonly: true,
services: {},
enabledServices: new Set(),
* fetchZoneInfos, the value */
zones: {},
activeZones: new Set(),
predefinedZones: ['drop', 'block', 'public', 'external',
'dmz', 'work', 'home', 'internal', 'trusted'],
defaultZone: null,
availableInterfaces: [],
ready: false
};
cockpit.event_target(firewall);
utils.list_interfaces().then(interfaces => {
firewall.availableInterfaces = interfaces;
});
const firewalld_service = service.proxy('firewalld');
let firewalld_dbus = null;
function debug() {
if (window.debugging === "all" || window.debugging?.includes("firewalld"))
console.debug("firewalld:", ...arguments);
}
firewall.debouncedGetZones = debounce(300, () => {
if (firewalld_dbus)
getZones()
.then(() => getServices())
.then(() => firewall.debouncedEvent('changed'))
.catch(error => console.warn(error));
});
* in a short period of time, prevent rapid succession of renders by
* debouncing the ('changed') event */
firewall.debouncedEvent = debounce(300, event => firewall.dispatchEvent(event));
* prevent rapid succession of GetServices call */
firewall.debouncedGetServices = debounce(300, () => {
getServices().then(() => firewall.debouncedEvent('changed'));
});
function initFirewalldDbus(owner) {
debug("initializing D-Bus connection", owner);
firewall.owner = owner;
firewall.enabled = !!owner;
firewall.zones = {};
firewall.activeZones = new Set();
firewall.services = {};
firewall.enabledServices = new Set();
if (!firewall.enabled) {
firewall.ready = true;
firewall.dispatchEvent('changed');
return;
}
firewalld_dbus = cockpit.dbus(owner, { superuser: "try", track: true });
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'ServiceAdded'
}, (path, iface, signal, args) => {
const zone = args[0];
const service = args[1];
fetchZoneInfos([zone])
.then(() => fetchServiceInfos([service]))
.then(info => firewall.enabledServices.add(info[0].id))
.then(() => firewall.debouncedEvent('changed'))
.catch(error => console.warn(error));
});
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'ServiceRemoved'
}, (path, iface, signal, args) => {
const zone = args[0];
const service = args[1];
firewall.zones[zone].services = firewall.zones[zone].services.filter(s => s !== service);
firewall.enabledServices.delete(service);
firewall.debouncedGetServices();
});
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'PortAdded'
}, (path, iface, signal, args) => {
const zone = args[0];
const port = args[1];
const protocol = args[2];
if (!firewall.zones[zone].ports.some(p => p.port === port && p.protocol === protocol)) {
firewall.zones[zone].ports.push({ port, protocol });
firewall.debouncedEvent('changed');
}
});
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'PortRemoved'
}, (path, iface, signal, args) => {
const zone = args[0];
const port = args[1];
const protocol = args[2];
firewall.zones[zone].ports = firewall.zones[zone].ports
.filter(p => p.port !== port || p.protocol !== protocol);
firewall.debouncedEvent('changed');
});
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1',
path: '/org/fedoraproject/FirewallD1',
member: 'Reloaded'
}, () => firewall.debouncedGetZones());
* add(Interface|Source). Listen to both of them for any background changes
* to zones. */
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'ZoneOfInterfaceChanged'
}, () => firewall.debouncedGetZones());
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'ZoneOfSourceChanged'
}, () => firewall.debouncedGetZones());
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'InterfaceAdded'
}, () => firewall.debouncedGetZones());
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'SourceAdded'
}, () => firewall.debouncedGetZones());
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'InterfaceRemoved'
}, () => firewall.debouncedGetZones());
firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.zone',
path: '/org/fedoraproject/FirewallD1',
member: 'SourceRemoved'
}, () => firewall.debouncedGetZones());
getZones()
.then(() => getServices())
.catch(error => console.warn(error))
.then(() => { firewall.ready = true })
.then(() => firewall.debouncedEvent('changed'));
}
firewalld_service.addEventListener('changed', () => {
const installed = !!firewalld_service.exists;
const is_running = firewalld_service.state === 'running';
if (is_running === firewalld_service.prev_running && firewall.installed === installed)
return;
firewall.installed = installed;
firewalld_service.prev_running = is_running;
debug("systemd service changed: exists", firewalld_service.exists, "state", firewalld_service.state,
"firewall.enabled:", JSON.stringify(firewall.enabled));
firewall.dispatchEvent('changed');
});
function getZones() {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'getActiveZones', [])
.then(reply => fetchZoneInfos(Object.keys(reply[0])))
.then(zones => {
firewall.activeZones = new Set(zones.map(z => z.id));
debug("getActiveZones succeeded:", JSON.stringify([...firewall.activeZones]));
})
.then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1',
'getDefaultZone', []))
.then(reply => {
firewall.defaultZone = reply[0];
})
.then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'getZones', []))
.then(reply => fetchZoneInfos(reply[0]));
}
function getServices() {
if (firewall.readonly)
return Promise.resolve();
firewall.enabledServices = new Set();
return Promise.all([...firewall.activeZones].map(z => {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'getServices', [z])
.then(reply => fetchServiceInfos(reply[0]))
.then(services => {
const promises = [];
for (const s of services) {
firewall.enabledServices.add(s.id);
if (s.includes.length)
promises.push(fetchServiceInfos(s.includes));
}
return Promise.all(promises);
});
}));
}
function fetchServiceInfos(services) {
return Promise.all(services.map(service => {
if (firewall.services[service])
return firewall.services[service];
let info;
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1',
'getServiceSettings2', [service])
.then(([{ short, description, ports }]) => {
short = short ? short.v : "";
description = description ? description.v : "";
ports = ports ? ports.v : [];
info = {
id: service,
name: short,
description,
ports: ports.map(p => ({ port: p[0], protocol: p[1] })),
includes: [],
};
firewall.services[service] = info;
return firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'getServiceByName', [service]);
})
.then(path => firewalld_dbus.call(path[0],
'org.fedoraproject.FirewallD1.config.service',
'getSettings2', []))
.then(reply => {
if (reply[0].includes) {
info.includes = reply[0].includes.v;
firewall.services[service] = info;
}
return info;
})
.catch(error => {
if (error.name === 'org.freedesktop.DBus.Error.UnknownMethod')
return info;
return Promise.reject(error);
});
}));
}
function fetchZoneInfos(zones) {
return Promise.all(zones.map(zone => {
if (firewall.readonly) {
const info = {
id: zone,
name: zone,
description: null,
target: null,
services: [],
ports: [],
interfaces: [],
source: [],
};
firewall.zones[zone] = info;
return info;
}
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'getZoneSettings2', [zone])
.then(([zoneInfo]) => {
const info = {
id: zone,
name: (zoneInfo.short || {}).v,
description: (zoneInfo.description || {}).v,
target: (zoneInfo.target || {}).v,
services: ((zoneInfo.services || {}).v || []),
ports: ((zoneInfo.ports || {}).v || []).map(p => ({ port: p[0], protocol: p[1] })),
interfaces: ((zoneInfo.interfaces || {}).v || []),
source: ((zoneInfo.sources || {}).v || []),
};
firewall.zones[zone] = info;
return info;
});
}));
}
The org.fedoraproject.FirewallD service might appear on D-Bus only
some time after the firewalld.service is active and thus we want to
only try connecting to it once we know that it is definitely there.
However, while cockpit.dbus() does work with services that
disappear and then reappear on the bus while the channel is open,
it doesn't work with names that are not present when the channel is
initially opened (see
https://github.com/cockpit-project/cockpit/pull/9125).
Thus, we track the name explicitly via the org.freedesktop.DBus
API.
*/
function dbus_watch_name_owner(name, owner_callback) {
const bus = cockpit.dbus('org.freedesktop.DBus', { superuser: "try" });
let owner = null;
function owner_changed(new_owner) {
if (new_owner !== owner) {
owner = new_owner;
owner_callback(owner);
}
}
bus.subscribe({
path: "/org/freedesktop/DBus",
interface: "org.freedesktop.DBus",
member: "NameOwnerChanged",
arg0: name,
}, (_path, _iface, _member, [_name, _old_owner, new_owner]) => {
owner_changed(new_owner);
});
bus.call(
"/org/freedesktop/DBus", "org.freedesktop.DBus", "GetNameOwner",
[name]
)
.then(([owner]) => {
owner_changed(owner);
})
.catch(ex => {
if (ex.name === "org.freedesktop.DBus.Error.NameHasNoOwner")
owner_changed("");
else
throw ex;
});
}
dbus_watch_name_owner('org.fedoraproject.FirewallD1', initFirewalldDbus);
cockpit.spawn(['sh', '-c', 'pkcheck --action-id org.fedoraproject.FirewallD1.all --process $$ --allow-user-interaction 2>&1'], { superuser: "try" })
.then(() => {
firewall.readonly = false;
firewall.debouncedEvent('changed');
firewall.debouncedGetZones();
})
.catch(error => {
console.log("pkcheck failed", error);
const permission = cockpit.permission({ admin: true });
const update_permissions = () => {
firewall.readonly = !permission.allowed;
firewall.debouncedEvent('changed');
firewall.debouncedGetZones();
};
permission.addEventListener("changed", update_permissions);
});
firewall.enable = () => firewalld_service.enable().then(() => firewalld_service.start());
firewall.disable = () => firewalld_service.disable().then(() => firewalld_service.stop());
firewall.getAvailableServices = () => {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1',
'listServices', [])
.then(reply => fetchServiceInfos(reply[0]))
.catch(error => console.warn(error));
};
* Only call this after defining a new service, as it will remove existing
* non-permanent configurations.
*/
firewall.reload = () => {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1',
'reload', [])
.catch(error => console.warn(error));
};
* Remove a service from the specified zone (i.e., close its ports).
*
* Returns a promise that resolves when the service is removed.
*/
firewall.removeService = (zone, service) => {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'removeService', [zone, service])
.then(reply => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'getZoneByName', [zone]))
.then(path => firewalld_dbus.call(path[0], 'org.fedoraproject.FirewallD1.config.zone',
'removeService', [service]));
};
firewall.removePort = (zone, port, protocol) => {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'removePort', [zone, port, protocol])
.then(reply => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'getZoneByName', [zone]))
.then(path => firewalld_dbus.call(path[0], 'org.fedoraproject.FirewallD1.config.zone',
'removePort', [port, protocol]));
};
* Create new firewalld service.
*
* Returns a promise that resolves when the service is created.
* It will also reload firewalld and enable the new service.
*/
firewall.createService = (service, ports, zones, desc = "") => {
const subscription = firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1',
path: '/org/fedoraproject/FirewallD1',
member: 'Reloaded'
}, () => {
firewall.addServices(zones, [service]);
subscription.remove();
});
return firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'addService2', [service, { description: { t: 's', v: desc }, ports: { t: 'a(ss)', v: ports } }])
.then(() => firewall.reload());
};
* Edit firewalld service.
*
* Returns a promise that resolves when the service is edited.
*/
firewall.editService = (service, ports, desc = "") => {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'getServiceByName', [service])
.then(path => firewalld_dbus.call(path[0],
'org.fedoraproject.FirewallD1.config.service',
'update2', [{ description: { t: 's', v: desc }, ports: { t: 'a(ss)', v: ports } }])
.then(() => {
firewall.services[service].description = desc;
firewall.services[service].ports = ports.map(port => ({ port: port[0], protocol: port[1] }));
firewall.debouncedEvent('changed');
return firewall.reload();
})
);
};
* Add a predefined firewalld service to the specified zone (i.e., open its
* ports).
*
* Returns a promise that resolves when the service is added.
*/
firewall.addService = (zone, service) => {
return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'addService', [zone, service, 0])
.then(reply => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'getZoneByName', [zone]))
.then(path => firewalld_dbus.call(path[0], 'org.fedoraproject.FirewallD1.config.zone',
'addService', [service]));
};
* Like addService(), but adds multiple predefined firewalld services at once
* to the specified zones.
*
* Returns a promise that resolves when all services are added.
*/
firewall.addServices = (zone, services) =>
Promise.all(services.map(s => firewall.addService(zone, s)));
firewall.removeServiceFromZones = (zones, service) =>
Promise.all(zones.map(z => firewall.removeService(z, service)));
firewall.activateZone = (zone, interfaces, sources) => {
let promises = interfaces.map(i => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'addInterface', [zone, i]));
promises = promises.concat(sources.map(s => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'addSource', [zone, s])));
let p = Promise.all(promises).then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'getZoneByName', [zone]));
p = p.then(path => {
* signal and thus update the UI */
const subscription = firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.config.zone',
path: path[0],
member: 'Updated'
}, (path, iface, signal, args) => {
getZones().then(() => getServices());
subscription.remove();
});
return firewalld_dbus.call(path[0],
'org.fedoraproject.FirewallD1.config.zone',
'update2', [{ interfaces: { t: 'as', v: interfaces }, sources: { t: 'as', v: sources } }]);
});
return p;
};
* A zone is considered deactivated when it has no interfaces or sources.
*/
firewall.deactivateZone = (zone) => {
const zoneObject = firewall.zones[zone];
let promises = zoneObject.interfaces.map(i => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'removeInterface', [zone, i]));
promises = promises.concat(zoneObject.source.map(s => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
'org.fedoraproject.FirewallD1.zone',
'removeSource', [zone, s])));
let p = Promise.all(promises).then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
'org.fedoraproject.FirewallD1.config',
'getZoneByName', [zone]));
p = p.then(path => {
* signal and thus update the UI */
const subscription = firewalld_dbus.subscribe({
interface: 'org.fedoraproject.FirewallD1.config.zone',
path: path[0],
member: 'Updated'
}, (path, iface, signal, args) => {
firewall.activeZones.delete(args[0]);
getZones().then(() => getServices());
subscription.remove();
});
return firewalld_dbus.call(path[0],
'org.fedoraproject.FirewallD1.config.zone',
'update2', [{ interfaces: { t: 'as', v: [] }, sources: { t: 'as', v: [] } }]);
});
return p.catch(error => console.warn(error));
};
export default firewall;