Files
Invoke-OPNSenseVMDeployment/Toolkit/Functions/New-OPNSenseConfigurationDocument.ps1
gsadmin de49257c8d feat: Separate authorised resolvers from what they may forward towards
Modelled on the operator's production configuration, which was supplied as a
sample. Name resolution was one permit and one blanket deny, which said who may
be queried but had nothing to say about what a resolver is then allowed to do,
and pinned the appliance to whatever name server the machine generating the
document happened to be using.

Two aliases now carry the policy. The internal alias holds the resolvers a client
may query, which is the appliance itself by default, and the upstream alias holds
what an authorised resolver may forward towards. Eight rules follow, four per
transport, because each transport carries a different set of ports.

The deny inverts its destination rather than being a blanket deny placed after
the permits. That states the policy as it is actually meant, which is that name
resolution towards anything unauthorised is refused, and it stays true as the
alias is edited instead of depending upon the deny sitting at a later sequence
than every permit.

The rule permitting an authorised resolver to forward upstream changes nothing
while the internal alias holds only the appliance, because traffic the appliance
originates is not evaluated by an inbound rule. It is not redundant for that
reason. It is what makes the alias worth having: adding a real resolver to the
internal alias gives it both directions at once, reached by clients through one
rule and forwarding upstream through the other, with nothing further to write.

Nothing about name resolution is read from the machine that generates the
document any more. A resolver that machine uses is a fact about that machine
rather than about the network the appliance joins, and seeding it wrote a site
specific address into every document produced. The upstream alias defaults to
public resolvers and is overridable, and the host inspection that remains is only
for the networks it is attached to, which the parent network selection excludes.

The appliance resolves through its own resolver, which is what allows a local
record to be defined and answered authoritatively for the networks behind it, and
takes its upstream from the wide area network lease. Three settings carry that:
no static name server address is written, the lease is permitted to populate the
system list, and the local host is preferred so the appliance asks itself first.

The resolver is also switched to forwarding. Left recursing it would query the
root servers directly and the upstream the joined network expects would never be
consulted, so the appliance would resolve correctly while ignoring its own
network's resolver, which is not what taking the upstream from the lease is meant
to mean. The local zone stays transparent, so a local record is still answered
before anything is forwarded.

Verified that the document carries the eight rules in the intended order with the
deny inverted, that the aliases hold the appliance address on every planned
network and the public upstreams, that an explicit upstream list is honoured, that
no address belonging to this host appears anywhere in the output, and that every
alias named by any rule still resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:29:42 -04:00

1658 lines
116 KiB
PowerShell

