From 97b80191048b23f2e870c9d6a27e368ebaaf594d Mon Sep 17 00:00:00 2001
From: Rob Mensching <rob@firegiant.com>
Date: Tue, 2 Jan 2018 23:14:58 -0800
Subject: Initial code commit

---
 src/wixext/FirewallCompiler.cs                     | 356 +++++++++++++++++++++
 src/wixext/FirewallConstants.cs                    |  21 ++
 src/wixext/FirewallDecompiler.cs                   | 169 ++++++++++
 src/wixext/FirewallErrors.cs                       |  42 +++
 src/wixext/FirewallExtensionData.cs                |  24 ++
 src/wixext/FirewallExtensionFactory.cs             |  18 ++
 .../FirewallWindowsInstallerBackendExtension.cs    |  26 ++
 src/wixext/Tuples/FirewallTupleDefinitions.cs      |  31 ++
 src/wixext/Tuples/WixFirewallExceptionTuple.cs     |  93 ++++++
 src/wixext/WixToolset.Firewall.wixext.csproj       |  36 +++
 src/wixext/WixToolset.Firewall.wixext.targets      |  11 +
 src/wixext/firewall.xsd                            | 211 ++++++++++++
 src/wixext/tables.xml                              |  28 ++
 13 files changed, 1066 insertions(+)
 create mode 100644 src/wixext/FirewallCompiler.cs
 create mode 100644 src/wixext/FirewallConstants.cs
 create mode 100644 src/wixext/FirewallDecompiler.cs
 create mode 100644 src/wixext/FirewallErrors.cs
 create mode 100644 src/wixext/FirewallExtensionData.cs
 create mode 100644 src/wixext/FirewallExtensionFactory.cs
 create mode 100644 src/wixext/FirewallWindowsInstallerBackendExtension.cs
 create mode 100644 src/wixext/Tuples/FirewallTupleDefinitions.cs
 create mode 100644 src/wixext/Tuples/WixFirewallExceptionTuple.cs
 create mode 100644 src/wixext/WixToolset.Firewall.wixext.csproj
 create mode 100644 src/wixext/WixToolset.Firewall.wixext.targets
 create mode 100644 src/wixext/firewall.xsd
 create mode 100644 src/wixext/tables.xml

(limited to 'src/wixext')

