Zone Based IPV4 Firewall Generator via Python

I’ve been using zoned based firewalls with increasing granularity. This has caused a spike in the number of zones and “copied” rules.

I read that an undocumented firewall is not a firewall, so I’ve been documenting by using a shorthand then using that to translate in my head what to write.

Being human I made a bunch of mistakes translating my own shorthand, so for my first python script I made a routine to parse my shorthand and write the firewall commands for me.

I’d like to post the script here for anyone who is interested, it has saved me a bunch of time and allows me to make significant changes to a large firewall fairly quickly.

It’s based on everything being in a firewall group, so you’ll need to create your port/ip/network groups in addition to this, and also to define your zones.

The following basic shorthand represents rules from the WAN0 zone to other zones (WAN0 to CA_LAN will be the best example of a zone pair):

WAN0:
to LOCAL: STOCK, IPSEC-IKE-ESP, [DT-REMOTE-NET](CA_LAN-NET|MATCH_IPSEC)  
to MGMT_LAN: STOCK
to CA_LAN: STOCK, <HTTPS-TCP>(ZPUSH-IP), <HTTPS-TCP>(ZIMBRA-PROXY-IP), [SPAM-FILTER-IP]<SMTP-TCP>(ZIMBRA-SERVER-IP), <SMTP-SUBMISSION-TCP>(ZIMBRA-SERVER-IP), <IMAPS-TCP>(ZIMBRA-PROXY-IP), <IT-OVPN-UDP>(IT-OVPN-IP), <UNIV-OVPN-UDP>(UNIV-OVPN-IP),  [DT-REMOTE-NET](CA_LAN-NET|MATCH_IPSEC)
to CA_LAN2: STOCK
to CA_WRKSHP: STOCK
to TSLA_LAN: STOCK
to BMW_ISPI: STOCK
to VOIP: STOCK
to VLVO_CAR_WIFI: STOCK
to VLVO_CUST_WIFI: STOCK
to MEDIA_LAN: STOCK
to SU_LAN: STOCK

The resulting commands after running it through the python script is:

echo =====WAN0 to LOCAL=====
echo delete firewall name WAN0.LOCAL
set firewall name WAN0.LOCAL description "WAN0 to LOCAL: STOCK, IPSEC-IKE-ESP, [DT-REMOTE-NET](CA_LAN-NET|MATCH_IPSEC)"
set firewall name WAN0.LOCAL rule 100 description "Allow established/related"
set firewall name WAN0.LOCAL rule 100 action accept
set firewall name WAN0.LOCAL rule 100 state established enable
set firewall name WAN0.LOCAL rule 100 state related enable
set firewall name WAN0.LOCAL rule 100 log disable
set firewall name WAN0.LOCAL rule 105 description "Drop invalid"
set firewall name WAN0.LOCAL rule 105 action drop
set firewall name WAN0.LOCAL rule 105 state invalid enable
set firewall name WAN0.LOCAL rule 105 log disable
set firewall name WAN0.LOCAL rule 110 description "IPSEC IKE/ESP/NAT-T Group; Accept IKE"
set firewall name WAN0.LOCAL rule 110 action accept
set firewall name WAN0.LOCAL rule 110 destination port 500
set firewall name WAN0.LOCAL rule 110 protocol udp
set firewall name WAN0.LOCAL rule 110 log enable
set firewall name WAN0.LOCAL rule 115 description "Accept ESP"
set firewall name WAN0.LOCAL rule 115 action accept
set firewall name WAN0.LOCAL rule 115 protocol esp
set firewall name WAN0.LOCAL rule 115 log enable
set firewall name WAN0.LOCAL rule 120 description "Accept NAT-T"
set firewall name WAN0.LOCAL rule 120 action accept
set firewall name WAN0.LOCAL rule 120 destination port 4500
set firewall name WAN0.LOCAL rule 120 protocol udp
set firewall name WAN0.LOCAL rule 120 log enable
set firewall name WAN0.LOCAL rule 125 description "from DT-REMOTE-NET to CA_LAN-NET"
set firewall name WAN0.LOCAL rule 125 action accept
set firewall name WAN0.LOCAL rule 125 source group network-group DT-REMOTE-NET
set firewall name WAN0.LOCAL rule 125 destination group network-group CA_LAN-NET
set firewall name WAN0.LOCAL rule 125 ipsec match-ipsec
set firewall name WAN0.LOCAL rule 125 state new enable
set firewall name WAN0.LOCAL rule 125 log enable
set firewall name WAN0.LOCAL enable-default-log
set zone-policy zone LOCAL from WAN0 firewall name WAN0.LOCAL
echo =====WAN0.LOCAL Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to MGMT_LAN=====
echo delete firewall name WAN0.MGMT_LAN
set firewall name WAN0.MGMT_LAN description "WAN0 to MGMT_LAN: STOCK"
set firewall name WAN0.MGMT_LAN rule 100 description "Allow established/related"
set firewall name WAN0.MGMT_LAN rule 100 action accept
set firewall name WAN0.MGMT_LAN rule 100 state established enable
set firewall name WAN0.MGMT_LAN rule 100 state related enable
set firewall name WAN0.MGMT_LAN rule 100 log disable
set firewall name WAN0.MGMT_LAN rule 105 description "Drop invalid"
set firewall name WAN0.MGMT_LAN rule 105 action drop
set firewall name WAN0.MGMT_LAN rule 105 state invalid enable
set firewall name WAN0.MGMT_LAN rule 105 log disable
set firewall name WAN0.MGMT_LAN enable-default-log
set zone-policy zone MGMT_LAN from WAN0 firewall name WAN0.MGMT_LAN
echo =====WAN0.MGMT_LAN Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to CA_LAN=====
echo delete firewall name WAN0.CA_LAN
set firewall name WAN0.CA_LAN description "WAN0 to CA_LAN: STOCK, <HTTPS-TCP>(ZPUSH-IP), <HTTPS-TCP>(ZIMBRA-PROXY-IP), [SPAM-FILTER-IP]<SMTP-TCP>(ZIMBRA-SERVER-IP), <SMTP-SUBMISSION-TCP>(ZIMBRA-SERVER-IP), <IMAPS-TCP>(ZIMBRA-PROXY-IP), <IT-OVPN-UDP>(IT-OVPN-IP), <UNIV-OVPN-UDP>(UNIV-OVPN-IP),  [DT-REMOTE-NET](CA_LAN-NET|MATCH_IPSEC)"
set firewall name WAN0.CA_LAN rule 100 description "Allow established/related"
set firewall name WAN0.CA_LAN rule 100 action accept
set firewall name WAN0.CA_LAN rule 100 state established enable
set firewall name WAN0.CA_LAN rule 100 state related enable
set firewall name WAN0.CA_LAN rule 100 log disable
set firewall name WAN0.CA_LAN rule 105 description "Drop invalid"
set firewall name WAN0.CA_LAN rule 105 action drop
set firewall name WAN0.CA_LAN rule 105 state invalid enable
set firewall name WAN0.CA_LAN rule 105 log disable
set firewall name WAN0.CA_LAN rule 110 description "to HTTPS-TCP to ZPUSH-IP"
set firewall name WAN0.CA_LAN rule 110 action accept
set firewall name WAN0.CA_LAN rule 110 destination group port-group HTTPS-TCP
set firewall name WAN0.CA_LAN rule 110 protocol tcp
set firewall name WAN0.CA_LAN rule 110 destination group address-group ZPUSH-IP
set firewall name WAN0.CA_LAN rule 110 state new enable
set firewall name WAN0.CA_LAN rule 110 log enable
set firewall name WAN0.CA_LAN rule 115 description "to HTTPS-TCP to ZIMBRA-PROXY-IP"
set firewall name WAN0.CA_LAN rule 115 action accept
set firewall name WAN0.CA_LAN rule 115 destination group port-group HTTPS-TCP
set firewall name WAN0.CA_LAN rule 115 protocol tcp
set firewall name WAN0.CA_LAN rule 115 destination group address-group ZIMBRA-PROXY-IP
set firewall name WAN0.CA_LAN rule 115 state new enable
set firewall name WAN0.CA_LAN rule 115 log enable
set firewall name WAN0.CA_LAN rule 120 description "from SPAM-FILTER-IP to SMTP-TCP to ZIMBRA-SERVER-IP"
set firewall name WAN0.CA_LAN rule 120 action accept
set firewall name WAN0.CA_LAN rule 120 source group address-group SPAM-FILTER-IP
set firewall name WAN0.CA_LAN rule 120 destination group port-group SMTP-TCP
set firewall name WAN0.CA_LAN rule 120 protocol tcp
set firewall name WAN0.CA_LAN rule 120 destination group address-group ZIMBRA-SERVER-IP
set firewall name WAN0.CA_LAN rule 120 state new enable
set firewall name WAN0.CA_LAN rule 120 log enable
set firewall name WAN0.CA_LAN rule 125 description "to SMTP-SUBMISSION-TCP to ZIMBRA-SERVER-IP"
set firewall name WAN0.CA_LAN rule 125 action accept
set firewall name WAN0.CA_LAN rule 125 destination group port-group SMTP-SUBMISSION-TCP
set firewall name WAN0.CA_LAN rule 125 protocol tcp
set firewall name WAN0.CA_LAN rule 125 destination group address-group ZIMBRA-SERVER-IP
set firewall name WAN0.CA_LAN rule 125 state new enable
set firewall name WAN0.CA_LAN rule 125 log enable
set firewall name WAN0.CA_LAN rule 130 description "to IMAPS-TCP to ZIMBRA-PROXY-IP"
set firewall name WAN0.CA_LAN rule 130 action accept
set firewall name WAN0.CA_LAN rule 130 destination group port-group IMAPS-TCP
set firewall name WAN0.CA_LAN rule 130 protocol tcp
set firewall name WAN0.CA_LAN rule 130 destination group address-group ZIMBRA-PROXY-IP
set firewall name WAN0.CA_LAN rule 130 state new enable
set firewall name WAN0.CA_LAN rule 130 log enable
set firewall name WAN0.CA_LAN rule 135 description "to IT-OVPN-UDP to IT-OVPN-IP"
set firewall name WAN0.CA_LAN rule 135 action accept
set firewall name WAN0.CA_LAN rule 135 destination group port-group IT-OVPN-UDP
set firewall name WAN0.CA_LAN rule 135 protocol udp
set firewall name WAN0.CA_LAN rule 135 destination group address-group IT-OVPN-IP
set firewall name WAN0.CA_LAN rule 135 state new enable
set firewall name WAN0.CA_LAN rule 135 log enable
set firewall name WAN0.CA_LAN rule 140 description "to UNIV-OVPN-UDP to UNIV-OVPN-IP"
set firewall name WAN0.CA_LAN rule 140 action accept
set firewall name WAN0.CA_LAN rule 140 destination group port-group UNIV-OVPN-UDP
set firewall name WAN0.CA_LAN rule 140 protocol udp
set firewall name WAN0.CA_LAN rule 140 destination group address-group UNIV-OVPN-IP
set firewall name WAN0.CA_LAN rule 140 state new enable
set firewall name WAN0.CA_LAN rule 140 log enable
set firewall name WAN0.CA_LAN rule 145 description "from DT-REMOTE-NET to CA_LAN-NET"
set firewall name WAN0.CA_LAN rule 145 action accept
set firewall name WAN0.CA_LAN rule 145 source group network-group DT-REMOTE-NET
set firewall name WAN0.CA_LAN rule 145 destination group network-group CA_LAN-NET
set firewall name WAN0.CA_LAN rule 145 ipsec match-ipsec
set firewall name WAN0.CA_LAN rule 145 state new enable
set firewall name WAN0.CA_LAN rule 145 log enable
set firewall name WAN0.CA_LAN enable-default-log
set zone-policy zone CA_LAN from WAN0 firewall name WAN0.CA_LAN
echo =====WAN0.CA_LAN Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to CA_LAN2=====
echo delete firewall name WAN0.CA_LAN2
set firewall name WAN0.CA_LAN2 description "WAN0 to CA_LAN2: STOCK"
set firewall name WAN0.CA_LAN2 rule 100 description "Allow established/related"
set firewall name WAN0.CA_LAN2 rule 100 action accept
set firewall name WAN0.CA_LAN2 rule 100 state established enable
set firewall name WAN0.CA_LAN2 rule 100 state related enable
set firewall name WAN0.CA_LAN2 rule 100 log disable
set firewall name WAN0.CA_LAN2 rule 105 description "Drop invalid"
set firewall name WAN0.CA_LAN2 rule 105 action drop
set firewall name WAN0.CA_LAN2 rule 105 state invalid enable
set firewall name WAN0.CA_LAN2 rule 105 log disable
set firewall name WAN0.CA_LAN2 enable-default-log
set zone-policy zone CA_LAN2 from WAN0 firewall name WAN0.CA_LAN2
echo =====WAN0.CA_LAN2 Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to CA_WRKSHP=====
echo delete firewall name WAN0.CA_WRKSHP
set firewall name WAN0.CA_WRKSHP description "WAN0 to CA_WRKSHP: STOCK"
set firewall name WAN0.CA_WRKSHP rule 100 description "Allow established/related"
set firewall name WAN0.CA_WRKSHP rule 100 action accept
set firewall name WAN0.CA_WRKSHP rule 100 state established enable
set firewall name WAN0.CA_WRKSHP rule 100 state related enable
set firewall name WAN0.CA_WRKSHP rule 100 log disable
set firewall name WAN0.CA_WRKSHP rule 105 description "Drop invalid"
set firewall name WAN0.CA_WRKSHP rule 105 action drop
set firewall name WAN0.CA_WRKSHP rule 105 state invalid enable
set firewall name WAN0.CA_WRKSHP rule 105 log disable
set firewall name WAN0.CA_WRKSHP enable-default-log
set zone-policy zone CA_WRKSHP from WAN0 firewall name WAN0.CA_WRKSHP
echo =====WAN0.CA_WRKSHP Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to TSLA_LAN=====
echo delete firewall name WAN0.TSLA_LAN
set firewall name WAN0.TSLA_LAN description "WAN0 to TSLA_LAN: STOCK"
set firewall name WAN0.TSLA_LAN rule 100 description "Allow established/related"
set firewall name WAN0.TSLA_LAN rule 100 action accept
set firewall name WAN0.TSLA_LAN rule 100 state established enable
set firewall name WAN0.TSLA_LAN rule 100 state related enable
set firewall name WAN0.TSLA_LAN rule 100 log disable
set firewall name WAN0.TSLA_LAN rule 105 description "Drop invalid"
set firewall name WAN0.TSLA_LAN rule 105 action drop
set firewall name WAN0.TSLA_LAN rule 105 state invalid enable
set firewall name WAN0.TSLA_LAN rule 105 log disable
set firewall name WAN0.TSLA_LAN enable-default-log
set zone-policy zone TSLA_LAN from WAN0 firewall name WAN0.TSLA_LAN
echo =====WAN0.TSLA_LAN Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to BMW_ISPI=====
echo delete firewall name WAN0.BMW_ISPI
set firewall name WAN0.BMW_ISPI description "WAN0 to BMW_ISPI: STOCK"
set firewall name WAN0.BMW_ISPI rule 100 description "Allow established/related"
set firewall name WAN0.BMW_ISPI rule 100 action accept
set firewall name WAN0.BMW_ISPI rule 100 state established enable
set firewall name WAN0.BMW_ISPI rule 100 state related enable
set firewall name WAN0.BMW_ISPI rule 100 log disable
set firewall name WAN0.BMW_ISPI rule 105 description "Drop invalid"
set firewall name WAN0.BMW_ISPI rule 105 action drop
set firewall name WAN0.BMW_ISPI rule 105 state invalid enable
set firewall name WAN0.BMW_ISPI rule 105 log disable
set firewall name WAN0.BMW_ISPI enable-default-log
set zone-policy zone BMW_ISPI from WAN0 firewall name WAN0.BMW_ISPI
echo =====WAN0.BMW_ISPI Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to VOIP=====
echo delete firewall name WAN0.VOIP
set firewall name WAN0.VOIP description "WAN0 to VOIP: STOCK"
set firewall name WAN0.VOIP rule 100 description "Allow established/related"
set firewall name WAN0.VOIP rule 100 action accept
set firewall name WAN0.VOIP rule 100 state established enable
set firewall name WAN0.VOIP rule 100 state related enable
set firewall name WAN0.VOIP rule 100 log disable
set firewall name WAN0.VOIP rule 105 description "Drop invalid"
set firewall name WAN0.VOIP rule 105 action drop
set firewall name WAN0.VOIP rule 105 state invalid enable
set firewall name WAN0.VOIP rule 105 log disable
set firewall name WAN0.VOIP enable-default-log
set zone-policy zone VOIP from WAN0 firewall name WAN0.VOIP
echo =====WAN0.VOIP Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to VLVO_CAR_WIFI=====
echo delete firewall name WAN0.VLVO_CAR_WIFI
set firewall name WAN0.VLVO_CAR_WIFI description "WAN0 to VLVO_CAR_WIFI: STOCK"
set firewall name WAN0.VLVO_CAR_WIFI rule 100 description "Allow established/related"
set firewall name WAN0.VLVO_CAR_WIFI rule 100 action accept
set firewall name WAN0.VLVO_CAR_WIFI rule 100 state established enable
set firewall name WAN0.VLVO_CAR_WIFI rule 100 state related enable
set firewall name WAN0.VLVO_CAR_WIFI rule 100 log disable
set firewall name WAN0.VLVO_CAR_WIFI rule 105 description "Drop invalid"
set firewall name WAN0.VLVO_CAR_WIFI rule 105 action drop
set firewall name WAN0.VLVO_CAR_WIFI rule 105 state invalid enable
set firewall name WAN0.VLVO_CAR_WIFI rule 105 log disable
set firewall name WAN0.VLVO_CAR_WIFI enable-default-log
set zone-policy zone VLVO_CAR_WIFI from WAN0 firewall name WAN0.VLVO_CAR_WIFI
echo =====WAN0.VLVO_CAR_WIFI Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to VLVO_CUST_WIFI=====
echo delete firewall name WAN0.VLVO_CUST_WIFI
set firewall name WAN0.VLVO_CUST_WIFI description "WAN0 to VLVO_CUST_WIFI: STOCK"
set firewall name WAN0.VLVO_CUST_WIFI rule 100 description "Allow established/related"
set firewall name WAN0.VLVO_CUST_WIFI rule 100 action accept
set firewall name WAN0.VLVO_CUST_WIFI rule 100 state established enable
set firewall name WAN0.VLVO_CUST_WIFI rule 100 state related enable
set firewall name WAN0.VLVO_CUST_WIFI rule 100 log disable
set firewall name WAN0.VLVO_CUST_WIFI rule 105 description "Drop invalid"
set firewall name WAN0.VLVO_CUST_WIFI rule 105 action drop
set firewall name WAN0.VLVO_CUST_WIFI rule 105 state invalid enable
set firewall name WAN0.VLVO_CUST_WIFI rule 105 log disable
set firewall name WAN0.VLVO_CUST_WIFI enable-default-log
set zone-policy zone VLVO_CUST_WIFI from WAN0 firewall name WAN0.VLVO_CUST_WIFI
echo =====WAN0.VLVO_CUST_WIFI Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to MEDIA_LAN=====
echo delete firewall name WAN0.MEDIA_LAN
set firewall name WAN0.MEDIA_LAN description "WAN0 to MEDIA_LAN: STOCK"
set firewall name WAN0.MEDIA_LAN rule 100 description "Allow established/related"
set firewall name WAN0.MEDIA_LAN rule 100 action accept
set firewall name WAN0.MEDIA_LAN rule 100 state established enable
set firewall name WAN0.MEDIA_LAN rule 100 state related enable
set firewall name WAN0.MEDIA_LAN rule 100 log disable
set firewall name WAN0.MEDIA_LAN rule 105 description "Drop invalid"
set firewall name WAN0.MEDIA_LAN rule 105 action drop
set firewall name WAN0.MEDIA_LAN rule 105 state invalid enable
set firewall name WAN0.MEDIA_LAN rule 105 log disable
set firewall name WAN0.MEDIA_LAN enable-default-log
set zone-policy zone MEDIA_LAN from WAN0 firewall name WAN0.MEDIA_LAN
echo =====WAN0.MEDIA_LAN Rules Generated 2022-07-20 19:13:10.297058=====


