1 /*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License, Version 1.0 only
6 * (the "License"). You may not use this file except in compliance
7 * with the License.
8 *
9 * You can obtain a copy of the license at http://smartos.org/CDDL
10 *
11 * See the License for the specific language governing permissions
12 * and limitations under the License.
13 *
14 * When distributing Covered Code, include this CDDL HEADER in each
15 * file.
16 *
17 * If applicable, add the following below this CDDL HEADER, with the
18 * fields enclosed by brackets "[]" replaced with your own identifying
19 * information: Portions Copyright [yyyy] [name of copyright owner]
20 *
21 * CDDL HEADER END
22 *
23 *
24 * Copyright (c) 2018, Joyent, Inc. All rights reserved.
25 *
26 *
27 * fwadm: Main entry points
28 */
29
30 var assert = require('assert-plus');
31 var clone = require('clone');
32 var filter = require('./filter');
33 var fs = require('fs');
34 // XXX not ok for firewaller-agent because this won't exist on old CNs
35 var features = require('/usr/node/node_modules/illumos-features');
36 var mkdirp = require('mkdirp');
37 var mod_addr = require('ip6addr');
38 var mod_ipf = require('./ipf');
39 var mod_lock = require('./locker');
40 var mod_obj = require('./util/obj');
41 var mod_rvm = require('./rvm');
42 var mod_rule = require('fwrule');
43 var pipeline = require('./pipeline').pipeline;
44 var sprintf = require('extsprintf').sprintf;
45 var util = require('util');
46 var util_err = require('./util/errors');
47 var util_log = require('./util/log');
48 var util_vm = require('./util/vm');
49 var vasync = require('vasync');
50 var verror = require('verror');
51
52 var createSubObjects = mod_obj.createSubObjects;
53 var forEachKey = mod_obj.forEachKey;
54 var hasKey = mod_obj.hasKey;
55 var objEmpty = mod_obj.objEmpty;
56 var mergeObjects = mod_obj.mergeObjects;
57
58
59
60 // --- Globals
61
62
63
64 var DIRECTIONS = ['from', 'to'];
65 var RULE_PATH = '/var/fw/rules';
66 var IPF_CONF = '%s/config/ipf.conf';
67 var IPF_CONF_OLD = '%s/config/ipf.conf.old';
68 var IPF6_CONF = '%s/config/ipf6.conf';
69 var IPF6_CONF_OLD = '%s/config/ipf6.conf.old';
70 var KEEP_FRAGS = ' keep frags';
71 var KEEP_STATE = ' keep state';
72 var NOT_RUNNING_MSG = 'Could not find running zone';
73 var FEATURE_INOUT_UUID = 'com.joyent.driver.ipf.rules.in-out-uuid';
74 // VM fields that affect filtering
75 var VM_FIELDS = [
76 'firewall_enabled',
77 'nics',
78 'owner_uuid',
79 'state',
80 'tags',
81 'uuid',
82 'zonepath'
83 ];
84 // VM fields required for filtering
85 var VM_FIELDS_REQUIRED = [
86 'nics',
87 'state',
88 'tags',
89 'uuid',
90 'zonepath'
91 ];
92
93 var icmpr = /^icmp6?$/;
94
95 var fallbacks = [
96 '',
97 '# fwadm fallbacks',
98 'block in all',
99 'pass out quick proto tcp from any to any flags S/SA keep state',
100 'pass out proto tcp from any to any',
101 'pass out proto udp from any to any keep state'];
102
103 var v4fallbacks = fallbacks.concat([
104 'pass out quick proto icmp from any to any keep state',
105 'pass out proto icmp from any to any']);
106
107 var v6fallbacks = fallbacks.concat([
108 'pass out quick proto ipv6-icmp from any to any keep state',
109 'pass out proto ipv6-icmp from any to any']);
110
111
112 // --- Internal helper functions
113
114
115
116 /**
117 * Assert that this is either a string or an object
118 */
119 function assertStringOrObject(obj, name) {
120 if (typeof (obj) !== 'string' && typeof (obj) !== 'object') {
121 assert.ok(false, name + ' ([string] or [object]) required');
122 }
123 }
124
125
126 /**
127 * For a rule and a direction, return whether or not we actually need to
128 * write ipf rules. FROM+ALLOW and TO+BLOCK are essentially no-ops, as
129 * they will be caught by the block / allow catch-all default rules.
130 *
131 * If the rule has a priority greater than 0, then we always need to write
132 * out ipf rules.
133 */
134 function noRulesNeeded(dir, rule) {
135 if (rule.priority !== 0) {
136 return false;
137 }
138
139 if ((dir === 'from' && rule.action === 'allow')
140 || (dir === 'to' && rule.action === 'block')) {
141 return true;
142 }
143 return false;
144 }
145
146
147 /**
148 * For each rule in rules, call cb for each target present in the rule,
149 * passing the rule, direction, target type and target itself.
150 *
151 * @param rules {Array} : rule objects to process
152 */
153 function ruleTypeDirWalk(rules, cb) {
154 rules.forEach(function (rule) {
155 DIRECTIONS.forEach(function (dir) {
156 ['ips', 'tags', 'vms'].forEach(function (type) {
157 if (hasKey(rule[dir], type)) {
158 rule[dir][type].forEach(function (t) {
159 cb(rule, dir, type, t);
160 });
161 }
162 });
163 });
164 });
165 }
166
167
168 /**
169 * Returns a list of rules with duplicates removed. Rules in list1 will
170 * override rules in list2
171 */
172 function dedupRules(list1, list2) {
173 var seenUUIDs = {};
174 var toReturn = [];
175
176 list1.concat(list2).forEach(function (r) {
177 if (hasKey(r, 'uuid') && !hasKey(seenUUIDs, r.uuid)) {
178 toReturn.push(r);
179 seenUUIDs[r.uuid] = 1;
180 }
181 });
182
183 return toReturn;
184 }
185
186
187 /**
188 * Given a list of new rules and a map of existing rules, build a list
189 * of the subset of new rules that are actually changing anything.
190 */
191 function getChangingRules(rules, existingRules, cb) {
192 var changing = rules.filter(function (rule) {
193 if (!hasKey(existingRules, rule.uuid)) {
194 return true;
195 }
196 return !mod_obj.shallowObjEqual(rule.serialize(),
197 existingRules[rule.uuid].serialize());
198 });
199
200 cb(null, changing);
201 }
202
203
204 /**
205 * This filter removes rules that aren't affected by adding a remote VM
206 * or updating a local VM (for example, simple wildcard rules), and would
207 * therefore require updating the rules of other VMs. This table shows which
208 * rules we keep. Each row represents whether or not a new VM or remote VM
209 * is a source or destination VM, plus whether or not the rule is a BLOCK
210 * or ALLOW rule. Each column represents a collection of targets we might
211 * have. Each cell says whether or not we need to update the VMs represented
212 * by that column.
213 *
214 * | From | To | From | To | From | To
215 * | All VMs | All VMs | Tags | Tags | VMs | VMs
216 * |------------|---------|---------|------|------|------|-----
217 * | Dest. VM / | No | No | No | No | No | No
218 * | ALLOW | | | | | |
219 * |------------|---------|---------|------|------|------|-----
220 * | Src VM / | No | Yes | No | Yes | No | Yes
221 * | ALLOW | | | | | |
222 * |------------|---------|---------|------|------|------|-----
223 * | Dest. VM / | Yes | No | Yes | No | Yes | No
224 * | BLOCK | | | | | |
225 * |------------|---------|---------|------|------|------|-----
226 * | Src VM / | No | No | No | No | No | No
227 * | BLOCK | | | | | |
228 *
229 */
230 function getAffectedRules(new_vms, log) {
231 return function _isAffectedRule(rule) {
232 if (rule.action === 'allow'
233 && vmsOnSide(new_vms, rule, 'from', log).length > 0) {
234 return rule.to.wildcards.indexOf('vmall') !== -1
235 || rule.to.tags.length > 0
236 || rule.to.vms.length > 0;
237 } else if (rule.action === 'block'
238 && vmsOnSide(new_vms, rule, 'to', log).length > 0) {
239 return rule.from.wildcards.indexOf('vmall') !== -1
240 || rule.from.tags.length > 0
241 || rule.from.vms.length > 0;
242 }
243 return false;
244 };
245 }
246
247
248 /**
249 * Starts ipf and reloads the rules for a VM
250 */
251 function reloadIPF(opts, log, callback) {
252 var ipfConf = util.format(IPF_CONF, opts.zonepath);
253 var ipf6Conf = util.format(IPF6_CONF, opts.zonepath);
254
255 mod_ipf.reload(opts.vm, ipfConf, ipf6Conf, log, callback);
256 }
257
258
259
260 // --- Internal functions
261
262
263
264 /**
265 * Validates the payload passed to the exported functions. Throws an error
266 * if not in the right format
267 */
268 function validateOpts(opts) {
269 assert.object(opts, 'opts');
270 assert.ok(Array.isArray(opts.vms),
271 'opts.vms ([object]) required');
272 // Allow opts.vms to be empty - it's possible, though unlikely, that
273 // there are no VMs on this system
274 if (opts.vms.length !== 0) {
275 assert.arrayOfObject(opts.vms, 'opts.vms');
276 }
277 }
278
279
280 /**
281 * Create rule objects from the rules
282 *
283 * @param {Array} inRules : raw rule input objects to create
284 * @param {Function} callback : `f(err, newRules)`
285 * - newRules {Array} : array of rule objects
286 */
287 function createRules(inRules, createdBy, callback) {
288 var errors = [];
289 var rules = [];
290 var ver = mod_rule.generateVersion();
291
292 if (!callback) {
293 callback = createdBy;
294 createdBy = null;
295 }
296
297 if (!inRules || inRules.length === 0) {
298 return callback(null, []);
299 }
300
301 inRules.forEach(function (payloadRule) {
302 var rule = clone(payloadRule);
303 if (!hasKey(rule, 'version')) {
304 rule.version = ver;
305 }
306
307 if (createdBy && !hasKey(rule, 'created_by')) {
308 rule.created_by = createdBy;
309 }
310
311 try {
312 var r = mod_rule.create(rule, { enforceGlobal: true });
313 rules.push(r);
314 } catch (err) {
315 errors.push(err);
316 }
317 });
318
319 if (errors.length !== 0) {
320 return callback(util_err.createMultiError(errors));
321 }
322
323 return callback(null, rules);
324 }
325
326
327 /**
328 * Merge updates from the rules in payload, and return the updated
329 * rule objects
330 */
331 function createUpdatedRules(opts, log, callback) {
332 log.trace('createUpdatedRules: entry');
333 var originals = opts.originalRules;
334 var updatedRules = opts.updatedRules;
335
336 if (!updatedRules || updatedRules.length === 0) {
337 return callback(null, []);
338 }
339
340 var mergedRule;
341 var origRule;
342 var updated = [];
343 var ver = mod_rule.generateVersion();
344
345 updatedRules.forEach(function (rule) {
346 // Assume that we're allowed to do adds - findRules() would have errored
347 // out if allowAdds was unset and an add was attempted
348 if (!hasKey(rule, 'version')) {
349 rule.version = ver;
350 }
351
352 if (opts.createdBy && !hasKey(rule, 'created_by')) {
353 rule.created_by = opts.createdBy;
354 }
355
356 if (hasKey(originals, rule.uuid)) {
357 origRule = originals[rule.uuid].serialize();
358 mergedRule = mergeObjects(rule, origRule);
359
360 if (!(hasKey(rule, 'owner_uuid')
361 && hasKey(rule, 'global'))) {
362 // If both owner_uuid and global are set - let
363 // this bubble up the appropriate error in createRules()
364
365 if (hasKey(rule, 'owner_uuid')
366 && hasKey(origRule, 'global')) {
367 // Updating from global -> owner_uuid rule
368 delete mergedRule.global;
369 }
370
371 if (hasKey(rule, 'global')
372 && hasKey(origRule, 'owner_uuid')) {
373 // Updating from owner_uuid -> global rule
374 delete mergedRule.owner_uuid;
375 }
376 }
377
378 if (mod_obj.shallowObjEqual(origRule, mergedRule)) {
379 // The rule hasn't changed - do nothing
380 delete originals[rule.uuid];
381 } else {
382 updated.push(mergedRule);
383 }
384 } else {
385 updated.push(rule);
386 }
387 });
388
389 log.debug(updated, 'createUpdatedRules: rules merged');
390 return createRules(updated, callback);
391 }
392
393
394 function mergeIntoLookup(vmStore, vm) {
395 vmStore.all[vm.uuid] = vm;
396 mod_obj.addToObj3(vmStore, 'vms', vm.uuid, vm.uuid, vm);
397
398 forEachKey(vm.tags, function (tag, val) {
399 createSubObjects(vmStore, 'tags', tag, vm.uuid, vm);
400 createSubObjects(vmStore, 'tagValues', tag, val, vm.uuid, vm);
401 });
402
403 vm.ips.forEach(function (ip) {
404 mod_obj.addToObj3(vmStore, 'ips', ip, vm.uuid, vm);
405 });
406
407 // XXX: subnet
408 }
409
410
411 /**
412 * Turns a list of VMs from VM.js into a lookup table, keyed by the various
413 * properties we'd like to filter VMs by (tags, ips, and vms),
414 * like so:
415 * {
416 * all: { uuid1: <vm 1> }
417 * tags: { tag2: <vm 2> }
418 * vms: { uuid1: <vm 1> }
419 * ips: { 10.0.0.1: <vm 3> }
420 * ips: { 10.0.0.1: <vm 3> }
421 * }
422 */
423 function createVMlookup(vms, log, callback) {
424 log.trace('createVMlookup: entry');
425
426 var errs = [];
427 var vmStore = {
428 all: {},
429 ips: {},
430 subnets: {},
431 tags: {},
432 tagValues: {},
433 vms: {},
434 wildcards: {}
435 };
436
437 vmStore.wildcards.vmall = vmStore.all;
438
439 vms.forEach(function (fullVM) {
440 var missing = [];
441 VM_FIELDS_REQUIRED.forEach(function (field) {
442 if (!hasKey(fullVM, field)) {
443 missing.push(field);
444 }
445 });
446
447 if (missing.length !== 0) {
448 log.error({ vm: fullVM, missing: missing }, 'missing VM fields');
449 errs.push(new verror.VError(
450 'VM %s: missing field%s required for firewall: %s',
451 fullVM.uuid,
452 missing.length === 0 ? '' : 's',
453 missing.join(', ')));
454 return;
455 }
456
457 var vm = {
458 enabled: fullVM.firewall_enabled || false,
459 ips: util_vm.ipsFromNICs(fullVM.nics).sort(mod_addr.compare),
460 owner_uuid: fullVM.owner_uuid,
461 state: fullVM.state,
462 tags: fullVM.tags,
463 uuid: fullVM.uuid,
464 zonepath: fullVM.zonepath
465 };
466 log.trace(vm, 'Adding VM "%s" to lookup', vm.uuid);
467
468 mergeIntoLookup(vmStore, vm);
469 });
470
471 if (errs.length !== 0) {
472 return callback(util_err.createMultiError(errs));
473 }
474
475 if (log.debug()) {
476 var truncated = { };
477 ['vms', 'tags', 'ips'].forEach(function (type) {
478 truncated[type] = {};
479 if (!hasKey(vmStore, type)) {
480 return;
481 }
482
483 Object.keys(vmStore[type]).forEach(function (t) {
484 truncated[type][t] = Object.keys(vmStore[type][t]);
485 });
486 });
487
488 log.debug(truncated, 'vmStore');
489 }
490
491 return callback(null, vmStore);
492 }
493
494
495 /**
496 * Create a lookup table for remote VMs
497 */
498 function createRemoteVMlookup(remoteVMs, log, callback) {
499 return callback(null, mod_rvm.createLookup(remoteVMs, log));
500 }
501
502
503 /**
504 * Load a single rule from disk, returning a rule object
505 *
506 * @param {String} uuid : UUID of the rule to load
507 * @param {Function} callback : `f(err, rule)`
508 * - vm {Object} : rule object (as per mod_rule)
509 */
510 function loadRule(uuid, log, callback) {
511 var file = util.format('%s/%s.json', RULE_PATH, uuid);
512 log.debug('loadRule: loading rule file "%s"', file);
513
514 return fs.readFile(file, function (err, raw) {
515 if (err) {
516 if (err.code == 'ENOENT') {
517 var uErr = new verror.VError('Unknown rule "%s"', uuid);
518 uErr.code = 'ENOENT';
519 return callback(uErr);
520 }
521
522 return callback(err);
523 }
524
525 var rule;
526
527 try {
528 var parsed = JSON.parse(raw);
529 log.trace(parsed, 'loadRule: loaded rule file "%s"', file);
530 // XXX: validate that the rule has a uuid
531 rule = mod_rule.create(parsed);
532 } catch (err2) {
533 log.error(err2, 'loadRule: error creating rule');
534 return callback(err2);
535 }
536
537 if (log.trace()) {
538 log.trace(rule.toString(), 'loadRule: created rule');
539 }
540
541 return callback(null, rule);
542 });
543 }
544
545
546 /**
547 * Loads all rules from disk
548 */
549 function loadAllRules(log, callback) {
550 var rules = [];
551
552 fs.readdir(RULE_PATH, function (err, files) {
553 if (err) {
554 if (err.code === 'ENOENT') {
555 return callback(null, []);
556 }
557 return callback(err);
558 }
559
560 return vasync.forEachParallel({
561 inputs: files,
562 func: function (file, cb) {
563 if (file.indexOf('.json', file.length - 5) === -1) {
564 return cb(null);
565 }
566
567 return loadRule(file.substring(0, file.length - 5),
568 log, function (err2, rule) {
569 if (rule) {
570 rules.push(rule);
571 }
572 return cb(err2);
573 });
574 }
575 }, function (err3, res) {
576 if (err3) {
577 log.error(err3, 'loadAllRules: return');
578 return callback(err3);
579 }
580
581 log.debug({ fullRules: rules }, 'loadAllRules: return');
582 return callback(null, rules);
583 });
584 });
585 }
586
587
588 /*
589 * Saves rules to disk
590 *
591 * @param {Array} rules : rule objects to save
592 * @param {Function} callback : `f(err)`
593 */
594 function saveRules(rules, log, callback) {
595 var uuids = [];
596 var versions = {};
597 log.debug({ rules: rules }, 'saveRules: entry');
598
599 return vasync.pipeline({
600 funcs: [
601 function _mkdir(_, cb) { mkdirp(RULE_PATH, cb); },
602 function _writeRules(_, cb) {
603 return vasync.forEachParallel({
604 inputs: rules,
605 func: function _writeRule(rule, cb2) {
606 var ser = rule.serialize();
607 // XXX: allow overriding version in the payload
608 var filename = util.format('%s/%s.json.%s', RULE_PATH,
609 rule.uuid, rule.version);
610 log.trace(ser, 'writing "%s"', filename);
611
612 return fs.writeFile(filename, JSON.stringify(ser, null, 2),
613 function (err) {
614 if (err) {
615 return cb2(err);
616 }
617 uuids.push(rule.uuid);
618 versions[rule.uuid] = rule.version;
619
620 return cb2(null);
621 });
622 }
623 // XXX: if there are failures here, we want to delete these files
624 }, cb);
625 },
626 function _renameRules(_, cb) {
627 return vasync.forEachParallel({
628 inputs: uuids,
629 func: function _renameRule(uuid, cb2) {
630 var before = util.format('%s/%s.json.%s', RULE_PATH, uuid,
631 versions[uuid]);
632 var after = util.format('%s/%s.json', RULE_PATH, uuid);
633 log.trace('renaming "%s" to "%s"', before, after);
634 fs.rename(before, after, cb2);
635 }
636 }, cb);
637 }
638 ]}, callback);
639 }
640
641
642 /*
643 * Deletes rules on disk
644 *
645 * @param {Array} rules : rule objects to delete
646 * @param {Function} callback : `f(err)`
647 */
648 function deleteRules(rules, log, callback) {
649 log.debug({ rules: rules }, 'deleteRules: entry');
650
651 return vasync.forEachParallel({
652 inputs: rules.map(function (r) { return r.uuid; }),
653 func: function _delRule(uuid, cb) {
654 var filename = util.format('%s/%s.json', RULE_PATH, uuid);
655 log.trace('deleting "%s"', filename);
656
657 fs.unlink(filename, function (err) {
658 if (err && err.code == 'ENOENT') {
659 return cb();
660 }
661
662 return cb(err);
663 });
664 }
665 }, callback);
666 }
667
668
669 /**
670 * Loads rules and remote VMs from disk
671 */
672 function loadDataFromDisk(log, callback) {
673 var onDisk = {};
674
675 vasync.parallel({
676 funcs: [
677 function _diskRules(cb) {
678 loadAllRules(log, function (err, res) {
679 if (res) {
680 onDisk.rules = res;
681 onDisk.rulesByUUID = {};
682 onDisk.rules.forEach(function (rule) {
683 onDisk.rulesByUUID[rule.uuid] = rule;
684 });
685 }
686
687 return cb(err);
688 });
689 },
690
691 function _diskRemoteVMs(cb) {
692 mod_rvm.loadAll(log, function (err, res) {
693 if (res) {
694 onDisk.remoteVMs = res;
695 }
696
697 return cb(err);
698 });
699 }
700 ]
701 }, function (err) {
702 if (err) {
703 return callback(err);
704 }
705
706 return callback(null, onDisk);
707 });
708 }
709
710
711 /**
712 * Finds rules in the list, returning an error if they can't be found
713 */
714 function findRules(opts, log, callback) {
715 log.trace('findRules: entry');
716 var allowAdds = opts.allowAdds || false;
717 var allRules = opts.allRules;
718 var rules = opts.rules;
719
720 if (!rules || rules.length === 0) {
721 return callback(null, []);
722 }
723
724 var errs = [];
725 var found = {};
726 var missing = {};
727
728 rules.forEach(function (r) {
729 if (!hasKey(r, 'uuid')) {
730 errs.push(new verror.VError('Missing UUID of rule: %j', r));
731 return;
732 }
733
734 if (hasKey(allRules, r.uuid)) {
735 found[r.uuid] = allRules[r.uuid];
736 } else {
737 missing[r.uuid] = 1;
738 }
739 });
740
741 // If we're allowing adds, missing rules aren't an error
742 if (!allowAdds && !objEmpty(missing)) {
743 Object.keys(missing).forEach(function (uuid) {
744 errs.push(new verror.VError('Unknown rule: %s', uuid));
745 });
746 }
747
748 if (log.debug()) {
749 var ret = { rules: found };
750 if (allowAdds) {
751 ret.adds = Object.keys(missing);
752 } else {
753 ret.missing = Object.keys(missing);
754 }
755 log.debug(ret, 'findRules: return');
756 }
757
758 if (errs.length !== 0) {
759 callback(util_err.createMultiError(errs));
760 return;
761 }
762
763 callback(null, found);
764 }
765
766
767 /**
768 * Looks up the given VMs in the VM lookup object, and returns an
769 * object mapping UUIDs to VM lookup objects
770 */
771 function lookupVMs(allVMs, vms, log, callback) {
772 log.debug({ vms: vms }, 'lookupVMs: entry');
773
774 if (!vms || vms.length === 0) {
775 log.debug('lookupVMs: no VMs to lookup: returning');
776 return callback(null, {});
777 }
778
779 var toReturn = {};
780 var errs = [];
781 vms.forEach(function (vm) {
782 if (!hasKey(vm, 'uuid')) {
783 errs.push(new verror.VError('VM missing uuid property: %j', vm));
784 return;
785 }
786 if (!hasKey(allVMs.all, vm.uuid)) {
787 errs.push(new verror.VError('Could not find VM "%s" in VM list',
788 vm.uuid));
789 return;
790 }
791 toReturn[vm.uuid] = allVMs.all[vm.uuid];
792 });
793
794 if (errs.length !== 0) {
795 return callback(util_err.createMultiError(errs));
796 }
797
798 log.debug({ vms: toReturn }, 'lookupVMs: return');
799 return callback(null, toReturn);
800 }
801
802
803 /**
804 * Validates the list of rules, ensuring that there's enough information
805 * to write each rule to disk
806 *
807 * @param vms {Object}: VM lookup table, as returned by createVMlookup()
808 * @param rvms {Object}: remote VM lookup table, as returned by
809 * createRemoteVMlookup()
810 * @param rules {Array}: array of rule objects
811 * @param callback {Function} `function (err)`
812 */
813 function validateRules(vms, rvms, rules, log, callback) {
814 log.trace(rules, 'validateRules: entry');
815 var sideData = {};
816 var errs = [];
817 var rulesLeft = rules.reduce(function (h, r) {
818 h[r.uuid] = r;
819 return h;
820 }, {});
821
822 // XXX: make owner uuid aware
823
824 // First go through the rules finding all the VMs we need rules for,
825 // and mark any missing types
826 ruleTypeDirWalk(rules, function _getRuleData(rule, dir, type, t) {
827 // Don't bother checking IPs, since we don't need any additional
828 // data in order to create an ipf rule
829 if (type == 'ips') {
830 return;
831 }
832
833 // Allow creating rules that target tags, but not any specific VMs
834 if (type == 'tags') {
835 delete rulesLeft[rule.uuid];
836 return;
837 }
838
839 createSubObjects(sideData, rule.uuid, dir, 'missing', type);
840 createSubObjects(sideData, rule.uuid, dir, 'vms');
841
842 if (hasKey(vms[type], t)) {
843 for (var vm in vms[type][t]) {
844 sideData[rule.uuid][dir].vms[vm] = 1;
845 }
846 delete rulesLeft[rule.uuid];
847
848 } else if (hasKey(rvms[type], t)) {
849 delete rulesLeft[rule.uuid];
850
851 } else {
852 sideData[rule.uuid][dir].missing[type][t] = 1;
853 }
854 });
855
856 for (var uuid in rulesLeft) {
857 errs.push(new verror.VError('No VMs found that match rule: %s',
858 rulesLeft[uuid].text()));
859 }
860
861 rules.forEach(function (rule) {
862 var missing = sideData[rule.uuid];
863
864 if (!missing) {
865 return;
866 }
867
868 DIRECTIONS.forEach(function (dir) {
869 var otherSide = (dir == 'to' ? 'from' : 'to');
870
871 if (!hasKey(missing, dir) || objEmpty(missing[dir].vms)
872 || !hasKey(missing, otherSide)) {
873 return;
874 }
875
876 for (var type in missing[otherSide].missing) {
877 for (var t in missing[otherSide].missing[type]) {
878 errs.push(new verror.VError('Missing %s %s for rule: %s',
879 type.replace(/s$/, ''), t, rule.text()));
880 }
881 }
882 });
883 });
884
885 if (errs.length !== 0) {
886 return callback(util_err.createMultiError(errs));
887 }
888
889 return callback();
890 }
891
892
893 /**
894 * Returns the appropriate target string based on the rule's protocol
895 * (eg: code for ICMP, port for TCP / UDP)
896 */
897 function protoTarget(rule, target) {
898 if (target === 'all') {
899 return '';
900 } else if (icmpr.test(rule.protocol)) {
901 var typeArr = target.split(':');
902 return 'icmp-type ' + typeArr[0]
903 + (typeArr.length === 1 ? '' : ' code ' + typeArr[1]);
904 } else {
905 if (hasKey(target, 'start')
906 && hasKey(target, 'end')) {
907
908 return 'port ' + target.start + ' : ' + target.end;
909 } else {
910 return 'port = ' + target;
911 }
912 }
913 }
914
915
916 /**
917 * Compare two port targets. Valid values are "all", numbers, or an object
918 * representing a port range containing "start" and "end" fields.
919 */
920 function comparePorts(p1, p2) {
921 // "all" comes before any port numbers
922 if (p1 === 'all') {
923 if (p2 === 'all') {
924 return 0;
925 } else {
926 return -1;
927 }
928 } else if (p2 === 'all') {
929 return 1;
930 }
931
932 var n1 = p1.hasOwnProperty('start') ? p1.start : p1;
933 var n2 = p2.hasOwnProperty('start') ? p2.start : p2;
934
935 return Number(n1) - Number(n2);
936 }
937
938
939 /**
940 * Compare two ICMP type targets. Valid values are "all" or strings like "5" or
941 * "5:3", representing the ICMP type number and code.
942 */
943 function compareTypes(t1, t2) {
944 // "all" comes before any types
945 if (t1 === 'all') {
946 if (t2 === 'all') {
947 return 0;
948 } else {
949 return -1;
950 }
951 } else if (t2 === 'all') {
952 return 1;
953 }
954
955 var p1 = t1.split(':');
956 var p2 = t2.split(':');
957 var c = Number(p1[0]) - Number(p2[0]);
958 if (c !== 0) {
959 return c;
960 }
961
962 if (p1.length === 1) {
963 if (p2.length === 1) {
964 return 0;
965 } else {
966 return -1;
967 }
968 } else if (p2.length === 1) {
969 return 1;
970 } else {
971 return Number(p1[1]) - Number(p2[1]);
972 }
973 }
974
975
976 /**
977 * Compare IP and subnet targets from an ipf rule object.
978 */
979 function compareAddrs(a1, a2) {
980 var s1 = a1.split('/');
981 var s2 = a2.split('/');
982
983 return mod_addr.compare(s1[0], s2[0]);
984 }
985
986
987 /**
988 * This comparison function is used to sort the output ipf rule objects
989 * in the following order:
990 *
991 * 1. First by protocol: icmp, icmp6, tcp, udp.
992 * 2. Then by direction: outbound rules, then inbound.
993 * 3. By priority level (higher priorities come first).
994 * 4. By action:
995 * a) For outbound traffic, block comes before allow.
996 * b) For inbound traffic, allow comes before block.
997 * 5. By the first port or type listed.
998 * 6. By the first targeted IP.
999 *
1000 * 1 and 2 help apply some organization to the output file.
1001 * 5 and 6 help make testing easier by putting things in a predictable order.
1002 *
1003 * 3 and 4 are the actual, important metric to sort on: priority and action
1004 * are important for ensuring that the actions taken by ipfilter are applied in
1005 * the order that fwadm(1M) describes.
1006 */
1007 function compareRules(r1, r2) {
1008 var res;
1009
1010 // Protocol:
1011 if (r1.protocol < r2.protocol) {
1012 return -1;
1013 }
1014
1015 if (r1.protocol > r2.protocol) {
1016 return 1;
1017 }
1018
1019 // Direction:
1020 if (r1.direction < r2.direction) {
1021 return -1;
1022 }
1023
1024 if (r1.direction > r2.direction) {
1025 return 1;
1026 }
1027
1028 // Priority levels:
1029 if (r1.priority < r2.priority) {
1030 return 1;
1031 }
1032
1033 if (r1.priority > r2.priority) {
1034 return -1;
1035 }
1036
1037 // Action:
1038 if (r1.direction === 'from') {
1039 if (r1.action === 'allow') {
1040 if (r2.action === 'block') {
1041 return 1;
1042 }
1043 } else if (r2.action === 'allow') {
1044 if (r1.action === 'block') {
1045 return -1;
1046 }
1047 }
1048 } else {
1049 if (r1.action === 'allow') {
1050 if (r2.action === 'block') {
1051 return -1;
1052 }
1053 } else if (r2.action === 'allow') {
1054 if (r1.action === 'block') {
1055 return 1;
1056 }
1057 }
1058 }
1059
1060 // Ports and types:
1061 if (icmpr.test(r1.protocol)) {
1062 res = compareTypes(r1.protoTargets[0], r2.protoTargets[0]);
1063 } else {
1064 res = comparePorts(r1.protoTargets[0], r2.protoTargets[0]);
1065 }
1066
1067 if (res !== 0) {
1068 return res;
1069 }
1070
1071 // Target IPs and subnets:
1072 return compareAddrs(r1.targets[0], r2.targets[0]);
1073 }
1074
1075
1076 /**
1077 * Returns an object containing ipf rule text and enough data to sort on
1078 */
1079 function ipfRuleObj(opts) {
1080 var dir = opts.direction;
1081 var rule = opts.rule;
1082
1083 var targets = Array.isArray(opts.targets) ?
1084 opts.targets : [ opts.targets ];
1085
1086 // ipfilter uses /etc/protocols which calls ICMPv6 'ipv6-icmp'
1087 var ipfProto = (rule.protocol === 'icmp6') ? 'ipv6-icmp' : rule.protocol;
1088
1089 var sortObj = {
1090 action: rule.action,
1091 direction: dir,
1092 priority: rule.priority,
1093 protocol: rule.protocol,
1094 header: util.format('\n# rule=%s, version=%s, %s=%s',
1095 rule.uuid, rule.version, opts.type, opts.value),
1096 v4text: [],
1097 v6text: [],
1098 targets: targets,
1099 protoTargets: rule.protoTargets,
1100 type: opts.type,
1101 uuid: rule.uuid,
1102 value: opts.value,
1103 version: rule.version,
1104 uuidTag: (features.feature[FEATURE_INOUT_UUID] && rule.uuid) ?
1105 sprintf(' set-tag(uuid=%s)', rule.uuid) : ''
1106 };
1107
1108 if (opts.type === 'wildcard' && opts.value === 'any') {
1109 rule.protoTargets.forEach(function (t) {
1110 var wild = util.format('%s %s quick proto %s from any to any %s',
1111 rule.action === 'allow' ? 'pass' : 'block',
1112 dir === 'from' ? 'out' : 'in',
1113 ipfProto,
1114 protoTarget(rule, t));
1115 if (rule.protocol !== 'icmp6')
1116 sortObj.v4text.push(wild);
1117 if (rule.protocol !== 'icmp')
1118 sortObj.v6text.push(wild);
1119 });
1120
1121 return sortObj;
1122 }
1123
1124 targets.forEach(function (target) {
1125 var isv6 = target.indexOf(':') !== -1;
1126
1127 // Don't generate rules for ICMPv4/IPv6 or ICMPv6/IPv4
1128 if ((isv6 && rule.protocol === 'icmp')
1129 || (!isv6 && rule.protocol === 'icmp6')) {
1130 return;
1131 }
1132
1133 var text = isv6 ? sortObj.v6text : sortObj.v4text;
1134
1135 rule.protoTargets.forEach(function (t) {
1136 text.push(
1137 util.format('%s %s quick proto %s from %s to %s %s',
1138 rule.action === 'allow' ? 'pass' : 'block',
1139 dir === 'from' ? 'out' : 'in',
1140 ipfProto,
1141 dir === 'to' ? target : 'any',
1142 dir === 'to' ? 'any' : target,
1143 protoTarget(rule, t)))
1144 });
1145 });
1146
1147 return sortObj;
1148 }
1149
1150
1151 /**
1152 * Returns an object containing all ipf files to be written to disk, based
1153 * on the given rules
1154 *
1155 * @param opts {Object} :
1156 * - @param allVMs {Object} : VM lookup table, as returned by createVMlookup()
1157 * - @param remoteVMs {Array} : array of remote VM objects (optional)
1158 * - @param rules {Array} : array of rule objects
1159 * - @param vms {Array} : object mapping VM UUIDs to VM objects. All VMs in
1160 * this object will have conf files written. This covers the case where
1161 * a rule used to target a VM, but no longer does, so we want to write the
1162 * config minus the rule that no longer applies.
1163 * @param callback {Function} `function (err)`
1164 */
1165 function prepareIPFdata(opts, log, callback) {
1166 var allVMs = opts.allVMs;
1167 var date = new Date();
1168 var rules = opts.rules;
1169 var vms = opts.vms;
1170 var remoteVMs = opts.remoteVMs || { ips: {}, vms: {}, tags: {} };
1171
1172 log.debug({ vms: vms, rules: rules }, 'prepareIPFdata: entry');
1173
1174 var conf = {};
1175 if (vms) {
1176 conf = Object.keys(vms).reduce(function (acc, v) {
1177 // If the VM's firewall is disabled, we don't need to write out
1178 // rules for it
1179 if (allVMs.all[v].enabled) {
1180 acc[v] = [];
1181 }
1182 return acc;
1183 }, {});
1184 }
1185
1186 /* Gather the VMs targeted on each side of every enabled rule. */
1187 var targetVMs = rules.map(function (rule) {
1188 if (!rule.enabled) {
1189 return null;
1190 }
1191
1192 return {
1193 from: vmsOnSide(allVMs, rule, 'from', log),
1194 to: vmsOnSide(allVMs, rule, 'to', log)
1195 };
1196 });
1197
1198 /*
1199 * If we block outbound traffic for a protocol, make sure to also track
1200 * inbound state for anything allowed, so that we'll allow response
1201 * packets.
1202 *
1203 * We could just always enable state tracking for all of our inbound
1204 * allow rules, but state tracking can get pretty expensive. There's no
1205 * need to penalize all firewall users.
1206 */
1207 var keepInboundState = { };
1208 rules.forEach(function (rule, i) {
1209 if (!rule.enabled || rule.action === 'allow') {
1210 return;
1211 }
1212
1213 targetVMs[i].from.forEach(function (uuid) {
1214 if (!hasKey(keepInboundState, uuid)) {
1215 keepInboundState[uuid] = {};
1216 }
1217 keepInboundState[uuid][rule.protocol] = true;
1218 });
1219 });
1220
1221 rules.forEach(function (rule, i) {
1222 if (!rule.enabled) {
1223 return;
1224 }
1225
1226 DIRECTIONS.forEach(function (dir) {
1227 // XXX: add to errors here if missing
1228
1229 // Default outgoing policy is 'allow' and default incoming policy
1230 // is 'block', so these are effectively no-ops:
1231 if (noRulesNeeded(dir, rule)) {
1232 return;
1233 }
1234
1235 var otherSideRules =
1236 rulesFromOtherSide(rule, dir, allVMs, remoteVMs);
1237
1238 targetVMs[i][dir].forEach(function (uuid) {
1239 /*
1240 * If the VM's firewall is disabled, we don't need to write out
1241 * rules for it.
1242 */
1243 if (!allVMs.all[uuid].enabled || !hasKey(conf, uuid)) {
1244 return;
1245 }
1246
1247 conf[uuid] = conf[uuid].concat(otherSideRules);
1248 });
1249 });
1250 });
1251
1252 var toReturn = [];
1253 for (var vm in conf) {
1254 var rulesIncluded = {};
1255 var ipf4Conf = [
1256 '# DO NOT EDIT THIS FILE. THIS FILE IS AUTO-GENERATED BY fwadm(1M)',
1257 '# AND MAY BE OVERWRITTEN AT ANY TIME.',
1258 '#',
1259 '# File generated at ' + date.toString(),
1260 '#',
1261 ''];
1262 var ipf6Conf = ipf4Conf.slice();
1263 var iks = hasKey(keepInboundState, vm) ? keepInboundState[vm] : {};
1264
1265 conf[vm].sort(compareRules).forEach(function (sortObj) {
1266 assert.string(sortObj.uuidTag, 'sortObj.uuidTag');
1267 var ktxt = KEEP_FRAGS;
1268 if (sortObj.uuidTag !== ''
1269 || (sortObj.direction === 'from' && sortObj.action === 'allow')
1270 || (sortObj.direction === 'to' && iks[sortObj.protocol])) {
1271 ktxt += KEEP_STATE + sortObj.uuidTag;
1272 }
1273
1274 if (!hasKey(rulesIncluded, sortObj.uuid)) {
1275 rulesIncluded[sortObj.uuid] = [];
1276 }
1277 rulesIncluded[sortObj.uuid].push(sortObj.direction);
1278
1279 ipf4Conf.push(sortObj.header);
1280 ipf6Conf.push(sortObj.header);
1281
1282 sortObj.v4text.forEach(function (line) {
1283 ipf4Conf.push(line + ktxt);
1284 });
1285 sortObj.v6text.forEach(function (line) {
1286 ipf6Conf.push(line + ktxt);
1287 });
1288 });
1289
1290 log.debug(rulesIncluded, 'VM %s: generated ipf(6).conf', vm);
1291
1292 var v4rules = ipf4Conf.concat(v4fallbacks);
1293 var v6rules = ipf6Conf.concat(v6fallbacks);
1294
1295 toReturn.push({
1296 uuid: vm,
1297 zonepath: allVMs.all[vm].zonepath,
1298 v4text: v4rules.join('\n') + '\n',
1299 v6text: v6rules.join('\n') + '\n'
1300 });
1301 }
1302
1303 return callback(null, toReturn);
1304 }
1305
1306
1307 /**
1308 * Returns an array of the UUIDs of VMs on the given side of a rule
1309 */
1310 function vmsOnSide(allVMs, rule, dir, log) {
1311 var matching = [];
1312
1313 ['vms', 'tags', 'wildcards'].forEach(function (walkType) {
1314 rule[dir][walkType].forEach(function (t) {
1315 var type = walkType;
1316 var value;
1317 if (typeof (t) !== 'string') {
1318 value = t[1];
1319 t = t[0];
1320 type = 'tagValues';
1321 }
1322
1323 if (type === 'wildcards' && t === 'any') {
1324 return;
1325 }
1326
1327 if (!allVMs[type] || !hasKey(allVMs[type], t)) {
1328 log.debug('No matching VMs found in lookup for %s=%s', type, t);
1329 return;
1330 }
1331
1332 var vmList = allVMs[type][t];
1333 if (value !== undefined) {
1334 if (!hasKey(vmList, value)) {
1335 return;
1336 }
1337 vmList = vmList[value];
1338 }
1339
1340 Object.keys(vmList).forEach(function (uuid) {
1341 if (hasKey(rule, 'owner_uuid')
1342 && (rule.owner_uuid != vmList[uuid].owner_uuid)) {
1343 return;
1344 }
1345
1346 matching.push(uuid);
1347 });
1348 });
1349 });
1350
1351 return matching;
1352 }
1353
1354
1355 /**
1356 * Returns the ipf rules for the opposite side of a rule
1357 */
1358 function rulesFromOtherSide(rule, dir, localVMs, remoteVMs) {
1359 var otherSide = dir === 'from' ? 'to' : 'from';
1360 var ipfRules = [];
1361
1362 if (rule[otherSide].wildcards.indexOf('any') !== -1) {
1363 ipfRules.push(ipfRuleObj({
1364 rule: rule,
1365 direction: dir,
1366 targets: [ '0.0.0.0' ],
1367 type: 'wildcard',
1368 value: 'any'
1369 }));
1370
1371 return ipfRules;
1372 }
1373
1374 // IPs and subnets don't need looking up in the local or remote VM
1375 // lookup objects, so just them as-is
1376 ['ip', 'subnet'].forEach(function (type) {
1377 rule[otherSide][type + 's'].forEach(function (value) {
1378 ipfRules.push(ipfRuleObj({
1379 rule: rule,
1380 direction: dir,
1381 targets: value,
1382 type: type,
1383 value: value
1384 }));
1385 });
1386 });
1387
1388 // Lookup the VMs in the local and remove VM lookups, and add their IPs
1389 // accordingly
1390 ['tag', 'vm', 'wildcard'].forEach(function (type) {
1391 var typePlural = type + 's';
1392 rule[otherSide][typePlural].forEach(function (value) {
1393 var lookupType = type;
1394 var lookupTypePlural = typePlural;
1395 var t;
1396
1397 if (typeof (value) !== 'string') {
1398 t = value[1];
1399 value = value[0];
1400 lookupType = 'tagValue';
1401 lookupTypePlural = 'tagValues';
1402 }
1403
1404 if (lookupTypePlural === 'wildcards' && value === 'any') {
1405 return;
1406 }
1407
1408 [localVMs, remoteVMs].forEach(function (lookup) {
1409 if (!hasKey(lookup, lookupTypePlural)
1410 || !hasKey(lookup[lookupTypePlural], value)) {
1411 return;
1412 }
1413
1414 var vmList = lookup[lookupTypePlural][value];
1415 if (t !== undefined) {
1416 if (!hasKey(vmList, t)) {
1417 return;
1418 }
1419 vmList = vmList[t];
1420 }
1421
1422 forEachKey(vmList, function (uuid, vm) {
1423 if (rule.owner_uuid && vm.owner_uuid
1424 && vm.owner_uuid != rule.owner_uuid) {
1425 return;
1426 }
1427
1428 if (vm.ips.length === 0) {
1429 return;
1430 }
1431
1432 ipfRules.push(ipfRuleObj({
1433 rule: rule,
1434 direction: dir,
1435 targets: vm.ips,
1436 type: lookupType,
1437 value: value
1438 }));
1439 });
1440 });
1441 });
1442 });
1443
1444 return ipfRules;
1445 }
1446
1447
1448 /**
1449 * Gets remote targets from the other side of the rule and adds them to
1450 * the targets object
1451 */
1452 function addOtherSideRemoteTargets(vms, rule, targets, dir, log) {
1453 var matching = vmsOnSide(vms, rule, dir, log);
1454 if (matching.length === 0) {
1455 return;
1456 }
1457
1458 var otherSide = dir === 'from' ? 'to' : 'from';
1459 if (rule[otherSide].tags.length !== 0) {
1460 if (!hasKey(targets, 'tags')) {
1461 targets.tags = {};
1462 }
1463
1464 // All tags (no value) wins out over tags with
1465 // a value. If multiple values for the same tag
1466 // are present, return them as an array
1467 rule[otherSide].tags.forEach(function (tag) {
1468 var key = tag;
1469 var val = true;
1470 if (typeof (tag) !== 'string') {
1471 key = tag[0];
1472 val = tag[1];
1473 }
1474
1475 if (!hasKey(targets.tags, key)) {
1476 targets.tags[key] = val;
1477 } else {
1478 if (targets.tags[key] !== true) {
1479 if (val === true) {
1480 targets.tags[key] = val;
1481 } else {
1482 if (!Array.isArray(targets.tags[key])) {
1483 targets.tags[key] = [ targets.tags[key] ];
1484 }
1485
1486 if (targets.tags[key].indexOf(val) === -1) {
1487 targets.tags[key].push(val);
1488 }
1489 }
1490 }
1491 }
1492 });
1493 }
1494
1495 if (rule[otherSide].vms.length !== 0) {
1496 if (!hasKey(targets, 'vms')) {
1497 targets.vms = {};
1498 }
1499
1500 rule[otherSide].vms.forEach(function (vm) {
1501 // Don't add if it's a local VM
1502 if (!hasKey(vms.all, vm)) {
1503 targets.vms[vm] = true;
1504 }
1505 });
1506 }
1507
1508 if (rule[otherSide].wildcards.indexOf('vmall') !== -1) {
1509 targets.allVMs = true;
1510 }
1511 }
1512
1513
1514 /**
1515 * Carefully move the new ipfilter configuration file into place. It's
1516 * important to make sure that if we fail or crash at any point that we
1517 * leave a file in place, since its presence is what determines whether
1518 * to enable the firewall at zone boot.
1519 */
1520 function replaceIPFconf(file, data, ver, callback) {
1521 var tempFile = util.format('%s.%s', file, ver);
1522 var oldFile = util.format('%s.old', file);
1523
1524 vasync.pipeline({
1525 funcs: [
1526 function _write(_, cb) {
1527 fs.writeFile(tempFile, data, cb);
1528 },
1529 function _unlinkOld(_, cb) {
1530 fs.unlink(oldFile, function (err) {
1531 if (err && err.code === 'ENOENT') {
1532 cb(null);
1533 return;
1534 }
1535
1536 cb(err);
1537 });
1538 },
1539 function _linkOld(_, cb) {
1540 fs.link(file, oldFile, function (err) {
1541 if (err && err.code === 'ENOENT') {
1542 cb(null);
1543 return;
1544 }
1545
1546 cb(err);
1547 });
1548 },
1549 function _renameTemp(_, cb) {
1550 fs.rename(tempFile, file, cb);
1551 }
1552 ]}, callback);
1553 }
1554
1555
1556 /**
1557 * Saves all of the generated ipfilter rules in ipfData to disk. We handle
1558 * each VM separately in parallel, so that failures for one don't impact
1559 * reloading others. For example, a VM may have filled up its disk, and we
1560 * now can't write out its configuration, or a VM may have stopped on us
1561 * before we had a chance to run ipf(1M) on it.
1562 */
1563 function saveConfsAndReload(opts, ipfData, log, callback) {
1564 var ver = Date.now(0) + '.' + sprintf('%06d', process.pid);
1565 var files = {};
1566 var uuids = [];
1567
1568 vasync.forEachParallel({
1569 inputs: ipfData,
1570 func: function (vm, cb) {
1571 uuids.push(vm.uuid);
1572
1573 vasync.pipeline({
1574 funcs: [
1575 // Write the new ipf.conf for IPv4 rules:
1576 function writeV4(_, cb2) {
1577 var filename = util.format(IPF_CONF, vm.zonepath);
1578 files[filename] = vm.v4text;
1579
1580 if (opts.dryrun) {
1581 cb2(null);
1582 return;
1583 }
1584
1585 replaceIPFconf(filename, vm.v4text, ver, cb2);
1586 },
1587
1588 // Write the new ipf6.conf for IPv6 rules:
1589 function writeV6(_, cb2) {
1590 var filename = util.format(IPF6_CONF, vm.zonepath);
1591 files[filename] = vm.v6text;
1592
1593 if (opts.dryrun) {
1594 cb2(null);
1595 return;
1596 }
1597
1598 replaceIPFconf(filename, vm.v6text, ver, cb2);
1599 },
1600
1601 // Restart the VM's firewall:
1602 function reload(_, cb2) {
1603 if (opts.dryrun) {
1604 cb2(null);
1605 return;
1606 }
1607
1608 restartFirewall(opts.allVMs, vm.uuid, log, cb2);
1609 }
1610 ]}, cb);
1611 }
1612 }, function (err) {
1613 if (err) {
1614 callback(err);
1615 return;
1616 }
1617
1618 callback(null, {
1619 files: files,
1620 vms: uuids
1621 });
1622 });
1623 }
1624
1625
1626 /**
1627 * Restart the given VMs firewall.
1628 *
1629 * @param vms {Object}: VM lookup table, as returned by createVMlookup()
1630 * @param uuid {UUID}: The UUID of the target VM
1631 * @param callback {Function} `function (err)`
1632 */
1633 function restartFirewall(vms, uuid, log, cb) {
1634 if (!vms.all[uuid].enabled || vms.all[uuid].state !== 'running') {
1635 log.debug('restartFirewalls: VM "%s": not restarting '
1636 + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1637 vms.all[uuid].state);
1638 cb(null);
1639 return;
1640 }
1641
1642 log.debug('restartFirewalls: reloading firewall for VM "%s" '
1643 + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1644 vms.all[uuid].state);
1645
1646 // Reload the firewall, and start it if necessary.
1647 reloadIPF({ vm: uuid, zonepath: vms.all[uuid].zonepath }, log,
1648 function (err, res) {
1649 if (err && zoneNotRunning(res)) {
1650 /*
1651 * An error starting the firewall due to the zone not
1652 * running isn't really an error.
1653 */
1654 cb();
1655 return;
1656 }
1657
1658 cb(err);
1659 });
1660 }
1661
1662
1663 /**
1664 * Applies firewall changes:
1665 * - saves / deletes rule files as needed
1666 * - writes out ipf conf files
1667 * - starts or restarts ipf in VMs
1668 *
1669 * @param {Object} opts :
1670 * - allRemoteVMs {Object} : VM lookup object of all remote VMs
1671 * - allVMs {Object} : VM lookup object of all local VMs
1672 * - del {Object} : Objects to delete from disk:
1673 * - rules {Array of Objects} : rules objects to delete
1674 * - rvms {Array of Objects} : remote VM UUIDs to delete
1675 * - dryRun {Bool} : if true, no files will be written or firewalls reloaded
1676 * - rules {Array of Objects} : rules to write out
1677 * - save {Object} : Objects to save to disk:
1678 * - rules {Array of Objects} : rule objects to save
1679 * - remoteVMs {Array of Objects} : remote VM objects to save
1680 * - vms {Object} : Mapping of UUID to VM object - VMs to write out
1681 * firewalls for, regardless of whether or not rules affect them
1682 * (necessary for catching the case where a VM used to have rules that
1683 * applied to it but no longer does)
1684 */
1685 function applyChanges(opts, log, callback) {
1686 log.trace(opts, 'applyChanges: entry');
1687
1688 assert.object(opts, 'opts');
1689 assert.optionalObject(opts.allRemoteVMs, 'opts.allRemoteVMs');
1690 assert.optionalObject(opts.allVMs, 'opts.allVMs');
1691 assert.optionalObject(opts.del, 'opts.del');
1692 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1693 assert.optionalObject(opts.vms, 'opts.vms');
1694 assert.optionalObject(opts.save, 'opts.save');
1695
1696 pipeline({
1697 funcs: [
1698 // Determine which platform-specific features are available
1699 function loadFeatures(res, cb) {
1700 features.load({log: log}, cb);
1701 },
1702
1703 // Generate the ipf files for each VM
1704 function reloadPlan(res, cb) {
1705 prepareIPFdata({
1706 allVMs: opts.allVMs,
1707 remoteVMs: opts.allRemoteVMs,
1708 rules: opts.rules,
1709 vms: opts.vms
1710 }, log, cb);
1711 },
1712
1713 // Save the remote VMs
1714 function saveVMs(res, cb) {
1715 if (opts.dryrun || !opts.save || !opts.save.remoteVMs
1716 || objEmpty(opts.save.remoteVMs)) {
1717 return cb(null);
1718 }
1719 mod_rvm.save(opts.save.remoteVMs, log, cb);
1720 },
1721
1722 // Save rule files (if specified)
1723 function save(res, cb) {
1724 if (opts.dryrun || !opts.save || !opts.save.rules
1725 || opts.save.rules.length === 0) {
1726 return cb(null);
1727 }
1728 saveRules(opts.save.rules, log, cb);
1729 },
1730
1731 // Delete rule files (if specified)
1732 function delRules(res, cb) {
1733 if (opts.dryrun || !opts.del || !opts.del.rules
1734 || opts.del.rules.length === 0) {
1735 return cb(null);
1736 }
1737 deleteRules(opts.del.rules, log, cb);
1738 },
1739
1740 // Delete remote VMs (if specified)
1741 function delRVMs(res, cb) {
1742 if (opts.dryrun || !opts.del || !opts.del.rvms
1743 || opts.del.rvms.length === 0) {
1744 return cb(null);
1745 }
1746 mod_rvm.del(opts.del.rvms, log, cb);
1747 },
1748
1749 // Write the new ipf files to disk and restart affected VMs
1750 function ipfData(res, cb) {
1751 saveConfsAndReload(opts, res.reloadPlan, log, cb);
1752 }
1753 ] }, function (err, res) {
1754 if (err) {
1755 callback(err);
1756 return;
1757 }
1758
1759 var toReturn = {
1760 vms: res.state.ipfData.vms
1761 };
1762
1763 if (opts.save) {
1764 if (opts.save.rules) {
1765 toReturn.rules = opts.save.rules.map(function (r) {
1766 return r.serialize();
1767 });
1768 }
1769
1770 if (opts.save.remoteVMs) {
1771 toReturn.remoteVMs = Object.keys(opts.save.remoteVMs).sort();
1772 }
1773 }
1774
1775 if (opts.del) {
1776 if (opts.del.rules) {
1777 toReturn.rules = opts.del.rules.map(function (r) {
1778 return r.serialize();
1779 });
1780 }
1781
1782 if (opts.del.rvms && opts.del.rvms.length !== 0) {
1783 toReturn.remoteVMs = opts.del.rvms.sort();
1784 }
1785 }
1786
1787 if (opts.filecontents) {
1788 toReturn.files = res.state.ipfData.files;
1789 }
1790
1791 callback(null, toReturn);
1792 });
1793 }
1794
1795
1796 /**
1797 * Examine the stderr from an ipf command and return true if the zone
1798 * wasn't running at the time
1799 */
1800 function zoneNotRunning(res) {
1801 return res && res.stderr && res.stderr.indexOf(NOT_RUNNING_MSG) !== -1;
1802 }
1803
1804
1805
1806 // --- Exported functions
1807
1808
1809 /**
1810 * Functions that touch anything in the following directories:
1811 *
1812 * - /var/fw (such as /var/fw/rules and /var/fw/vms)
1813 * - /zones/<uuid>/config (e.g. ipf.conf and ipf6.conf)
1814 *
1815 * Should acquire the appropriate type of lock so that they read and write
1816 * a consistent view of the local firewall rules, and so that parallel
1817 * executions don't run into each other while operating. (See FWAPI-240.)
1818 */
1819
1820
1821 /**
1822 * Add rules, local VMs or remote VMs
1823 *
1824 * @param {Object} opts : options
1825 * - localVMs {Array} : list of local VMs to update
1826 * - remoteVMs {Array} : list of remote VMs to add
1827 * - rules {Array} : list of rules
1828 * - vms {Array} : list of VMs from vmadm
1829 * @param {Function} callback : `f(err, res)`
1830 */
1831 function add(opts, callback) {
1832 try {
1833 validateOpts(opts);
1834 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1835 assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
1836 assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
1837 assert.optionalString(opts.createdBy, 'opts.createdBy');
1838
1839 var optRules = opts.rules || [];
1840 var optLocalVMs = opts.localVMs || [];
1841 var optRemoteVMs = opts.remoteVMs || [];
1842 if (optRules.length === 0 && optLocalVMs.length === 0
1843 && optRemoteVMs.length === 0) {
1844 throw new Error(
1845 'Payload must contain one of: rules, localVMs, remoteVMs');
1846 }
1847 } catch (err) {
1848 return callback(err);
1849 }
1850 var log = util_log.entry(opts, 'add');
1851
1852 pipeline({
1853 funcs: [
1854 function lock(_, cb) {
1855 mod_lock.acquireExclusiveLock(cb);
1856 },
1857
1858 function originalRules(_, cb) {
1859 createRules(opts.rules, opts.createdBy, cb);
1860 },
1861
1862 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
1863
1864 function disk(_, cb) { loadDataFromDisk(log, cb); },
1865
1866 // If we're trying to add a rule that already exists and looks
1867 // the same, drop it.
1868 function rules(res, cb) {
1869 getChangingRules(res.originalRules, res.disk.rulesByUUID, cb);
1870 },
1871
1872 function newRemoteVMs(res, cb) {
1873 mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
1874 opts.remoteVMs, cb);
1875 },
1876
1877 // Create remote VMs (if any) from payload
1878 function remoteVMs(res, cb) {
1879 createRemoteVMlookup(res.newRemoteVMs, log, cb);
1880 },
1881
1882 // Create a combined remote VM lookup of remote VMs on disk plus
1883 // new remote VMs in the payload
1884 function allRemoteVMs(res, cb) {
1885 createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
1886 log, cb);
1887 },
1888
1889 function localVMs(res, cb) {
1890 lookupVMs(res.vms, opts.localVMs, log, cb);
1891 },
1892
1893 // Build a table for information about newly added local/remote VMs
1894 function newVMs(res, cb) {
1895 var nvms = clone(res.remoteVMs);
1896 mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
1897 cb(null, nvms);
1898 },
1899
1900 function allRules(res, cb) {
1901 return cb(null, dedupRules(res.rules, res.disk.rules));
1902 },
1903
1904 // Get VMs the added rules affect
1905 function matchingVMs(res, cb) {
1906 filter.vmsByRules({
1907 log: log,
1908 rules: res.rules,
1909 vms: res.vms
1910 }, cb);
1911 },
1912
1913 // Get rules the added remote VMs affect
1914 function remoteVMrules(res, cb) {
1915 filter.rulesByRVMs(res.remoteVMs, res.allRules, log, cb);
1916 },
1917
1918 // Get any rules that the added local VMs target
1919 function localVMrules(res, cb) {
1920 filter.rulesByVMs(res.vms, res.localVMs, res.allRules, log, cb);
1921 },
1922
1923 // Merge the local and remote VM rules, and use that list to find
1924 // the VMs affected.
1925 function localAndRemoteVMsAffected(res, cb) {
1926 var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
1927 .filter(getAffectedRules(res.newVMs, log));
1928 filter.vmsByRules({
1929 log: log,
1930 rules: affectedRules,
1931 vms: res.vms
1932 }, cb);
1933 },
1934
1935 function mergedVMs(res, cb) {
1936 var ruleVMs = mergeObjects(res.localVMs, res.matchingVMs);
1937 return cb(null, mergeObjects(ruleVMs,
1938 res.localAndRemoteVMsAffected));
1939 },
1940
1941 // Get the rules that need to be written out for all VMs, before and
1942 // after the update
1943 function vmRules(res, cb) {
1944 filter.rulesByVMs(res.vms, res.mergedVMs, res.allRules, log, cb);
1945 },
1946
1947 function apply(res, cb) {
1948 applyChanges({
1949 allVMs: res.vms,
1950 dryrun: opts.dryrun,
1951 filecontents: opts.filecontents,
1952 allRemoteVMs: res.allRemoteVMs,
1953 rules: res.vmRules,
1954 save: {
1955 rules: res.rules,
1956 remoteVMs: res.newRemoteVMs
1957 },
1958 vms: res.mergedVMs
1959 }, log, cb);
1960 }
1961 ]}, function (err, res) {
1962 mod_lock.releaseLock(res.state.lock);
1963
1964 if (err) {
1965 util_log.finishErr(log, err, 'add: finish');
1966 return callback(err);
1967 }
1968
1969 log.debug(res.state.apply, 'add: finish');
1970 return callback(err, res.state.apply);
1971 });
1972 }
1973
1974
1975 /**
1976 * Delete rules
1977 *
1978 * @param {Object} opts : options
1979 * - uuids {Array} : list of rules
1980 * - vms {Array} : list of VMs from vmadm
1981 * @param {Function} callback : `f(err, res)`
1982 */
1983 function del(opts, callback) {
1984 try {
1985 assert.object(opts, 'opts');
1986 assert.optionalArrayOfString(opts.rvmUUIDs, 'opts.rvmUUIDs');
1987 assert.optionalArrayOfString(opts.uuids, 'opts.uuids');
1988 assert.arrayOfObject(opts.vms, 'vms');
1989
1990 var rvmUUIDs = opts.rvmUUIDs || [];
1991 var uuids = opts.uuids || [];
1992 if (rvmUUIDs.length === 0 && uuids.length === 0) {
1993 throw new Error(
1994 'Payload must contain one of: rvmUUIDs, uuids');
1995 }
1996
1997 } catch (err) {
1998 return callback(err);
1999 }
2000 var log = util_log.entry(opts, 'del');
2001
2002 pipeline({
2003 funcs: [
2004 function lock(_, cb) {
2005 mod_lock.acquireExclusiveLock(cb);
2006 },
2007 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2008
2009 function disk(_, cb) { loadDataFromDisk(log, cb); },
2010
2011 function allRemoteVMs(state, cb) {
2012 createRemoteVMlookup(state.disk.remoteVMs, log, cb);
2013 },
2014
2015 // Get matching remote VMs
2016 function remoteVMs(state, cb) {
2017 filter.rvmsByUUIDs(state.allRemoteVMs, opts.rvmUUIDs, log, cb);
2018 },
2019
2020 // Get rules the delted remote VMs affect
2021 function remoteVMrules(res, cb) {
2022 filter.rulesByRVMs(res.remoteVMs.matching, res.disk.rules,
2023 log, cb);
2024 },
2025
2026 // Get VMs that are affected by the remote VM rules
2027 function rvmVMs(res, cb) {
2028 filter.vmsByRules({
2029 log: log,
2030 rules: res.remoteVMrules,
2031 vms: res.vms
2032 }, cb);
2033 },
2034
2035 // Get the deleted rules
2036 function rules(res, cb) {
2037 filter.rulesByUUIDs(res.disk.rules, opts.uuids, log, cb);
2038 },
2039
2040 // Get VMs the deleted rules affect
2041 function ruleVMs(res, cb) {
2042 filter.vmsByRules({
2043 log: log,
2044 rules: res.rules.matching,
2045 vms: res.vms
2046 }, cb);
2047 },
2048
2049 // Now find all rules that apply to those VMs, omitting the
2050 // rules that are deleted
2051 function vmRules(res, cb) {
2052 filter.rulesByVMs(res.vms,
2053 mergeObjects(res.ruleVMs, res.rvmVMs),
2054 res.rules.notMatching, log, cb);
2055 },
2056
2057 function apply(res, cb) {
2058 applyChanges({
2059 allVMs: res.vms,
2060 dryrun: opts.dryrun,
2061 filecontents: opts.filecontents,
2062 allRemoteVMs: res.remoteVMs.notMatching,
2063 rules: res.vmRules,
2064 del: {
2065 rules: res.rules.matching,
2066 rvms: objEmpty(res.remoteVMs.matching.all) ?
2067 null : Object.keys(res.remoteVMs.matching.all)
2068 },
2069 vms: mergeObjects(res.ruleVMs, res.rvmVMs)
2070 }, log, cb);
2071 }
2072 ]}, function (err, res) {
2073 mod_lock.releaseLock(res.state.lock);
2074
2075 if (err) {
2076 util_log.finishErr(log, err, 'del: finish');
2077 return callback(err);
2078 }
2079
2080 log.debug(res.state.apply, 'del: finish');
2081 return callback(err, res.state.apply);
2082 });
2083 }
2084
2085
2086 /**
2087 * Returns a remote VM
2088 *
2089 * @param opts {Object} : options:
2090 * - remoteVM {String} : UUID of remote VM to get
2091 * @param callback {Function} : `function (err, rvm)`
2092 */
2093 function getRemoteVM(opts, callback) {
2094 try {
2095 assert.object(opts, 'opts');
2096 assert.string(opts.remoteVM, 'opts.remoteVM');
2097 } catch (err) {
2098 return callback(err);
2099 }
2100 var log = util_log.entry(opts, 'getRemoteVM', true);
2101
2102 mod_lock.acquireSharedLock(function (lErr, fd) {
2103 if (lErr) {
2104 callback(lErr);
2105 return;
2106 }
2107
2108 mod_rvm.load(opts.remoteVM, log, function (err, rvm) {
2109 mod_lock.releaseLock(fd);
2110
2111 if (err) {
2112 if (err.code == 'ENOENT') {
2113 // Don't write a log file for "not found"
2114 log.info(err, 'getRemoteVM: finish');
2115 } else {
2116 util_log.finishErr(log, err, 'getRemoteVM: finish');
2117 }
2118 return callback(err);
2119 }
2120
2121 log.debug(rvm, 'getRemoteVM: finish');
2122 return callback(null, rvm);
2123 });
2124 });
2125 }
2126
2127
2128 /**
2129 * Returns a rule
2130 *
2131 * @param opts {Object} : options:
2132 * - uuid {String} : UUID of rule to get
2133 * @param callback {Function} : `function (err, rule)`
2134 */
2135 function getRule(opts, callback) {
2136 try {
2137 assert.object(opts, 'opts');
2138 assert.string(opts.uuid, 'opts.uuid');
2139 } catch (err) {
2140 return callback(err);
2141 }
2142 var log = util_log.entry(opts, 'get', true);
2143
2144 mod_lock.acquireSharedLock(function (lErr, fd) {
2145 if (lErr) {
2146 callback(lErr);
2147 return;
2148 }
2149
2150 loadRule(opts.uuid, log, function (err, rule) {
2151 mod_lock.releaseLock(fd);
2152
2153 if (err) {
2154 if (err.code == 'ENOENT') {
2155 // Don't write a log file for "not found"
2156 log.info(err, 'get: finish');
2157 } else {
2158 util_log.finishErr(log, err, 'get: finish');
2159 }
2160 return callback(err);
2161 }
2162
2163 var ser = rule.serialize();
2164 log.debug(ser, 'get: finish');
2165 return callback(null, ser);
2166 });
2167 });
2168 }
2169
2170
2171 /**
2172 * List remote VMs
2173 */
2174 function listRemoteVMs(opts, callback) {
2175 try {
2176 assert.object(opts, 'opts');
2177 } catch (err) {
2178 return callback(err);
2179 }
2180 var log = util_log.entry(opts, 'listRemoteVMs', true);
2181
2182 mod_lock.acquireSharedLock(function (lErr, fd) {
2183 if (lErr) {
2184 callback(lErr);
2185 return;
2186 }
2187
2188 mod_rvm.loadAll(log, function (err, res) {
2189 mod_lock.releaseLock(fd);
2190
2191 if (err) {
2192 util_log.finishErr(log, err, 'listRemoteVMs: finish');
2193 return callback(err);
2194 }
2195
2196 // XXX: support sorting by other fields, filtering
2197 var sortFn = function _sort(a, b) {
2198 return (a.uuid > b.uuid) ? 1: -1;
2199 };
2200
2201 log.debug('listRemoteVMs: finish');
2202 return callback(null, Object.keys(res).map(function (r) {
2203 return res[r];
2204 }).sort(sortFn));
2205 });
2206 });
2207 }
2208
2209
2210 /**
2211 * List rules
2212 */
2213 function listRules(opts, callback) {
2214 try {
2215 assert.object(opts, 'opts');
2216 assert.optionalArrayOfString(opts.fields, 'opts.fields');
2217 if (opts.fields) {
2218 var invalid = [];
2219 opts.fields.forEach(function (f) {
2220 if (mod_rule.FIELDS.indexOf(f) === -1) {
2221 invalid.push(f);
2222 }
2223 });
2224
2225 if (invalid.length > 0) {
2226 throw new verror.VError('Invalid display field%s: %s',
2227 invalid.length == 1 ? '' : 's',
2228 invalid.sort().join(', '));
2229 }
2230 }
2231 } catch (err) {
2232 return callback(err);
2233 }
2234 var log = util_log.entry(opts, 'list', true);
2235
2236 mod_lock.acquireSharedLock(function (lErr, fd) {
2237 if (lErr) {
2238 callback(lErr);
2239 return;
2240 }
2241
2242 loadAllRules(log, function (err, res) {
2243 mod_lock.releaseLock(fd);
2244
2245 if (err) {
2246 util_log.finishErr(log, err, 'list: finish');
2247 return callback(err);
2248 }
2249
2250 // XXX: support sorting by other fields, filtering
2251 // (eg: enabled=true vm=<uuid>)
2252 var sortFn = function _defaultSort(a, b) {
2253 return (a.uuid > b.uuid) ? 1: -1;
2254 };
2255 var mapFn = function _defaultMap(r) {
2256 return r.serialize();
2257 };
2258
2259 if (opts.fields) {
2260 var filterFields = opts.fields;
2261 // If we didn't include uuid in the fields to list, include
2262 // it here so that we can sort by it - we'll remove it after
2263 if (opts.fields.indexOf('uuid') === -1) {
2264 filterFields = opts.fields.concat(['uuid']);
2265 }
2266
2267 mapFn = function _fieldMap(r) {
2268 return r.serialize(filterFields);
2269 };
2270 }
2271
2272 var rules = res.map(mapFn).sort(sortFn);
2273 if (opts.fields && opts.fields.indexOf('uuid') === -1) {
2274 rules = rules.map(function (r) {
2275 delete r.uuid;
2276 return r;
2277 });
2278 }
2279
2280 log.debug('list: finish');
2281 return callback(null, rules);
2282 });
2283 });
2284 }
2285
2286
2287 /**
2288 * Enable the firewall for a VM. If the VM is running, start ipf for that VM.
2289 *
2290 * @param opts {Object} : options:
2291 * - vms {Array} : array of VM objects (as per VM.js)
2292 * - vm {Object} : VM object for the VM to enable
2293 * - dryrun {Boolean} : don't write any files to disk (Optional)
2294 * - filecontents {Boolean} : return contents of files written to
2295 * disk (Optional)
2296 * @param callback {Function} `function (err, res)`
2297 * - Where res is an object, optionall containing a files subhash
2298 * if opts.filecontents is set
2299 */
2300 function enableVM(opts, callback) {
2301 try {
2302 assert.object(opts, 'opts');
2303 assert.object(opts.vm, 'opts.vm');
2304 assert.arrayOfObject(opts.vms, 'opts.vms');
2305 } catch (err) {
2306 return callback(err);
2307 }
2308 var log = util_log.entry(opts, 'enable');
2309
2310 var vmFilter = {};
2311
2312 pipeline({
2313 funcs: [
2314 function lock(_, cb) {
2315 mod_lock.acquireExclusiveLock(cb);
2316 },
2317 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2318
2319 function disk(_, cb) { loadDataFromDisk(log, cb); },
2320
2321 function getVM(res, cb) {
2322 var vm = res.vms.all[opts.vm.uuid];
2323 if (!vm) {
2324 return cb(new verror.VError('VM "%s" not found', opts.vm.uuid));
2325 }
2326
2327 vmFilter[opts.vm.uuid] = vm;
2328 return cb();
2329 },
2330
2331 // Find all rules that apply to the VM
2332 function vmRules(res, cb) {
2333 filter.rulesByVMs(res.vms, vmFilter, res.disk.rules, log, cb);
2334 },
2335
2336 function allRemoteVMs(res, cb) {
2337 createRemoteVMlookup(res.disk.remoteVMs, log, cb);
2338 },
2339
2340 function apply(res, cb) {
2341 applyChanges({
2342 allVMs: res.vms,
2343 dryrun: opts.dryrun,
2344 filecontents: opts.filecontents,
2345 allRemoteVMs: res.allRemoteVMs,
2346 rules: res.vmRules,
2347 vms: vmFilter
2348 }, log, cb);
2349 }
2350 ]}, function _afterEnable(err, res) {
2351 mod_lock.releaseLock(res.state.lock);
2352
2353 if (err) {
2354 util_log.finishErr(log, err, 'enable: finish');
2355 return callback(err);
2356 }
2357
2358 var toReturn = res.state.apply;
2359 log.debug(toReturn, 'enable: finish');
2360 return callback(null, toReturn);
2361 });
2362 }
2363
2364
2365 /**
2366 * Disable the firewall for a VM. If the VM is running, stop ipf for that VM.
2367 *
2368 * @param opts {Object} : options:
2369 * - vm {Object} : VM object for the VM to disable
2370 * @param callback {Function} `function (err)`
2371 */
2372 function disableVM(opts, callback) {
2373 try {
2374 assert.object(opts, 'opts');
2375 assert.object(opts.vm, 'opts.vm');
2376 } catch (err) {
2377 return callback(err);
2378 }
2379 var log = util_log.entry(opts, 'disable');
2380
2381 function moveConf(new_fmt, old_fmt, _, cb) {
2382 // Move config out of the way - on zone boot, the firewall
2383 // will start again if it's present
2384 var new_cfg = util.format(new_fmt, opts.vm.zonepath);
2385 var old_cfg = util.format(old_fmt, opts.vm.zonepath);
2386 return fs.rename(new_cfg, old_cfg, function (err) {
2387 // If the file's already gone, that's OK
2388 if (err && err.code !== 'ENOENT') {
2389 return cb(err);
2390 }
2391
2392 return cb(null);
2393 });
2394 }
2395
2396 pipeline({
2397 funcs: [
2398 function lock(_, cb) {
2399 mod_lock.acquireExclusiveLock(cb);
2400 },
2401 moveConf.bind(null, IPF_CONF, IPF_CONF_OLD),
2402 moveConf.bind(null, IPF6_CONF, IPF6_CONF_OLD),
2403 function stop(_, cb) {
2404 if (opts.vm.state !== 'running') {
2405 log.debug('disableVM: VM "%s": not stopping ipf (state=%s)',
2406 opts.vm.uuid, opts.vm.state);
2407 return cb(null);
2408 }
2409
2410 log.debug('disableVM: stopping ipf for VM "%s"', opts.vm.uuid);
2411 return mod_ipf.stop(opts.vm.uuid, log, cb);
2412 }
2413 ]}, function _afterDisable(err, res) {
2414 mod_lock.releaseLock(res.state.lock);
2415
2416 if (err) {
2417 util_log.finishErr(log, err, 'disable: finish');
2418 return callback(err);
2419 }
2420
2421 log.debug('disable: finish');
2422 return callback();
2423 });
2424 }
2425
2426
2427 /**
2428 * Gets the firewall status for a VM
2429 *
2430 * @param opts {Object} : options:
2431 * - uuid {String} : VM UUID
2432 * @param callback {Function} `function (err, res)`
2433 */
2434 function vmStatus(opts, callback) {
2435 try {
2436 assert.object(opts, 'opts');
2437 assert.string(opts.uuid, 'opts.uuid');
2438 } catch (err) {
2439 return callback(err);
2440 }
2441 var log = util_log.entry(opts, 'status', true);
2442
2443 return mod_ipf.status(opts.uuid, log, function (err, res) {
2444 if (err) {
2445 // 'No such device' is returned when the zone is down
2446 if (zoneNotRunning(res)) {
2447 log.debug({ running: false }, 'status: finish');
2448 return callback(null, { running: false });
2449 }
2450
2451 util_log.finishErr(log, err, 'status: finish');
2452 return callback(err);
2453 }
2454
2455 log.debug(res, 'status: finish');
2456 return callback(null, res);
2457 });
2458 }
2459
2460
2461 /**
2462 * Gets the firewall statistics for a VM
2463 *
2464 * @param opts {Object} : options:
2465 * - uuid {String} : VM UUID
2466 * @param callback {Function} `function (err, res)`
2467 */
2468 function vmStats(opts, callback) {
2469 try {
2470 assert.object(opts, 'opts');
2471 assert.string(opts.uuid, 'opts.uuid');
2472 } catch (err) {
2473 return callback(err);
2474 }
2475 var log = util_log.entry(opts, 'stats', true);
2476
2477 return mod_ipf.ruleStats(opts.uuid, log, function (err, res) {
2478 if (err) {
2479 if (res && res.stderr) {
2480 // Zone is down
2481 if (zoneNotRunning(res)) {
2482 log.debug('stats: finish: zone not running');
2483 return callback(new verror.VError(
2484 'Firewall is not running for VM "%s"', opts.uuid));
2485 }
2486
2487 // No rules loaded: return an error if the firewall
2488 // isn't running
2489 if (res.stderr.indexOf('empty list') !== -1) {
2490 return vmStatus(opts, function (err2, res2) {
2491 if (err2) {
2492 util_log.finishErr(log, err2, 'stats: finish');
2493 return callback(err2);
2494 }
2495
2496 if (res2.running) {
2497 log.debug({ rules: [] }, 'stats: finish');
2498 return callback(null, { rules: [] });
2499 } else {
2500 log.debug('stats: finish: firewall not running');
2501 return callback(new verror.VError(
2502 'Firewall is not running for VM "%s"',
2503 opts.uuid));
2504 }
2505 });
2506 }
2507 }
2508
2509 return callback(err);
2510 }
2511
2512 log.debug({ rules: res }, 'stats: finish');
2513 return callback(null, { rules: res });
2514 });
2515 }
2516
2517
2518 /**
2519 * Update rules, local VMs or remote VMs
2520 *
2521 * @param {Object} opts : options
2522 * - localVMs {Array} : list of local VMs to update
2523 * - remoteVMs {Array} : list of remote VMs to update
2524 * - rules {Array} : list of rules
2525 * - vms {Array} : list of VMs from vmadm
2526 * @param {Function} callback : `f(err, res)`
2527 */
2528 function update(opts, callback) {
2529 try {
2530 validateOpts(opts);
2531 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2532 assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2533 assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2534 assert.optionalString(opts.createdBy, 'opts.createdBy');
2535
2536 var optRules = opts.rules || [];
2537 var optLocalVMs = opts.localVMs || [];
2538 var optRemoteVMs = opts.remoteVMs || [];
2539 if (optRules.length === 0 && optLocalVMs.length === 0
2540 && optRemoteVMs.length === 0) {
2541 throw new Error(
2542 'Payload must contain one of: rules, localVMs, remoteVMs');
2543 }
2544 } catch (err) {
2545 return callback(err);
2546 }
2547 var log = util_log.entry(opts, 'update');
2548
2549 pipeline({
2550 funcs: [
2551 function lock(_, cb) {
2552 mod_lock.acquireExclusiveLock(cb);
2553 },
2554 function disk(_, cb) { loadDataFromDisk(log, cb); },
2555
2556 // Make sure the rules exist
2557 function originalRules(res, cb) {
2558 findRules({
2559 allRules: res.disk.rulesByUUID,
2560 allowAdds: opts.allowAdds,
2561 rules: opts.rules
2562 }, log, cb);
2563 },
2564
2565 // Apply updates to the found rules
2566 function rules(res, cb) {
2567 createUpdatedRules({
2568 createdBy: opts.createdBy,
2569 originalRules: res.originalRules,
2570 updatedRules: opts.rules
2571 }, log, cb);
2572 },
2573
2574 // Create list of rules that are being replaced
2575 function replacedRules(res, cb) {
2576 cb(null, mod_obj.values(res.originalRules));
2577 },
2578
2579 // Create the VM lookup
2580 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2581
2582 // Create remote VMs (if any) from payload
2583 function newRemoteVMs(res, cb) {
2584 mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
2585 opts.remoteVMs, cb);
2586 },
2587
2588 // Create a lookup for the new remote VMs
2589 function newRemoteVMsLookup(res, cb) {
2590 createRemoteVMlookup(res.newRemoteVMs, log, cb);
2591 },
2592
2593 function allRemoteVMs(res, cb) {
2594 createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
2595 log, cb);
2596 },
2597
2598 // Lookup any local VMs in the payload
2599 function localVMs(res, cb) {
2600 lookupVMs(res.vms, opts.localVMs, log, cb);
2601 },
2602
2603 // Build a table for information about updated local/remote VMs
2604 function newVMs(res, cb) {
2605 var nvms = clone(res.newRemoteVMsLookup);
2606 mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
2607 cb(null, nvms);
2608 },
2609
2610 // Get the VMs the rules applied to before the update
2611 function originalVMs(res, cb) {
2612 filter.vmsByRules({
2613 log: log,
2614 rules: res.replacedRules,
2615 vms: res.vms
2616 }, cb);
2617 },
2618
2619 // Now get the VMs the updated rules apply to
2620 function matchingVMs(res, cb) {
2621 filter.vmsByRules({
2622 log: log,
2623 rules: res.rules,
2624 vms: res.vms
2625 }, cb);
2626 },
2627
2628 // Replace the rules with their updated versions
2629 function updatedRules(res, cb) {
2630 return cb(null, dedupRules(res.rules, res.disk.rules));
2631 },
2632
2633 // Get any rules that the added remote VMs target
2634 function remoteVMrules(res, cb) {
2635 filter.rulesByRVMs(res.newRemoteVMsLookup,
2636 res.updatedRules, log, cb);
2637 },
2638
2639 // Get any rules that the added local VMs target
2640 function localVMrules(res, cb) {
2641 filter.rulesByVMs(res.vms, res.localVMs, res.updatedRules, log, cb);
2642 },
2643
2644 // Merge the local and remote VM rules, and use that list to find
2645 // the VMs affected.
2646 function localAndRemoteVMsAffected(res, cb) {
2647 var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
2648 .filter(getAffectedRules(res.newVMs, log));
2649 filter.vmsByRules({
2650 log: log,
2651 rules: affectedRules,
2652 vms: res.vms
2653 }, cb);
2654 },
2655
2656 function mergedVMs(res, cb) {
2657 // These are VMs affected by changing rules:
2658 var ruleVMs = mergeObjects(res.originalVMs, res.matchingVMs);
2659 // These are VMs affected by changing VM information:
2660 var updatedVMs = mergeObjects(res.localVMs,
2661 res.localAndRemoteVMsAffected);
2662 return cb(null, mergeObjects(ruleVMs, updatedVMs));
2663 },
2664
2665 // Get the rules that need to be written out for all VMs, before and
2666 // after the update
2667 function vmRules(res, cb) {
2668 filter.rulesByVMs(res.vms, res.mergedVMs, res.updatedRules, log,
2669 cb);
2670 },
2671
2672 function apply(res, cb) {
2673 applyChanges({
2674 allVMs: res.vms,
2675 dryrun: opts.dryrun,
2676 filecontents: opts.filecontents,
2677 allRemoteVMs: res.allRemoteVMs,
2678 rules: res.vmRules,
2679 save: {
2680 rules: res.rules,
2681 remoteVMs: res.newRemoteVMs
2682 },
2683 vms: res.mergedVMs
2684 }, log, cb);
2685 }
2686 ]}, function (err, res) {
2687 mod_lock.releaseLock(res.state.lock);
2688
2689 if (err) {
2690 util_log.finishErr(log, err, 'update: finish');
2691 return callback(err);
2692 }
2693
2694 log.debug(res.state.apply, 'update: finish');
2695 return callback(err, res.state.apply);
2696 });
2697 }
2698
2699
2700 /**
2701 * Given the list of local VMs and a list of rules, return an object with
2702 * the non-local targets on the other side of the rules.
2703 *
2704 * @param opts {Object} : options:
2705 * - vms {Array} : array of VM objects (as per VM.js)
2706 * - rules {Array of Objects} : firewall rules
2707 * @param callback {Function} `function (err, targets)`
2708 * - Where targets is an object like:
2709 * {
2710 * tags: { some: ['one', 'two'], other: true },
2711 * vms: [ '<UUID>' ],
2712 * allVMs: true
2713 * }
2714 */
2715 function getRemoteTargets(opts, callback) {
2716 try {
2717 assert.object(opts, 'opts');
2718 assert.arrayOfObject(opts.vms, 'opts.vms');
2719 assert.arrayOfObject(opts.rules, 'opts.rules');
2720
2721 if (opts.rules.length === 0) {
2722 throw new Error('Must specify rules');
2723 }
2724
2725 } catch (err) {
2726 return callback(err);
2727 }
2728 var log = util_log.entry(opts, 'remoteTargets', true);
2729
2730 pipeline({
2731 funcs: [
2732 function lock(_, cb) {
2733 mod_lock.acquireSharedLock(cb);
2734 },
2735 function rules(_, cb) {
2736 createRules(opts.rules, cb);
2737 },
2738 function vms(_, cb) {
2739 createVMlookup(opts.vms, log, cb);
2740 }
2741 ] }, function (err, res) {
2742 mod_lock.releaseLock(res.state.lock);
2743
2744 if (err) {
2745 util_log.finishErr(log, err, 'remoteTargets: createRules: finish');
2746 return callback(err);
2747 }
2748
2749 var targets = {};
2750
2751 for (var r in res.state.rules) {
2752 var rule = res.state.rules[r];
2753
2754 for (var d in DIRECTIONS) {
2755 var dir = DIRECTIONS[d];
2756 addOtherSideRemoteTargets(
2757 res.state.vms, rule, targets, dir, log);
2758 }
2759 }
2760
2761 if (hasKey(targets, 'vms')) {
2762 targets.vms = Object.keys(targets.vms);
2763 if (targets.vms.length === 0) {
2764 delete targets.vms;
2765 }
2766 }
2767
2768 log.debug(targets, 'remoteTargets: finish');
2769 return callback(null, targets);
2770 });
2771 }
2772
2773
2774 /**
2775 * Gets VMs that are affected by a rule
2776 *
2777 * @param opts {Object} : options:
2778 * - vms {Array} : array of VM objects (as per VM.js)
2779 * - rule {UUID or Object} : UUID of pre-existing rule, or a rule object
2780 * - includeDisabled {Boolean, optional} : if set, include VMs that have
2781 * their firewalls disabled in the search
2782 * @param callback {Function} `function (err, vms)`
2783 * - Where vms is an array of VMs that are affected by that rule
2784 */
2785 function getRuleVMs(opts, callback) {
2786 try {
2787 assert.object(opts, 'opts');
2788 assert.arrayOfObject(opts.vms, 'opts.vms');
2789 assertStringOrObject(opts.rule, 'opts.rule');
2790 assert.optionalBool(opts.includeDisabled, 'opts.includeDisabled');
2791 } catch (err) {
2792 return callback(err);
2793 }
2794 var log = util_log.entry(opts, 'vms', true);
2795
2796 pipeline({
2797 funcs: [
2798 function lock(_, cb) {
2799 mod_lock.acquireSharedLock(cb);
2800 },
2801 function rules(_, cb) {
2802 if (typeof (opts.rule) === 'string') {
2803 return loadRule(opts.rule, log, cb);
2804 }
2805
2806 createRules([ opts.rule ], cb);
2807 },
2808 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2809 function ruleVMs(state, cb) {
2810 if (!Array.isArray(state.rules)) {
2811 state.rules = [ state.rules ];
2812 }
2813
2814 filter.vmsByRules({
2815 includeDisabled: opts.includeDisabled,
2816 log: log,
2817 rules: state.rules,
2818 vms: state.vms
2819 }, cb);
2820 }
2821 ]}, function (err, res) {
2822 mod_lock.releaseLock(res.state.lock);
2823
2824 if (err) {
2825 util_log.finishErr(log, err, 'vms: finish');
2826 return callback(err);
2827 }
2828
2829 var matched = Object.keys(res.state.ruleVMs);
2830 log.debug(matched, 'vms: finish');
2831 return callback(null, matched);
2832 });
2833 }
2834
2835
2836 /**
2837 * Gets rules that apply to a Remote VM
2838 *
2839 * @param opts {Object} : options:
2840 * - vms {Array} : array of VM objects (as per VM.js)
2841 * - vm {UUID} : UUID of VM to get the rules for
2842 * @param callback {Function} `function (err, rules)`
2843 * - Where rules is an array of rules that apply to the VM
2844 */
2845 function getRemoteVMrules(opts, callback) {
2846 try {
2847 assert.object(opts, 'opts');
2848 assertStringOrObject(opts.remoteVM, 'opts.remoteVM');
2849 assert.arrayOfObject(opts.vms, 'opts.vms');
2850 } catch (err) {
2851 return callback(err);
2852 }
2853 var log = util_log.entry(opts, 'rvmRules', true);
2854
2855 pipeline({
2856 funcs: [
2857 function lock(_, cb) {
2858 mod_lock.acquireSharedLock(cb);
2859 },
2860 function allRules(_, cb) { loadAllRules(log, cb); },
2861 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2862 function rvm(_, cb) {
2863 if (typeof (opts.remoteVM) === 'object') {
2864 return cb(null, opts.remoteVM);
2865 }
2866
2867 return mod_rvm.load(opts.remoteVM, log, cb);
2868 },
2869 function rvms(state, cb) {
2870 mod_rvm.create({ allVMs: state.vms, requireIPs: false, log: log },
2871 [ state.rvm ], function (e, rvmList) {
2872 if (e) {
2873 return cb(e);
2874 }
2875
2876 createRemoteVMlookup(rvmList, log, cb);
2877 });
2878 },
2879 function rvmRules(state, cb) {
2880 filter.rulesByRVMs(state.rvms, state.allRules, log, cb);
2881 }
2882 ]}, function (err, res) {
2883 mod_lock.releaseLock(res.state.lock);
2884
2885 if (err) {
2886 util_log.finishErr(log, err, 'rvmRules: finish (vm=%s)',
2887 opts.remoteVM);
2888 return callback(err);
2889 }
2890
2891 var toReturn = res.state.rvmRules.map(function (r) {
2892 return r.serialize();
2893 });
2894
2895 log.debug(toReturn, 'rvmRules: finish (vm=%s)', opts.remoteVM);
2896 return callback(null, toReturn);
2897 });
2898 }
2899
2900
2901 /**
2902 * Gets rules that apply to a VM
2903 *
2904 * @param opts {Object} : options:
2905 * - vms {Array} : array of VM objects (as per VM.js)
2906 * - vm {UUID} : UUID of VM to get the rules for
2907 * @param callback {Function} `function (err, rules)`
2908 * - Where rules is an array of rules that apply to the VM
2909 */
2910 function getVMrules(opts, callback) {
2911 try {
2912 assert.object(opts, 'opts');
2913 assert.string(opts.vm, 'opts.vm');
2914 assert.arrayOfObject(opts.vms, 'opts.vms');
2915 } catch (err) {
2916 return callback(err);
2917 }
2918 var log = util_log.entry(opts, 'vmRules', true);
2919
2920 var toFind = {};
2921 toFind[opts.vm] = opts.vm;
2922
2923 pipeline({
2924 funcs: [
2925 function lock(_, cb) {
2926 mod_lock.acquireSharedLock(cb);
2927 },
2928 function allRules(_, cb) { loadAllRules(log, cb); },
2929 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2930 function vmRules(state, cb) {
2931 filter.rulesByVMs(state.vms, toFind, state.allRules, log, cb);
2932 }
2933 ]}, function (err, res) {
2934 mod_lock.releaseLock(res.state.lock);
2935
2936 if (err) {
2937 util_log.finishErr(log, err, 'vmRules: finish (vm=%s)', opts.vm);
2938 return callback(err);
2939 }
2940
2941 var toReturn = res.state.vmRules.map(function (r) {
2942 return r.serialize();
2943 });
2944
2945 log.debug(toReturn, 'vmRules: finish (vm=%s)', opts.vm);
2946 return callback(null, toReturn);
2947 });
2948 }
2949
2950
2951 /**
2952 * Validates an add / update payload
2953 *
2954 * @param opts {Object} : options:
2955 * - localVMs {Array} : list of local VMs
2956 * - remoteVMs {Array} : list of remote VMs
2957 * - rules {Array} : list of rules
2958 * - vms {Array} : array of VM objects (as per VM.js)
2959 * @param callback {Function} `function (err, rules)`
2960 * - Where rules is an array of rules that apply to the VM
2961 */
2962 function validatePayload(opts, callback) {
2963 try {
2964 assert.object(opts, 'opts');
2965 assert.arrayOfObject(opts.vms, 'opts.vms');
2966 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2967 assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2968 assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2969
2970 var optRules = opts.rules || [];
2971 var optLocalVMs = opts.localVMs || [];
2972 var optRemoteVMs = opts.remoteVMs || [];
2973 if (optRules.length === 0 && optLocalVMs.length === 0
2974 && optRemoteVMs.length === 0) {
2975 throw new Error(
2976 'Payload must contain one of: rules, localVMs, remoteVMs');
2977 }
2978 } catch (err) {
2979 return callback(err);
2980 }
2981 var log = util_log.entry(opts, 'validatePayload');
2982
2983 pipeline({
2984 funcs: [
2985 function lock(_, cb) {
2986 mod_lock.acquireSharedLock(cb);
2987 },
2988 function rules(_, cb) {
2989 createRules(opts.rules, cb);
2990 },
2991 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2992 function remoteVMs(_, cb) { mod_rvm.loadAll(log, cb); },
2993 function newRemoteVMs(state, cb) {
2994 mod_rvm.create({ allVMs: state.vms, requireIPs: true, log: log },
2995 opts.remoteVMs, cb);
2996 },
2997 // Create a combined remote VM lookup of remote VMs on disk plus
2998 // new remote VMs in the payload
2999 function allRemoteVMs(state, cb) {
3000 createRemoteVMlookup([state.remoteVMs, state.newRemoteVMs],
3001 log, cb);
3002 },
3003
3004 function validate(state, cb) {
3005 validateRules(state.vms, state.allRemoteVMs, state.rules, log, cb);
3006 }
3007 ]}, function (err, res) {
3008 mod_lock.releaseLock(res.state.lock);
3009
3010 if (err) {
3011 util_log.finishErr(log, err, 'validatePayload: finish');
3012 return callback(err);
3013 }
3014
3015 log.debug(opts.payload, 'validatePayload: finish');
3016 return callback();
3017 });
3018 }
3019
3020
3021
3022 module.exports = {
3023 _setOldIPF: mod_ipf._setOld,
3024 add: add,
3025 del: del,
3026 disable: disableVM,
3027 enable: enableVM,
3028 get: getRule,
3029 getRVM: getRemoteVM,
3030 list: listRules,
3031 listRVMs: listRemoteVMs,
3032 remoteTargets: getRemoteTargets,
3033 rvmRules: getRemoteVMrules,
3034 setBunyan: util_log.setBunyan,
3035 stats: vmStats,
3036 status: vmStatus,
3037 update: update,
3038 validatePayload: validatePayload,
3039 VM_FIELDS: VM_FIELDS,
3040 vmRules: getVMrules,
3041 vms: getRuleVMs
3042 };