Class: Mt::Wall::Compiler
- Inherits:
-
Object
- Object
- Mt::Wall::Compiler
- Defined in:
- lib/mt/wall/compiler.rb
Overview
Compiles the abstract Configuration into a transport-agnostic DesiredState of concrete RouterOS resources for one device.
This is where the domain -> RouterOS mapping happens:
* objects -> address-list entries, partitioned BY FAMILY
into /ip/firewall/address-list (v4) and
/ipv6/firewall/address-list (v6)
* groups -> FLATTENED into address-list entries
(RouterOS has no nested lists)
* allow/deny grants -> filter rules with src-address-list /
dst-address-list, emitted PER FAMILY into the
v4 and/or v6 filter tables
* device filter_rules -> /ip + /ipv6 filter rules (both families
unless scoped by `family:`)
* device nat_rules -> /ip/firewall/nat rules (IPv4-only, v1).
PURELY Layer-B (per-box): NAT is never the
target of Layer-A grant injection.
* global + per-device -> trailing default rules per chain
policies
Group flattening, address de-duplication, family partitioning and deterministic rule ordering all live here, so the resulting DesiredState is stable and diffable.
── OWNERSHIP & THE SAFE CHAIN ─────────────────────────────────────────mt-wall owns the ENTIRE filter/nat tables (filter v4 + v6, nat v4): apply replaces them wholesale, including RouterOS default-config rules. (The address-list tables are NOT owned wholesale — only the list NAMES the compiler emits; see DesiredState.) The Compiler is therefore RESPONSIBLE for emitting a complete, safe chain — and the preamble is FAMILY-SPECIFIC, NOT one generic “both families” block.
Per chain the emitted order is: [family-specific essentials] -> [stateful preamble] -> [mgmt-protection, INPUT chain only] -> [operator/grant rules] -> [trailing DEFAULT-POLICY rule, LAST].
See the project CLAUDE.md and Model docs for the full contract; the implementation below follows it verbatim.
Constant Summary collapse
- MANAGED_COMMENT_PREFIX =
Prefix of the machine identity tag embedded in every managed rule’s ‘comment`. Plan.diff keys on `“#MANAGED_COMMENT_PREFIX<hash>”`.
"mt-wall:"- COMMENT_SEPARATOR =
Separator between the machine identity tag and the operator note inside a rendered RouterOS ‘comment`.
" | "- ANY_REFERENCE =
Reserved match-all source/destination. When a grant’s source/destination or a chain rule’s ‘src:`/`dst:` is this name, the compiled filter rule OMITS the corresponding address-list field (RouterOS treats an absent list as “match any”) and the reference imposes NO family constraint —it contributes “all families” to the auto-scope intersection. Reserved at the Configuration boundary (no host/group may be named “any”). Distinct from the SERVICE slot `:any`, which means “any protocol/port”.
"any"- IPV4_ADDRESS_LIST =
"/ip/firewall/address-list"- IPV6_ADDRESS_LIST =
"/ipv6/firewall/address-list"- IPV4_FILTER =
"/ip/firewall/filter"- IPV6_FILTER =
"/ipv6/firewall/filter"- IPV4_NAT =
"/ip/firewall/nat"- MGMT_SERVICES =
Built-in management service set used both for the inferred backstop and as a fallback when an explicit ‘management service:` is not a declared Service. Maps name => [protocol, [ports]].
{ "winbox" => [:tcp, [8291]], "ssh" => [:tcp, [22]], "api" => [:tcp, [8728]], "rest" => [:tcp, [80, 443]] }.freeze
- FILTER_CHAINS =
Chains assembled into the filter tables, in deterministic emit order.
%i[input forward output].freeze
- SIMPLE_MATCH_KEYS =
Abstract match keys that translate straight to a row field (no host/group resolution). ‘:src`/`:dst` are handled separately (address-list refs).
{ state: :connection_state, protocol: :protocol, dst_port: :dst_port, src_port: :src_port, in_interface: :in_interface, out_interface: :out_interface, in_interface_list: :in_interface_list, out_interface_list: :out_interface_list }.freeze
- NON_IDENTITY_FIELDS =
Row fields that are rule ATTRIBUTES, not match content: excluded from the content-only identity tag so toggling them (e.g. log/disabled) yields a stable ‘(tag, ordinal)` and an in-place :update, never delete+create.
%i[comment log log_prefix disabled].freeze
Class Method Summary collapse
-
.identity_tag(resource) ⇒ String
Deterministic, CONTENT-ONLY identity tag for a normalized resource row: hashes chain + normalized match + action + src/dst list references, and excludes position/order, the ‘comment` (which carries the tag) AND the rule-level attributes log/log_prefix/disabled (NON_IDENTITY_FIELDS) — so toggling those is an in-place :update, not a delete+create churn.
-
.tag_in_comment(comment) ⇒ String?
Extract the ‘mt-wall:<hash>` identity tag from a rendered comment, or nil.
Instance Method Summary collapse
- #compile(device:) ⇒ DesiredState
-
#initialize(configuration) ⇒ Compiler
constructor
A new instance of Compiler.
-
#managed_list_names ⇒ Array<String>
The set of address-list NAMES this configuration emits (from hosts and flattened groups).
Constructor Details
#initialize(configuration) ⇒ Compiler
Returns a new instance of Compiler.
96 97 98 |
# File 'lib/mt/wall/compiler.rb', line 96 def initialize(configuration) @configuration = configuration end |
Class Method Details
.identity_tag(resource) ⇒ String
Deterministic, CONTENT-ONLY identity tag for a normalized resource row: hashes chain + normalized match + action + src/dst list references, and excludes position/order, the ‘comment` (which carries the tag) AND the rule-level attributes log/log_prefix/disabled (NON_IDENTITY_FIELDS) — so toggling those is an in-place :update, not a delete+create churn. Stable across runs for the same content.
127 128 129 130 131 132 133 |
# File 'lib/mt/wall/compiler.rb', line 127 def self.identity_tag(resource) material = resource.except(*NON_IDENTITY_FIELDS) canonical = material.sort_by { |key, _| key.to_s } .map { |key, value| "#{key}=#{value}" } .join("") "#{MANAGED_COMMENT_PREFIX}#{Digest::SHA1.hexdigest(canonical)[0, 12]}" end |
.tag_in_comment(comment) ⇒ String?
Extract the ‘mt-wall:<hash>` identity tag from a rendered comment, or nil.
138 139 140 141 142 143 |
# File 'lib/mt/wall/compiler.rb', line 138 def self.tag_in_comment(comment) return nil if comment.nil? token = comment.to_s.split(COMMENT_SEPARATOR, 2).first token if token&.start_with?(MANAGED_COMMENT_PREFIX) end |
Instance Method Details
#compile(device:) ⇒ DesiredState
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/mt/wall/compiler.rb', line 102 def compile(device:) @device = device @flatten_cache = {} @reference_families = {} assert_no_list_name_clash! assert_management_present! rows = {} emit_address_lists(rows) emit_filters(rows) emit_nat(rows) build_state(rows) end |
#managed_list_names ⇒ Array<String>
The set of address-list NAMES this configuration emits (from hosts and flattened groups). Plan.diff scopes address-list ownership to these names so foreign/static lists are never diffed or deleted.
150 151 152 |
# File 'lib/mt/wall/compiler.rb', line 150 def managed_list_names (@configuration.objects.keys + @configuration.groups.keys).uniq.sort end |