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