echo =====WAN0 to SU_LAN=====
echo delete firewall name WAN0.SU_LAN
set firewall name WAN0.SU_LAN description "WAN0 to SU_LAN: STOCK"
set firewall name WAN0.SU_LAN rule 100 description "Allow established/related"
set firewall name WAN0.SU_LAN rule 100 action accept
set firewall name WAN0.SU_LAN rule 100 state established enable
set firewall name WAN0.SU_LAN rule 100 state related enable
set firewall name WAN0.SU_LAN rule 100 log disable
set firewall name WAN0.SU_LAN rule 105 description "Drop invalid"
set firewall name WAN0.SU_LAN rule 105 action drop
set firewall name WAN0.SU_LAN rule 105 state invalid enable
set firewall name WAN0.SU_LAN rule 105 log disable
set firewall name WAN0.SU_LAN enable-default-log
set zone-policy zone SU_LAN from WAN0 firewall name WAN0.SU_LAN
echo =====WAN0.SU_LAN Rules Generated 2022-07-20 19:13:10.297058=====

Here you can see that the description contains the original shorthand so you can verify it quickly. The echo line with the title is to give it structure for easier browsing while allowing you to copy and paste without worrying about errors from the title. The “echo delete” is in case you want to start over for that set of rules, you can just remove the echo from “echo delete” everwhere to rebuild the whole firewall or just on one group if you’re only updating a single zone pair. The date generated is included so you can see when a group was last updated since you’ll likely update single zone pairs more often that the whole firewall. Maybe I should be put the generated date in the description too???