#region New-OPNSenseConfigurationDocument
Function New-OPNSenseConfigurationDocument
{
<#
.SYNOPSIS
Generates the OPNsense configuration document from a network plan.
.DESCRIPTION
The document is assembled within a "System.Xml.XmlDocument" and every node is located or created through an XPath expression, which allows an existing configuration document to be supplied and extended instead of a newly generated one.
The following sections are populated.
- System identity, name resolution, console, and administrative credentials.
- The wide area network interface, the parent local area network interface, and one optional interface per planned virtual local area network.
- The virtual local area network device for each planned network, carrying the tag that the network plan assigned to it.
- A Kea dynamic host configuration protocol scope per network, including the preboot execution environment options.
- The Unbound resolver.
- A small set of firewall aliases that the rules are written against.
- An ordered firewall rule set. Every rule is a quick rule, so the lowest sequence value that matches wins and the ordering is the policy itself.
- Automatic outbound network address translation.
.PARAMETER NetworkPlan
The object that is returned by "New-OPNSenseNetworkPlan".
.PARAMETER HostName
The host name of the appliance.
.PARAMETER DomainName
The domain name of the appliance.
.PARAMETER WANInterfaceDevice
The name of the network interface device within the appliance that is attached to the wide area network.
.PARAMETER WANInterfaceName
The description that is assigned to the wide area network interface.
.PARAMETER UpstreamDNSServerAddressList
The addresses that populate the "AUTHORIZED_DNS_SERVERS_UPSTREAM" alias, which an authorised internal name server is permitted to forward towards.
These are not written into the system section of the appliance. The appliance resolves through the resolver that it runs itself and takes its own upstream name server addresses from the wide area network lease, so this list describes what an internal resolver is allowed to forward towards rather than what the appliance is configured to use.
.PARAMETER TimeZone
The time zone of the appliance.
.PARAMETER TimeServerList
One or more network time protocol server names.
.PARAMETER RootPasswordHash
The blowfish crypt hash that is assigned to the root account. When this parameter is not specified, the stock appliance credentials are retained.
.PARAMETER DHCPValidLifetime
The dynamic host configuration protocol lease lifetime, expressed in seconds.
.PARAMETER PXENextServerAddress
The address of the preboot execution environment boot server. When this parameter is not specified, the gateway address of the parent network is used.
.PARAMETER PXEBootFileNameBIOS
The boot file that is offered to a legacy basic input output system client.
.PARAMETER PXEBootFileNameUEFI
The boot file that is offered to a unified extensible firmware interface client.
.PARAMETER SkipPXEConfiguration
Do not emit any preboot execution environment option.
.PARAMETER DNSServerAddressList
The addresses that populate the "AUTHORIZED_DNS_SERVERS_INTERNAL" alias. When this parameter is not specified, the address of the firewall on every planned network is used, because the appliance is the resolver that each network is handed. Add the address of an internal name server of your own to authorise it.
.PARAMETER BastionServerAddressList
The addresses that populate the "BASTION_SERVERS_001" alias. When this parameter is not specified, the address of the firewall on the management network is used as a placeholder.
.PARAMETER DNSTCPPortList
The ports that populate the "AUTHORIZED_DNS_PORTS_TCP" alias.
.PARAMETER DNSUDPPortList
The ports that populate the "AUTHORIZED_DNS_PORTS_UDP" alias.
.PARAMETER BastionPortList
The ports that populate the "BASTION_PORTS_001" alias.
.PARAMETER EnableZoneIsolation
Do not emit the per interface rule that permits all traffic from a network to any destination. Only the management, jump host, name resolution, and zone pairing rules will then apply, which turns the zones into genuinely isolated segments.
.PARAMETER DHCPSocketType
The socket type that the Kea dynamic host configuration protocol service binds with. This defaults to "raw", which is what the appliance ships with and which is the only one of the two that reliably answers a client that holds no address yet. That is precisely the preboot execution environment case.
.PARAMETER EnableIPv6
Emit the internet protocol version 6 companion firewall rules and permit internet protocol version 6 on the appliance.
.PARAMETER EnableSSH
Enable the secure shell service on the appliance.
.PARAMETER BaseConfigurationPath
An optional path to an existing configuration document that the generated content is applied to.
.PARAMETER ContinueOnError
Ignore failures.
.EXAMPLE
$NewOPNSenseConfigurationDocumentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$NewOPNSenseConfigurationDocumentParameters.NetworkPlan = $NewOPNSenseNetworkPlanResult
$NewOPNSenseConfigurationDocumentParameters.HostName = 'vFW-HQ-00001'
$NewOPNSenseConfigurationDocumentParameters.DomainName = 'lab.prv'
$NewOPNSenseConfigurationDocumentParameters.UpstreamDNSServerAddressList = @('192.168.1.1')
$NewOPNSenseConfigurationDocumentParameters.Verbose = $True
$ConfigurationDocument = New-OPNSenseConfigurationDocument @NewOPNSenseConfigurationDocumentParameters
$Null = Save-OPNSenseConfigurationDocument -XmlDocument ($ConfigurationDocument) -Path "$($ContentDirectory.FullName)\Configurations\config.xml"
.NOTES
The generated document targets the model definitions that are published within the OPNsense core repository. Model version attributes are deliberately omitted so that the appliance applies every applicable migration when the document is imported.
.LINK
https://docs.opnsense.org/manual/kea.html
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[Alias('NP')]
[System.Management.Automation.PSObject]$NetworkPlan,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('HN')]
[System.String]$HostName,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('DN')]
[System.String]$DomainName,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('WID')]
[System.String]$WANInterfaceDevice,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('WIN')]
[System.String]$WANInterfaceName,
[Parameter(Mandatory=$False)]
[AllowEmptyCollection()]
[AllowNull()]
[Alias('UDSAL')]
[System.String[]]$UpstreamDNSServerAddressList,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('TZ')]
[System.String]$TimeZone,
[Parameter(Mandatory=$False)]
[AllowEmptyCollection()]
[AllowNull()]
[Alias('TSL')]
[System.String[]]$TimeServerList,
[Parameter(Mandatory=$False)]
[AllowEmptyString()]
[AllowNull()]
[Alias('RPH')]
[System.String]$RootPasswordHash,
[Parameter(Mandatory=$False)]
[ValidateRange(60, 604800)]
[Alias('DVL')]
[System.Int32]$DHCPValidLifetime,
[Parameter(Mandatory=$False)]
[AllowEmptyString()]
[AllowNull()]
[Alias('PNSA')]
[System.String]$PXENextServerAddress,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('PBFNB')]
[System.String]$PXEBootFileNameBIOS,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('PBFNU')]
[System.String]$PXEBootFileNameUEFI,
[Parameter(Mandatory=$False)]
[Alias('SPC')]
[Switch]$SkipPXEConfiguration,
[Parameter(Mandatory=$False)]
[AllowEmptyCollection()]
[AllowNull()]
[Alias('DSAL')]
[System.String[]]$DNSServerAddressList,
[Parameter(Mandatory=$False)]
[AllowEmptyCollection()]
[AllowNull()]
[Alias('BSAL')]
[System.String[]]$BastionServerAddressList,
[Parameter(Mandatory=$False)]
[AllowEmptyCollection()]
[AllowNull()]
[Alias('DTPL')]
[System.String[]]$DNSTCPPortList,
[Parameter(Mandatory=$False)]
[AllowEmptyCollection()]
[AllowNull()]
[Alias('DUPL')]
[System.String[]]$DNSUDPPortList,
[Parameter(Mandatory=$False)]
[AllowEmptyCollection()]
[AllowNull()]
[Alias('BPL')]
[System.String[]]$BastionPortList,
[Parameter(Mandatory=$False)]
[Alias('EZI')]
[Switch]$EnableZoneIsolation,
[Parameter(Mandatory=$False)]
[Alias('STS')]
[Switch]$SkipTrafficShaping,
[Parameter(Mandatory=$False)]
[ValidateRange(0, 100)]
[Alias('DSBRC')]
[System.Int32]$DHCPSocketBindRetryCount,
[Parameter(Mandatory=$False)]
[ValidateRange(0, 60000)]
[Alias('DSBRW')]
[System.Int32]$DHCPSocketBindRetryWaitMilliseconds,
[Parameter(Mandatory=$False)]
[ValidateSet('raw', 'udp')]
[Alias('DST')]
[System.String]$DHCPSocketType,
[Parameter(Mandatory=$False)]
[AllowEmptyString()]
[AllowNull()]
[Alias('PSMA')]
[System.String]$PXEServerMACAddress,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('PSNR')]
[System.String]$PXEServerNetworkRole,
[Parameter(Mandatory=$False)]
[ValidateRange(2, 254)]
[Alias('PSHO')]
[System.Int32]$PXEServerHostOffset,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('PSHN')]
[System.String]$PXEServerHostName,
[Parameter(Mandatory=$False)]
[Alias('EI6')]
[Switch]$EnableIPv6,
[Parameter(Mandatory=$False)]
[Alias('ES')]
[Switch]$EnableSSH,
[Parameter(Mandatory=$False)]
[AllowNull()]
[Alias('BCP')]
[System.IO.FileInfo]$BaseConfigurationPath,
[Parameter(Mandatory=$False)]
[Alias('COE')]
[Switch]$ContinueOnError
)
Try
{
[System.String]$CmdletName = $MyInvocation.MyCommand.Name
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is beginning. Please Wait..."))
#region Set default parameter value(s)
Switch ($True)
{
{([System.String]::IsNullOrEmpty($HostName) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($HostName) -eq $True)}
{
[System.String]$HostName = 'vFW-HQ-00001'
}
{([System.String]::IsNullOrEmpty($DomainName) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($DomainName) -eq $True)}
{
[System.String]$DomainName = 'lab.prv'
}
{([System.String]::IsNullOrEmpty($WANInterfaceDevice) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($WANInterfaceDevice) -eq $True)}
{
[System.String]$WANInterfaceDevice = 'hn1'
}
{([System.String]::IsNullOrEmpty($WANInterfaceName) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($WANInterfaceName) -eq $True)}
{
[System.String]$WANInterfaceName = 'WAN'
Switch (([System.String]::IsNullOrEmpty($NetworkPlan.NamePrefix) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($NetworkPlan.NamePrefix) -eq $False))
{
{($_ -eq $True)}
{
[System.String]$WANInterfaceName = "$($NetworkPlan.NamePrefix)_WAN"
}
}
}
{([System.String]::IsNullOrEmpty($TimeZone) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($TimeZone) -eq $True)}
{
[System.String]$TimeZone = 'Etc/UTC'
}
{($Null -ieq $TimeServerList) -or ($TimeServerList.Count -eq 0)}
{
[System.String[]]$TimeServerList = @('0.opnsense.pool.ntp.org', '1.opnsense.pool.ntp.org', '2.opnsense.pool.ntp.org', '3.opnsense.pool.ntp.org')
}
{($DHCPValidLifetime -le 0)}
{
[System.Int32]$DHCPValidLifetime = 4000
}
{($PSBoundParameters.ContainsKey('DHCPSocketBindRetryCount') -eq $False)}
{
[System.Int32]$DHCPSocketBindRetryCount = 5
}
{($PSBoundParameters.ContainsKey('DHCPSocketBindRetryWaitMilliseconds') -eq $False)}
{
[System.Int32]$DHCPSocketBindRetryWaitMilliseconds = 5000
}
{([System.String]::IsNullOrEmpty($DHCPSocketType) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($DHCPSocketType) -eq $True)}
{
# Raw is what the appliance ships with, and it is the only one of the two that reliably answers a client which
# holds no address yet. That is exactly the preboot execution environment case.
[System.String]$DHCPSocketType = 'raw'
}
{([System.String]::IsNullOrEmpty($PXEServerNetworkRole) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($PXEServerNetworkRole) -eq $True)}
{
[System.String]$PXEServerNetworkRole = 'Infrastructure'
}
{($PXEServerHostOffset -le 0)}
{
[System.Int32]$PXEServerHostOffset = 10
}
{([System.String]::IsNullOrEmpty($PXEServerHostName) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($PXEServerHostName) -eq $True)}
{
[System.String]$PXEServerHostName = 'pxe-server-001'
}
{([System.String]::IsNullOrEmpty($PXEBootFileNameBIOS) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($PXEBootFileNameBIOS) -eq $True)}
{
[System.String]$PXEBootFileNameBIOS = 'undionly.kpxe'
}
{([System.String]::IsNullOrEmpty($PXEBootFileNameUEFI) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($PXEBootFileNameUEFI) -eq $True)}
{
[System.String]$PXEBootFileNameUEFI = 'ipxe.efi'
}
}
$UntaggedNetwork = $NetworkPlan.NetworkList | Where-Object {($_.IsUntaggedNetwork -eq $True)} | Select-Object -First 1
$ManagementNetwork = $NetworkPlan.NetworkList | Where-Object {($_.IsManagementNetwork -eq $True)} | Select-Object -First 1
#region Interpret the traffic shaping profiles
# Each profile produces a matched pair of pipes and a matched pair of aliases, named after the profile so that the intent is
# legible at a glance within the alias list. "45MBPS" becomes INBOUND_45MBPS and OUTBOUND_45MBPS.
$TrafficShaperProfileObjectList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
Switch ($SkipTrafficShaping.IsPresent)
{
{($_ -eq $False)}
{
$BandwidthMetricDictionary = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$BandwidthMetricDictionary.K = 'Kbit'
$BandwidthMetricDictionary.M = 'Mbit'
$BandwidthMetricDictionary.G = 'Gbit'
# A ladder of link speeds covering the scenarios a lab is normally asked to imitate, from a badly congested
# connection through to gigabit. Ordered slowest first, which is also the order the rules are evaluated in.
$TrafficShaperProfileList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$TrafficShaperProfileList.Add('512KBPS')
$TrafficShaperProfileList.Add('1MBPS')
$TrafficShaperProfileList.Add('5MBPS')
$TrafficShaperProfileList.Add('10MBPS')
$TrafficShaperProfileList.Add('25MBPS')
$TrafficShaperProfileList.Add('45MBPS')
$TrafficShaperProfileList.Add('100MBPS')
$TrafficShaperProfileList.Add('250MBPS')
$TrafficShaperProfileList.Add('500MBPS')
$TrafficShaperProfileList.Add('1000MBPS')
ForEach ($TrafficShaperProfile In $TrafficShaperProfileList)
{
[System.String]$TrafficShaperProfileName = "$($TrafficShaperProfile)".Trim().ToUpper()
$TrafficShaperProfileMatch = [System.Text.RegularExpressions.Regex]::Match($TrafficShaperProfileName, '^(?<Bandwidth>\d+)(?<Metric>K|M|G)BPS$')
Switch ($TrafficShaperProfileMatch.Success)
{
{($_ -eq $False)}
{
Throw "The traffic shaping profile `"$($TrafficShaperProfile)`" could not be interpreted. Express a profile as a bandwidth followed by KBPS, MBPS, or GBPS, such as `"45MBPS`"."
}
}
$TrafficShaperProfileProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$TrafficShaperProfileProperties.Name = $TrafficShaperProfileName
$TrafficShaperProfileProperties.InboundName = "INBOUND_$($TrafficShaperProfileName)"
$TrafficShaperProfileProperties.OutboundName = "OUTBOUND_$($TrafficShaperProfileName)"
$TrafficShaperProfileProperties.Bandwidth = [System.Int32]$TrafficShaperProfileMatch.Groups['Bandwidth'].Value
$TrafficShaperProfileProperties.Metric = $BandwidthMetricDictionary["$($TrafficShaperProfileMatch.Groups['Metric'].Value)"]
$TrafficShaperProfileProperties.InboundPipeUUID = $Null
$TrafficShaperProfileProperties.OutboundPipeUUID = $Null
$TrafficShaperProfileObjectList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($TrafficShaperProfileProperties)))
}
$WriteLogMessage.Invoke(0, @("$($TrafficShaperProfileObjectList.Count) traffic shaping profile(s) will be configured. [Profiles: $(($TrafficShaperProfileObjectList | ForEach-Object {$_.Name}) -Join '; ')]"))
}
}
#endregion
#region Determine where the preboot execution environment server lives
# The server is placed on the network whose role matches, falling back to the parent network, and it is given a fixed address
# low within that network. Every scope then points at that single address rather than at its own gateway.
$PXEServerNetwork = $NetworkPlan.NetworkList | Where-Object {($_.Role -ieq $PXEServerNetworkRole)} | Select-Object -First 1
Switch ($Null -ieq $PXEServerNetwork)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(2, @("A network with the role of `"$($PXEServerNetworkRole)`" was not found within the network plan. The parent network will host the preboot execution environment server instead."))
$PXEServerNetwork = $UntaggedNetwork
}
}
[System.String]$PXEServerAddress = "$(ConvertTo-DottedDecimalIP -IPAddress ((ConvertTo-DecimalIP -IPAddress ($PXEServerNetwork.NetworkAddress)) + $PXEServerHostOffset))"
$WriteLogMessage.Invoke(0, @("The preboot execution environment server will be reserved at `"$($PXEServerAddress)`" on the $($PXEServerNetwork.InterfaceName) network. [MAC Address: $($PXEServerMACAddress)]"))
#endregion
Switch ($True)
{
{([System.String]::IsNullOrEmpty($PXENextServerAddress) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($PXENextServerAddress) -eq $True)}
{
[System.String]$PXENextServerAddress = $PXEServerAddress
}
{($Null -ieq $DNSTCPPortList) -or ($DNSTCPPortList.Count -eq 0)}
{
# 53 is classic name resolution and 853 is DNS over TLS. Multicast name resolution is not carried over TCP.
[System.String[]]$DNSTCPPortList = @('53', '853')
}
{($Null -ieq $DNSUDPPortList) -or ($DNSUDPPortList.Count -eq 0)}
{
# 53 is classic name resolution, 853 is DNS over QUIC, and 5353 is multicast name resolution.
[System.String[]]$DNSUDPPortList = @('53', '853', '5353')
}
{($Null -ieq $BastionPortList) -or ($BastionPortList.Count -eq 0)}
{
[System.String[]]$BastionPortList = @('22', '80', '443', '3389', '5985', '5986')
}
{($Null -ieq $UpstreamDNSServerAddressList) -or ($UpstreamDNSServerAddressList.Count -eq 0)}
{
# Public resolvers, deliberately, rather than anything read from the machine that generated the document.
# A name server that the generating machine happens to use is a fact about that machine and not about the
# network the appliance ends up on, and seeding it here would quietly bake a site specific address into
# every deployment.
$DefaultUpstreamDNSServerAddressList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$DefaultUpstreamDNSServerAddressList.Add('1.1.1.1')
$DefaultUpstreamDNSServerAddressList.Add('1.0.0.1')
[System.String[]]$UpstreamDNSServerAddressList = $DefaultUpstreamDNSServerAddressList.ToArray()
}
{($Null -ieq $DNSServerAddressList) -or ($DNSServerAddressList.Count -eq 0)}
{
# Every network resolves through the address of the firewall on its own network, so every one of those
# addresses is an approved resolver.
$DefaultDNSServerAddressList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
ForEach ($PlannedNetwork In $NetworkPlan.NetworkList)
{
$DefaultDNSServerAddressList.Add($PlannedNetwork.GatewayAddress)
}
[System.String[]]$DNSServerAddressList = $DefaultDNSServerAddressList.ToArray()
}
{($Null -ieq $BastionServerAddressList) -or ($BastionServerAddressList.Count -eq 0)}
{
# Placeholder content so that the alias is valid on import. Replace it with the addresses of the
# administrative jump hosts once they exist.
$DefaultBastionNetwork = $UntaggedNetwork
Switch ($Null -ine $ManagementNetwork)
{
{($_ -eq $True)}
{
$DefaultBastionNetwork = $ManagementNetwork
}
}
[System.String[]]$BastionServerAddressList = @($DefaultBastionNetwork.GatewayAddress)
}
}
#endregion
#region Define the document manipulation scriptblocks
[ScriptBlock]$GetOrCreateElement = {
Param
(
[System.Xml.XmlNode]$ParentNode,
[System.String]$ElementName
)
$ElementNode = $ParentNode.SelectSingleNode("./$($ElementName)")
Switch ($Null -ieq $ElementNode)
{
{($_ -eq $True)}
{
$ElementNode = $ParentNode.OwnerDocument.CreateElement($ElementName)
$Null = $ParentNode.AppendChild($ElementNode)
}
}
Write-Output -InputObject ($ElementNode)
}
[ScriptBlock]$SetElementValue = {
Param
(
[System.Xml.XmlNode]$ParentNode,
[System.String]$ElementName,
[System.String]$ElementValue
)
$ElementNode = $GetOrCreateElement.InvokeReturnAsIs($ParentNode, $ElementName)
$ElementNode.InnerText = "$($ElementValue)"
Write-Output -InputObject ($ElementNode)
}
[ScriptBlock]$AddElement = {
Param
(
[System.Xml.XmlNode]$ParentNode,
[System.String]$ElementName,
[System.String]$ElementValue
)
$ElementNode = $ParentNode.OwnerDocument.CreateElement($ElementName)
Switch ([System.String]::IsNullOrEmpty($ElementValue))
{
{($_ -eq $False)}
{
$ElementNode.InnerText = "$($ElementValue)"
}
}
$Null = $ParentNode.AppendChild($ElementNode)
Write-Output -InputObject ($ElementNode)
}
[ScriptBlock]$RemoveChildElement = {
Param
(
[System.Xml.XmlNode]$ParentNode,
[System.String]$XPathExpression
)
$ChildNodeList = $ParentNode.SelectNodes($XPathExpression)
ForEach ($ChildNode In $ChildNodeList)
{
$Null = $ChildNode.ParentNode.RemoveChild($ChildNode)
}
}
[ScriptBlock]$AddUUIDAttribute = {
Param
(
[System.Xml.XmlNode]$ElementNode
)
[System.String]$UUIDValue = [System.Guid]::NewGuid().ToString().ToLower()
$Null = $ElementNode.SetAttribute('uuid', $UUIDValue)
Write-Output -InputObject ($UUIDValue)
}
#endregion
#region Create or load the configuration document
$XmlDocument = New-Object -TypeName 'System.Xml.XmlDocument'
$XmlDocument.PreserveWhitespace = $False
Switch (($Null -ine $BaseConfigurationPath) -and ([System.IO.File]::Exists($BaseConfigurationPath.FullName)))
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Attempting to load the base configuration document. Please Wait... [Path: $($BaseConfigurationPath.FullName)]"))
[System.String]$BaseConfigurationContent = [System.IO.File]::ReadAllText($BaseConfigurationPath.FullName)
$Null = $XmlDocument.LoadXml($BaseConfigurationContent)
}
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(0, @("Attempting to generate a new OPNsense configuration document. Please Wait..."))
$XmlDeclaration = $XmlDocument.CreateXmlDeclaration('1.0', $Null, $Null)
$Null = $XmlDocument.AppendChild($XmlDeclaration)
$Null = $XmlDocument.AppendChild($XmlDocument.CreateElement('opnsense'))
}
}
$RootNode = $XmlDocument.SelectSingleNode('/opnsense')
Switch ($Null -ieq $RootNode)
{
{($_ -eq $True)}
{
Throw "The configuration document does not contain an `"opnsense`" root element."
}
}
$Null = $RemoveChildElement.InvokeReturnAsIs($RootNode, './trigger_initial_wizard')
#endregion
#region Populate the system section
$WriteLogMessage.Invoke(0, @("Attempting to populate the system section of the configuration document. Please Wait..."))
$Null = $SetElementValue.InvokeReturnAsIs($RootNode, 'theme', 'opnsense')
$SystemNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'system')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'optimization', 'normal')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'hostname', $HostName)
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'domain', $DomainName)
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'timezone', $TimeZone)
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'timeservers', ($TimeServerList -Join ' '))
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'language', 'en_US')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'dnsallowoverride', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'dnslocalhost', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'prefer_ipv4', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'usevirtualterminal', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'disablebeep', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'serialspeed', '115200')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'primaryconsole', 'video')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'powerd_ac_mode', 'hadp')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'powerd_battery_mode', 'hadp')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'powerd_normal_mode', 'hadp')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'pf_share_forward', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'lb_use_sticky', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'enablenatreflectionhelper', 'yes')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'sudo_allow_wheel', '1')
Switch ($EnableIPv6.IsPresent)
{
{($_ -eq $True)}
{
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'ipv6allow', '1')
}
}
$BogonsNode = $GetOrCreateElement.InvokeReturnAsIs($SystemNode, 'bogons')
$Null = $SetElementValue.InvokeReturnAsIs($BogonsNode, 'interval', 'monthly')
#region Populate the name server address(es)
# No name server address is written here, and that is the point. The appliance resolves through the resolver that it runs itself, which
# is what allows a local record to be defined and to be answered authoritatively for the networks behind it, and the resolver forwards
# towards whatever the wide area network lease hands over. Writing an address here instead would pin the appliance to a name server that
# was true of the machine that generated the document rather than of the network the appliance ends up on.
#
# Two settings carry that. The private override is permitted, so the lease populates the system resolver list at boot, and the local
# host is preferred, so the appliance asks its own resolver first and only what that resolver forwards leaves the appliance.
$Null = $RemoveChildElement.InvokeReturnAsIs($SystemNode, './dnsserver')
$WriteLogMessage.Invoke(0, @("No static name server address was written into the system section. The appliance will resolve through its own resolver and take its upstream name server(s) from the wide area network lease."))
$WriteLogMessage.Invoke(0, @("Authorised Upstream Name Server List: $($UpstreamDNSServerAddressList -Join '; ')"))
#endregion
#region Populate the web interface and secure shell configuration
$WebGUINode = $GetOrCreateElement.InvokeReturnAsIs($SystemNode, 'webgui')
$Null = $SetElementValue.InvokeReturnAsIs($WebGUINode, 'protocol', 'https')
$SSHNode = $GetOrCreateElement.InvokeReturnAsIs($SystemNode, 'ssh')
$Null = $SetElementValue.InvokeReturnAsIs($SSHNode, 'group', 'admins')
Switch ($EnableSSH.IsPresent)
{
{($_ -eq $True)}
{
$Null = $SetElementValue.InvokeReturnAsIs($SSHNode, 'enabled', 'enabled')
$Null = $SetElementValue.InvokeReturnAsIs($SSHNode, 'permitrootlogin', '1')
$Null = $SetElementValue.InvokeReturnAsIs($SSHNode, 'passwordauth', '1')
}
}
#endregion
#region Populate the administrative group and account
$Null = $RemoveChildElement.InvokeReturnAsIs($SystemNode, './group')
$Null = $RemoveChildElement.InvokeReturnAsIs($SystemNode, './user')
$GroupNode = $AddElement.InvokeReturnAsIs($SystemNode, 'group', $Null)
$Null = $AddElement.InvokeReturnAsIs($GroupNode, 'name', 'admins')
$Null = $AddElement.InvokeReturnAsIs($GroupNode, 'description', 'System Administrators')
$Null = $AddElement.InvokeReturnAsIs($GroupNode, 'scope', 'system')
$Null = $AddElement.InvokeReturnAsIs($GroupNode, 'gid', '1999')
$Null = $AddElement.InvokeReturnAsIs($GroupNode, 'member', '0')
$Null = $AddElement.InvokeReturnAsIs($GroupNode, 'priv', 'page-all')
$UserNode = $AddElement.InvokeReturnAsIs($SystemNode, 'user', $Null)
$Null = $AddElement.InvokeReturnAsIs($UserNode, 'name', 'root')
$Null = $AddElement.InvokeReturnAsIs($UserNode, 'descr', 'System Administrator')
$Null = $AddElement.InvokeReturnAsIs($UserNode, 'scope', 'system')
$Null = $AddElement.InvokeReturnAsIs($UserNode, 'groupname', 'admins')
$Null = $AddElement.InvokeReturnAsIs($UserNode, 'uid', '0')
Switch (([System.String]::IsNullOrEmpty($RootPasswordHash) -eq $False) -and ([System.String]::IsNullOrWhiteSpace($RootPasswordHash) -eq $False))
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("A generated password hash will be assigned to the `"root`" account."))
$Null = $AddElement.InvokeReturnAsIs($UserNode, 'password', $RootPasswordHash)
}
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(2, @("A password hash was not supplied. The stock appliance credentials of `"root`" and `"opnsense`" will be retained."))
$Null = $AddElement.InvokeReturnAsIs($UserNode, 'password', '$2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS')
}
}
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'nextuid', '2000')
$Null = $SetElementValue.InvokeReturnAsIs($SystemNode, 'nextgid', '2000')
#endregion
#endregion
#region Populate the interface section
$WriteLogMessage.Invoke(0, @("Attempting to populate the interface section of the configuration document. Please Wait..."))
$Null = $RemoveChildElement.InvokeReturnAsIs($RootNode, './interfaces')
$InterfacesNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'interfaces')
#region Wide area network interface
# The private network block is disabled on this interface, which is what allows traffic to ingress from the host.
#
# The block discards any packet whose source address falls within the private ranges, and it is applied ahead of the rule set, so it
# takes effect before any rule is consulted. The host sits on the upstream network and therefore carries a private address itself, so
# with the block enabled every packet the host sent towards this interface would be discarded on arrival. No rule could permit it,
# because the packet would never reach the rules. Disabling the block is what lets those packets through to be evaluated, and the
# rule at sequence 40 is what then admits them.
#
# It is independently correct here regardless of that path, because this interface faces a lab network that is itself private, so the
# block would otherwise discard the upstream gateway as well.
#
# The bogon block stays enabled. The bogon list does not include the private ranges, so it costs nothing here and still discards the
# genuinely unroutable source addresses.
$WANNode = $AddElement.InvokeReturnAsIs($InterfacesNode, 'wan', $Null)
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'if', $WANInterfaceDevice)
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'descr', $WANInterfaceName)
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'enable', '1')
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'lock', '1')
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'spoofmac', $Null)
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'ipaddr', 'dhcp')
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'dhcphostname', $HostName)
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'blockpriv', '0')
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'blockbogons', '1')
Switch ($EnableIPv6.IsPresent)
{
{($_ -eq $True)}
{
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'ipaddrv6', 'dhcp6')
$Null = $AddElement.InvokeReturnAsIs($WANNode, 'dhcp6-ia-pd-len', '0')
}
}
#endregion
#region Internal interface(s)
For ($NetworkListIndex = 0; $NetworkListIndex -lt $NetworkPlan.NetworkList.Count; $NetworkListIndex++)
{
$PlannedNetwork = $NetworkPlan.NetworkList[$NetworkListIndex]
$InterfaceNode = $AddElement.InvokeReturnAsIs($InterfacesNode, $PlannedNetwork.InterfaceKey, $Null)
$Null = $AddElement.InvokeReturnAsIs($InterfaceNode, 'if', $PlannedNetwork.InterfaceDevice)
$Null = $AddElement.InvokeReturnAsIs($InterfaceNode, 'descr', $PlannedNetwork.InterfaceName)
$Null = $AddElement.InvokeReturnAsIs($InterfaceNode, 'enable', '1')
$Null = $AddElement.InvokeReturnAsIs($InterfaceNode, 'lock', '1')
$Null = $AddElement.InvokeReturnAsIs($InterfaceNode, 'spoofmac', $Null)
$Null = $AddElement.InvokeReturnAsIs($InterfaceNode, 'ipaddr', $PlannedNetwork.GatewayAddress)
$Null = $AddElement.InvokeReturnAsIs($InterfaceNode, 'subnet', $PlannedNetwork.MaskLength)
$WriteLogMessage.Invoke(0, @("Configured the `"$($PlannedNetwork.InterfaceKey)`" interface. [Device: $($PlannedNetwork.InterfaceDevice)] [Address: $($PlannedNetwork.GatewayAddress)/$($PlannedNetwork.MaskLength)] [Description: $($PlannedNetwork.InterfaceName)]"))
}
#endregion
#endregion
#region Populate the virtual local area network section
$WriteLogMessage.Invoke(0, @("Attempting to populate the virtual local area network section of the configuration document. Please Wait..."))
$Null = $RemoveChildElement.InvokeReturnAsIs($RootNode, './vlans')
$VlansNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'vlans')
$TaggedNetworkList = $NetworkPlan.NetworkList | Where-Object {($Null -ine $_.VlanId)}
ForEach ($TaggedNetwork In $TaggedNetworkList)
{
$VlanNode = $AddElement.InvokeReturnAsIs($VlansNode, 'vlan', $Null)
$Null = $AddUUIDAttribute.InvokeReturnAsIs($VlanNode)
$Null = $AddElement.InvokeReturnAsIs($VlanNode, 'if', $TaggedNetwork.VlanParentDevice)
$Null = $AddElement.InvokeReturnAsIs($VlanNode, 'tag', $TaggedNetwork.VlanId)
$Null = $AddElement.InvokeReturnAsIs($VlanNode, 'pcp', '0')
$Null = $AddElement.InvokeReturnAsIs($VlanNode, 'proto', $Null)
$Null = $AddElement.InvokeReturnAsIs($VlanNode, 'descr', $TaggedNetwork.VlanName)
$Null = $AddElement.InvokeReturnAsIs($VlanNode, 'vlanif', $TaggedNetwork.InterfaceDevice)
}
$WriteLogMessage.Invoke(0, @("$(($TaggedNetworkList | Measure-Object).Count) virtual local area network device(s) were configured. [Tags: $($NetworkPlan.VlanIdList -Join '; ')]"))
#endregion
#region Populate the name resolution, translation, and monitoring sections
$UnboundNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'unbound')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundNode, 'enable', '1')
#region Ensure that nothing else competes with Kea for the dynamic host configuration protocol sockets
# A stock installation enables dnsmasq and hands it the address pool. Two services cannot bind the same socket, and the
# loser of that race is the one that fails to start, so dnsmasq is explicitly reduced to resolution only and the legacy
# ISC section is explicitly emptied.
$Null = $RemoveChildElement.InvokeReturnAsIs($RootNode, './dhcpd')
$Null = $RemoveChildElement.InvokeReturnAsIs($RootNode, './dhcpdv6')
$DnsmasqNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'dnsmasq')
$Null = $RemoveChildElement.InvokeReturnAsIs($DnsmasqNode, './dhcp_ranges')
$Null = $RemoveChildElement.InvokeReturnAsIs($DnsmasqNode, './enable')
$DnsmasqDHCPNode = $GetOrCreateElement.InvokeReturnAsIs($DnsmasqNode, 'dhcp')
$Null = $SetElementValue.InvokeReturnAsIs($DnsmasqDHCPNode, 'no_interface', 'all')
#endregion
$NATNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'nat')
$OutboundNATNode = $GetOrCreateElement.InvokeReturnAsIs($NATNode, 'outbound')
# Hybrid keeps the automatic rules and allows manual rules to be added alongside them, which is the mode a lab usually ends up
# wanting. No manual rules are emitted here.
$Null = $SetElementValue.InvokeReturnAsIs($OutboundNATNode, 'mode', 'hybrid')
$Null = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'filter')
$RRDNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'rrd')
$Null = $GetOrCreateElement.InvokeReturnAsIs($RRDNode, 'enable')
$NTPDNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'ntpd')
$Null = $SetElementValue.InvokeReturnAsIs($NTPDNode, 'prefer', $TimeServerList[0])
#endregion
#region Populate the plugin section
$OPNsenseNode = $GetOrCreateElement.InvokeReturnAsIs($RootNode, 'OPNsense')
$FirewallNode = $GetOrCreateElement.InvokeReturnAsIs($OPNsenseNode, 'Firewall')
#region Firewall aliases
$WriteLogMessage.Invoke(0, @("Attempting to populate the firewall alias section of the configuration document. Please Wait..."))
$Null = $RemoveChildElement.InvokeReturnAsIs($FirewallNode, './Alias')
$AliasSectionNode = $GetOrCreateElement.InvokeReturnAsIs($FirewallNode, 'Alias')
$GeoIPNode = $GetOrCreateElement.InvokeReturnAsIs($AliasSectionNode, 'geoip')
$Null = $GetOrCreateElement.InvokeReturnAsIs($GeoIPNode, 'url')
$AliasesNode = $GetOrCreateElement.InvokeReturnAsIs($AliasSectionNode, 'aliases')
$AliasDefinitionList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'RFC1918'; Type = 'network'; Content = @('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'); Description = 'Every private address range that is defined by RFC 1918.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'CGNAT'; Type = 'network'; Content = @('100.64.0.0/10'); Description = 'The carrier grade network address translation range that is defined by RFC 6598.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'PARENT_NETWORK_001'; Type = 'network'; Content = @($NetworkPlan.ParentNetwork); Description = 'The parent network that every network behind this appliance was subnetted out of.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'AUTHORIZED_DNS_SERVERS_INTERNAL'; Type = 'host'; Content = $DNSServerAddressList; Description = 'The name servers that internal networks are permitted to resolve against. Add an internal resolver of your own to authorise it.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'AUTHORIZED_DNS_SERVERS_UPSTREAM'; Type = 'host'; Content = $UpstreamDNSServerAddressList; Description = 'The name servers that an authorised internal name server is permitted to forward towards.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'AUTHORIZED_DNS_PORTS_TCP'; Type = 'port'; Content = $DNSTCPPortList; Description = 'Every port that carries name resolution traffic over TCP.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'AUTHORIZED_DNS_PORTS_UDP'; Type = 'port'; Content = $DNSUDPPortList; Description = 'Every port that carries name resolution traffic over UDP.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'BASTION_SERVERS_001'; Type = 'host'; Content = $BastionServerAddressList; Description = 'The administrative jump hosts that may reach every network on the management ports.'}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'BASTION_PORTS_001'; Type = 'port'; Content = $BastionPortList; Description = 'The well known management ports that the jump hosts connect over.'}))
Switch ($SkipPXEConfiguration.IsPresent)
{
{($_ -eq $False)}
{
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = 'PXE_SERVERS_001'; Type = 'host'; Content = @($PXEServerAddress); Description = 'The preboot execution environment servers that every network boots from.'}))
}
}
# Every shaping alias is deliberately empty. An alias with no content produces an empty table, which matches nothing, so the
# shaping rules that reference them exist and are enabled but have no effect until an address is added to one.
ForEach ($TrafficShaperProfileObject In $TrafficShaperProfileObjectList)
{
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = $TrafficShaperProfileObject.InboundName; Type = 'network'; Content = @(); Description = "Addresses limited to $($TrafficShaperProfileObject.Bandwidth) $($TrafficShaperProfileObject.Metric) inbound. Add an address or a network to impose it."}))
$AliasDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Name = $TrafficShaperProfileObject.OutboundName; Type = 'network'; Content = @(); Description = "Addresses limited to $($TrafficShaperProfileObject.Bandwidth) $($TrafficShaperProfileObject.Metric) outbound. Add an address or a network to impose it."}))
}
For ($AliasDefinitionListIndex = 0; $AliasDefinitionListIndex -lt $AliasDefinitionList.Count; $AliasDefinitionListIndex++)
{
$AliasDefinition = $AliasDefinitionList[$AliasDefinitionListIndex]
$AliasNode = $AddElement.InvokeReturnAsIs($AliasesNode, 'alias', $Null)
$Null = $AddUUIDAttribute.InvokeReturnAsIs($AliasNode)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'enabled', '1')
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'name', $AliasDefinition.Name)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'type', $AliasDefinition.Type)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'path_expression', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'proto', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'interface', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'counters', '1')
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'updatefreq', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'content', ($AliasDefinition.Content -Join "`n"))
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'password', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'username', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'authtype', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'expire', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'categories', $Null)
$Null = $AddElement.InvokeReturnAsIs($AliasNode, 'description', $AliasDefinition.Description)
$WriteLogMessage.Invoke(0, @("Configured the firewall alias `"$($AliasDefinition.Name)`". [Type: $($AliasDefinition.Type)] [Entries: $(($AliasDefinition.Content | Measure-Object).Count)]"))
}
#endregion
#region Traffic shaper pipes
# The shaping is imposed by the packet filter rather than by a shaper rule, because a shaper rule matches on a plain address
# and cannot reference an alias, whereas a filter rule can carry both an alias and a pipe. That is what allows the operator to
# impose shaping later by editing one alias rather than by editing a rule.
Switch ($TrafficShaperProfileObjectList.Count -gt 0)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Attempting to populate the traffic shaper section of the configuration document. Please Wait..."))
$Null = $RemoveChildElement.InvokeReturnAsIs($OPNsenseNode, './TrafficShaper')
$TrafficShaperNode = $GetOrCreateElement.InvokeReturnAsIs($OPNsenseNode, 'TrafficShaper')
$TrafficShaperPipesNode = $GetOrCreateElement.InvokeReturnAsIs($TrafficShaperNode, 'pipes')
# No mask is applied, so each pipe is a single shared bucket. A mask does not subdivide a pipe, it instantiates
# a separate copy of it for every distinct value of the masked field, so masking by address would give every
# member of an alias its own full allowance and the aggregate would grow with the number of members. Five
# members of a 20 Mbit profile would carry 100 Mbit between them, which is not a 20 Mbit link.
#
# A link is a bottleneck that everyone behind it contends for, so the pipe is shared and the contention is the
# behaviour being reproduced.
[System.Int32]$ShaperPipeNumber = 1
ForEach ($TrafficShaperProfileObject In $TrafficShaperProfileObjectList)
{
$ShaperPipeDefinitionList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
$ShaperPipeDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Direction = 'Inbound'; Name = $TrafficShaperProfileObject.InboundName; Mask = 'none'}))
$ShaperPipeDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Direction = 'Outbound'; Name = $TrafficShaperProfileObject.OutboundName; Mask = 'none'}))
For ($ShaperPipeDefinitionListIndex = 0; $ShaperPipeDefinitionListIndex -lt $ShaperPipeDefinitionList.Count; $ShaperPipeDefinitionListIndex++)
{
$ShaperPipeDefinition = $ShaperPipeDefinitionList[$ShaperPipeDefinitionListIndex]
$ShaperPipeNode = $AddElement.InvokeReturnAsIs($TrafficShaperPipesNode, 'pipe', $Null)
[System.String]$ShaperPipeUUID = $AddUUIDAttribute.InvokeReturnAsIs($ShaperPipeNode)
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'number', $ShaperPipeNumber)
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'enabled', '1')
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'bandwidth', $TrafficShaperProfileObject.Bandwidth)
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'bandwidthMetric', $TrafficShaperProfileObject.Metric)
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'mask', $ShaperPipeDefinition.Mask)
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'codel_enable', '0')
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'codel_ecn_enable', '0')
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'pie_enable', '0')
$Null = $AddElement.InvokeReturnAsIs($ShaperPipeNode, 'description', $ShaperPipeDefinition.Name)
Switch ($ShaperPipeDefinition.Direction)
{
{($_ -ieq 'Inbound')}
{
$TrafficShaperProfileObject.InboundPipeUUID = $ShaperPipeUUID
}
{($_ -ieq 'Outbound')}
{
$TrafficShaperProfileObject.OutboundPipeUUID = $ShaperPipeUUID
}
}
[System.Int32]$ShaperPipeNumber = $ShaperPipeNumber + 1
}
$WriteLogMessage.Invoke(0, @("Configured the `"$($TrafficShaperProfileObject.Name)`" shaping profile. [Bandwidth: $($TrafficShaperProfileObject.Bandwidth) $($TrafficShaperProfileObject.Metric)] [Aliases: $($TrafficShaperProfileObject.InboundName); $($TrafficShaperProfileObject.OutboundName)]"))
}
# The shaper carries its own rule collection, which is left empty. Those rules match on a plain address and
# would therefore have to be edited directly rather than through an alias.
$Null = $GetOrCreateElement.InvokeReturnAsIs($TrafficShaperNode, 'queues')
$Null = $GetOrCreateElement.InvokeReturnAsIs($TrafficShaperNode, 'rules')
$WriteLogMessage.Invoke(0, @("$(($TrafficShaperPipesNode.SelectNodes('./pipe') | Measure-Object).Count) traffic shaper pipe(s) were configured."))
}
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(0, @("Skipping the traffic shaper configuration. [Reason: The `"SkipTrafficShaping`" parameter was specified.]"))
}
}
#endregion
#region Firewall rules
$WriteLogMessage.Invoke(0, @("Attempting to populate the firewall rule section of the configuration document. Please Wait..."))
$Null = $RemoveChildElement.InvokeReturnAsIs($FirewallNode, './Filter')
$FilterNode = $GetOrCreateElement.InvokeReturnAsIs($FirewallNode, 'Filter')
$RulesNode = $GetOrCreateElement.InvokeReturnAsIs($FilterNode, 'rules')
[System.String[]]$InternalInterfaceKeyList = $NetworkPlan.NetworkList | ForEach-Object {$_.InterfaceKey}
#region Assemble the rule definitions
# Every rule is a quick rule, so the lowest sequence value that matches wins. The ordering is therefore the policy.
#
# 10 The management network reaches everything.
# 20 The jump hosts reach everything over the well known management ports.
# 100+ Name resolution towards an approved name server is permitted.
# 200+ Name resolution towards anything else is blocked.
# 300+ A client zone reaches the server zone that carries the same index.
# 900+ Everything else that originates from an internal network is permitted, unless zone isolation is enabled.
$RuleDefinitionList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
[ScriptBlock]$AddRuleDefinition = {
Param
(
[System.Int32]$Sequence,
[System.String]$Action,
[System.String]$Interface,
[System.String]$Protocol,
[System.String]$SourceNetwork,
[System.String]$DestinationNetwork,
[System.String]$DestinationPort,
[System.String]$Description,
[System.String]$Direction,
[System.String]$ShaperTarget,
[System.String]$Enabled,
[System.String]$DisableReplyTo,
[System.String]$DestinationNot
)
Switch ([System.String]::IsNullOrEmpty($Direction))
{
{($_ -eq $True)}
{
[System.String]$Direction = 'in'
}
}
Switch ([System.String]::IsNullOrEmpty($Enabled))
{
{($_ -eq $True)}
{
[System.String]$Enabled = '1'
}
}
Switch ([System.String]::IsNullOrEmpty($DisableReplyTo))
{
{($_ -eq $True)}
{
[System.String]$DisableReplyTo = '0'
}
}
Switch ([System.String]::IsNullOrEmpty($DestinationNot))
{
{($_ -eq $True)}
{
[System.String]$DestinationNot = '0'
}
}
$RuleDefinitionProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RuleDefinitionProperties.Sequence = $Sequence
$RuleDefinitionProperties.Action = $Action
$RuleDefinitionProperties.Interface = $Interface
$RuleDefinitionProperties.Protocol = $Protocol
$RuleDefinitionProperties.SourceNetwork = $SourceNetwork
$RuleDefinitionProperties.DestinationNetwork = $DestinationNetwork
$RuleDefinitionProperties.DestinationPort = $DestinationPort
$RuleDefinitionProperties.Description = $Description
$RuleDefinitionProperties.Direction = $Direction
$RuleDefinitionProperties.ShaperTarget = $ShaperTarget
$RuleDefinitionProperties.Enabled = $Enabled
$RuleDefinitionProperties.DisableReplyTo = $DisableReplyTo
$RuleDefinitionProperties.DestinationNot = $DestinationNot
$RuleDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($RuleDefinitionProperties)))
}
#region The management network reaches everything
# Emitted as a floating rule. A floating rule carries an empty interface, which applies it to every interface at once
# rather than repeating an otherwise identical rule per interface.
Switch ($Null -ine $ManagementNetwork)
{
{($_ -eq $True)}
{
$Null = $AddRuleDefinition.InvokeReturnAsIs(10, 'pass', $Null, 'any', $ManagementNetwork.InterfaceKey, 'any', $Null, 'Allow the management network to reach every destination.')
}
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(2, @("A management network was not found within the network plan, so the management access rule will not be created."))
}
}
#endregion
#region The jump hosts reach everything over the well known management ports
$Null = $AddRuleDefinition.InvokeReturnAsIs(20, 'pass', $Null, 'TCP', 'BASTION_SERVERS_001', 'any', 'BASTION_PORTS_001', 'Allow the administrative jump hosts to reach every destination over the well known management ports.')
#endregion
#region Every network reaches the preboot execution environment server
Switch ($SkipPXEConfiguration.IsPresent)
{
{($_ -eq $False)}
{
$Null = $AddRuleDefinition.InvokeReturnAsIs(30, 'pass', $Null, 'any', 'RFC1918', 'PXE_SERVERS_001', $Null, 'Allow every internal network to reach the preboot execution environment server.')
}
}
#endregion
#region A private source reaches the networks behind the appliance over the wide area network interface
# Enabled, because a route on the host is not sufficient on its own. The route only decides where the host sends the packet.
# This rule is what admits it once it arrives, so without the rule the traffic reaches the wide area network interface and is
# then dropped, and the path does not work at all. The private network block being disabled on the same interface is the other
# half of that, since the block is applied ahead of the rule set and would discard the traffic before this rule was reached.
#
# The path this opens is a private source on the upstream network reaching the networks behind the appliance. That is what
# allows the host to manage the lab and to reach the machines within it without holding a virtual network adapter inside it,
# which is the arrangement a private local area network switch produces.
#
# Bound to the interface rather than floating. A floating rule would apply this to every interface, which would widen it far
# beyond the one path it is meant to open.
#
# The reply-to behaviour is disabled for this rule specifically. OPNsense adds reply-to automatically to any rule that names an
# interface and a direction, which forces the reply traffic of a matched connection to the gateway of that interface rather
# than letting it follow the routing table. The source here is deliberately a host on the same segment as the wide area network
# interface, so that would send every reply out to the upstream router only for the router to turn it around and deliver it back
# onto the segment it came from. Disabling it lets the reply go straight back to the host, which is both correct and direct.
#
# Note that the automatic behaviour is not suppressed by writing a zero into the element, because the value is tested in PHP
# with empty(), and a string holding zero satisfies that test. The element has to carry a one.
# The destination is the parent network rather than every private range. The path this opens is inbound from the upstream
# network, so it is scoped to the addresses this appliance is actually responsible for. Naming every private range as the
# destination would additionally permit anything this appliance happens to route towards, which is wider than the intent
# and widens further the moment another route is added.
#
# Both ends are aliases, so an operator reading the rule set sees what the rule means rather than a pair of literal ranges,
# and the parent network alias tracks whatever parent network the plan resolved.
$Null = $AddRuleDefinition.InvokeReturnAsIs(40, 'pass', 'wan', 'any', 'RFC1918', 'PARENT_NETWORK_001', $Null, 'Allow a private source on the upstream network to reach the networks behind this appliance.', 'in', $Null, '1', '1')
#endregion
#region Name resolution
# Four rules per transport, and the order of them is the policy. The transports are handled separately because each one carries
# a different set of ports, and every rule is floating, so this is eight rules rather than eight per interface.
#
# 1. An authorised name server may forward towards an authorised upstream name server. This is what lets a resolver answer
# anything it does not already hold. While the internal alias holds only this appliance the rule changes nothing on its own,
# because traffic that the appliance originates is not evaluated by an inbound rule, but it is what makes the alias useful:
# add a real resolver of your own to the internal alias and this rule already permits it to forward.
#
# 2. Any internal network may query an authorised name server.
#
# 3. Any internal network may query this appliance directly. The internal alias holds the address of this appliance on every
# planned network, so this is ordinarily covered by the rule above, but the interface address is not the only address an
# appliance can answer on and this makes the intent explicit rather than incidental.
#
# 4. Everything else is blocked, expressed as a destination that is inverted rather than as a separate deny of all name
# resolution. Inverting the destination states the policy directly, which is that name resolution towards anything other
# than an authorised name server is refused, and it keeps that true as the alias is edited rather than depending upon this
# rule sitting at a later sequence than every permit.
$ResolverProtocolList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
$ResolverProtocolList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Protocol = 'TCP'; PortAlias = 'AUTHORIZED_DNS_PORTS_TCP'}))
$ResolverProtocolList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{Protocol = 'UDP'; PortAlias = 'AUTHORIZED_DNS_PORTS_UDP'}))
[System.Int32]$UpstreamResolverSequence = 100
[System.Int32]$AuthorizedResolverSequence = 110
[System.Int32]$ApplianceResolverSequence = 120
[System.Int32]$DeniedResolverSequence = 200
ForEach ($ResolverProtocol In $ResolverProtocolList)
{
$Null = $AddRuleDefinition.InvokeReturnAsIs($UpstreamResolverSequence, 'pass', $Null, $ResolverProtocol.Protocol, 'AUTHORIZED_DNS_SERVERS_INTERNAL', 'AUTHORIZED_DNS_SERVERS_UPSTREAM', $ResolverProtocol.PortAlias, "Allow an authorised name server to forward name resolution over $($ResolverProtocol.Protocol) towards an authorised upstream name server.")
$Null = $AddRuleDefinition.InvokeReturnAsIs($AuthorizedResolverSequence, 'pass', $Null, $ResolverProtocol.Protocol, 'RFC1918', 'AUTHORIZED_DNS_SERVERS_INTERNAL', $ResolverProtocol.PortAlias, "Allow name resolution over $($ResolverProtocol.Protocol) towards an authorised name server.")
$Null = $AddRuleDefinition.InvokeReturnAsIs($ApplianceResolverSequence, 'pass', $Null, $ResolverProtocol.Protocol, 'RFC1918', '(self)', $ResolverProtocol.PortAlias, "Allow name resolution over $($ResolverProtocol.Protocol) towards the resolver that this appliance runs.")
$Null = $AddRuleDefinition.InvokeReturnAsIs($DeniedResolverSequence, 'block', $Null, $ResolverProtocol.Protocol, 'RFC1918', 'AUTHORIZED_DNS_SERVERS_INTERNAL', $ResolverProtocol.PortAlias, "Block name resolution over $($ResolverProtocol.Protocol) towards any name server that is not authorised.", $Null, $Null, $Null, $Null, '1')
[System.Int32]$UpstreamResolverSequence = $UpstreamResolverSequence + 1
[System.Int32]$AuthorizedResolverSequence = $AuthorizedResolverSequence + 1
[System.Int32]$ApplianceResolverSequence = $ApplianceResolverSequence + 1
[System.Int32]$DeniedResolverSequence = $DeniedResolverSequence + 1
}
#endregion
#region A client zone reaches the server zone that carries the same index
# These stay bound to their interface, because each one names a different source and destination network.
[System.Int32]$ZonePairSequence = 300
$ClientZoneList = $NetworkPlan.NetworkList | Where-Object {($_.ZoneType -ieq 'Clients')} | Sort-Object -Property @('ZoneIndex')
ForEach ($ClientZone In $ClientZoneList)
{
$PairedServerZone = $NetworkPlan.NetworkList | Where-Object {($_.ZoneType -ieq 'Servers') -and ($_.ZoneIndex -eq $ClientZone.ZoneIndex)} | Select-Object -First 1
Switch ($Null -ine $PairedServerZone)
{
{($_ -eq $True)}
{
$Null = $AddRuleDefinition.InvokeReturnAsIs($ZonePairSequence, 'pass', $ClientZone.InterfaceKey, 'any', $ClientZone.InterfaceKey, $PairedServerZone.InterfaceKey, $Null, 'Allow this client zone to reach the server zone that carries the same index.')
[System.Int32]$ZonePairSequence = $ZonePairSequence + 1
}
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(2, @("A server zone carrying the index $($ClientZone.ZoneIndex) was not found, so the $($ClientZone.InterfaceName) network will not receive a zone pairing rule."))
}
}
}
#endregion
#region Traffic shaping
# Two rules, because a pipe carries traffic in one direction only. Shaping a host in both directions therefore needs a
# rule that matches it as a source and a second that matches it as a destination.
#
# Outbound is what the host sends, so it is matched entering the firewall from the network the host sits on.
# Inbound is what the host receives, so it is matched leaving the firewall towards that network.
#
# Both are floating and both reference an alias that starts empty, so they are live but inert until an address is added.
# One pair of rules per profile. An address belongs in at most one inbound alias and one outbound alias, because the
# rules are quick and the profiles are emitted slowest first, so the slowest profile an address appears in is the one
# that takes effect.
[System.Int32]$TrafficShapingSequence = 500
ForEach ($TrafficShaperProfileObject In $TrafficShaperProfileObjectList)
{
$Null = $AddRuleDefinition.InvokeReturnAsIs($TrafficShapingSequence, 'pass', $Null, 'any', $TrafficShaperProfileObject.OutboundName, 'any', $Null, "Shape the outbound traffic of every address within $($TrafficShaperProfileObject.OutboundName) to $($TrafficShaperProfileObject.Bandwidth) $($TrafficShaperProfileObject.Metric).", 'in', $TrafficShaperProfileObject.OutboundPipeUUID)
[System.Int32]$TrafficShapingSequence = $TrafficShapingSequence + 1
$Null = $AddRuleDefinition.InvokeReturnAsIs($TrafficShapingSequence, 'pass', $Null, 'any', 'any', $TrafficShaperProfileObject.InboundName, $Null, "Shape the inbound traffic of every address within $($TrafficShaperProfileObject.InboundName) to $($TrafficShaperProfileObject.Bandwidth) $($TrafficShaperProfileObject.Metric).", 'out', $TrafficShaperProfileObject.InboundPipeUUID)
[System.Int32]$TrafficShapingSequence = $TrafficShapingSequence + 1
}
#endregion
#region Everything else that originates from an internal network
Switch ($EnableZoneIsolation.IsPresent)
{
{($_ -eq $False)}
{
[System.Int32]$DefaultAccessSequence = 900
For ($NetworkListIndex = 0; $NetworkListIndex -lt $NetworkPlan.NetworkList.Count; $NetworkListIndex++)
{
$PlannedNetwork = $NetworkPlan.NetworkList[$NetworkListIndex]
$Null = $AddRuleDefinition.InvokeReturnAsIs($DefaultAccessSequence, 'pass', $PlannedNetwork.InterfaceKey, 'any', $PlannedNetwork.InterfaceKey, 'any', $Null, 'Allow all traffic that originates from this network.')
[System.Int32]$DefaultAccessSequence = $DefaultAccessSequence + 1
}
}
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Skipping the default per interface access rules. [Reason: The `"EnableZoneIsolation`" parameter was specified, so only the management, jump host, name resolution, and zone pairing rules will apply.]"))
}
}
#endregion
#endregion
#region Emit the rule definitions
$InternetProtocolVersionList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
$InternetProtocolVersionList.Add('inet')
Switch ($EnableIPv6.IsPresent)
{
{($_ -eq $True)}
{
$InternetProtocolVersionList.Add('inet6')
}
}
For ($RuleDefinitionListIndex = 0; $RuleDefinitionListIndex -lt $RuleDefinitionList.Count; $RuleDefinitionListIndex++)
{
$RuleDefinition = $RuleDefinitionList[$RuleDefinitionListIndex]
ForEach ($InternetProtocolVersion In $InternetProtocolVersionList)
{
$RuleNode = $AddElement.InvokeReturnAsIs($RulesNode, 'rule', $Null)
$Null = $AddUUIDAttribute.InvokeReturnAsIs($RuleNode)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'enabled', $RuleDefinition.Enabled)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'statetype', 'keep')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'state-policy', $Null)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'sequence', $RuleDefinition.Sequence)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'action', $RuleDefinition.Action)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'quick', '1')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'interfacenot', '0')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'interface', $RuleDefinition.Interface)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'direction', $RuleDefinition.Direction)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'ipprotocol', $InternetProtocolVersion)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'protocol', $RuleDefinition.Protocol)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'source_net', $RuleDefinition.SourceNetwork)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'source_not', '0')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'source_port', $Null)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'destination_net', $RuleDefinition.DestinationNetwork)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'destination_not', $RuleDefinition.DestinationNot)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'destination_port', $RuleDefinition.DestinationPort)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'disablereplyto', $RuleDefinition.DisableReplyTo)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'log', '0')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'allowopts', '0')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'nosync', '0')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'nopfsync', '0')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'statetimeout', $Null)
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'tcpflags_any', '0')
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'categories', $Null)
Switch ([System.String]::IsNullOrEmpty($RuleDefinition.ShaperTarget) -eq $False)
{
{($_ -eq $True)}
{
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'shaper1', $RuleDefinition.ShaperTarget)
}
}
$Null = $AddElement.InvokeReturnAsIs($RuleNode, 'description', $RuleDefinition.Description)
}
}
#endregion
$Null = $GetOrCreateElement.InvokeReturnAsIs($FilterNode, 'snatrules')
$Null = $GetOrCreateElement.InvokeReturnAsIs($FilterNode, 'npt')
$Null = $GetOrCreateElement.InvokeReturnAsIs($FilterNode, 'onetoone')
$WriteLogMessage.Invoke(0, @("$(($RulesNode.SelectNodes('./rule') | Measure-Object).Count) firewall rule(s) were configured."))
#endregion
#region Unbound resolver
$Null = $RemoveChildElement.InvokeReturnAsIs($OPNsenseNode, './unboundplus')
$UnboundPlusNode = $GetOrCreateElement.InvokeReturnAsIs($OPNsenseNode, 'unboundplus')
$UnboundPlusGeneralNode = $GetOrCreateElement.InvokeReturnAsIs($UnboundPlusNode, 'general')
[System.String[]]$InternalInterfaceKeyList = $NetworkPlan.NetworkList | ForEach-Object {$_.InterfaceKey}
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'enabled', '1')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'port', '53')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'stats', '1')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'active_interface', ($InternalInterfaceKeyList -Join ','))
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'dnssec', '1')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'regdhcp', '1')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'regdhcpstatic', '1')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'regdhcpdomain', $DomainName)
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusGeneralNode, 'local_zone_type', 'transparent')
#region Forward towards the upstream name server(s) rather than recursing
# Forwarding is what makes the wide area network lease meaningful. Left to recurse, the resolver would query the root servers
# directly and the upstream name server that the lease supplies would never be consulted, so the appliance would resolve correctly
# but would ignore the upstream that the network it joined is expecting it to use. Forwarding sends everything the resolver cannot
# answer itself to the system name server list, which the lease populates.
#
# A local record is still answered locally, because the local zone is transparent and the resolver answers what it knows before it
# forwards anything.
$UnboundPlusForwardingNode = $GetOrCreateElement.InvokeReturnAsIs($UnboundPlusNode, 'forwarding')
$Null = $SetElementValue.InvokeReturnAsIs($UnboundPlusForwardingNode, 'enabled', '1')
#endregion
#endregion
#region Kea dynamic host configuration protocol
$WriteLogMessage.Invoke(0, @("Attempting to populate the Kea dynamic host configuration protocol section of the configuration document. Please Wait..."))
$Null = $RemoveChildElement.InvokeReturnAsIs($OPNsenseNode, './Kea')
$KeaNode = $GetOrCreateElement.InvokeReturnAsIs($OPNsenseNode, 'Kea')
$KeaDHCP4Node = $GetOrCreateElement.InvokeReturnAsIs($KeaNode, 'dhcp4')
$KeaGeneralNode = $GetOrCreateElement.InvokeReturnAsIs($KeaDHCP4Node, 'general')
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'enabled', '1')
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'manual_config', '0')
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'interfaces', ($InternalInterfaceKeyList -Join ','))
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'valid_lifetime', $DHCPValidLifetime)
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'fwrules', '1')
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'dhcp_socket_type', $DHCPSocketType)
# A virtual local area network interface is frequently not ready at the moment that the service starts, and the service
# otherwise sits there attempting to bind. These two settings bound that wait rather than leaving it open ended.
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'service_sockets_max_retries', $DHCPSocketBindRetryCount)
$Null = $SetElementValue.InvokeReturnAsIs($KeaGeneralNode, 'service_sockets_retry_wait_time', $DHCPSocketBindRetryWaitMilliseconds)
$KeaHANode = $GetOrCreateElement.InvokeReturnAsIs($KeaDHCP4Node, 'ha')
$Null = $SetElementValue.InvokeReturnAsIs($KeaHANode, 'enabled', '0')
$KeaOptionsNode = $GetOrCreateElement.InvokeReturnAsIs($KeaDHCP4Node, 'options')
#region Preboot execution environment option(s)
$PXEOptionUUIDList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
Switch ($SkipPXEConfiguration.IsPresent)
{
{($_ -eq $False)}
{
$PXEOptionDefinitionList = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.PSObject]'
$PXEOptionDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{MatchData = '0'; BootFileName = $PXEBootFileNameBIOS; Description = 'PXE boot file for a legacy BIOS client (client-system 0)'}))
$PXEOptionDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{MatchData = '7'; BootFileName = $PXEBootFileNameUEFI; Description = 'PXE boot file for a UEFI x64 client (client-system 7)'}))
$PXEOptionDefinitionList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property @{MatchData = '9'; BootFileName = $PXEBootFileNameUEFI; Description = 'PXE boot file for a UEFI x64 client (client-system 9)'}))
For ($PXEOptionDefinitionListIndex = 0; $PXEOptionDefinitionListIndex -lt $PXEOptionDefinitionList.Count; $PXEOptionDefinitionListIndex++)
{
$PXEOptionDefinition = $PXEOptionDefinitionList[$PXEOptionDefinitionListIndex]
$KeaOptionNode = $AddElement.InvokeReturnAsIs($KeaOptionsNode, 'option', $Null)
$PXEOptionUUIDList.Add($AddUUIDAttribute.InvokeReturnAsIs($KeaOptionNode))
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'code', '67')
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'encoding', 'string')
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'data', $PXEOptionDefinition.BootFileName)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'force', '1')
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'match_code', '93')
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'match_encoding', 'uint16')
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'match_data', $PXEOptionDefinition.MatchData)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionNode, 'description', $PXEOptionDefinition.Description)
}
$WriteLogMessage.Invoke(0, @("$($PXEOptionUUIDList.Count) preboot execution environment option(s) were configured. [Next Server: $($PXENextServerAddress)] [BIOS Boot File: $($PXEBootFileNameBIOS)] [UEFI Boot File: $($PXEBootFileNameUEFI)]"))
}
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(0, @("Skipping the preboot execution environment configuration. [Reason: The `"SkipPXEConfiguration`" parameter was specified.]"))
}
}
#endregion
#region Scope(s)
$KeaSubnetsNode = $GetOrCreateElement.InvokeReturnAsIs($KeaDHCP4Node, 'subnets')
[System.String]$PXEServerSubnetUUID = $Null
For ($NetworkListIndex = 0; $NetworkListIndex -lt $NetworkPlan.NetworkList.Count; $NetworkListIndex++)
{
$PlannedNetwork = $NetworkPlan.NetworkList[$NetworkListIndex]
$KeaSubnetNode = $AddElement.InvokeReturnAsIs($KeaSubnetsNode, 'subnet4', $Null)
[System.String]$KeaSubnetUUID = $AddUUIDAttribute.InvokeReturnAsIs($KeaSubnetNode)
Switch ($PlannedNetwork.Network -ieq $PXEServerNetwork.Network)
{
{($_ -eq $True)}
{
[System.String]$PXEServerSubnetUUID = $KeaSubnetUUID
}
}
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'subnet', $PlannedNetwork.Network)
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'valid_lifetime', $DHCPValidLifetime)
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'option_data_autocollect', '1')
$KeaOptionDataNode = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'option_data', $Null)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionDataNode, 'domain_name_servers', $PlannedNetwork.GatewayAddress)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionDataNode, 'domain_search', $DomainName)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionDataNode, 'routers', $PlannedNetwork.GatewayAddress)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionDataNode, 'domain_name', $DomainName)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionDataNode, 'ntp_servers', $PlannedNetwork.GatewayAddress)
Switch ($SkipPXEConfiguration.IsPresent)
{
{($_ -eq $False)}
{
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'next_server', $PXENextServerAddress)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionDataNode, 'tftp_server_name', $PXENextServerAddress)
$Null = $AddElement.InvokeReturnAsIs($KeaOptionDataNode, 'boot_file_name', $PXEBootFileNameUEFI)
}
}
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'option', ($PXEOptionUUIDList -Join ','))
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'match-client-id', '1')
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'pools', ($PlannedNetwork.DHCPPoolRangeList -Join "`n"))
$Null = $AddElement.InvokeReturnAsIs($KeaSubnetNode, 'description', $PlannedNetwork.InterfaceName)
$WriteLogMessage.Invoke(0, @("Configured the dynamic host configuration protocol scope for `"$($PlannedNetwork.Network)`". [Pool: $($PlannedNetwork.DHCPPoolRangeList -Join '; ')] [Pool Size: $($PlannedNetwork.DHCPPoolAddressCount)] [Router: $($PlannedNetwork.GatewayAddress)]"))
}
#region Reserve the address of the preboot execution environment server
$KeaReservationsNode = $GetOrCreateElement.InvokeReturnAsIs($KeaDHCP4Node, 'reservations')
Switch (($SkipPXEConfiguration.IsPresent -eq $False) -and ([System.String]::IsNullOrEmpty($PXEServerMACAddress) -eq $False) -and ([System.String]::IsNullOrEmpty($PXEServerSubnetUUID) -eq $False))
{
{($_ -eq $True)}
{
$KeaReservationNode = $AddElement.InvokeReturnAsIs($KeaReservationsNode, 'reservation', $Null)
$Null = $AddUUIDAttribute.InvokeReturnAsIs($KeaReservationNode)
$Null = $AddElement.InvokeReturnAsIs($KeaReservationNode, 'subnet', $PXEServerSubnetUUID)
$Null = $AddElement.InvokeReturnAsIs($KeaReservationNode, 'ip_address', $PXEServerAddress)
$Null = $AddElement.InvokeReturnAsIs($KeaReservationNode, 'hw_address', $PXEServerMACAddress)
$Null = $AddElement.InvokeReturnAsIs($KeaReservationNode, 'hostname', $PXEServerHostName)
$Null = $AddElement.InvokeReturnAsIs($KeaReservationNode, 'description', 'Preboot execution environment server')
$WriteLogMessage.Invoke(0, @("Reserved `"$($PXEServerAddress)`" for the preboot execution environment server. [MAC Address: $($PXEServerMACAddress)] [Host Name: $($PXEServerHostName)]"))
}
{($_ -eq $False)}
{
$WriteLogMessage.Invoke(0, @("Skipping the preboot execution environment server reservation. [Reason: A media access control address was not supplied, or the preboot execution environment configuration was skipped.]"))
}
}
#endregion
$Null = $GetOrCreateElement.InvokeReturnAsIs($KeaDHCP4Node, 'ha_peers')
#endregion
$KeaControlAgentNode = $GetOrCreateElement.InvokeReturnAsIs($KeaNode, 'ctrl_agent')
$KeaControlAgentGeneralNode = $GetOrCreateElement.InvokeReturnAsIs($KeaControlAgentNode, 'general')
$Null = $SetElementValue.InvokeReturnAsIs($KeaControlAgentGeneralNode, 'enabled', '0')
$Null = $SetElementValue.InvokeReturnAsIs($KeaControlAgentGeneralNode, 'http_host', '127.0.0.1')
$Null = $SetElementValue.InvokeReturnAsIs($KeaControlAgentGeneralNode, 'http_port', '8000')
#endregion
#endregion
$WriteLogMessage.Invoke(0, @("The OPNsense configuration document was generated successfully. [Element Count: $(($XmlDocument.SelectNodes('//*') | Measure-Object).Count)]"))
Write-Output -InputObject ($XmlDocument)
}
Catch
{
$ErrorRecord = $_
Switch ($ContinueOnError.IsPresent)
{
{($_ -eq $True)}
{
$WriteLogMessage.Invoke(2, @("[Message: $($ErrorRecord.Exception.Message)] [LineNumber: $($ErrorRecord.InvocationInfo.ScriptLineNumber)] [Code: $($ErrorRecord.InvocationInfo.Line.Trim())]"))
}
{($_ -eq $False)}
{
Throw
}
}
}
Finally
{
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is completed."))
}
}
#endregion