1 /*
   2  * CDDL HEADER START
   3  *
   4  * The contents of this file are subject to the terms of the
   5  * Common Development and Distribution License, Version 1.0 only
   6  * (the "License").  You may not use this file except in compliance
   7  * with the License.
   8  *
   9  * You can obtain a copy of the license at http://smartos.org/CDDL
  10  *
  11  * See the License for the specific language governing permissions
  12  * and limitations under the License.
  13  *
  14  * When distributing Covered Code, include this CDDL HEADER in each
  15  * file.
  16  *
  17  * If applicable, add the following below this CDDL HEADER, with the
  18  * fields enclosed by brackets "[]" replaced with your own identifying
  19  * information: Portions Copyright [yyyy] [name of copyright owner]
  20  *
  21  * CDDL HEADER END
  22  *
  23  * Copyright (c) 2018, Joyent, Inc. All rights reserved.
  24  *
  25  *
  26  * fwadm: firewall rule model
  27  */
  28 
  29 'use strict';
  30 
  31 var mod_net = require('net');
  32 var mod_uuid = require('uuid');
  33 var sprintf = require('extsprintf').sprintf;
  34 var util = require('util');
  35 var validators = require('./validators');
  36 var verror = require('verror');
  37 
  38 
  39 
  40 // --- Globals
  41 
  42 
  43 
  44 var DIRECTIONS = ['to', 'from'];
  45 // Exported fields that can be in the serialized rule:
  46 var FIELDS = [
  47     'created_by',
  48     'description',
  49     'enabled',
  50     'global',
  51     'owner_uuid',
  52     'rule',
  53     'uuid',
  54     'version',
  55     'log'
  56 ];
  57 // Maximum number of targets per side:
  58 var MAX_TARGETS_PER_SIDE = 24;
  59 // Maximum number of protocol targets:
  60 var MAX_PROTOCOL_TARGETS = 24;
  61 // Minimum version for using a larger list of protocol targets:
  62 var MINVER_LGPROTOTARG = 4;
  63 // The old maximum number of protocol targets:
  64 var OLD_MAX_PORTS = 8;
  65 var STRING_PROPS = ['created_by', 'description'];
  66 var TARGET_TYPES = ['wildcard', 'ip', 'subnet', 'tag', 'vm'];
  67 
  68 var icmpr = /^icmp6?$/;
  69 
  70 // --- Internal functions
  71 
  72 
  73 /**
  74  * Safely check if an object has a property
  75  */
  76 function hasOwnProperty(obj, prop) {
  77     return Object.prototype.hasOwnProperty.call(obj, prop);
  78 }
  79 
  80 
  81 /**
  82  * Calls callback for all of the firewall target types
  83  */
  84 function forEachTarget(obj, callback) {
  85     DIRECTIONS.forEach(function (dir) {
  86         if (!hasOwnProperty(obj, dir)) {
  87             return;
  88         }
  89 
  90         TARGET_TYPES.forEach(function (type) {
  91             var name = type + 's';
  92             if (!hasOwnProperty(obj[dir], name)) {
  93                 return;
  94             }
  95 
  96             callback(dir, type, name, obj[dir][name]);
  97         });
  98     });
  99 }
 100 
 101 
 102 /**
 103  * Sorts a list of ICMP types (with optional codes)
 104  */
 105 function icmpTypeSort(types) {
 106     return types.map(function (type) {
 107         return type.toString().split(':');
 108     }).sort(function (a, b) {
 109         var aTot = (Number(a[0]) << 8) + (a.length === 1 ? 0 : Number(a[1]));
 110         var bTot = (Number(b[0]) << 8) + (a.length === 1 ? 0 : Number(b[1]));
 111         return aTot - bTot;
 112     }).map(function (typeArr) {
 113         return typeArr.join(':');
 114     });
 115 }
 116 
 117 
 118 /**
 119  * Adds a tag to an object
 120  */
 121 function addTag(obj, tag, val) {
 122     if (!hasOwnProperty(obj, tag)) {
 123         obj[tag] = {};
 124     }
 125 
 126     if (val === undefined || val === null) {
 127         obj[tag].all = true;
 128         return;
 129     }
 130 
 131     if (!hasOwnProperty(obj[tag], 'values')) {
 132         obj[tag].values = {};
 133     }
 134 
 135     obj[tag].values[val] = true;
 136 }
 137 
 138 
 139 /**
 140  * Creates a list of tags based on an object populated by addTag() above
 141  */
 142 function tagList(obj) {
 143     var tags = [];
 144     Object.keys(obj).sort().forEach(function (tag) {
 145         if (hasOwnProperty(obj[tag], 'all')) {
 146             tags.push(tag);
 147         } else {
 148             Object.keys(obj[tag].values).sort().forEach(function (val) {
 149                 tags.push([tag, val]);
 150             });
 151         }
 152     });
 153     return tags;
 154 }
 155 
 156 
 157 /**
 158  * The following characters are allowed to come after an escape, and get
 159  * escaped when producing rule text.
 160  *
 161  * Parentheses don't need to be escaped with newer parsers, but will cause
 162  * errors with older parsers which expect them to be escaped. We therefore
 163  * always escape them when generating rule text, to make sure we don't
 164  * cause issues for older parsers.
 165  */
 166 var escapes = {
 167     '"': '"',
 168     'b': '\b',
 169     'f': '\f',
 170     'n': '\n',
 171     'r': '\r',
 172     't': '\t',
 173     '/': '/',
 174     '(': '(',
 175     ')': ')',
 176     '\\': '\\'
 177 };
 178 
 179 
 180 /**
 181  * When producing text versions of a rule, we escape Unicode whitespace
 182  * characters. These characters don't need to be escaped, but we do so
 183  * to reduce the chance that an operator will look at a rule and mistake
 184  * any of them for the ASCII space character (\u0020), or not see them
 185  * because they're non-printing.
 186  */
 187 var unescapes = {
 188     // Things that need to be escaped for the fwrule parser
 189     '"': '"',
 190     '(': '(',
 191     ')': ')',
 192     '\\': '\\',
 193 
 194     // Special ASCII characters we don't want to print
 195     '\u0000': 'u0000', // null (NUL)
 196     '\u0001': 'u0001', // start of heading (SOH)
 197     '\u0002': 'u0002', // start of text (STX)
 198     '\u0003': 'u0003', // end of text (ETX)
 199     '\u0004': 'u0004', // end of transmission (EOT)
 200     '\u0005': 'u0005', // enquiry (ENQ)
 201     '\u0006': 'u0006', // acknowledgement (ACK)
 202     '\u0007': 'u0007', // bell (BEL)
 203     '\u0008': 'b',     // backspace (BS)
 204     '\u0009': 't',     // horizontal tab (HT)
 205     '\u000A': 'n',     // newline (NL)
 206     '\u000B': 'u000B', // vertical tab (VT)
 207     '\u000C': 'f',     // form feed/next page (NP)
 208     '\u000D': 'r',     // carriage return (CR)
 209     '\u000E': 'u000E', // shift out (SO)
 210     '\u000F': 'u000F', // shift in (SI)
 211     '\u0010': 'u0010', // data link escape (DLE)
 212     '\u0011': 'u0011', // device control 1 (DC1)/XON
 213     '\u0012': 'u0012', // device control 2 (DC2)
 214     '\u0013': 'u0013', // device control 3 (DC3)/XOFF
 215     '\u0014': 'u0014', // device control 4 (DC4)
 216     '\u0015': 'u0015', // negative acknowledgement (NAK)
 217     '\u0016': 'u0016', // synchronous idle (SYN)
 218     '\u0017': 'u0017', // end of transmission block (ETB)
 219     '\u0018': 'u0018', // cancel (CAN)
 220     '\u0019': 'u0019', // end of medium (EM)
 221     '\u001A': 'u001A', // substitute (SUB)
 222     '\u001B': 'u001B', // escape (ESC)
 223     '\u001C': 'u001C', // file separator (FS)
 224     '\u001D': 'u001D', // group separator (GS)
 225     '\u001E': 'u001E', // record separator (RS)
 226     '\u001F': 'u001F', // unit separator (US)
 227     '\u007F': 'u007F', // delete (DEL)
 228 
 229     // Unicode whitespace characters
 230     '\u0085': 'u0085', // next line
 231     '\u00A0': 'u00A0', // non-breaking space
 232     '\u1680': 'u1680', // ogham space mark
 233     '\u180E': 'u180E', // mongolian vowel separator
 234     '\u2000': 'u2000', // en quad
 235     '\u2001': 'u2001', // em quad
 236     '\u2002': 'u2002', // en space
 237     '\u2003': 'u2003', // em space
 238     '\u2004': 'u2004', // three-per-em space
 239     '\u2005': 'u2005', // four-per-em space
 240     '\u2006': 'u2006', // six-per-em space
 241     '\u2007': 'u2007', // figure space
 242     '\u2008': 'u2008', // punctuation space
 243     '\u2009': 'u2009', // thin space
 244     '\u200A': 'u200A', // hair space
 245     '\u200B': 'u200B', // zero width space
 246     '\u200C': 'u200C', // zero width non-joiner
 247     '\u200D': 'u200D', // zero width joiner
 248     '\u2028': 'u2028', // line separator
 249     '\u2029': 'u2029', // paragraph separator
 250     '\u202F': 'u202F', // narrow no-break space
 251     '\u205F': 'u205F', // medium mathematical space
 252     '\u2060': 'u2060', // word joiner
 253     '\u3000': 'u3000', // ideographic space
 254     '\uFEFF': 'uFEFF'  // zero width no-break space
 255 };
 256 
 257 
 258 /**
 259  * Unescape a string that's been escaped so that it can be used
 260  * in a firewall rule.
 261  */
 262 function tagUnescape(ostr) {
 263     var nstr = '';
 264     var len = ostr.length;
 265 
 266     for (var cur = 0; cur < len; cur += 1) {
 267         var val = ostr[cur];
 268         if (val === '\\') {
 269             var escaped = ostr[cur + 1];
 270             if (escaped === 'u') {
 271                 nstr += String.fromCharCode(
 272                     parseInt(ostr.substring(cur + 2, cur + 6), 16));
 273                 cur += 5;
 274             } else if (escapes[escaped] !== undefined) {
 275                 nstr += escapes[escaped];
 276                 cur += 1;
 277             } else {
 278                 throw new Error('Invalid escape sequence "\\' + escaped + '"!');
 279             }
 280         } else {
 281             nstr += val;
 282         }
 283     }
 284 
 285     return nstr;
 286 }
 287 
 288 
 289 /**
 290  * Escape a string so that it can be placed, quoted, into a
 291  * firewall rule.
 292  */
 293 function tagEscape(ostr) {
 294     var nstr = '';
 295     var len = ostr.length;
 296 
 297     for (var cur = 0; cur < len; cur += 1) {
 298         var val = ostr[cur];
 299         if (unescapes[val] !== undefined) {
 300             nstr += '\\' + unescapes[val];
 301         } else {
 302             nstr += val;
 303         }
 304     }
 305 
 306     return nstr;
 307 }
 308 
 309 
 310 /**
 311  * Quotes a string in case it contains non-alphanumeric
 312  * characters or keywords for firewall rules.
 313  */
 314 function quote(str) {
 315     return '"' + tagEscape(str) + '"';
 316 }
 317 
 318 
 319 
 320 // --- Firewall object and methods
 321 
 322 
 323 
 324 /**
 325  * Firewall rule constructor
 326  */
 327 function FwRule(data, opts) {
 328     var errs = [];
 329     var parsed;
 330 
 331     if (!opts) {
 332         opts = {};
 333     }
 334 
 335     // -- validation --
 336 
 337     if (!data.rule && !data.parsed) {
 338         errs.push(new validators.InvalidParamError('rule',
 339             'No rule specified'));
 340     } else {
 341         try {
 342             parsed = data.parsed || require('./').parse(data.rule, opts);
 343         } catch (err) {
 344             errs.push(err);
 345         }
 346     }
 347 
 348     if (hasOwnProperty(data, 'uuid')) {
 349         if (!validators.validateUUID(data.uuid)) {
 350             errs.push(new validators.InvalidParamError('uuid',
 351                 'Invalid rule UUID'));
 352         }
 353 
 354         this.uuid = data.uuid;
 355     } else {
 356         this.uuid = mod_uuid.v4();
 357     }
 358 
 359     this.version = data.version || generateVersion();
 360 
 361     if (hasOwnProperty(data, 'owner_uuid')) {
 362         if (!validators.validateUUID(data.owner_uuid)) {
 363             errs.push(new validators.InvalidParamError('owner_uuid',
 364                 'Invalid owner UUID'));
 365         }
 366         this.owner_uuid = data.owner_uuid;
 367     } else {
 368         // No owner: this rule will affect all VMs
 369         this.global = true;
 370     }
 371 
 372     if (hasOwnProperty(data, 'enabled')) {
 373         if (!validators.bool(data.enabled)) {
 374             errs.push(new validators.InvalidParamError('enabled',
 375                 'enabled must be true or false'));
 376         }
 377 
 378         this.enabled = data.enabled;
 379     } else {
 380         this.enabled = false;
 381     }
 382 
 383     if (hasOwnProperty(data, 'log')) {
 384         if (!validators.bool(data.log)) {
 385             errs.push(new validators.InvalidParamError('log',
 386                 'log must be true or false'));
 387         }
 388 
 389         this.log = data.log;
 390     } else {
 391         this.log = false;
 392     }
 393 
 394     for (var s in STRING_PROPS) {
 395         var str = STRING_PROPS[s];
 396         if (hasOwnProperty(data, str)) {
 397             try {
 398                 validators.validateString(str, data[str]);
 399                 this[str] = data[str];
 400             } catch (valErr) {
 401                 errs.push(valErr);
 402             }
 403         }
 404     }
 405 
 406     if (opts.enforceGlobal) {
 407         if (hasOwnProperty(data, 'global') && !validators.bool(data.global)) {
 408             errs.push(new validators.InvalidParamError('global',
 409                 'global must be true or false'));
 410         }
 411 
 412         if (hasOwnProperty(data, 'global') &&
 413             hasOwnProperty(data, 'owner_uuid') && data.global) {
 414             errs.push(new validators.InvalidParamError('global',
 415                 'cannot specify both global and owner_uuid'));
 416         }
 417 
 418         if (!hasOwnProperty(data, 'global') &&
 419             !hasOwnProperty(data, 'owner_uuid')) {
 420             errs.push(new validators.InvalidParamError('owner_uuid',
 421                 'owner_uuid required'));
 422         }
 423     }
 424 
 425     if (errs.length !== 0) {
 426         if (errs.length === 1) {
 427             throw errs[0];
 428         }
 429 
 430         throw new verror.MultiError(errs);
 431     }
 432 
 433     // -- translate into the internal rule format --
 434 
 435     var d;
 436     var dir;
 437 
 438     this.action = parsed.action;
 439     this.priority = parsed.priority || 0;
 440     this.protocol = parsed.protocol.name;
 441 
 442     switch (this.protocol) {
 443     case 'icmp':
 444     case 'icmp6':
 445         this.types = icmpTypeSort(parsed.protocol.targets);
 446         this.protoTargets = this.types;
 447         break;
 448     case 'ah':
 449     case 'esp':
 450         this.protoTargets = parsed.protocol.targets;
 451         break;
 452     case 'tcp':
 453     case 'udp':
 454         this.ports = parsed.protocol.targets.sort(function (a, b) {
 455             var first = hasOwnProperty(a, 'start') ? a.start : a;
 456             var second = hasOwnProperty(b, 'start') ? b.start : b;
 457             return Number(first) - Number(second);
 458         });
 459         this.protoTargets = this.ports;
 460         break;
 461     default:
 462         throw new validators.InvalidParamError('rule',
 463             'unknown protocol "%s"', this.protocol);
 464     }
 465 
 466     if (opts.maxVersion < MINVER_LGPROTOTARG) {
 467         if (this.protoTargets.length > OLD_MAX_PORTS) {
 468             throw new validators.InvalidParamError('rule',
 469                 'maximum of %d %s allowed', OLD_MAX_PORTS,
 470                 icmpr.test(this.protocol) ? 'types' : 'ports');
 471         }
 472     } else if (this.protoTargets.length > MAX_PROTOCOL_TARGETS) {
 473         throw new validators.InvalidParamError('rule',
 474             'maximum of %d %s allowed', MAX_PROTOCOL_TARGETS,
 475             icmpr.test(this.protocol) ? 'types' : 'ports');
 476     }
 477 
 478     this.from = {};
 479     this.to = {};
 480 
 481     this.allVMs = false;
 482     this.ips = {};
 483     this.tags = {};
 484     this.vms = {};
 485     this.subnets = {};
 486     this.wildcards = {};
 487 
 488     var dirs = {
 489         'to': {},
 490         'from': {}
 491     };
 492     var numTargets;
 493 
 494     for (d in DIRECTIONS) {
 495         dir = DIRECTIONS[d];
 496         numTargets = 0;
 497         for (var j in parsed[dir]) {
 498             var target = parsed[dir][j];
 499             var targetName;
 500             var name = target[0] + 's';
 501 
 502             numTargets++;
 503             if (!hasOwnProperty(dirs[dir], name)) {
 504                 dirs[dir][name] = {};
 505             }
 506 
 507             if (name === 'tags') {
 508                 var targetVal = null;
 509                 if (typeof (target[1]) === 'string') {
 510                     targetName = target[1];
 511                 } else {
 512                     targetName = target[1][0];
 513                     targetVal = target[1][1];
 514                 }
 515 
 516                 addTag(this[name], targetName, targetVal);
 517                 addTag(dirs[dir][name], targetName, targetVal);
 518 
 519             } else {
 520                 targetName = target[1];
 521                 this[name][targetName] = target[1];
 522                 dirs[dir][name][targetName] = target[1];
 523             }
 524         }
 525 
 526         if (numTargets > MAX_TARGETS_PER_SIDE) {
 527             throw new validators.InvalidParamError('rule',
 528                 'maximum of %d targets allowed per side',
 529                 MAX_TARGETS_PER_SIDE);
 530         }
 531     }
 532 
 533     // Now dedup and sort
 534     for (d in DIRECTIONS) {
 535         dir = DIRECTIONS[d];
 536         for (var t in TARGET_TYPES) {
 537             var type = TARGET_TYPES[t] + 's';
 538             if (hasOwnProperty(dirs[dir], type)) {
 539                 if (type === 'tags') {
 540                     this[dir][type] = tagList(dirs[dir][type]);
 541 
 542                 } else {
 543                     this[dir][type] = Object.keys(dirs[dir][type]).sort();
 544                 }
 545             } else {
 546                 this[dir][type] = [];
 547             }
 548         }
 549     }
 550 
 551     this.ips = Object.keys(this.ips).sort();
 552     this.tags = tagList(this.tags);
 553     this.vms = Object.keys(this.vms).sort();
 554     this.subnets = Object.keys(this.subnets).sort();
 555     this.wildcards = Object.keys(this.wildcards).sort();
 556 
 557     if (this.wildcards.length !== 0 && this.wildcards.indexOf('vmall') !== -1) {
 558         this.allVMs = true;
 559     }
 560 
 561     // Check for rules that obviously don't make sense
 562     if (this.protocol === 'icmp') {
 563         this.ips.map(function (ip) {
 564             if (!mod_net.isIPv4(ip)) {
 565                 throw new validators.InvalidParamError('rule',
 566                     'rule affects ICMPv4 but contains a non-IPv4 address');
 567             }
 568         });
 569         this.subnets.map(function (subnet) {
 570             if (!mod_net.isIPv4(subnet.split('/')[0])) {
 571                 throw new validators.InvalidParamError('rule',
 572                     'rule affects ICMPv4 but contains a non-IPv4 subnet');
 573             }
 574         });
 575     } else if (this.protocol === 'icmp6') {
 576         this.ips.map(function (ip) {
 577             if (!mod_net.isIPv6(ip)) {
 578                 throw new validators.InvalidParamError('rule',
 579                     'rule affects ICMPv6 but contains a non-IPv6 address');
 580             }
 581         });
 582         this.subnets.map(function (subnet) {
 583             if (!mod_net.isIPv6(subnet.split('/')[0])) {
 584                 throw new validators.InvalidParamError('rule',
 585                     'rule affects ICMPv6 but contains a non-IPv6 subnet');
 586             }
 587         });
 588     }
 589 
 590     // Final check: does this rule actually contain targets that can actually
 591     // affect VMs?
 592     if (!this.allVMs && this.tags.length === 0 && this.vms.length === 0) {
 593         throw new validators.InvalidParamError('rule',
 594             'rule does not affect VMs');
 595     }
 596 }
 597 
 598 
 599 /**
 600  * Returns the internal representation of the rule
 601  */
 602 FwRule.prototype.raw = function () {
 603     var raw = {
 604         action: this.action,
 605         enabled: this.enabled,
 606         from: this.from,
 607         priority: this.priority,
 608         protocol: this.protocol,
 609         to: this.to,
 610         uuid: this.uuid,
 611         version: this.version,
 612         log: this.log
 613     };
 614 
 615     if (this.owner_uuid) {
 616         raw.owner_uuid = this.owner_uuid;
 617     }
 618 
 619     switch (this.protocol) {
 620     case 'icmp':
 621     case 'icmp6':
 622         raw.types = this.types;
 623         break;
 624     case 'ah':
 625     case 'esp':
 626         break;
 627     case 'tcp':
 628     case 'udp':
 629         raw.ports = this.ports;
 630         break;
 631     default:
 632         throw new Error('unknown protocol: ' + this.protocol);
 633     }
 634 
 635     for (var s in STRING_PROPS) {
 636         var str = STRING_PROPS[s];
 637         if (hasOwnProperty(this, str)) {
 638             raw[str] = this[str];
 639         }
 640     }
 641 
 642     return raw;
 643 };
 644 
 645 
 646 /**
 647  * Returns the serialized version of the rule, suitable for storing
 648  *
 649  * @param fields {Array}: fields to return (optional)
 650  */
 651 FwRule.prototype.serialize = function (fields) {
 652     var ser = {};
 653     if (!fields) {
 654         fields = FIELDS;
 655     }
 656 
 657     for (var f in fields) {
 658         var field = fields[f];
 659         if (field === 'rule') {
 660             ser.rule = this.text();
 661         } else if (field === 'global') {
 662             // Only display the global flag if true
 663             if (this.global) {
 664                 ser.global = true;
 665             }
 666         } else {
 667             if (hasOwnProperty(this, field)) {
 668                 ser[field] = this[field];
 669             }
 670         }
 671     }
 672 
 673     return ser;
 674 };
 675 
 676 
 677 /**
 678  * Returns the text of the rule
 679  */
 680 FwRule.prototype.text = function () {
 681     var containsRange;
 682     var ports;
 683     var protoTxt;
 684     var prioTxt = '';
 685     var targets = {
 686         from: [],
 687         to: []
 688     };
 689 
 690     forEachTarget(this, function (dir, type, _, arr) {
 691         for (var i in arr) {
 692             var txt;
 693             if (type === 'tag') {
 694                 txt = util.format('%s %s', type,
 695                     typeof (arr[i]) === 'string' ? quote(arr[i])
 696                     : (quote(arr[i][0]) + ' = ' + quote(arr[i][1])));
 697             } else {
 698                 txt = util.format('%s %s', type, arr[i]);
 699             }
 700 
 701             if (type === 'wildcard') {
 702                 txt = arr[i] === 'vmall' ? 'all vms' : arr[i];
 703             }
 704             targets[dir].push(txt);
 705         }
 706     });
 707 
 708     // Protocol-specific text: different for ICMP rather than TCP/UDP
 709     switch (this.protocol) {
 710     case 'icmp':
 711     case 'icmp6':
 712         protoTxt = util.format(' %sTYPE %s%s',
 713             this.types.length > 1 ? '(' : '',
 714             this.types.map(function (type) {
 715                 return type.toString().split(':');
 716             }).map(function (code) {
 717                 return code[0] + (code.length === 1 ? '' : ' CODE ' + code[1]);
 718             }).join(' AND TYPE '),
 719             this.types.length > 1 ? ')' : ''
 720         );
 721         break;
 722     case 'ah':
 723     case 'esp':
 724         protoTxt = '';
 725         break;
 726     case 'tcp':
 727     case 'udp':
 728         ports = this.ports.map(function (port) {
 729             if (hasOwnProperty(port, 'start') &&
 730                 hasOwnProperty(port, 'end')) {
 731                 /*
 732                  * We only output PORTS when we have a range, since we don't
 733                  * distinguish PORTS 1, 2 from (PORT 1 AND PORT 2) after
 734                  * parsing.
 735                  */
 736                 containsRange = true;
 737                 return port.start + ' - ' + port.end;
 738             } else {
 739                 return port;
 740             }
 741         });
 742         if (containsRange) {
 743             protoTxt = util.format(' PORTS %s', ports.join(', '));
 744         } else {
 745             protoTxt = util.format(' %sPORT %s%s',
 746                 ports.length > 1 ? '(' : '',
 747                 ports.join(' AND PORT '),
 748                 ports.length > 1 ? ')' : ''
 749             );
 750         }
 751         break;
 752     default:
 753         throw new Error('unknown protocol: ' + this.protocol);
 754     }
 755 
 756     if (this.priority > 0) {
 757         prioTxt += ' PRIORITY ' + this.priority.toString();
 758     }
 759 
 760     return util.format('FROM %s%s%s TO %s%s%s %s %s%s%s',
 761             targets.from.length > 1 ? '(' : '',
 762             targets.from.join(' OR '),
 763             targets.from.length > 1 ? ')' : '',
 764             targets.to.length > 1 ? '(' : '',
 765             targets.to.join(' OR '),
 766             targets.to.length > 1 ? ')' : '',
 767             this.action.toUpperCase(),
 768             this.protocol.toLowerCase(),
 769             protoTxt,
 770             prioTxt
 771     );
 772 };
 773 
 774 
 775 /**
 776  * Returns the string representation of the rule
 777  */
 778 FwRule.prototype.toString = function () {
 779     return util.format('[%s,%s%s] %s', this.uuid, this.enabled,
 780             (this.owner_uuid ? ',' + this.owner_uuid : ''),
 781             this.text());
 782 };
 783 
 784 
 785 
 786 // --- Exported functions
 787 
 788 
 789 
 790 /**
 791  * Creates a new firewall rule from the payload
 792  */
 793 function createRule(payload, opts) {
 794     return new FwRule(payload, opts);
 795 }
 796 
 797 
 798 function generateVersion() {
 799     return Date.now(0) + '.' + sprintf('%06d', process.pid);
 800 }
 801 
 802 module.exports = {
 803     create: createRule,
 804     generateVersion: generateVersion,
 805     tagEscape: tagEscape,
 806     tagUnescape: tagUnescape,
 807     DIRECTIONS: DIRECTIONS,
 808     FIELDS: FIELDS,
 809     FwRule: FwRule,
 810     TARGET_TYPES: TARGET_TYPES
 811 };