Here is a rough overview of the rules involved in writing the shorthand:

==Shorthand Syntax Rules==
Brackets define whether it's a source port/ip or destination port/ip. Options and preconfigured rules are allowed.
Defaults for <destination-port>,(destination-ip),{source-port},[source-ip] rules unless otherwise specified:
action = accept
state = new enable (if you specify any state then you must specify the new state as well since it will be removed with the addition of other states, per rule that is)
log = enable

Zone names must be 14 characters or less, be all caps and use only the characters A-Z, 0-9, -,_

Rule names must be 31 characters or less, be all caps and use only the characters A-Z, 0-9, -, _, !

Rules for port groups must end in: -TCP, -UDP, -TCP_UDP
Rules for ip address groups must end in -IP
Rules for network addresss groups must end in -NET

SYNTAX:
SRC_ZONE:
to DST_ZONE_A: [SOURCE-IP]{SOURCE-PORT-TCP}<DESTINATION-TCP>(DESTINATION-IP), [SOURCE-IP|OPTION_A;OPTION_B;OPTION_C]
to DST_ZONE_B: [!SOURCE-IP]{SOURCE-PORT-TCP}<!DESTINATION-TCP>(DESTINATION-IP), [SOURCE-IP|OPTION_A;OPTION_B;OPTION_C]

Preconfigured Rules (preconfigured rules don't go inside brackets):
STOCK = allow establised/related, drop invalid, no log
PING = allow ICMP echo, no log
OSPF = allow OSPF, no log

! means NOT

Options:
NOLOG = log disable
LOG = log enable
DROP = action drop
REJECT = action reject
ACCEPT = action accept
ST_ESTB_ENBL = state established enable
ST_ESTB_DSBL = state established disable
ST_RLTD_ENBL = state related enable
ST_RLTD_DSBL = state related disable
ST_INVLD_ENBL = state invalid enable
ST_INVLD_DSBL = state invalid disable
ST_NEW_ENBL = state new disable
ST_NEW_DSBL = state new disable
MATCH-IPSEC = ipsec match-ipsec

I’ve setup extensive error checking in the script to validate the shorthand so that human error will be noticed right away. Things like if you put 2 separate destination port rules in the same rule #, or if you have conflicting options. Most of the error checking is documented in the script. Also, as it’s my first python script it includes all the stack exchange/etc references I used to cobble it together. And it is cobbled together, not as good a peach cobbler but it’s fit for consumption.

Before I posted it I wanted to make sure this is acceptable for this forum. And if allowed should I post as code or attach it as txt file? It’s just under 900 lines long.

4 Likes