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

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.

Returns:

  • (Symbol)

    :ip4 or :ip6



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.

Returns:

  • (Boolean)

    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.

Returns:

  • (Hash)

    the normalized match



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).

Returns:

  • (Array<Integer>)


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.

Returns:

  • (Array<Symbol>)

    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.

Returns:

  • (IPAddr)


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.

Returns:

  • (Integer)

    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`).

Returns:

  • (Boolean)

    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.

Returns:

  • (Array(IPAddr, IPAddr))

Raises:



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.

Returns:

  • (Hash)


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).

Returns:

  • (String)

    the original address string



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.

Returns:

  • (Symbol)

    the validated chain symbol

Raises:



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.

Returns:

  • (Symbol)

    the validated family symbol

Raises:



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.

Returns:

  • (Boolean)

Raises:



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.

Returns:

  • (String)


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).

Returns:

  • (String)


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.

Returns:

  • (String)

    the validated prefix



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).

Returns:

  • (String)

    the validated 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).

Returns:

  • (Symbol)

    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.

Returns:

  • the original ports value



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.

Returns:

  • (Symbol)

    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.

Returns:

  • (String)

    the original range string



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