Module: Mt::Wall::DSL::Validators
- Defined in:
- lib/mt/wall/dsl/validators.rb
Overview
Shared fail-fast validation/normalization helpers for the DSL capture layer. Every value that reaches a Model::* object passes through here so that typos surface loudly AND so that hostile values cannot inject into a later rendered ‘.rsc` / JSON payload (names are charset-restricted, addresses must parse via stdlib IPAddr, ports are bounded, protocols are allowlisted). All failures raise Mt::Wall::ConfigurationError.
Constant Summary collapse
- NAME_RE =
Strict identifier charset shared by host/group/service/interface names.
/\A[\w.-]+\z/- LOG_PREFIX_RE =
Charset allowed in a firewall ‘log-prefix` label. Restricted (no shell/ rsc/JSON metacharacters) so the value cannot inject into a rendered `.rsc` script or REST payload; spaces and a few separators are allowed because operators routinely use them in prefixes.
%r{\A[\w .:/-]{1,64}\z}- PROTOCOLS =
Protocols accepted in ‘protocol:` match conditions / services.
%i[ tcp udp icmp icmpv6 igmp gre esp ah sctp ospf vrrp pim ipsec-esp ipsec-ah l2tp ipencap ddp udplite ].freeze
- STATES =
Connection-tracking states accepted in ‘state:`.
%i[established related invalid new untracked].freeze
- CHAINS =
Firewall chains (policy + Layer-B chain blocks).
%i[input output forward].freeze
- POLICY_ACTIONS =
Chain default policy actions.
%i[accept drop].freeze
- FAMILIES =
Address families.
%i[ip4 ip6].freeze
Class Method Summary collapse
-
.collect_ports(value, out) ⇒ Object
Recursively validate (and collect into
out) every port in a port specification, expanding Ranges and “a-b”/“n” Strings. -
.collect_string_ports(value, out) ⇒ Object
rubocop:enable Metrics/MethodLength.
-
.infer_family(address) ⇒ Symbol
Infer the address family of an address/CIDR/range, validating it parses.
-
.looks_like_address?(token) ⇒ Boolean
True if the token parses as an IP address / CIDR.
-
.normalize_match(match, allow_state: true) ⇒ Hash
Validate, and optionally normalize, a match-condition Hash for a Layer-B filter/nat rule.
-
.normalize_match_value(key, value, allow_state:) ⇒ Object
rubocop:disable Metrics/MethodLength.
-
.normalize_service_ports(ports) ⇒ Array<Integer>
Validate and expand a service port specification into a flat, sorted, de-duplicated Array<Integer> (Model::Service stores discrete ports).
-
.normalize_states(state) ⇒ Array<Symbol>
Normalized, validated connection states.
-
.parse_address(address) ⇒ IPAddr
Parse an address/CIDR via IPAddr, normalizing failures to ConfigurationError.
-
.port_in_range!(port) ⇒ Integer
The validated port.
-
.range?(token) ⇒ Boolean
True if the token is an IP RANGE (‘low-high`).
-
.range_endpoints(range) ⇒ Array(IPAddr, IPAddr)
Parse the two endpoints of a ‘low-high` range into IPAddr objects.
-
.rule_flags(log: false, log_prefix: nil, disabled: false) ⇒ Hash
Validate+normalize the shared rule-level attribute keywords (‘log:`/`log_prefix:`/`disabled:`) into a kwargs Hash ready to splat into a Model::* (FilterRule / Rule / Policy).
-
.validate_address!(address) ⇒ String
Validate an IPv4/IPv6 address, CIDR subnet, OR IP range (‘10.0.0.1-10.0.0.10`, both endpoints the same family).
-
.validate_chain!(chain) ⇒ Symbol
The validated chain symbol.
-
.validate_family!(family) ⇒ Symbol
The validated family symbol.
-
.validate_flag!(value, label: "flag") ⇒ Boolean
Validate a boolean rule flag (‘log:` / `disabled:`).
-
.validate_group_token!(token) ⇒ String
Validate a group token used as host-side membership.
-
.validate_ipv4!(address) ⇒ String
Validate an IPv4-only address/CIDR (NAT targets, v1).
-
.validate_log_prefix!(prefix) ⇒ String
Validate a firewall ‘log-prefix` label against LOG_PREFIX_RE.
-
.validate_name!(name, label: "name") ⇒ String
Validate a strict identifier (host/group/service/interface name).
-
.validate_policy_action!(action) ⇒ Symbol
The validated policy action symbol (:accept/:drop).
-
.validate_ports!(ports) ⇒ Object
Validate a port specification (Integer, Array, Range or “a-b”/“n” String, nested arrays allowed) without altering it.
-
.validate_protocol!(protocol) ⇒ Symbol
The validated protocol symbol.
-
.validate_range!(range) ⇒ String
Validate an IP range: both endpoints parse and share one family.
Class Method Details
.collect_ports(value, out) ⇒ Object
Recursively validate (and collect into out) every port in a port specification, expanding Ranges and “a-b”/“n” Strings. rubocop:disable Metrics/MethodLength
277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/mt/wall/dsl/validators.rb', line 277 def collect_ports(value, out) case value when Integer out << port_in_range!(value) when Range value.each { |port| out << port_in_range!(port) } when String collect_string_ports(value, out) when Array value.each { |element| collect_ports(element, out) } else raise ConfigurationError, "invalid port specification #{value.inspect}" end end |
.collect_string_ports(value, out) ⇒ Object
rubocop:enable Metrics/MethodLength
293 294 295 296 297 298 299 300 301 302 |
# File 'lib/mt/wall/dsl/validators.rb', line 293 def collect_string_ports(value, out) case value when /\A(\d+)-(\d+)\z/ (Regexp.last_match(1).to_i..Regexp.last_match(2).to_i).each { |port| out << port_in_range!(port) } when /\A\d+\z/ out << port_in_range!(value.to_i) else raise ConfigurationError, "invalid port specification #{value.inspect}" end end |
.infer_family(address) ⇒ Symbol
Infer the address family of an address/CIDR/range, validating it parses. For a range, the family is taken from its (validated, same-family) endpoints.
79 80 81 82 83 84 |
# File 'lib/mt/wall/dsl/validators.rb', line 79 def infer_family(address) str = address.to_s return range_endpoints(str).first.ipv4? ? :ip4 : :ip6 if range?(str) parse_address(str).ipv4? ? :ip4 : :ip6 end |
.looks_like_address?(token) ⇒ Boolean
Returns true if the token parses as an IP address / CIDR.
68 69 70 71 72 73 |
# File 'lib/mt/wall/dsl/validators.rb', line 68 def looks_like_address?(token) IPAddr.new(token.to_s) true rescue StandardError false end |
.normalize_match(match, allow_state: true) ⇒ Hash
Validate, and optionally normalize, a match-condition Hash for a Layer-B filter/nat rule. Unknown keys fail fast.
248 249 250 251 252 |
# File 'lib/mt/wall/dsl/validators.rb', line 248 def normalize_match(match, allow_state: true) match.each_with_object({}) do |(key, value), normalized| normalized[key] = normalize_match_value(key, value, allow_state: allow_state) end end |
.normalize_match_value(key, value, allow_state:) ⇒ Object
rubocop:disable Metrics/MethodLength
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/mt/wall/dsl/validators.rb', line 255 def normalize_match_value(key, value, allow_state:) case key when :state raise ConfigurationError, "`state:` is not valid for a NAT rule" unless allow_state normalize_states(value) when :protocol validate_protocol!(value) when :dst_port, :src_port validate_ports!(value) value when :in_interface, :out_interface, :in_interface_list, :out_interface_list, :src, :dst validate_name!(value, label: key.to_s) else raise ConfigurationError, "unknown match condition #{key.inspect}" end end |
.normalize_service_ports(ports) ⇒ Array<Integer>
Validate and expand a service port specification into a flat, sorted, de-duplicated Array<Integer> (Model::Service stores discrete ports).
230 231 232 233 234 |
# File 'lib/mt/wall/dsl/validators.rb', line 230 def normalize_service_ports(ports) out = [] collect_ports(ports, out) out.uniq.sort end |
.normalize_states(state) ⇒ Array<Symbol>
Returns normalized, validated connection states.
210 211 212 213 214 215 216 217 |
# File 'lib/mt/wall/dsl/validators.rb', line 210 def normalize_states(state) Array(state).map do |s| sym = s.to_s.to_sym raise ConfigurationError, "invalid connection state #{s.inspect}" unless STATES.include?(sym) sym end end |
.parse_address(address) ⇒ IPAddr
Parse an address/CIDR via IPAddr, normalizing failures to ConfigurationError. IPAddr::Error descends from ArgumentError.
133 134 135 136 137 |
# File 'lib/mt/wall/dsl/validators.rb', line 133 def parse_address(address) IPAddr.new(address.to_s) rescue ArgumentError raise ConfigurationError, "invalid address #{address.inspect}" end |
.port_in_range!(port) ⇒ Integer
Returns the validated port.
237 238 239 240 241 242 243 |
# File 'lib/mt/wall/dsl/validators.rb', line 237 def port_in_range!(port) unless port.is_a?(Integer) && port.between?(1, 65_535) raise ConfigurationError, "port out of range (1..65535): #{port.inspect}" end port end |
.range?(token) ⇒ Boolean
Returns true if the token is an IP RANGE (‘low-high`).
96 97 98 |
# File 'lib/mt/wall/dsl/validators.rb', line 96 def range?(token) token.to_s.include?("-") end |
.range_endpoints(range) ⇒ Array(IPAddr, IPAddr)
Parse the two endpoints of a ‘low-high` range into IPAddr objects.
113 114 115 116 117 118 |
# File 'lib/mt/wall/dsl/validators.rb', line 113 def range_endpoints(range) low, high, extra = range.to_s.split("-", 3) raise ConfigurationError, "invalid address range #{range.inspect}" if high.nil? || extra [parse_address(low), parse_address(high)] end |
.rule_flags(log: false, log_prefix: nil, disabled: false) ⇒ Hash
Validate+normalize the shared rule-level attribute keywords (‘log:`/`log_prefix:`/`disabled:`) into a kwargs Hash ready to splat into a Model::* (FilterRule / Rule / Policy). Keeps the flag handling DRY across the chain/rule/policy DSL verbs.
190 191 192 193 194 195 196 |
# File 'lib/mt/wall/dsl/validators.rb', line 190 def rule_flags(log: false, log_prefix: nil, disabled: false) { log: validate_flag!(log, label: "log"), log_prefix: log_prefix && validate_log_prefix!(log_prefix), disabled: validate_flag!(disabled, label: "disabled") } end |
.validate_address!(address) ⇒ String
Validate an IPv4/IPv6 address, CIDR subnet, OR IP range (‘10.0.0.1-10.0.0.10`, both endpoints the same family).
89 90 91 92 93 |
# File 'lib/mt/wall/dsl/validators.rb', line 89 def validate_address!(address) str = address.to_s range?(str) ? validate_range!(str) : parse_address(str) str end |
.validate_chain!(chain) ⇒ Symbol
Returns the validated chain symbol.
150 151 152 153 154 155 |
# File 'lib/mt/wall/dsl/validators.rb', line 150 def validate_chain!(chain) sym = chain.to_s.to_sym raise ConfigurationError, "invalid chain #{chain.inspect}" unless CHAINS.include?(sym) sym end |
.validate_family!(family) ⇒ Symbol
Returns the validated family symbol.
168 169 170 171 172 173 |
# File 'lib/mt/wall/dsl/validators.rb', line 168 def validate_family!(family) sym = family.to_s.to_sym raise ConfigurationError, "invalid family #{family.inspect}; use :ip4 or :ip6" unless FAMILIES.include?(sym) sym end |
.validate_flag!(value, label: "flag") ⇒ Boolean
Validate a boolean rule flag (‘log:` / `disabled:`). nil is treated as false (the default); anything other than true/false fails fast.
178 179 180 181 182 183 |
# File 'lib/mt/wall/dsl/validators.rb', line 178 def validate_flag!(value, label: "flag") return false if value.nil? || value == false return true if value == true raise ConfigurationError, "#{label} must be true or false, got #{value.inspect}" end |
.validate_group_token!(token) ⇒ String
Validate a group token used as host-side membership. Guards the common ‘host “web”, “10.0.0.5”` slip where an address is passed where a group name is expected.
59 60 61 62 63 64 65 |
# File 'lib/mt/wall/dsl/validators.rb', line 59 def validate_group_token!(token) if looks_like_address?(token) raise ConfigurationError, "#{token.inspect} looks like an address — did you mean `address:`?" end validate_name!(token, label: "group") end |
.validate_ipv4!(address) ⇒ String
Validate an IPv4-only address/CIDR (NAT targets, v1).
122 123 124 125 126 127 128 |
# File 'lib/mt/wall/dsl/validators.rb', line 122 def validate_ipv4!(address) unless parse_address(address).ipv4? raise ConfigurationError, "IPv6 NAT target #{address.inspect} is not supported (NAT is IPv4-only in v1)" end address.to_s end |
.validate_log_prefix!(prefix) ⇒ String
Validate a firewall ‘log-prefix` label against LOG_PREFIX_RE.
200 201 202 203 204 205 206 207 |
# File 'lib/mt/wall/dsl/validators.rb', line 200 def validate_log_prefix!(prefix) str = prefix.to_s unless LOG_PREFIX_RE.match?(str) raise ConfigurationError, "invalid log_prefix #{prefix.inspect}: must match #{LOG_PREFIX_RE.inspect}" end str end |
.validate_name!(name, label: "name") ⇒ String
Validate a strict identifier (host/group/service/interface name).
46 47 48 49 50 51 52 53 |
# File 'lib/mt/wall/dsl/validators.rb', line 46 def validate_name!(name, label: "name") str = name.to_s unless NAME_RE.match?(str) raise ConfigurationError, "invalid #{label} #{name.inspect}: must match #{NAME_RE.inspect}" end str end |
.validate_policy_action!(action) ⇒ Symbol
Returns the validated policy action symbol (:accept/:drop).
158 159 160 161 162 163 164 165 |
# File 'lib/mt/wall/dsl/validators.rb', line 158 def validate_policy_action!(action) sym = action.to_s.to_sym unless POLICY_ACTIONS.include?(sym) raise ConfigurationError, "invalid policy action #{action.inspect}; use :accept or :drop" end sym end |
.validate_ports!(ports) ⇒ Object
Validate a port specification (Integer, Array, Range or “a-b”/“n” String, nested arrays allowed) without altering it.
222 223 224 225 |
# File 'lib/mt/wall/dsl/validators.rb', line 222 def validate_ports!(ports) collect_ports(ports, []) ports end |
.validate_protocol!(protocol) ⇒ Symbol
Returns the validated protocol symbol.
140 141 142 143 144 145 146 147 |
# File 'lib/mt/wall/dsl/validators.rb', line 140 def validate_protocol!(protocol) sym = protocol.to_s.downcase.to_sym unless PROTOCOLS.include?(sym) raise ConfigurationError, "unknown protocol #{protocol.inspect}; allowed: #{PROTOCOLS.join(', ')}" end sym end |
.validate_range!(range) ⇒ String
Validate an IP range: both endpoints parse and share one family.
102 103 104 105 106 107 108 109 |
# File 'lib/mt/wall/dsl/validators.rb', line 102 def validate_range!(range) low, high = range_endpoints(range) if low.ipv4? != high.ipv4? raise ConfigurationError, "address range #{range.inspect} mixes IPv4 and IPv6 endpoints" end range.to_s end |