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 readtags = [];
1090 if (features.feature[FEATURE_INOUT_UUID]) {
1091 if (rule.uuid) {
1092 readtags.push(util.format('uuid=%s', rule.uuid));
1093 }
1094 if (rule.log) {
1095 readtags.push('cfwlog');
1096 }
1097 }
1098
1099 var sortObj = {
1100 action: rule.action,
1101 direction: dir,
1102 priority: rule.priority,
1103 protocol: rule.protocol,
1104 header: util.format('\n# rule=%s, version=%s, %s=%s',
1105 rule.uuid, rule.version, opts.type, opts.value),
1106 v4text: [],
1107 v6text: [],
1108 targets: targets,
1109 protoTargets: rule.protoTargets,
1110 type: opts.type,
1111 uuid: rule.uuid,
1112 value: opts.value,
1113 version: rule.version,
1114 allTags: readtags.length !== 0 ?
1115 util.format(' set-tag(%s)', readtags.join(', ')) : ''
1116 };
1117
1118 if (opts.type === 'wildcard' && opts.value === 'any') {
1119 rule.protoTargets.forEach(function (t) {
1120 var wild = util.format('%s %s quick proto %s from any to any %s',
1121 rule.action === 'allow' ? 'pass' : 'block',
1122 dir === 'from' ? 'out' : 'in',
1123 ipfProto,
1124 protoTarget(rule, t));
1125 if (rule.protocol !== 'icmp6')
1126 sortObj.v4text.push(wild);
1127 if (rule.protocol !== 'icmp')
1128 sortObj.v6text.push(wild);
1129 });
1130
1131 return sortObj;
1132 }
1133
1134 targets.forEach(function (target) {
1135 var isv6 = target.indexOf(':') !== -1;
1136
1137 // Don't generate rules for ICMPv4/IPv6 or ICMPv6/IPv4
1138 if ((isv6 && rule.protocol === 'icmp')
1139 || (!isv6 && rule.protocol === 'icmp6')) {
1140 return;
1141 }
1142
1143 var text = isv6 ? sortObj.v6text : sortObj.v4text;
1144
1145 rule.protoTargets.forEach(function (t) {
1146 text.push(
1147 util.format('%s %s quick proto %s from %s to %s %s',
1148 rule.action === 'allow' ? 'pass' : 'block',
1149 dir === 'from' ? 'out' : 'in',
1150 ipfProto,
1151 dir === 'to' ? target : 'any',
1152 dir === 'to' ? 'any' : target,
1153 protoTarget(rule, t)))
1154 });
1155 });
1156
1157 return sortObj;
1158 }
1159
1160
1161 /**
1162 * Returns an object containing all ipf files to be written to disk, based
1163 * on the given rules
1164 *
1165 * @param opts {Object} :
1166 * - @param allVMs {Object} : VM lookup table, as returned by createVMlookup()
1167 * - @param remoteVMs {Array} : array of remote VM objects (optional)
1168 * - @param rules {Array} : array of rule objects
1169 * - @param vms {Array} : object mapping VM UUIDs to VM objects. All VMs in
1170 * this object will have conf files written. This covers the case where
1171 * a rule used to target a VM, but no longer does, so we want to write the
1172 * config minus the rule that no longer applies.
1173 * @param callback {Function} `function (err)`
1174 */
1175 function prepareIPFdata(opts, log, callback) {
1176 var allVMs = opts.allVMs;
1177 var date = new Date();
1178 var rules = opts.rules;
1179 var vms = opts.vms;
1180 var remoteVMs = opts.remoteVMs || { ips: {}, vms: {}, tags: {} };
1181
1182 log.debug({ vms: vms, rules: rules }, 'prepareIPFdata: entry');
1183
1184 var conf = {};
1185 if (vms) {
1186 conf = Object.keys(vms).reduce(function (acc, v) {
1187 // If the VM's firewall is disabled, we don't need to write out
1188 // rules for it
1189 if (allVMs.all[v].enabled) {
1190 acc[v] = [];
1191 }
1192 return acc;
1193 }, {});
1194 }
1195
1196 /* Gather the VMs targeted on each side of every enabled rule. */
1197 var targetVMs = rules.map(function (rule) {
1198 if (!rule.enabled) {
1199 return null;
1200 }
1201
1202 return {
1203 from: vmsOnSide(allVMs, rule, 'from', log),
1204 to: vmsOnSide(allVMs, rule, 'to', log)
1205 };
1206 });
1207
1208 /*
1209 * If we block outbound traffic for a protocol, make sure to also track
1210 * inbound state for anything allowed, so that we'll allow response
1211 * packets.
1212 *
1213 * We could just always enable state tracking for all of our inbound
1214 * allow rules, but state tracking can get pretty expensive. There's no
1215 * need to penalize all firewall users.
1216 */
1217 var keepInboundState = { };
1218 rules.forEach(function (rule, i) {
1219 if (!rule.enabled || rule.action === 'allow') {
1220 return;
1221 }
1222
1223 targetVMs[i].from.forEach(function (uuid) {
1224 if (!hasKey(keepInboundState, uuid)) {
1225 keepInboundState[uuid] = {};
1226 }
1227 keepInboundState[uuid][rule.protocol] = true;
1228 });
1229 });
1230
1231 rules.forEach(function (rule, i) {
1232 if (!rule.enabled) {
1233 return;
1234 }
1235
1236 DIRECTIONS.forEach(function (dir) {
1237 // XXX: add to errors here if missing
1238
1239 // Default outgoing policy is 'allow' and default incoming policy
1240 // is 'block', so these are effectively no-ops:
1241 if (noRulesNeeded(dir, rule)) {
1242 return;
1243 }
1244
1245 var otherSideRules =
1246 rulesFromOtherSide(rule, dir, allVMs, remoteVMs);
1247
1248 targetVMs[i][dir].forEach(function (uuid) {
1249 /*
1250 * If the VM's firewall is disabled, we don't need to write out
1251 * rules for it.
1252 */
1253 if (!allVMs.all[uuid].enabled || !hasKey(conf, uuid)) {
1254 return;
1255 }
1256
1257 conf[uuid] = conf[uuid].concat(otherSideRules);
1258 });
1259 });
1260 });
1261
1262 var toReturn = [];
1263 for (var vm in conf) {
1264 var rulesIncluded = {};
1265 var ipf4Conf = [
1266 '# DO NOT EDIT THIS FILE. THIS FILE IS AUTO-GENERATED BY fwadm(1M)',
1267 '# AND MAY BE OVERWRITTEN AT ANY TIME.',
1268 '#',
1269 '# File generated at ' + date.toString(),
1270 '#',
1271 ''];
1272 var ipf6Conf = ipf4Conf.slice();
1273 var iks = hasKey(keepInboundState, vm) ? keepInboundState[vm] : {};
1274
1275 conf[vm].sort(compareRules).forEach(function (sortObj) {
1276 assert.string(sortObj.allTags, 'sortObj.allTags');
1277 var ktxt = KEEP_FRAGS;
1278 if (sortObj.allTags !== ''
1279 || (sortObj.direction === 'from' && sortObj.action === 'allow')
1280 || (sortObj.direction === 'to' && iks[sortObj.protocol])) {
1281 ktxt += KEEP_STATE + sortObj.allTags;
1282 }
1283
1284 if (!hasKey(rulesIncluded, sortObj.uuid)) {
1285 rulesIncluded[sortObj.uuid] = [];
1286 }
1287 rulesIncluded[sortObj.uuid].push(sortObj.direction);
1288
1289 ipf4Conf.push(sortObj.header);
1290 ipf6Conf.push(sortObj.header);
1291
1292 sortObj.v4text.forEach(function (line) {
1293 ipf4Conf.push(line + ktxt);
1294 });
1295 sortObj.v6text.forEach(function (line) {
1296 ipf6Conf.push(line + ktxt);
1297 });
1298 });
1299
1300 log.debug(rulesIncluded, 'VM %s: generated ipf(6).conf', vm);
1301
1302 var v4rules = ipf4Conf.concat(v4fallbacks);
1303 var v6rules = ipf6Conf.concat(v6fallbacks);
1304
1305 toReturn.push({
1306 uuid: vm,
1307 zonepath: allVMs.all[vm].zonepath,
1308 v4text: v4rules.join('\n') + '\n',
1309 v6text: v6rules.join('\n') + '\n'
1310 });
1311 }
1312
1313 return callback(null, toReturn);
1314 }
1315
1316
1317 /**
1318 * Returns an array of the UUIDs of VMs on the given side of a rule
1319 */
1320 function vmsOnSide(allVMs, rule, dir, log) {
1321 var matching = [];
1322
1323 ['vms', 'tags', 'wildcards'].forEach(function (walkType) {
1324 rule[dir][walkType].forEach(function (t) {
1325 var type = walkType;
1326 var value;
1327 if (typeof (t) !== 'string') {
1328 value = t[1];
1329 t = t[0];
1330 type = 'tagValues';
1331 }
1332
1333 if (type === 'wildcards' && t === 'any') {
1334 return;
1335 }
1336
1337 if (!allVMs[type] || !hasKey(allVMs[type], t)) {
1338 log.debug('No matching VMs found in lookup for %s=%s', type, t);
1339 return;
1340 }
1341
1342 var vmList = allVMs[type][t];
1343 if (value !== undefined) {
1344 if (!hasKey(vmList, value)) {
1345 return;
1346 }
1347 vmList = vmList[value];
1348 }
1349
1350 Object.keys(vmList).forEach(function (uuid) {
1351 if (hasKey(rule, 'owner_uuid')
1352 && (rule.owner_uuid != vmList[uuid].owner_uuid)) {
1353 return;
1354 }
1355
1356 matching.push(uuid);
1357 });
1358 });
1359 });
1360
1361 return matching;
1362 }
1363
1364
1365 /**
1366 * Returns the ipf rules for the opposite side of a rule
1367 */
1368 function rulesFromOtherSide(rule, dir, localVMs, remoteVMs) {
1369 var otherSide = dir === 'from' ? 'to' : 'from';
1370 var ipfRules = [];
1371
1372 if (rule[otherSide].wildcards.indexOf('any') !== -1) {
1373 ipfRules.push(ipfRuleObj({
1374 rule: rule,
1375 direction: dir,
1376 targets: [ '0.0.0.0' ],
1377 type: 'wildcard',
1378 value: 'any'
1379 }));
1380
1381 return ipfRules;
1382 }
1383
1384 // IPs and subnets don't need looking up in the local or remote VM
1385 // lookup objects, so just them as-is
1386 ['ip', 'subnet'].forEach(function (type) {
1387 rule[otherSide][type + 's'].forEach(function (value) {
1388 ipfRules.push(ipfRuleObj({
1389 rule: rule,
1390 direction: dir,
1391 targets: value,
1392 type: type,
1393 value: value
1394 }));
1395 });
1396 });
1397
1398 // Lookup the VMs in the local and remove VM lookups, and add their IPs
1399 // accordingly
1400 ['tag', 'vm', 'wildcard'].forEach(function (type) {
1401 var typePlural = type + 's';
1402 rule[otherSide][typePlural].forEach(function (value) {
1403 var lookupType = type;
1404 var lookupTypePlural = typePlural;
1405 var t;
1406
1407 if (typeof (value) !== 'string') {
1408 t = value[1];
1409 value = value[0];
1410 lookupType = 'tagValue';
1411 lookupTypePlural = 'tagValues';
1412 }
1413
1414 if (lookupTypePlural === 'wildcards' && value === 'any') {
1415 return;
1416 }
1417
1418 [localVMs, remoteVMs].forEach(function (lookup) {
1419 if (!hasKey(lookup, lookupTypePlural)
1420 || !hasKey(lookup[lookupTypePlural], value)) {
1421 return;
1422 }
1423
1424 var vmList = lookup[lookupTypePlural][value];
1425 if (t !== undefined) {
1426 if (!hasKey(vmList, t)) {
1427 return;
1428 }
1429 vmList = vmList[t];
1430 }
1431
1432 forEachKey(vmList, function (uuid, vm) {
1433 if (rule.owner_uuid && vm.owner_uuid
1434 && vm.owner_uuid != rule.owner_uuid) {
1435 return;
1436 }
1437
1438 if (vm.ips.length === 0) {
1439 return;
1440 }
1441
1442 ipfRules.push(ipfRuleObj({
1443 rule: rule,
1444 direction: dir,
1445 targets: vm.ips,
1446 type: lookupType,
1447 value: value
1448 }));
1449 });
1450 });
1451 });
1452 });
1453
1454 return ipfRules;
1455 }
1456
1457
1458 /**
1459 * Gets remote targets from the other side of the rule and adds them to
1460 * the targets object
1461 */
1462 function addOtherSideRemoteTargets(vms, rule, targets, dir, log) {
1463 var matching = vmsOnSide(vms, rule, dir, log);
1464 if (matching.length === 0) {
1465 return;
1466 }
1467
1468 var otherSide = dir === 'from' ? 'to' : 'from';
1469 if (rule[otherSide].tags.length !== 0) {
1470 if (!hasKey(targets, 'tags')) {
1471 targets.tags = {};
1472 }
1473
1474 // All tags (no value) wins out over tags with
1475 // a value. If multiple values for the same tag
1476 // are present, return them as an array
1477 rule[otherSide].tags.forEach(function (tag) {
1478 var key = tag;
1479 var val = true;
1480 if (typeof (tag) !== 'string') {
1481 key = tag[0];
1482 val = tag[1];
1483 }
1484
1485 if (!hasKey(targets.tags, key)) {
1486 targets.tags[key] = val;
1487 } else {
1488 if (targets.tags[key] !== true) {
1489 if (val === true) {
1490 targets.tags[key] = val;
1491 } else {
1492 if (!Array.isArray(targets.tags[key])) {
1493 targets.tags[key] = [ targets.tags[key] ];
1494 }
1495
1496 if (targets.tags[key].indexOf(val) === -1) {
1497 targets.tags[key].push(val);
1498 }
1499 }
1500 }
1501 }
1502 });
1503 }
1504
1505 if (rule[otherSide].vms.length !== 0) {
1506 if (!hasKey(targets, 'vms')) {
1507 targets.vms = {};
1508 }
1509
1510 rule[otherSide].vms.forEach(function (vm) {
1511 // Don't add if it's a local VM
1512 if (!hasKey(vms.all, vm)) {
1513 targets.vms[vm] = true;
1514 }
1515 });
1516 }
1517
1518 if (rule[otherSide].wildcards.indexOf('vmall') !== -1) {
1519 targets.allVMs = true;
1520 }
1521 }
1522
1523
1524 /**
1525 * Carefully move the new ipfilter configuration file into place. It's
1526 * important to make sure that if we fail or crash at any point that we
1527 * leave a file in place, since its presence is what determines whether
1528 * to enable the firewall at zone boot.
1529 */
1530 function replaceIPFconf(file, data, ver, callback) {
1531 var tempFile = util.format('%s.%s', file, ver);
1532 var oldFile = util.format('%s.old', file);
1533
1534 vasync.pipeline({
1535 funcs: [
1536 function _write(_, cb) {
1537 fs.writeFile(tempFile, data, cb);
1538 },
1539 function _unlinkOld(_, cb) {
1540 fs.unlink(oldFile, function (err) {
1541 if (err && err.code === 'ENOENT') {
1542 cb(null);
1543 return;
1544 }
1545
1546 cb(err);
1547 });
1548 },
1549 function _linkOld(_, cb) {
1550 fs.link(file, oldFile, function (err) {
1551 if (err && err.code === 'ENOENT') {
1552 cb(null);
1553 return;
1554 }
1555
1556 cb(err);
1557 });
1558 },
1559 function _renameTemp(_, cb) {
1560 fs.rename(tempFile, file, cb);
1561 }
1562 ]}, callback);
1563 }
1564
1565
1566 /**
1567 * Saves all of the generated ipfilter rules in ipfData to disk. We handle
1568 * each VM separately in parallel, so that failures for one don't impact
1569 * reloading others. For example, a VM may have filled up its disk, and we
1570 * now can't write out its configuration, or a VM may have stopped on us
1571 * before we had a chance to run ipf(1M) on it.
1572 */
1573 function saveConfsAndReload(opts, ipfData, log, callback) {
1574 var ver = Date.now(0) + '.' + sprintf('%06d', process.pid);
1575 var files = {};
1576 var uuids = [];
1577
1578 vasync.forEachParallel({
1579 inputs: ipfData,
1580 func: function (vm, cb) {
1581 uuids.push(vm.uuid);
1582
1583 vasync.pipeline({
1584 funcs: [
1585 // Write the new ipf.conf for IPv4 rules:
1586 function writeV4(_, cb2) {
1587 var filename = util.format(IPF_CONF, vm.zonepath);
1588 files[filename] = vm.v4text;
1589
1590 if (opts.dryrun) {
1591 cb2(null);
1592 return;
1593 }
1594
1595 replaceIPFconf(filename, vm.v4text, ver, cb2);
1596 },
1597
1598 // Write the new ipf6.conf for IPv6 rules:
1599 function writeV6(_, cb2) {
1600 var filename = util.format(IPF6_CONF, vm.zonepath);
1601 files[filename] = vm.v6text;
1602
1603 if (opts.dryrun) {
1604 cb2(null);
1605 return;
1606 }
1607
1608 replaceIPFconf(filename, vm.v6text, ver, cb2);
1609 },
1610
1611 // Restart the VM's firewall:
1612 function reload(_, cb2) {
1613 if (opts.dryrun) {
1614 cb2(null);
1615 return;
1616 }
1617
1618 restartFirewall(opts.allVMs, vm.uuid, log, cb2);
1619 }
1620 ]}, cb);
1621 }
1622 }, function (err) {
1623 if (err) {
1624 callback(err);
1625 return;
1626 }
1627
1628 callback(null, {
1629 files: files,
1630 vms: uuids
1631 });
1632 });
1633 }
1634
1635
1636 /**
1637 * Restart the given VMs firewall.
1638 *
1639 * @param vms {Object}: VM lookup table, as returned by createVMlookup()
1640 * @param uuid {UUID}: The UUID of the target VM
1641 * @param callback {Function} `function (err)`
1642 */
1643 function restartFirewall(vms, uuid, log, cb) {
1644 if (!vms.all[uuid].enabled || vms.all[uuid].state !== 'running') {
1645 log.debug('restartFirewalls: VM "%s": not restarting '
1646 + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1647 vms.all[uuid].state);
1648 cb(null);
1649 return;
1650 }
1651
1652 log.debug('restartFirewalls: reloading firewall for VM "%s" '
1653 + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1654 vms.all[uuid].state);
1655
1656 // Reload the firewall, and start it if necessary.
1657 reloadIPF({ vm: uuid, zonepath: vms.all[uuid].zonepath }, log,
1658 function (err, res) {
1659 if (err && zoneNotRunning(res)) {
1660 /*
1661 * An error starting the firewall due to the zone not
1662 * running isn't really an error.
1663 */
1664 cb();
1665 return;
1666 }
1667
1668 cb(err);
1669 });
1670 }
1671
1672
1673 /**
1674 * Applies firewall changes:
1675 * - saves / deletes rule files as needed
1676 * - writes out ipf conf files
1677 * - starts or restarts ipf in VMs
1678 *
1679 * @param {Object} opts :
1680 * - allRemoteVMs {Object} : VM lookup object of all remote VMs
1681 * - allVMs {Object} : VM lookup object of all local VMs
1682 * - del {Object} : Objects to delete from disk:
1683 * - rules {Array of Objects} : rules objects to delete
1684 * - rvms {Array of Objects} : remote VM UUIDs to delete
1685 * - dryRun {Bool} : if true, no files will be written or firewalls reloaded
1686 * - rules {Array of Objects} : rules to write out
1687 * - save {Object} : Objects to save to disk:
1688 * - rules {Array of Objects} : rule objects to save
1689 * - remoteVMs {Array of Objects} : remote VM objects to save
1690 * - vms {Object} : Mapping of UUID to VM object - VMs to write out
1691 * firewalls for, regardless of whether or not rules affect them
1692 * (necessary for catching the case where a VM used to have rules that
1693 * applied to it but no longer does)
1694 */
1695 function applyChanges(opts, log, callback) {
1696 log.trace(opts, 'applyChanges: entry');
1697
1698 assert.object(opts, 'opts');
1699 assert.optionalObject(opts.allRemoteVMs, 'opts.allRemoteVMs');
1700 assert.optionalObject(opts.allVMs, 'opts.allVMs');
1701 assert.optionalObject(opts.del, 'opts.del');
1702 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1703 assert.optionalObject(opts.vms, 'opts.vms');
1704 assert.optionalObject(opts.save, 'opts.save');
1705
1706 pipeline({
1707 funcs: [
1708 // Determine which platform-specific features are available
1709 function loadFeatures(res, cb) {
1710 features.load({log: log}, cb);
1711 },
1712
1713 // Generate the ipf files for each VM
1714 function reloadPlan(res, cb) {
1715 prepareIPFdata({
1716 allVMs: opts.allVMs,
1717 remoteVMs: opts.allRemoteVMs,
1718 rules: opts.rules,
1719 vms: opts.vms
1720 }, log, cb);
1721 },
1722
1723 // Save the remote VMs
1724 function saveVMs(res, cb) {
1725 if (opts.dryrun || !opts.save || !opts.save.remoteVMs
1726 || objEmpty(opts.save.remoteVMs)) {
1727 return cb(null);
1728 }
1729 mod_rvm.save(opts.save.remoteVMs, log, cb);
1730 },
1731
1732 // Save rule files (if specified)
1733 function save(res, cb) {
1734 if (opts.dryrun || !opts.save || !opts.save.rules
1735 || opts.save.rules.length === 0) {
1736 return cb(null);
1737 }
1738 saveRules(opts.save.rules, log, cb);
1739 },
1740
1741 // Delete rule files (if specified)
1742 function delRules(res, cb) {
1743 if (opts.dryrun || !opts.del || !opts.del.rules
1744 || opts.del.rules.length === 0) {
1745 return cb(null);
1746 }
1747 deleteRules(opts.del.rules, log, cb);
1748 },
1749
1750 // Delete remote VMs (if specified)
1751 function delRVMs(res, cb) {
1752 if (opts.dryrun || !opts.del || !opts.del.rvms
1753 || opts.del.rvms.length === 0) {
1754 return cb(null);
1755 }
1756 mod_rvm.del(opts.del.rvms, log, cb);
1757 },
1758
1759 // Write the new ipf files to disk and restart affected VMs
1760 function ipfData(res, cb) {
1761 saveConfsAndReload(opts, res.reloadPlan, log, cb);
1762 }
1763 ] }, function (err, res) {
1764 if (err) {
1765 callback(err);
1766 return;
1767 }
1768
1769 var toReturn = {
1770 vms: res.state.ipfData.vms
1771 };
1772
1773 if (opts.save) {
1774 if (opts.save.rules) {
1775 toReturn.rules = opts.save.rules.map(function (r) {
1776 return r.serialize();
1777 });
1778 }
1779
1780 if (opts.save.remoteVMs) {
1781 toReturn.remoteVMs = Object.keys(opts.save.remoteVMs).sort();
1782 }
1783 }
1784
1785 if (opts.del) {
1786 if (opts.del.rules) {
1787 toReturn.rules = opts.del.rules.map(function (r) {
1788 return r.serialize();
1789 });
1790 }
1791
1792 if (opts.del.rvms && opts.del.rvms.length !== 0) {
1793 toReturn.remoteVMs = opts.del.rvms.sort();
1794 }
1795 }
1796
1797 if (opts.filecontents) {
1798 toReturn.files = res.state.ipfData.files;
1799 }
1800
1801 callback(null, toReturn);
1802 });
1803 }
1804
1805
1806 /**
1807 * Examine the stderr from an ipf command and return true if the zone
1808 * wasn't running at the time
1809 */
1810 function zoneNotRunning(res) {
1811 return res && res.stderr && res.stderr.indexOf(NOT_RUNNING_MSG) !== -1;
1812 }
1813
1814
1815
1816 // --- Exported functions
1817
1818
1819 /**
1820 * Functions that touch anything in the following directories:
1821 *
1822 * - /var/fw (such as /var/fw/rules and /var/fw/vms)
1823 * - /zones/<uuid>/config (e.g. ipf.conf and ipf6.conf)
1824 *
1825 * Should acquire the appropriate type of lock so that they read and write
1826 * a consistent view of the local firewall rules, and so that parallel
1827 * executions don't run into each other while operating. (See FWAPI-240.)
1828 */
1829
1830
1831 /**
1832 * Add rules, local VMs or remote VMs
1833 *
1834 * @param {Object} opts : options
1835 * - localVMs {Array} : list of local VMs to update
1836 * - remoteVMs {Array} : list of remote VMs to add
1837 * - rules {Array} : list of rules
1838 * - vms {Array} : list of VMs from vmadm
1839 * @param {Function} callback : `f(err, res)`
1840 */
1841 function add(opts, callback) {
1842 try {
1843 validateOpts(opts);
1844 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1845 assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
1846 assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
1847 assert.optionalString(opts.createdBy, 'opts.createdBy');
1848
1849 var optRules = opts.rules || [];
1850 var optLocalVMs = opts.localVMs || [];
1851 var optRemoteVMs = opts.remoteVMs || [];
1852 if (optRules.length === 0 && optLocalVMs.length === 0
1853 && optRemoteVMs.length === 0) {
1854 throw new Error(
1855 'Payload must contain one of: rules, localVMs, remoteVMs');
1856 }
1857 } catch (err) {
1858 return callback(err);
1859 }
1860 var log = util_log.entry(opts, 'add');
1861
1862 pipeline({
1863 funcs: [
1864 function lock(_, cb) {
1865 mod_lock.acquireExclusiveLock(cb);
1866 },
1867
1868 function originalRules(_, cb) {
1869 createRules(opts.rules, opts.createdBy, cb);
1870 },
1871
1872 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
1873
1874 function disk(_, cb) { loadDataFromDisk(log, cb); },
1875
1876 // If we're trying to add a rule that already exists and looks
1877 // the same, drop it.
1878 function rules(res, cb) {
1879 getChangingRules(res.originalRules, res.disk.rulesByUUID, cb);
1880 },
1881
1882 function newRemoteVMs(res, cb) {
1883 mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
1884 opts.remoteVMs, cb);
1885 },
1886
1887 // Create remote VMs (if any) from payload
1888 function remoteVMs(res, cb) {
1889 createRemoteVMlookup(res.newRemoteVMs, log, cb);
1890 },
1891
1892 // Create a combined remote VM lookup of remote VMs on disk plus
1893 // new remote VMs in the payload
1894 function allRemoteVMs(res, cb) {
1895 createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
1896 log, cb);
1897 },
1898
1899 function localVMs(res, cb) {
1900 lookupVMs(res.vms, opts.localVMs, log, cb);
1901 },
1902
1903 // Build a table for information about newly added local/remote VMs
1904 function newVMs(res, cb) {
1905 var nvms = clone(res.remoteVMs);
1906 mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
1907 cb(null, nvms);
1908 },
1909
1910 function allRules(res, cb) {
1911 return cb(null, dedupRules(res.rules, res.disk.rules));
1912 },
1913
1914 // Get VMs the added rules affect
1915 function matchingVMs(res, cb) {
1916 filter.vmsByRules({
1917 log: log,
1918 rules: res.rules,
1919 vms: res.vms
1920 }, cb);
1921 },
1922
1923 // Get rules the added remote VMs affect
1924 function remoteVMrules(res, cb) {
1925 filter.rulesByRVMs(res.remoteVMs, res.allRules, log, cb);
1926 },
1927
1928 // Get any rules that the added local VMs target
1929 function localVMrules(res, cb) {
1930 filter.rulesByVMs(res.vms, res.localVMs, res.allRules, log, cb);
1931 },
1932
1933 // Merge the local and remote VM rules, and use that list to find
1934 // the VMs affected.
1935 function localAndRemoteVMsAffected(res, cb) {
1936 var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
1937 .filter(getAffectedRules(res.newVMs, log));
1938 filter.vmsByRules({
1939 log: log,
1940 rules: affectedRules,
1941 vms: res.vms
1942 }, cb);
1943 },
1944
1945 function mergedVMs(res, cb) {
1946 var ruleVMs = mergeObjects(res.localVMs, res.matchingVMs);
1947 return cb(null, mergeObjects(ruleVMs,
1948 res.localAndRemoteVMsAffected));
1949 },
1950
1951 // Get the rules that need to be written out for all VMs, before and
1952 // after the update
1953 function vmRules(res, cb) {
1954 filter.rulesByVMs(res.vms, res.mergedVMs, res.allRules, log, cb);
1955 },
1956
1957 function apply(res, cb) {
1958 applyChanges({
1959 allVMs: res.vms,
1960 dryrun: opts.dryrun,
1961 filecontents: opts.filecontents,
1962 allRemoteVMs: res.allRemoteVMs,
1963 rules: res.vmRules,
1964 save: {
1965 rules: res.rules,
1966 remoteVMs: res.newRemoteVMs
1967 },
1968 vms: res.mergedVMs
1969 }, log, cb);
1970 }
1971 ]}, function (err, res) {
1972 mod_lock.releaseLock(res.state.lock);
1973
1974 if (err) {
1975 util_log.finishErr(log, err, 'add: finish');
1976 return callback(err);
1977 }
1978
1979 log.debug(res.state.apply, 'add: finish');
1980 return callback(err, res.state.apply);
1981 });
1982 }
1983
1984
1985 /**
1986 * Delete rules
1987 *
1988 * @param {Object} opts : options
1989 * - uuids {Array} : list of rules
1990 * - vms {Array} : list of VMs from vmadm
1991 * @param {Function} callback : `f(err, res)`
1992 */
1993 function del(opts, callback) {
1994 try {
1995 assert.object(opts, 'opts');
1996 assert.optionalArrayOfString(opts.rvmUUIDs, 'opts.rvmUUIDs');
1997 assert.optionalArrayOfString(opts.uuids, 'opts.uuids');
1998 assert.arrayOfObject(opts.vms, 'vms');
1999
2000 var rvmUUIDs = opts.rvmUUIDs || [];
2001 var uuids = opts.uuids || [];
2002 if (rvmUUIDs.length === 0 && uuids.length === 0) {
2003 throw new Error(
2004 'Payload must contain one of: rvmUUIDs, uuids');
2005 }
2006
2007 } catch (err) {
2008 return callback(err);
2009 }
2010 var log = util_log.entry(opts, 'del');
2011
2012 pipeline({
2013 funcs: [
2014 function lock(_, cb) {
2015 mod_lock.acquireExclusiveLock(cb);
2016 },
2017 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2018
2019 function disk(_, cb) { loadDataFromDisk(log, cb); },
2020
2021 function allRemoteVMs(state, cb) {
2022 createRemoteVMlookup(state.disk.remoteVMs, log, cb);
2023 },
2024
2025 // Get matching remote VMs
2026 function remoteVMs(state, cb) {
2027 filter.rvmsByUUIDs(state.allRemoteVMs, opts.rvmUUIDs, log, cb);
2028 },
2029
2030 // Get rules the delted remote VMs affect
2031 function remoteVMrules(res, cb) {
2032 filter.rulesByRVMs(res.remoteVMs.matching, res.disk.rules,
2033 log, cb);
2034 },
2035
2036 // Get VMs that are affected by the remote VM rules
2037 function rvmVMs(res, cb) {
2038 filter.vmsByRules({
2039 log: log,
2040 rules: res.remoteVMrules,
2041 vms: res.vms
2042 }, cb);
2043 },
2044
2045 // Get the deleted rules
2046 function rules(res, cb) {
2047 filter.rulesByUUIDs(res.disk.rules, opts.uuids, log, cb);
2048 },
2049
2050 // Get VMs the deleted rules affect
2051 function ruleVMs(res, cb) {
2052 filter.vmsByRules({
2053 log: log,
2054 rules: res.rules.matching,
2055 vms: res.vms
2056 }, cb);
2057 },
2058
2059 // Now find all rules that apply to those VMs, omitting the
2060 // rules that are deleted
2061 function vmRules(res, cb) {
2062 filter.rulesByVMs(res.vms,
2063 mergeObjects(res.ruleVMs, res.rvmVMs),
2064 res.rules.notMatching, log, cb);
2065 },
2066
2067 function apply(res, cb) {
2068 applyChanges({
2069 allVMs: res.vms,
2070 dryrun: opts.dryrun,
2071 filecontents: opts.filecontents,
2072 allRemoteVMs: res.remoteVMs.notMatching,
2073 rules: res.vmRules,
2074 del: {
2075 rules: res.rules.matching,
2076 rvms: objEmpty(res.remoteVMs.matching.all) ?
2077 null : Object.keys(res.remoteVMs.matching.all)
2078 },
2079 vms: mergeObjects(res.ruleVMs, res.rvmVMs)
2080 }, log, cb);
2081 }
2082 ]}, function (err, res) {
2083 mod_lock.releaseLock(res.state.lock);
2084
2085 if (err) {
2086 util_log.finishErr(log, err, 'del: finish');
2087 return callback(err);
2088 }
2089
2090 log.debug(res.state.apply, 'del: finish');
2091 return callback(err, res.state.apply);
2092 });
2093 }
2094
2095
2096 /**
2097 * Returns a remote VM
2098 *
2099 * @param opts {Object} : options:
2100 * - remoteVM {String} : UUID of remote VM to get
2101 * @param callback {Function} : `function (err, rvm)`
2102 */
2103 function getRemoteVM(opts, callback) {
2104 try {
2105 assert.object(opts, 'opts');
2106 assert.string(opts.remoteVM, 'opts.remoteVM');
2107 } catch (err) {
2108 return callback(err);
2109 }
2110 var log = util_log.entry(opts, 'getRemoteVM', true);
2111
2112 mod_lock.acquireSharedLock(function (lErr, fd) {
2113 if (lErr) {
2114 callback(lErr);
2115 return;
2116 }
2117
2118 mod_rvm.load(opts.remoteVM, log, function (err, rvm) {
2119 mod_lock.releaseLock(fd);
2120
2121 if (err) {
2122 if (err.code == 'ENOENT') {
2123 // Don't write a log file for "not found"
2124 log.info(err, 'getRemoteVM: finish');
2125 } else {
2126 util_log.finishErr(log, err, 'getRemoteVM: finish');
2127 }
2128 return callback(err);
2129 }
2130
2131 log.debug(rvm, 'getRemoteVM: finish');
2132 return callback(null, rvm);
2133 });
2134 });
2135 }
2136
2137
2138 /**
2139 * Returns a rule
2140 *
2141 * @param opts {Object} : options:
2142 * - uuid {String} : UUID of rule to get
2143 * @param callback {Function} : `function (err, rule)`
2144 */
2145 function getRule(opts, callback) {
2146 try {
2147 assert.object(opts, 'opts');
2148 assert.string(opts.uuid, 'opts.uuid');
2149 } catch (err) {
2150 return callback(err);
2151 }
2152 var log = util_log.entry(opts, 'get', true);
2153
2154 mod_lock.acquireSharedLock(function (lErr, fd) {
2155 if (lErr) {
2156 callback(lErr);
2157 return;
2158 }
2159
2160 loadRule(opts.uuid, log, function (err, rule) {
2161 mod_lock.releaseLock(fd);
2162
2163 if (err) {
2164 if (err.code == 'ENOENT') {
2165 // Don't write a log file for "not found"
2166 log.info(err, 'get: finish');
2167 } else {
2168 util_log.finishErr(log, err, 'get: finish');
2169 }
2170 return callback(err);
2171 }
2172
2173 var ser = rule.serialize();
2174 log.debug(ser, 'get: finish');
2175 return callback(null, ser);
2176 });
2177 });
2178 }
2179
2180
2181 /**
2182 * List remote VMs
2183 */
2184 function listRemoteVMs(opts, callback) {
2185 try {
2186 assert.object(opts, 'opts');
2187 } catch (err) {
2188 return callback(err);
2189 }
2190 var log = util_log.entry(opts, 'listRemoteVMs', true);
2191
2192 mod_lock.acquireSharedLock(function (lErr, fd) {
2193 if (lErr) {
2194 callback(lErr);
2195 return;
2196 }
2197
2198 mod_rvm.loadAll(log, function (err, res) {
2199 mod_lock.releaseLock(fd);
2200
2201 if (err) {
2202 util_log.finishErr(log, err, 'listRemoteVMs: finish');
2203 return callback(err);
2204 }
2205
2206 // XXX: support sorting by other fields, filtering
2207 var sortFn = function _sort(a, b) {
2208 return (a.uuid > b.uuid) ? 1: -1;
2209 };
2210
2211 log.debug('listRemoteVMs: finish');
2212 return callback(null, Object.keys(res).map(function (r) {
2213 return res[r];
2214 }).sort(sortFn));
2215 });
2216 });
2217 }
2218
2219
2220 /**
2221 * List rules
2222 */
2223 function listRules(opts, callback) {
2224 try {
2225 assert.object(opts, 'opts');
2226 assert.optionalArrayOfString(opts.fields, 'opts.fields');
2227 if (opts.fields) {
2228 var invalid = [];
2229 opts.fields.forEach(function (f) {
2230 if (mod_rule.FIELDS.indexOf(f) === -1) {
2231 invalid.push(f);
2232 }
2233 });
2234
2235 if (invalid.length > 0) {
2236 throw new verror.VError('Invalid display field%s: %s',
2237 invalid.length == 1 ? '' : 's',
2238 invalid.sort().join(', '));
2239 }
2240 }
2241 } catch (err) {
2242 return callback(err);
2243 }
2244 var log = util_log.entry(opts, 'list', true);
2245
2246 mod_lock.acquireSharedLock(function (lErr, fd) {
2247 if (lErr) {
2248 callback(lErr);
2249 return;
2250 }
2251
2252 loadAllRules(log, function (err, res) {
2253 mod_lock.releaseLock(fd);
2254
2255 if (err) {
2256 util_log.finishErr(log, err, 'list: finish');
2257 return callback(err);
2258 }
2259
2260 // XXX: support sorting by other fields, filtering
2261 // (eg: enabled=true vm=<uuid>)
2262 var sortFn = function _defaultSort(a, b) {
2263 return (a.uuid > b.uuid) ? 1: -1;
2264 };
2265 var mapFn = function _defaultMap(r) {
2266 return r.serialize();
2267 };
2268
2269 if (opts.fields) {
2270 var filterFields = opts.fields;
2271 // If we didn't include uuid in the fields to list, include
2272 // it here so that we can sort by it - we'll remove it after
2273 if (opts.fields.indexOf('uuid') === -1) {
2274 filterFields = opts.fields.concat(['uuid']);
2275 }
2276
2277 mapFn = function _fieldMap(r) {
2278 return r.serialize(filterFields);
2279 };
2280 }
2281
2282 var rules = res.map(mapFn).sort(sortFn);
2283 if (opts.fields && opts.fields.indexOf('uuid') === -1) {
2284 rules = rules.map(function (r) {
2285 delete r.uuid;
2286 return r;
2287 });
2288 }
2289
2290 log.debug('list: finish');
2291 return callback(null, rules);
2292 });
2293 });
2294 }
2295
2296
2297 /**
2298 * Enable the firewall for a VM. If the VM is running, start ipf for that VM.
2299 *
2300 * @param opts {Object} : options:
2301 * - vms {Array} : array of VM objects (as per VM.js)
2302 * - vm {Object} : VM object for the VM to enable
2303 * - dryrun {Boolean} : don't write any files to disk (Optional)
2304 * - filecontents {Boolean} : return contents of files written to
2305 * disk (Optional)
2306 * @param callback {Function} `function (err, res)`
2307 * - Where res is an object, optionall containing a files subhash
2308 * if opts.filecontents is set
2309 */
2310 function enableVM(opts, callback) {
2311 try {
2312 assert.object(opts, 'opts');
2313 assert.object(opts.vm, 'opts.vm');
2314 assert.arrayOfObject(opts.vms, 'opts.vms');
2315 } catch (err) {
2316 return callback(err);
2317 }
2318 var log = util_log.entry(opts, 'enable');
2319
2320 var vmFilter = {};
2321
2322 pipeline({
2323 funcs: [
2324 function lock(_, cb) {
2325 mod_lock.acquireExclusiveLock(cb);
2326 },
2327 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2328
2329 function disk(_, cb) { loadDataFromDisk(log, cb); },
2330
2331 function getVM(res, cb) {
2332 var vm = res.vms.all[opts.vm.uuid];
2333 if (!vm) {
2334 return cb(new verror.VError('VM "%s" not found', opts.vm.uuid));
2335 }
2336
2337 vmFilter[opts.vm.uuid] = vm;
2338 return cb();
2339 },
2340
2341 // Find all rules that apply to the VM
2342 function vmRules(res, cb) {
2343 filter.rulesByVMs(res.vms, vmFilter, res.disk.rules, log, cb);
2344 },
2345
2346 function allRemoteVMs(res, cb) {
2347 createRemoteVMlookup(res.disk.remoteVMs, log, cb);
2348 },
2349
2350 function apply(res, cb) {
2351 applyChanges({
2352 allVMs: res.vms,
2353 dryrun: opts.dryrun,
2354 filecontents: opts.filecontents,
2355 allRemoteVMs: res.allRemoteVMs,
2356 rules: res.vmRules,
2357 vms: vmFilter
2358 }, log, cb);
2359 }
2360 ]}, function _afterEnable(err, res) {
2361 mod_lock.releaseLock(res.state.lock);
2362
2363 if (err) {
2364 util_log.finishErr(log, err, 'enable: finish');
2365 return callback(err);
2366 }
2367
2368 var toReturn = res.state.apply;
2369 log.debug(toReturn, 'enable: finish');
2370 return callback(null, toReturn);
2371 });
2372 }
2373
2374
2375 /**
2376 * Disable the firewall for a VM. If the VM is running, stop ipf for that VM.
2377 *
2378 * @param opts {Object} : options:
2379 * - vm {Object} : VM object for the VM to disable
2380 * @param callback {Function} `function (err)`
2381 */
2382 function disableVM(opts, callback) {
2383 try {
2384 assert.object(opts, 'opts');
2385 assert.object(opts.vm, 'opts.vm');
2386 } catch (err) {
2387 return callback(err);
2388 }
2389 var log = util_log.entry(opts, 'disable');
2390
2391 function moveConf(new_fmt, old_fmt, _, cb) {
2392 // Move config out of the way - on zone boot, the firewall
2393 // will start again if it's present
2394 var new_cfg = util.format(new_fmt, opts.vm.zonepath);
2395 var old_cfg = util.format(old_fmt, opts.vm.zonepath);
2396 return fs.rename(new_cfg, old_cfg, function (err) {
2397 // If the file's already gone, that's OK
2398 if (err && err.code !== 'ENOENT') {
2399 return cb(err);
2400 }
2401
2402 return cb(null);
2403 });
2404 }
2405
2406 pipeline({
2407 funcs: [
2408 function lock(_, cb) {
2409 mod_lock.acquireExclusiveLock(cb);
2410 },
2411 moveConf.bind(null, IPF_CONF, IPF_CONF_OLD),
2412 moveConf.bind(null, IPF6_CONF, IPF6_CONF_OLD),
2413 function stop(_, cb) {
2414 if (opts.vm.state !== 'running') {
2415 log.debug('disableVM: VM "%s": not stopping ipf (state=%s)',
2416 opts.vm.uuid, opts.vm.state);
2417 return cb(null);
2418 }
2419
2420 log.debug('disableVM: stopping ipf for VM "%s"', opts.vm.uuid);
2421 return mod_ipf.stop(opts.vm.uuid, log, cb);
2422 }
2423 ]}, function _afterDisable(err, res) {
2424 mod_lock.releaseLock(res.state.lock);
2425
2426 if (err) {
2427 util_log.finishErr(log, err, 'disable: finish');
2428 return callback(err);
2429 }
2430
2431 log.debug('disable: finish');
2432 return callback();
2433 });
2434 }
2435
2436
2437 /**
2438 * Gets the firewall status for a VM
2439 *
2440 * @param opts {Object} : options:
2441 * - uuid {String} : VM UUID
2442 * @param callback {Function} `function (err, res)`
2443 */
2444 function vmStatus(opts, callback) {
2445 try {
2446 assert.object(opts, 'opts');
2447 assert.string(opts.uuid, 'opts.uuid');
2448 } catch (err) {
2449 return callback(err);
2450 }
2451 var log = util_log.entry(opts, 'status', true);
2452
2453 return mod_ipf.status(opts.uuid, log, function (err, res) {
2454 if (err) {
2455 // 'No such device' is returned when the zone is down
2456 if (zoneNotRunning(res)) {
2457 log.debug({ running: false }, 'status: finish');
2458 return callback(null, { running: false });
2459 }
2460
2461 util_log.finishErr(log, err, 'status: finish');
2462 return callback(err);
2463 }
2464
2465 log.debug(res, 'status: finish');
2466 return callback(null, res);
2467 });
2468 }
2469
2470
2471 /**
2472 * Gets the firewall statistics for a VM
2473 *
2474 * @param opts {Object} : options:
2475 * - uuid {String} : VM UUID
2476 * @param callback {Function} `function (err, res)`
2477 */
2478 function vmStats(opts, callback) {
2479 try {
2480 assert.object(opts, 'opts');
2481 assert.string(opts.uuid, 'opts.uuid');
2482 } catch (err) {
2483 return callback(err);
2484 }
2485 var log = util_log.entry(opts, 'stats', true);
2486
2487 return mod_ipf.ruleStats(opts.uuid, log, function (err, res) {
2488 if (err) {
2489 if (res && res.stderr) {
2490 // Zone is down
2491 if (zoneNotRunning(res)) {
2492 log.debug('stats: finish: zone not running');
2493 return callback(new verror.VError(
2494 'Firewall is not running for VM "%s"', opts.uuid));
2495 }
2496
2497 // No rules loaded: return an error if the firewall
2498 // isn't running
2499 if (res.stderr.indexOf('empty list') !== -1) {
2500 return vmStatus(opts, function (err2, res2) {
2501 if (err2) {
2502 util_log.finishErr(log, err2, 'stats: finish');
2503 return callback(err2);
2504 }
2505
2506 if (res2.running) {
2507 log.debug({ rules: [] }, 'stats: finish');
2508 return callback(null, { rules: [] });
2509 } else {
2510 log.debug('stats: finish: firewall not running');
2511 return callback(new verror.VError(
2512 'Firewall is not running for VM "%s"',
2513 opts.uuid));
2514 }
2515 });
2516 }
2517 }
2518
2519 return callback(err);
2520 }
2521
2522 log.debug({ rules: res }, 'stats: finish');
2523 return callback(null, { rules: res });
2524 });
2525 }
2526
2527
2528 /**
2529 * Update rules, local VMs or remote VMs
2530 *
2531 * @param {Object} opts : options
2532 * - localVMs {Array} : list of local VMs to update
2533 * - remoteVMs {Array} : list of remote VMs to update
2534 * - rules {Array} : list of rules
2535 * - vms {Array} : list of VMs from vmadm
2536 * @param {Function} callback : `f(err, res)`
2537 */
2538 function update(opts, callback) {
2539 try {
2540 validateOpts(opts);
2541 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2542 assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2543 assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2544 assert.optionalString(opts.createdBy, 'opts.createdBy');
2545
2546 var optRules = opts.rules || [];
2547 var optLocalVMs = opts.localVMs || [];
2548 var optRemoteVMs = opts.remoteVMs || [];
2549 if (optRules.length === 0 && optLocalVMs.length === 0
2550 && optRemoteVMs.length === 0) {
2551 throw new Error(
2552 'Payload must contain one of: rules, localVMs, remoteVMs');
2553 }
2554 } catch (err) {
2555 return callback(err);
2556 }
2557 var log = util_log.entry(opts, 'update');
2558
2559 pipeline({
2560 funcs: [
2561 function lock(_, cb) {
2562 mod_lock.acquireExclusiveLock(cb);
2563 },
2564 function disk(_, cb) { loadDataFromDisk(log, cb); },
2565
2566 // Make sure the rules exist
2567 function originalRules(res, cb) {
2568 findRules({
2569 allRules: res.disk.rulesByUUID,
2570 allowAdds: opts.allowAdds,
2571 rules: opts.rules
2572 }, log, cb);
2573 },
2574
2575 // Apply updates to the found rules
2576 function rules(res, cb) {
2577 createUpdatedRules({
2578 createdBy: opts.createdBy,
2579 originalRules: res.originalRules,
2580 updatedRules: opts.rules
2581 }, log, cb);
2582 },
2583
2584 // Create list of rules that are being replaced
2585 function replacedRules(res, cb) {
2586 cb(null, mod_obj.values(res.originalRules));
2587 },
2588
2589 // Create the VM lookup
2590 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2591
2592 // Create remote VMs (if any) from payload
2593 function newRemoteVMs(res, cb) {
2594 mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
2595 opts.remoteVMs, cb);
2596 },
2597
2598 // Create a lookup for the new remote VMs
2599 function newRemoteVMsLookup(res, cb) {
2600 createRemoteVMlookup(res.newRemoteVMs, log, cb);
2601 },
2602
2603 function allRemoteVMs(res, cb) {
2604 createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
2605 log, cb);
2606 },
2607
2608 // Lookup any local VMs in the payload
2609 function localVMs(res, cb) {
2610 lookupVMs(res.vms, opts.localVMs, log, cb);
2611 },
2612
2613 // Build a table for information about updated local/remote VMs
2614 function newVMs(res, cb) {
2615 var nvms = clone(res.newRemoteVMsLookup);
2616 mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
2617 cb(null, nvms);
2618 },
2619
2620 // Get the VMs the rules applied to before the update
2621 function originalVMs(res, cb) {
2622 filter.vmsByRules({
2623 log: log,
2624 rules: res.replacedRules,
2625 vms: res.vms
2626 }, cb);
2627 },
2628
2629 // Now get the VMs the updated rules apply to
2630 function matchingVMs(res, cb) {
2631 filter.vmsByRules({
2632 log: log,
2633 rules: res.rules,
2634 vms: res.vms
2635 }, cb);
2636 },
2637
2638 // Replace the rules with their updated versions
2639 function updatedRules(res, cb) {
2640 return cb(null, dedupRules(res.rules, res.disk.rules));
2641 },
2642
2643 // Get any rules that the added remote VMs target
2644 function remoteVMrules(res, cb) {
2645 filter.rulesByRVMs(res.newRemoteVMsLookup,
2646 res.updatedRules, log, cb);
2647 },
2648
2649 // Get any rules that the added local VMs target
2650 function localVMrules(res, cb) {
2651 filter.rulesByVMs(res.vms, res.localVMs, res.updatedRules, log, cb);
2652 },
2653
2654 // Merge the local and remote VM rules, and use that list to find
2655 // the VMs affected.
2656 function localAndRemoteVMsAffected(res, cb) {
2657 var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
2658 .filter(getAffectedRules(res.newVMs, log));
2659 filter.vmsByRules({
2660 log: log,
2661 rules: affectedRules,
2662 vms: res.vms
2663 }, cb);
2664 },
2665
2666 function mergedVMs(res, cb) {
2667 // These are VMs affected by changing rules:
2668 var ruleVMs = mergeObjects(res.originalVMs, res.matchingVMs);
2669 // These are VMs affected by changing VM information:
2670 var updatedVMs = mergeObjects(res.localVMs,
2671 res.localAndRemoteVMsAffected);
2672 return cb(null, mergeObjects(ruleVMs, updatedVMs));
2673 },
2674
2675 // Get the rules that need to be written out for all VMs, before and
2676 // after the update
2677 function vmRules(res, cb) {
2678 filter.rulesByVMs(res.vms, res.mergedVMs, res.updatedRules, log,
2679 cb);
2680 },
2681
2682 function apply(res, cb) {
2683 applyChanges({
2684 allVMs: res.vms,
2685 dryrun: opts.dryrun,
2686 filecontents: opts.filecontents,
2687 allRemoteVMs: res.allRemoteVMs,
2688 rules: res.vmRules,
2689 save: {
2690 rules: res.rules,
2691 remoteVMs: res.newRemoteVMs
2692 },
2693 vms: res.mergedVMs
2694 }, log, cb);
2695 }
2696 ]}, function (err, res) {
2697 mod_lock.releaseLock(res.state.lock);
2698
2699 if (err) {
2700 util_log.finishErr(log, err, 'update: finish');
2701 return callback(err);
2702 }
2703
2704 log.debug(res.state.apply, 'update: finish');
2705 return callback(err, res.state.apply);
2706 });
2707 }
2708
2709
2710 /**
2711 * Given the list of local VMs and a list of rules, return an object with
2712 * the non-local targets on the other side of the rules.
2713 *
2714 * @param opts {Object} : options:
2715 * - vms {Array} : array of VM objects (as per VM.js)
2716 * - rules {Array of Objects} : firewall rules
2717 * @param callback {Function} `function (err, targets)`
2718 * - Where targets is an object like:
2719 * {
2720 * tags: { some: ['one', 'two'], other: true },
2721 * vms: [ '<UUID>' ],
2722 * allVMs: true
2723 * }
2724 */
2725 function getRemoteTargets(opts, callback) {
2726 try {
2727 assert.object(opts, 'opts');
2728 assert.arrayOfObject(opts.vms, 'opts.vms');
2729 assert.arrayOfObject(opts.rules, 'opts.rules');
2730
2731 if (opts.rules.length === 0) {
2732 throw new Error('Must specify rules');
2733 }
2734
2735 } catch (err) {
2736 return callback(err);
2737 }
2738 var log = util_log.entry(opts, 'remoteTargets', true);
2739
2740 pipeline({
2741 funcs: [
2742 function lock(_, cb) {
2743 mod_lock.acquireSharedLock(cb);
2744 },
2745 function rules(_, cb) {
2746 createRules(opts.rules, cb);
2747 },
2748 function vms(_, cb) {
2749 createVMlookup(opts.vms, log, cb);
2750 }
2751 ] }, function (err, res) {
2752 mod_lock.releaseLock(res.state.lock);
2753
2754 if (err) {
2755 util_log.finishErr(log, err, 'remoteTargets: createRules: finish');
2756 return callback(err);
2757 }
2758
2759 var targets = {};
2760
2761 for (var r in res.state.rules) {
2762 var rule = res.state.rules[r];
2763
2764 for (var d in DIRECTIONS) {
2765 var dir = DIRECTIONS[d];
2766 addOtherSideRemoteTargets(
2767 res.state.vms, rule, targets, dir, log);
2768 }
2769 }
2770
2771 if (hasKey(targets, 'vms')) {
2772 targets.vms = Object.keys(targets.vms);
2773 if (targets.vms.length === 0) {
2774 delete targets.vms;
2775 }
2776 }
2777
2778 log.debug(targets, 'remoteTargets: finish');
2779 return callback(null, targets);
2780 });
2781 }
2782
2783
2784 /**
2785 * Gets VMs that are affected by a rule
2786 *
2787 * @param opts {Object} : options:
2788 * - vms {Array} : array of VM objects (as per VM.js)
2789 * - rule {UUID or Object} : UUID of pre-existing rule, or a rule object
2790 * - includeDisabled {Boolean, optional} : if set, include VMs that have
2791 * their firewalls disabled in the search
2792 * @param callback {Function} `function (err, vms)`
2793 * - Where vms is an array of VMs that are affected by that rule
2794 */
2795 function getRuleVMs(opts, callback) {
2796 try {
2797 assert.object(opts, 'opts');
2798 assert.arrayOfObject(opts.vms, 'opts.vms');
2799 assertStringOrObject(opts.rule, 'opts.rule');
2800 assert.optionalBool(opts.includeDisabled, 'opts.includeDisabled');
2801 } catch (err) {
2802 return callback(err);
2803 }
2804 var log = util_log.entry(opts, 'vms', true);
2805
2806 pipeline({
2807 funcs: [
2808 function lock(_, cb) {
2809 mod_lock.acquireSharedLock(cb);
2810 },
2811 function rules(_, cb) {
2812 if (typeof (opts.rule) === 'string') {
2813 return loadRule(opts.rule, log, cb);
2814 }
2815
2816 createRules([ opts.rule ], cb);
2817 },
2818 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2819 function ruleVMs(state, cb) {
2820 if (!Array.isArray(state.rules)) {
2821 state.rules = [ state.rules ];
2822 }
2823
2824 filter.vmsByRules({
2825 includeDisabled: opts.includeDisabled,
2826 log: log,
2827 rules: state.rules,
2828 vms: state.vms
2829 }, cb);
2830 }
2831 ]}, function (err, res) {
2832 mod_lock.releaseLock(res.state.lock);
2833
2834 if (err) {
2835 util_log.finishErr(log, err, 'vms: finish');
2836 return callback(err);
2837 }
2838
2839 var matched = Object.keys(res.state.ruleVMs);
2840 log.debug(matched, 'vms: finish');
2841 return callback(null, matched);
2842 });
2843 }
2844
2845
2846 /**
2847 * Gets rules that apply to a Remote VM
2848 *
2849 * @param opts {Object} : options:
2850 * - vms {Array} : array of VM objects (as per VM.js)
2851 * - vm {UUID} : UUID of VM to get the rules for
2852 * @param callback {Function} `function (err, rules)`
2853 * - Where rules is an array of rules that apply to the VM
2854 */
2855 function getRemoteVMrules(opts, callback) {
2856 try {
2857 assert.object(opts, 'opts');
2858 assertStringOrObject(opts.remoteVM, 'opts.remoteVM');
2859 assert.arrayOfObject(opts.vms, 'opts.vms');
2860 } catch (err) {
2861 return callback(err);
2862 }
2863 var log = util_log.entry(opts, 'rvmRules', true);
2864
2865 pipeline({
2866 funcs: [
2867 function lock(_, cb) {
2868 mod_lock.acquireSharedLock(cb);
2869 },
2870 function allRules(_, cb) { loadAllRules(log, cb); },
2871 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2872 function rvm(_, cb) {
2873 if (typeof (opts.remoteVM) === 'object') {
2874 return cb(null, opts.remoteVM);
2875 }
2876
2877 return mod_rvm.load(opts.remoteVM, log, cb);
2878 },
2879 function rvms(state, cb) {
2880 mod_rvm.create({ allVMs: state.vms, requireIPs: false, log: log },
2881 [ state.rvm ], function (e, rvmList) {
2882 if (e) {
2883 return cb(e);
2884 }
2885
2886 createRemoteVMlookup(rvmList, log, cb);
2887 });
2888 },
2889 function rvmRules(state, cb) {
2890 filter.rulesByRVMs(state.rvms, state.allRules, log, cb);
2891 }
2892 ]}, function (err, res) {
2893 mod_lock.releaseLock(res.state.lock);
2894
2895 if (err) {
2896 util_log.finishErr(log, err, 'rvmRules: finish (vm=%s)',
2897 opts.remoteVM);
2898 return callback(err);
2899 }
2900
2901 var toReturn = res.state.rvmRules.map(function (r) {
2902 return r.serialize();
2903 });
2904
2905 log.debug(toReturn, 'rvmRules: finish (vm=%s)', opts.remoteVM);
2906 return callback(null, toReturn);
2907 });
2908 }
2909
2910
2911 /**
2912 * Gets rules that apply to a VM
2913 *
2914 * @param opts {Object} : options:
2915 * - vms {Array} : array of VM objects (as per VM.js)
2916 * - vm {UUID} : UUID of VM to get the rules for
2917 * @param callback {Function} `function (err, rules)`
2918 * - Where rules is an array of rules that apply to the VM
2919 */
2920 function getVMrules(opts, callback) {
2921 try {
2922 assert.object(opts, 'opts');
2923 assert.string(opts.vm, 'opts.vm');
2924 assert.arrayOfObject(opts.vms, 'opts.vms');
2925 } catch (err) {
2926 return callback(err);
2927 }
2928 var log = util_log.entry(opts, 'vmRules', true);
2929
2930 var toFind = {};
2931 toFind[opts.vm] = opts.vm;
2932
2933 pipeline({
2934 funcs: [
2935 function lock(_, cb) {
2936 mod_lock.acquireSharedLock(cb);
2937 },
2938 function allRules(_, cb) { loadAllRules(log, cb); },
2939 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2940 function vmRules(state, cb) {
2941 filter.rulesByVMs(state.vms, toFind, state.allRules, log, cb);
2942 }
2943 ]}, function (err, res) {
2944 mod_lock.releaseLock(res.state.lock);
2945
2946 if (err) {
2947 util_log.finishErr(log, err, 'vmRules: finish (vm=%s)', opts.vm);
2948 return callback(err);
2949 }
2950
2951 var toReturn = res.state.vmRules.map(function (r) {
2952 return r.serialize();
2953 });
2954
2955 log.debug(toReturn, 'vmRules: finish (vm=%s)', opts.vm);
2956 return callback(null, toReturn);
2957 });
2958 }
2959
2960
2961 /**
2962 * Validates an add / update payload
2963 *
2964 * @param opts {Object} : options:
2965 * - localVMs {Array} : list of local VMs
2966 * - remoteVMs {Array} : list of remote VMs
2967 * - rules {Array} : list of rules
2968 * - vms {Array} : array of VM objects (as per VM.js)
2969 * @param callback {Function} `function (err, rules)`
2970 * - Where rules is an array of rules that apply to the VM
2971 */
2972 function validatePayload(opts, callback) {
2973 try {
2974 assert.object(opts, 'opts');
2975 assert.arrayOfObject(opts.vms, 'opts.vms');
2976 assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2977 assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2978 assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2979
2980 var optRules = opts.rules || [];
2981 var optLocalVMs = opts.localVMs || [];
2982 var optRemoteVMs = opts.remoteVMs || [];
2983 if (optRules.length === 0 && optLocalVMs.length === 0
2984 && optRemoteVMs.length === 0) {
2985 throw new Error(
2986 'Payload must contain one of: rules, localVMs, remoteVMs');
2987 }
2988 } catch (err) {
2989 return callback(err);
2990 }
2991 var log = util_log.entry(opts, 'validatePayload');
2992
2993 pipeline({
2994 funcs: [
2995 function lock(_, cb) {
2996 mod_lock.acquireSharedLock(cb);
2997 },
2998 function rules(_, cb) {
2999 createRules(opts.rules, cb);
3000 },
3001 function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
3002 function remoteVMs(_, cb) { mod_rvm.loadAll(log, cb); },
3003 function newRemoteVMs(state, cb) {
3004 mod_rvm.create({ allVMs: state.vms, requireIPs: true, log: log },
3005 opts.remoteVMs, cb);
3006 },
3007 // Create a combined remote VM lookup of remote VMs on disk plus
3008 // new remote VMs in the payload
3009 function allRemoteVMs(state, cb) {
3010 createRemoteVMlookup([state.remoteVMs, state.newRemoteVMs],
3011 log, cb);
3012 },
3013
3014 function validate(state, cb) {
3015 validateRules(state.vms, state.allRemoteVMs, state.rules, log, cb);
3016 }
3017 ]}, function (err, res) {
3018 mod_lock.releaseLock(res.state.lock);
3019
3020 if (err) {
3021 util_log.finishErr(log, err, 'validatePayload: finish');
3022 return callback(err);
3023 }
3024
3025 log.debug(opts.payload, 'validatePayload: finish');
3026 return callback();
3027 });
3028 }
3029
3030
3031
3032 module.exports = {
3033 _setOldIPF: mod_ipf._setOld,
3034 add: add,
3035 del: del,
3036 disable: disableVM,
3037 enable: enableVM,
3038 get: getRule,
3039 getRVM: getRemoteVM,
3040 list: listRules,
3041 listRVMs: listRemoteVMs,
3042 remoteTargets: getRemoteTargets,
3043 rvmRules: getRemoteVMrules,
3044 setBunyan: util_log.setBunyan,
3045 stats: vmStats,
3046 status: vmStatus,
3047 update: update,
3048 validatePayload: validatePayload,
3049 VM_FIELDS: VM_FIELDS,
3050 vmRules: getVMrules,
3051 vms: getRuleVMs
3052 };