diff --git a/src/wixext/FirewallCompiler.cs b/src/wixext/FirewallCompiler.cs
new file mode 100644
index 00000000..0696b4b1
--- /dev/null
+++ b/src/wixext/FirewallCompiler.cs
@@ -0,0 +1,356 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Globalization;
+    using System.Xml.Linq;
+    using WixToolset.Data;
+    using WixToolset.Extensibility;
+
+    /// <summary>
+    /// The compiler for the WiX Toolset Firewall Extension.
+    /// </summary>
+    public sealed class FirewallCompiler : BaseCompilerExtension
+    {
+        public override XNamespace Namespace => "http://wixtoolset.org/schemas/v4/wxs/firewall";
+
+        /// <summary>
+        /// Processes an element for the Compiler.
+        /// </summary>
+        /// <param name="sourceLineNumbers">Source line number for the parent element.</param>
+        /// <param name="parentElement">Parent element of element to process.</param>
+        /// <param name="element">Element to process.</param>
+        /// <param name="contextValues">Extra information about the context in which this element is being parsed.</param>
+        public override void ParseElement(Intermediate intermediate, IntermediateSection section, XElement parentElement, XElement element, IDictionary<string, string> context)
+        {
+            switch (parentElement.Name.LocalName)
+            {
+                case "File":
+                    string fileId = context["FileId"];
+                    string fileComponentId = context["ComponentId"];
+
+                    switch (element.Name.LocalName)
+                    {
+                        case "FirewallException":
+                            this.ParseFirewallExceptionElement(intermediate, section, element, fileComponentId, fileId);
+                            break;
+                        default:
+                            this.ParseHelper.UnexpectedElement(parentElement, element);
+                            break;
+                    }
+                    break;
+                case "Component":
+                    string componentId = context["ComponentId"];
+
+                    switch (element.Name.LocalName)
+                    {
+                        case "FirewallException":
+                            this.ParseFirewallExceptionElement(intermediate, section, element, componentId, null);
+                            break;
+                        default:
+                            this.ParseHelper.UnexpectedElement(parentElement, element);
+                            break;
+                    }
+                    break;
+                default:
+                    this.ParseHelper.UnexpectedElement(parentElement, element);
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Parses a FirewallException element.
+        /// </summary>
+        /// <param name="element">The element to parse.</param>
+        /// <param name="componentId">Identifier of the component that owns this firewall exception.</param>
+        /// <param name="fileId">The file identifier of the parent element (null if nested under Component).</param>
+        private void ParseFirewallExceptionElement(Intermediate intermediate, IntermediateSection section, XElement element, string componentId, string fileId)
+        {
+            SourceLineNumber sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(element);
+            Identifier id = null;
+            string name = null;
+            int attributes = 0;
+            string file = null;
+            string program = null;
+            string port = null;
+            string protocolValue = null;
+            int? protocol = null;
+            string profileValue = null;
+            int? profile = null;
+            string scope = null;
+            string remoteAddresses = null;
+            string description = null;
+
+            foreach (XAttribute attrib in element.Attributes())
+            {
+                if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
+                {
+                    switch (attrib.Name.LocalName)
+                    {
+                        case "Id":
+                            id = this.ParseHelper.GetAttributeIdentifier(sourceLineNumbers, attrib);
+                            break;
+                        case "Name":
+                            name = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
+                            break;
+                        case "File":
+                            if (null != fileId)
+                            {
+                                this.Messaging.Write(ErrorMessages.IllegalAttributeWhenNested(sourceLineNumbers, element.Name.LocalName, "File", "File"));
+                            }
+                            else
+                            {
+                                file = this.ParseHelper.GetAttributeIdentifierValue(sourceLineNumbers, attrib);
+                            }
+                            break;
+                        case "IgnoreFailure":
+                            if (YesNoType.Yes == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib))
+                            {
+                                attributes |= 0x1; // feaIgnoreFailures
+                            }
+                            break;
+                        case "Program":
+                            if (null != fileId)
+                            {
+                                this.Messaging.Write(ErrorMessages.IllegalAttributeWhenNested(sourceLineNumbers, element.Name.LocalName, "Program", "File"));
+                            }
+                            else
+                            {
+                                program = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
+                            }
+                            break;
+                        case "Port":
+                            port = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
+                            break;
+                        case "Protocol":
+                            protocolValue = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
+                            switch (protocolValue)
+                            {
+                                case "tcp":
+                                    protocol = FirewallConstants.NET_FW_IP_PROTOCOL_TCP;
+                                    break;
+                                case "udp":
+                                    protocol = FirewallConstants.NET_FW_IP_PROTOCOL_UDP;
+                                    break;
+                                default:
+                                    this.Messaging.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, element.Name.LocalName, "Protocol", protocolValue, "tcp", "udp"));
+                                    break;
+                            }
+                            break;
+                        case "Scope":
+                            scope = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
+                            switch (scope)
+                            {
+                                case "any":
+                                    remoteAddresses = "*";
+                                    break;
+                                case "localSubnet":
+                                    remoteAddresses = "LocalSubnet";
+                                    break;
+                                default:
+                                    this.Messaging.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, element.Name.LocalName, "Scope", scope, "any", "localSubnet"));
+                                    break;
+                            }
+                            break;
+                        case "Profile":
+                            profileValue = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
+                            switch (profileValue)
+                            {
+                                case "domain":
+                                    profile = FirewallConstants.NET_FW_PROFILE2_DOMAIN;
+                                    break;
+                                case "private":
+                                    profile = FirewallConstants.NET_FW_PROFILE2_PRIVATE;
+                                    break;
+                                case "public":
+                                    profile = FirewallConstants.NET_FW_PROFILE2_PUBLIC;
+                                    break;
+                                case "all":
+                                    profile = FirewallConstants.NET_FW_PROFILE2_ALL;
+                                    break;
+                                default:
+                                    this.Messaging.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, element.Name.LocalName, "Profile", profileValue, "domain", "private", "public", "all"));
+                                    break;
+                            }
+                            break;
+                        case "Description":
+                            description = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
+                            break;
+                        default:
+                            this.ParseHelper.UnexpectedAttribute(element, attrib);
+                            break;
+                    }
+                }
+                else
+                {
+                    this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, element, attrib);
+                }
+            }
+
+            // parse RemoteAddress children
+            foreach (XElement child in element.Elements())
+            {
+                if (this.Namespace == child.Name.Namespace)
+                {
+                    SourceLineNumber childSourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(child);
+                    switch (child.Name.LocalName)
+                    {
+                        case "RemoteAddress":
+                            if (null != scope)
+                            {
+                                this.Messaging.Write(FirewallErrors.IllegalRemoteAddressWithScopeAttribute(sourceLineNumbers));
+                            }
+                            else
+                            {
+                                this.ParseRemoteAddressElement(intermediate, section, child, ref remoteAddresses);
+                            }
+                            break;
+                        default:
+                            this.ParseHelper.UnexpectedElement(element, child);
+                            break;
+                    }
+                }
+                else
+                {
+                    this.ParseHelper.ParseExtensionElement(this.Context.Extensions, intermediate, section, element, child);
+                }
+            }
+
+            // Id and Name are required
+            if (null == id)
+            {
+                this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, element.Name.LocalName, "Id"));
+            }
+
+            if (null == name)
+            {
+                this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, element.Name.LocalName, "Name"));
+            }
+
+            // Scope or child RemoteAddress(es) are required
+            if (null == remoteAddresses)
+            {
+                this.Messaging.Write(ErrorMessages.ExpectedAttributeOrElement(sourceLineNumbers, element.Name.LocalName, "Scope", "RemoteAddress"));
+            }
+
+            // can't have both Program and File
+            if (null != program && null != file)
+            {
+                this.Messaging.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(sourceLineNumbers, element.Name.LocalName, "File", "Program"));
+            }
+
+            // must be nested under File, have File or Program attributes, or have Port attribute
+            if (String.IsNullOrEmpty(fileId) && String.IsNullOrEmpty(file) && String.IsNullOrEmpty(program) && String.IsNullOrEmpty(port))
+            {
+                this.Messaging.Write(FirewallErrors.NoExceptionSpecified(sourceLineNumbers));
+            }
+
+            if (!this.Messaging.EncounteredError)
+            {
+                // at this point, File attribute and File parent element are treated the same
+                if (null != file)
+                {
+                    fileId = file;
+                }
+
+                var row = this.ParseHelper.CreateRow(section, sourceLineNumbers, "WixFirewallException", id);
+                row.Set(1, name);
+                row.Set(2, remoteAddresses);
+
+                if (!String.IsNullOrEmpty(port))
+                {
+                    row.Set(3, port);
+
+                    if (!protocol.HasValue)
+                    {
+                        // default protocol is "TCP"
+                        protocol = FirewallConstants.NET_FW_IP_PROTOCOL_TCP;
+                    }
+                }
+
+                if (protocol.HasValue)
+                {
+                    row.Set(4, protocol);
+                }
+
+                if (!String.IsNullOrEmpty(fileId))
+                {
+                    row.Set(5, $"[#{fileId}]");
+                    this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, "File", fileId);
+                }
+                else if (!String.IsNullOrEmpty(program))
+                {
+                    row.Set(5, program);
+                }
+
+                if (CompilerConstants.IntegerNotSet != attributes)
+                {
+                    row.Set(6, attributes);
+                }
+
+                // Default is "all"
+                row.Set(7, profile ?? FirewallConstants.NET_FW_PROFILE2_ALL);
+
+                row.Set(8, componentId);
+
+                row.Set(9, description);
+
+                if (this.Context.Platform == Platform.ARM)
+                {
+                    // Ensure ARM version of the CA is referenced
+                    this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, "CustomAction", "WixSchedFirewallExceptionsInstall_ARM");
+                    this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, "CustomAction", "WixSchedFirewallExceptionsUninstall_ARM");
+                }
+                else
+                {
+                    // All other supported platforms use x86
+                    this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, "CustomAction", "WixSchedFirewallExceptionsInstall");
+                    this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, "CustomAction", "WixSchedFirewallExceptionsUninstall");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Parses a RemoteAddress element
+        /// </summary>
+        /// <param name="element">The element to parse.</param>
+        private void ParseRemoteAddressElement(Intermediate intermediate, IntermediateSection section, XElement element, ref string remoteAddresses)
+        {
+            SourceLineNumber sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(element);
+
+            // no attributes
+            foreach (XAttribute attrib in element.Attributes())
+            {
+                if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
+                {
+                    this.ParseHelper.UnexpectedAttribute(element, attrib);
+                }
+                else
+                {
+                    this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, element, attrib);
+                }
+            }
+
+            this.ParseHelper.ParseForExtensionElements(this.Context.Extensions, intermediate, section, element);
+
+            string address = this.ParseHelper.GetTrimmedInnerText(element);
+            if (String.IsNullOrEmpty(address))
+            {
+                this.Messaging.Write(FirewallErrors.IllegalEmptyRemoteAddress(sourceLineNumbers));
+            }
+            else
+            {
+                if (String.IsNullOrEmpty(remoteAddresses))
+                {
+                    remoteAddresses = address;
+                }
+                else
+                {
+                    remoteAddresses = String.Concat(remoteAddresses, ",", address);
+                }
+            }
+        }
+    }
+}
diff --git a/src/wixext/FirewallConstants.cs b/src/wixext/FirewallConstants.cs
new file mode 100644
index 00000000..16caa5b4
--- /dev/null
+++ b/src/wixext/FirewallConstants.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Text;
+
+    static class FirewallConstants
+    {
+        // from icftypes.h
+        public const int NET_FW_IP_PROTOCOL_TCP = 6;
+        public const int NET_FW_IP_PROTOCOL_UDP = 17;
+
+        // from icftypes.h
+        public const int NET_FW_PROFILE2_DOMAIN = 0x0001;
+        public const int NET_FW_PROFILE2_PRIVATE = 0x0002;
+        public const int NET_FW_PROFILE2_PUBLIC = 0x0004;
+        public const int NET_FW_PROFILE2_ALL = 0x7FFFFFFF;
+    }
+}
diff --git a/src/wixext/FirewallDecompiler.cs b/src/wixext/FirewallDecompiler.cs
new file mode 100644
index 00000000..b060f8e2
--- /dev/null
+++ b/src/wixext/FirewallDecompiler.cs
@@ -0,0 +1,169 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall
+{
+#if TODO_CONSIDER_DECOMPILER
+    using System;
+    using System.Collections;
+    using System.Diagnostics;
+    using System.Globalization;
+    using WixToolset.Data;
+    using WixToolset.Extensibility;
+    using Firewall = WixToolset.Extensions.Serialize.Firewall;
+    using Wix = WixToolset.Data.Serialize;
+
+    /// <summary>
+    /// The decompiler for the WiX Toolset Firewall Extension.
+    /// </summary>
+    public sealed class FirewallDecompiler : DecompilerExtension
+    {
+        /// <summary>
+        /// Creates a decompiler for Firewall Extension.
+        /// </summary>
+        public FirewallDecompiler()
+        {
+            this.TableDefinitions = FirewallExtensionData.GetExtensionTableDefinitions();
+        }
+
+        /// <summary>
+        /// Get the extensions library to be removed.
+        /// </summary>
+        /// <param name="tableDefinitions">Table definitions for library.</param>
+        /// <returns>Library to remove from decompiled output.</returns>
+        public override Library GetLibraryToRemove(TableDefinitionCollection tableDefinitions)
+        {
+            return FirewallExtensionData.GetExtensionLibrary(tableDefinitions);
+        }
+
+        /// <summary>
+        /// Decompiles an extension table.
+        /// </summary>
+        /// <param name="table">The table to decompile.</param>
+        public override void DecompileTable(Table table)
+        {
+            switch (table.Name)
+            {
+                case "WixFirewallException":
+                    this.DecompileWixFirewallExceptionTable(table);
+                    break;
+                default:
+                    base.DecompileTable(table);
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Decompile the WixFirewallException table.
+        /// </summary>
+        /// <param name="table">The table to decompile.</param>
+        private void DecompileWixFirewallExceptionTable(Table table)
+        {
+            foreach (Row row in table.Rows)
+            {
+                Firewall.FirewallException fire = new Firewall.FirewallException();
+                fire.Id = (string)row[0];
+                fire.Name = (string)row[1];
+
+                string[] addresses = ((string)row[2]).Split(',');
+                if (1 == addresses.Length)
+                {
+                    // special-case the Scope attribute values
+                    if ("*" == addresses[0])
+                    {
+                        fire.Scope = Firewall.FirewallException.ScopeType.any;
+                    }
+                    else if ("LocalSubnet" == addresses[0])
+                    {
+                        fire.Scope = Firewall.FirewallException.ScopeType.localSubnet;
+                    }
+                    else
+                    {
+                        FirewallDecompiler.AddRemoteAddress(fire, addresses[0]);
+                    }
+                }
+                else
+                {
+                    foreach (string address in addresses)
+                    {
+                        FirewallDecompiler.AddRemoteAddress(fire, address);
+                    }
+                }
+
+                if (!row.IsColumnEmpty(3))
+                {
+                    fire.Port = (string)row[3];
+                }
+
+                if (!row.IsColumnEmpty(4))
+                {
+                    switch (Convert.ToInt32(row[4]))
+                    {
+                        case FirewallConstants.NET_FW_IP_PROTOCOL_TCP:
+                            fire.Protocol = Firewall.FirewallException.ProtocolType.tcp;
+                            break;
+                        case FirewallConstants.NET_FW_IP_PROTOCOL_UDP:
+                            fire.Protocol = Firewall.FirewallException.ProtocolType.udp;
+                            break;
+                    }
+                }
+
+                if (!row.IsColumnEmpty(5))
+                {
+                    fire.Program = (string)row[5];
+                }
+
+                if (!row.IsColumnEmpty(6))
+                {
+                    int attr = Convert.ToInt32(row[6]);
+                    if (0x1 == (attr & 0x1)) // feaIgnoreFailures
+                    {
+                        fire.IgnoreFailure = Firewall.YesNoType.yes;
+                    }
+                }
+
+                if (!row.IsColumnEmpty(7))
+                {
+                    switch (Convert.ToInt32(row[7]))
+                    {
+                        case FirewallConstants.NET_FW_PROFILE2_DOMAIN:
+                            fire.Profile = Firewall.FirewallException.ProfileType.domain;
+                            break;
+                        case FirewallConstants.NET_FW_PROFILE2_PRIVATE:
+                            fire.Profile = Firewall.FirewallException.ProfileType.@private;
+                            break;
+                        case FirewallConstants.NET_FW_PROFILE2_PUBLIC:
+                            fire.Profile = Firewall.FirewallException.ProfileType.@public;
+                            break;
+                        case FirewallConstants.NET_FW_PROFILE2_ALL:
+                            fire.Profile = Firewall.FirewallException.ProfileType.all;
+                            break;
+                    }
+                }
+
+                // Description column is new in v3.6
+                if (9 < row.Fields.Length && !row.IsColumnEmpty(9))
+                {
+                    fire.Description = (string)row[9];
+                }
+
+                Wix.Component component = (Wix.Component)this.Core.GetIndexedElement("Component", (string)row[8]);
+                if (null != component)
+                {
+                    component.AddChild(fire);
+                }
+                else
+                {
+                    this.Core.OnMessage(WixWarnings.ExpectedForeignRow(row.SourceLineNumbers, table.Name, row.GetPrimaryKey(DecompilerConstants.PrimaryKeyDelimiter), "Component_", (string)row[6], "Component"));
+                }
+            }
+        }
+
+        private static void AddRemoteAddress(Firewall.FirewallException fire, string address)
+        {
+            Firewall.RemoteAddress remote = new Firewall.RemoteAddress();
+            remote.Content = address;
+            fire.AddChild(remote);
+        }
+    }
+#endif
+}
diff --git a/src/wixext/FirewallErrors.cs b/src/wixext/FirewallErrors.cs
new file mode 100644
index 00000000..3fff8c8d
--- /dev/null
+++ b/src/wixext/FirewallErrors.cs
@@ -0,0 +1,42 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall
+{
+    using System.Resources;
+    using WixToolset.Data;
+
+    public static class FirewallErrors
+    {
+        public static Message IllegalRemoteAddressWithScopeAttribute(SourceLineNumber sourceLineNumbers)
+        {
+            return Message(sourceLineNumbers, Ids.IllegalRemoteAddressWithScopeAttribute, "The RemoteAddress element cannot be specified because its parent FirewallException already specified the Scope attribute. To use RemoteAddress elements, omit the Scope attribute.");
+        }
+
+        public static Message IllegalEmptyRemoteAddress(SourceLineNumber sourceLineNumbers)
+        {
+            return Message(sourceLineNumbers, Ids.IllegalEmptyRemoteAddress, "The RemoteAddress element's inner text cannot be an empty string or completely whitespace.");
+        }
+
+        public static Message NoExceptionSpecified(SourceLineNumber sourceLineNumbers)
+        {
+            return Message(sourceLineNumbers, Ids.NoExceptionSpecified, "The FirewallException element doesn't identify the target of the firewall exception. To create an application exception, nest the FirewallException element under a File element or provide a value for the File or Program attributes. To create a port exception, provide a value for the Port attribute.");
+        }
+
+        private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args)
+        {
+            return new Message(sourceLineNumber, MessageLevel.Error, (int)id, format, args);
+        }
+
+        private static Message Message(SourceLineNumber sourceLineNumber, Ids id, ResourceManager resourceManager, string resourceName, params object[] args)
+        {
+            return new Message(sourceLineNumber, MessageLevel.Error, (int)id, resourceManager, resourceName, args);
+        }
+
+        public enum Ids
+        {
+            IllegalRemoteAddressWithScopeAttribute = 6401,
+            IllegalEmptyRemoteAddress = 6402,
+            NoExceptionSpecified = 6403,
+        }
+    }
+}
diff --git a/src/wixext/FirewallExtensionData.cs b/src/wixext/FirewallExtensionData.cs
new file mode 100644
index 00000000..78939c4e
--- /dev/null
+++ b/src/wixext/FirewallExtensionData.cs
@@ -0,0 +1,24 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall
+{
+    using WixToolset.Data;
+    using WixToolset.Extensibility;
+    using WixToolset.Firewall.Tuples;
+
+    public sealed class FirewallExtensionData : BaseExtensionData
+    {
+        public override string DefaultCulture => "en-US";
+
+        public override bool TryGetTupleDefinitionByName(string name, out IntermediateTupleDefinition tupleDefinition)
+        {
+            tupleDefinition = (name == FirewallTupleDefinitionNames.WixFirewallException) ? FirewallTupleDefinitions.WixFirewallException : null;
+            return tupleDefinition != null;
+        }
+
+        public override Intermediate GetLibrary(ITupleDefinitionCreator tupleDefinitions)
+        {
+            return Intermediate.Load(typeof(FirewallExtensionData).Assembly, "WixToolset.Firewall.firewall.wixlib", tupleDefinitions);
+        }
+    }
+}
diff --git a/src/wixext/FirewallExtensionFactory.cs b/src/wixext/FirewallExtensionFactory.cs
new file mode 100644
index 00000000..4594419d
--- /dev/null
+++ b/src/wixext/FirewallExtensionFactory.cs
@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall
+{
+    using System;
+    using System.Collections.Generic;
+    using WixToolset.Extensibility;
+
+    public class FirewallExtensionFactory : BaseExtensionFactory
+    {
+        protected override IEnumerable<Type> ExtensionTypes => new[]
+        {
+            typeof(FirewallCompiler),
+            typeof(FirewallExtensionData),
+            typeof(FirewallWindowsInstallerBackendExtension),
+        };
+    }
+}
diff --git a/src/wixext/FirewallWindowsInstallerBackendExtension.cs b/src/wixext/FirewallWindowsInstallerBackendExtension.cs
new file mode 100644
index 00000000..a1c78f04
--- /dev/null
+++ b/src/wixext/FirewallWindowsInstallerBackendExtension.cs
@@ -0,0 +1,26 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall
+{
+    using System.Linq;
+    using System.Xml;
+    using WixToolset.Data.WindowsInstaller;
+    using WixToolset.Extensibility;
+
+    public class FirewallWindowsInstallerBackendExtension : BaseWindowsInstallerBackendExtension
+    {
+        private static readonly TableDefinition[] Tables = LoadTables();
+
+        protected override TableDefinition[] TableDefinitionsForTuples => Tables;
+
+        private static TableDefinition[] LoadTables()
+        {
+            using (var resourceStream = typeof(FirewallWindowsInstallerBackendExtension).Assembly.GetManifestResourceStream("WixToolset.Firewall.tables.xml"))
+            using (var reader = XmlReader.Create(resourceStream))
+            {
+                var tables = TableDefinitionCollection.Load(reader);
+                return tables.ToArray();
+            }
+        }
+    }
+}
diff --git a/src/wixext/Tuples/FirewallTupleDefinitions.cs b/src/wixext/Tuples/FirewallTupleDefinitions.cs
new file mode 100644
index 00000000..79fc28cf
--- /dev/null
+++ b/src/wixext/Tuples/FirewallTupleDefinitions.cs
@@ -0,0 +1,31 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall.Tuples
+{
+    using WixToolset.Data;
+
+    public static class FirewallTupleDefinitionNames
+    {
+        public static string WixFirewallException { get; } = "WixFirewallException";
+    }
+
+    public static partial class FirewallTupleDefinitions
+    {
+        public static readonly IntermediateTupleDefinition WixFirewallException = new IntermediateTupleDefinition(
+            FirewallTupleDefinitionNames.WixFirewallException,
+            new[]
+            {
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.WixFirewallException), IntermediateFieldType.String),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Name), IntermediateFieldType.String),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.RemoteAddresses), IntermediateFieldType.String),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Port), IntermediateFieldType.String),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Protocol), IntermediateFieldType.Number),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Program), IntermediateFieldType.String),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Attributes), IntermediateFieldType.Number),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Profile), IntermediateFieldType.Number),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Component_), IntermediateFieldType.String),
+                new IntermediateFieldDefinition(nameof(WixFirewallExceptionTupleFields.Description), IntermediateFieldType.String),
+            },
+            typeof(WixFirewallExceptionTuple));
+    }
+}
diff --git a/src/wixext/Tuples/WixFirewallExceptionTuple.cs b/src/wixext/Tuples/WixFirewallExceptionTuple.cs
new file mode 100644
index 00000000..715a4b9b
--- /dev/null
+++ b/src/wixext/Tuples/WixFirewallExceptionTuple.cs
@@ -0,0 +1,93 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Firewall.Tuples
+{
+    using WixToolset.Data;
+
+    public enum WixFirewallExceptionTupleFields
+    {
+        WixFirewallException,
+        Name,
+        RemoteAddresses,
+        Port,
+        Protocol,
+        Program,
+        Attributes,
+        Profile,
+        Component_,
+        Description,
+    }
+
+    public class WixFirewallExceptionTuple : IntermediateTuple
+    {
+        public WixFirewallExceptionTuple() : base(FirewallTupleDefinitions.WixFirewallException, null, null)
+        {
+        }
+
+        public WixFirewallExceptionTuple(SourceLineNumber sourceLineNumber, Identifier id = null) : base(FirewallTupleDefinitions.WixFirewallException, sourceLineNumber, id)
+        {
+        }
+
+        public IntermediateField this[WixFirewallExceptionTupleFields index] => this.Fields[(int)index];
+
+        public string WixFirewallException
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.WixFirewallException].AsString();
+            set => this.Set((int)WixFirewallExceptionTupleFields.WixFirewallException, value);
+        }
+
+        public string Name
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Name].AsString();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Name, value);
+        }
+
+        public string RemoteAddresses
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.RemoteAddresses].AsString();
+            set => this.Set((int)WixFirewallExceptionTupleFields.RemoteAddresses, value);
+        }
+
+        public string Port
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Port].AsString();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Port, value);
+        }
+
+        public int Protocol
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Protocol].AsNumber();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Protocol, value);
+        }
+
+        public string Program
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Program].AsString();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Program, value);
+        }
+
+        public int Attributes
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Attributes].AsNumber();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Attributes, value);
+        }
+
+        public int Profile
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Profile].AsNumber();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Profile, value);
+        }
+
+        public string Component_
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Component_].AsString();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Component_, value);
+        }
+
+        public string Description
+        {
+            get => this.Fields[(int)WixFirewallExceptionTupleFields.Description].AsString();
+            set => this.Set((int)WixFirewallExceptionTupleFields.Description, value);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/wixext/WixToolset.Firewall.wixext.csproj b/src/wixext/WixToolset.Firewall.wixext.csproj
new file mode 100644
index 00000000..2d89911c
--- /dev/null
+++ b/src/wixext/WixToolset.Firewall.wixext.csproj
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
+
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <RootNamespace>WixToolset.Firewall</RootNamespace>
+    <Description>WiX Toolset Firewallity Extension</Description>
+    <Title>WiX Toolset Firewall Extension</Title>
+    <IsTool>true</IsTool>
+    <ContentTargetFolders>build</ContentTargetFolders>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Content Include="$(MSBuildThisFileName).targets" />
+    <Content Include="firewall.xsd" PackagePath="tools" />
+    <EmbeddedResource Include="tables.xml" />
+    <EmbeddedResource Include="$(OutputPath)..\firewall.wixlib" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="$(WixToolsetRootFolder)\Data\src\WixToolset.Data\WixToolset.Data.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Data\README.md') " />
+    <PackageReference Include="WixToolset.Data" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Data\README.md') " PrivateAssets="all" />
+
+    <ProjectReference Include="$(WixToolsetRootFolder)\Extensibility\src\WixToolset.Extensibility\WixToolset.Extensibility.csproj" Condition=" '$(Configuration)' == 'Debug' And Exists('$(WixToolsetRootFolder)\Extensibility\README.md') " />
+    <PackageReference Include="WixToolset.Extensibility" Version="4.0.*" Condition=" '$(Configuration)' == 'Release' Or !Exists('$(WixToolsetRootFolder)\Extensibility\README.md') " PrivateAssets="all" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\wixlib\firewall.wixproj" ReferenceOutputAssembly="false" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Nerdbank.GitVersioning" Version="2.1.7" PrivateAssets="all" />
+  </ItemGroup>
+</Project>
diff --git a/src/wixext/WixToolset.Firewall.wixext.targets b/src/wixext/WixToolset.Firewall.wixext.targets
new file mode 100644
index 00000000..c717450f
--- /dev/null
+++ b/src/wixext/WixToolset.Firewall.wixext.targets
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
+
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
+  <PropertyGroup>
+    <WixToolsetFirewallWixextPath Condition=" '$(WixToolsetFirewallWixextPath)' == '' ">$(MSBuildThisFileDirectory)..\tools\WixToolset.Firewall.wixext.dll</WixToolsetFirewallWixextPath>
+  </PropertyGroup>
+  <ItemGroup>
+    <WixExtension Include="$(WixToolsetFirewallWixextPath)" />
+  </ItemGroup>
+</Project>
diff --git a/src/wixext/firewall.xsd b/src/wixext/firewall.xsd
new file mode 100644
index 00000000..d64aafef
--- /dev/null
+++ b/src/wixext/firewall.xsd
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
+
+
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+          xmlns:xse=" http://wixtoolset.org/schemas/XmlSchemaExtension"
+         xmlns:html="http://www.w3.org/1999/xhtml"
+    targetNamespace="http://wixtoolset.org/schemas/v4/wxs/firewall"
+              xmlns="http://wixtoolset.org/schemas/v4/wxs/firewall">
+    <xs:annotation>
+        <xs:documentation>
+            The source code schema for the WiX Toolset Firewall Extension.
+        </xs:documentation>
+    </xs:annotation>
+
+    <xs:import namespace="http://wixtoolset.org/schemas/v4/wxs" />
+
+    <xs:element name="FirewallException">
+        <xs:annotation>
+            <xs:documentation>
+                Registers an exception for a program or a specific port and protocol in the Windows Firewall
+                on Windows XP SP2, Windows Server 2003 SP1, and later. For more information about the Windows
+                Firewall, see <html:a href="http://msdn.microsoft.com/en-us/library/aa364679.aspx">
+                About Windows Firewall API</html:a>.
+            </xs:documentation>
+            <xs:appinfo>
+                <xse:parent namespace="http://wixtoolset.org/schemas/v4/wxs" ref="Component" />
+                <xse:parent namespace="http://wixtoolset.org/schemas/v4/wxs" ref="File" />
+            </xs:appinfo>
+        </xs:annotation>
+
+        <xs:complexType>
+            <xs:choice minOccurs="0" maxOccurs="unbounded">
+                <xs:annotation>
+                    <xs:documentation>
+                        Explicitly-listed remote addresses that this exception allows through the 
+                        firewall.
+                    </xs:documentation>
+                </xs:annotation>
+                <xs:element ref="RemoteAddress" />
+            </xs:choice>
+
+            <xs:attribute name="Id" type="xs:string" use="required">
+                <xs:annotation>
+                    <xs:documentation>
+                        Unique ID of this firewall exception.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+
+            <xs:attribute name="Name" type="xs:string" use="required">
+                <xs:annotation>
+                    <xs:documentation>
+                        Name of this firewall exception, visible to the user in the firewall 
+                        control panel.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+
+            <xs:attribute name="Scope">
+                <xs:annotation>
+                    <xs:documentation>
+                        The scope of this firewall exception, which indicates whether incoming
+                        connections can come from any computer including those on the Internet
+                        or only those on the local network subnet. To more precisely specify
+                        allowed remote address, specify a custom scope using RemoteAddress 
+                        child elements.
+                    </xs:documentation>
+                </xs:annotation>
+                <xs:simpleType>
+                    <xs:restriction base="xs:NMTOKEN">
+                        <xs:enumeration value="any" />
+                        <xs:enumeration value="localSubnet" />
+                    </xs:restriction>
+                </xs:simpleType>
+            </xs:attribute>
+
+            <xs:attribute name="Port" type="xs:string">
+                <xs:annotation>
+                    <xs:documentation>
+                        Port to allow through the firewall for this exception. 
+
+                        If you use Port and also File or Program in the same 
+                        FirewallException element, the exception will fail to install on 
+                        Windows XP and Windows Server 2003. IgnoreFailure="yes" can be used to
+                        ignore the resulting failure, but the exception will not be added.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+
+            <xs:attribute name="Protocol">
+                <xs:annotation>
+                    <xs:documentation>
+                        IP protocol used for this firewall exception. If Port is defined, 
+                        "tcp" is assumed if the protocol is not specified. 
+
+                        If you use Protocol and also File or Program in the same 
+                        FirewallException element, the exception will fail to install on 
+                        Windows XP and Windows Server 2003. IgnoreFailure="yes" can be used to
+                        ignore the resulting failure, but the exception will not be added.
+                    </xs:documentation>
+                </xs:annotation>
+                <xs:simpleType>
+                    <xs:restriction base="xs:NMTOKEN">
+                        <xs:enumeration value="tcp" />
+                        <xs:enumeration value="udp" />
+                    </xs:restriction>
+                </xs:simpleType>
+            </xs:attribute>
+
+            <xs:attribute name="File" type="xs:string">
+                <xs:annotation>
+                    <xs:documentation>
+                        Identifier of a file to be granted access to all incoming ports and 
+                        protocols. If you use File, you cannot also use Program.
+
+                        If you use File and also Port or Protocol in the same 
+                        FirewallException element, the exception will fail to install on 
+                        Windows XP and Windows Server 2003. IgnoreFailure="yes" can be used to
+                        ignore the resulting failure, but the exception will not be added.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+
+            <xs:attribute name="Program" type="xs:string">
+                <xs:annotation>
+                    <xs:documentation>
+                        Path to a target program to be granted access to all incoming ports and 
+                        protocols. Note that this is a formatted field, so you can use [#fileId] 
+                        syntax to refer to a file being installed. If you use Program, you cannot 
+                        also use File.
+
+                        If you use Program and also Port or Protocol in the same 
+                        FirewallException element, the exception will fail to install on 
+                        Windows XP and Windows Server 2003. IgnoreFailure="yes" can be used to
+                        ignore the resulting failure, but the exception will not be added.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+
+            <xs:attribute name="IgnoreFailure" type="YesNoType">
+                <xs:annotation>
+                    <xs:documentation>
+                        If "yes," failures to register this firewall exception will be silently 
+                        ignored. If "no" (the default), failures will cause rollback.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+
+            <xs:attribute name="Profile">
+              <xs:annotation>
+                <xs:documentation>
+                  Profile type for this firewall exception. Default is "all".
+                </xs:documentation>
+              </xs:annotation>
+              <xs:simpleType>
+                <xs:restriction base="xs:NMTOKEN">
+                  <xs:enumeration value="domain" />
+                  <xs:enumeration value="private" />
+                  <xs:enumeration value="public" />
+                  <xs:enumeration value="all" />
+                </xs:restriction>
+              </xs:simpleType>
+            </xs:attribute>
+            <xs:attribute name="Description" type="xs:string">
+                <xs:annotation>
+                    <xs:documentation>
+                      Description for this firewall rule displayed in Windows Firewall manager in 
+                      Windows Vista and later.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:attribute>
+        </xs:complexType>
+    </xs:element>
+
+    <xs:element name="RemoteAddress">
+        <xs:annotation>
+            <xs:documentation>
+                A remote address to which the port or program can listen. Address formats vary 
+                based on the version of Windows and Windows Firewall the program is being installed
+                on. For Windows XP SP2 and Windows Server 2003 SP1, see
+                <html:a href="http://msdn.microsoft.com/en-us/library/aa365270.aspx">
+                    RemoteAddresses Property</html:a>.
+                For Windows Vista and Windows Server 2008, see
+                <html:a href="http://msdn.microsoft.com/en-us/library/aa365366.aspx">
+                    RemoteAddresses Property</html:a>.
+            </xs:documentation>
+        </xs:annotation>
+        <xs:complexType>
+            <xs:simpleContent>
+                <xs:extension base="xs:string">
+                    <xs:annotation>
+                        <xs:documentation>
+                            A remote address.
+                        </xs:documentation>
+                    </xs:annotation>
+                </xs:extension>
+            </xs:simpleContent>
+        </xs:complexType>
+    </xs:element>
+
+    <xs:simpleType name="YesNoType">
+        <xs:annotation>
+            <xs:documentation>Values of this type will either be "yes" or "no".</xs:documentation>
+        </xs:annotation>
+        <xs:restriction base='xs:NMTOKEN'>
+            <xs:enumeration value="no"/>
+            <xs:enumeration value="yes"/>
+        </xs:restriction>
+    </xs:simpleType>
+</xs:schema>
diff --git a/src/wixext/tables.xml b/src/wixext/tables.xml
new file mode 100644
index 00000000..5b408b96
--- /dev/null
+++ b/src/wixext/tables.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
+
+
+<tableDefinitions xmlns="http://wixtoolset.org/schemas/v4/wi/tables">
+    <tableDefinition name="WixFirewallException">
+        <columnDefinition name="WixFirewallException" type="string" length="72" primaryKey="yes" modularize="column"
+            category="identifier" description="The primary key, a non-localized token." />
+        <columnDefinition name="Name" type="localized" length="255" nullable="yes" modularize="property" 
+            category="formatted" description="Localizable display name." />
+        <columnDefinition name="RemoteAddresses" type="string" length="0" modularize="property" 
+            category="formatted" description="Remote address to accept incoming connections from." />
+        <columnDefinition name="Port" type="string" length="0" modularize="property" nullable="yes" 
+            category="formatted" minValue="1" description="Port number." />
+        <columnDefinition name="Protocol" type="number" length="1" nullable="yes" 
+            category="integer" minValue="6" maxValue="17" description="Protocol (6=TCP; 17=UDP)." />
+        <columnDefinition name="Program" type="string" length="255" nullable="yes" modularize="property"
+            category="formatted" description="Exception for a program (formatted path name)." />
+        <columnDefinition name="Attributes" type="number" length="4" nullable="yes" 
+            minValue="0" maxValue="65536" description="Vital=1" />
+        <columnDefinition name="Profile" type="number" length="4" nullable="no"
+            category="integer" minValue="1" maxValue="2147483647" description="Profile (1=domain; 2=private; 4=public; 2147483647=all)." />
+        <columnDefinition name="Component_" type="string" length="72" modularize="column"
+            keyTable="Component" keyColumn="1" category="identifier" description="Foreign key into the Component table referencing component that controls the firewall configuration."/>
+        <columnDefinition name="Description" type="string" length="255" nullable="yes"
+            category="formatted" description="Description displayed in Windows Firewall manager for this firewall rule."/>
+    </tableDefinition>
+</tableDefinitions>
-- 
cgit v1.2.3-55-g6feb