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