mt-wall 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +55 -0
- data/LICENSE.txt +21 -0
- data/README.md +166 -0
- data/docs/dsl-reference.md +388 -0
- data/docs/gitops.md +173 -0
- data/docs/security.md +142 -0
- data/examples/README.md +67 -0
- data/examples/config/devices/edge-1.rb +44 -0
- data/examples/config/devices/edge-2.rb +41 -0
- data/examples/config/objects.rb +46 -0
- data/examples/config/policy.rb +30 -0
- data/examples/config/services.rb +19 -0
- data/exe/mt-wall +6 -0
- data/lib/mt/wall/cli.rb +473 -0
- data/lib/mt/wall/compiler.rb +613 -0
- data/lib/mt/wall/configuration.rb +123 -0
- data/lib/mt/wall/desired_state.rb +200 -0
- data/lib/mt/wall/dsl/chain_builder.rb +112 -0
- data/lib/mt/wall/dsl/device_builder.rb +149 -0
- data/lib/mt/wall/dsl/group_builder.rb +36 -0
- data/lib/mt/wall/dsl/host_builder.rb +64 -0
- data/lib/mt/wall/dsl/nat_builder.rb +114 -0
- data/lib/mt/wall/dsl/policy_scope.rb +31 -0
- data/lib/mt/wall/dsl/root_builder.rb +141 -0
- data/lib/mt/wall/dsl/rule_builder.rb +86 -0
- data/lib/mt/wall/dsl/rule_scope.rb +35 -0
- data/lib/mt/wall/dsl/validators.rb +306 -0
- data/lib/mt/wall/dsl.rb +61 -0
- data/lib/mt/wall/errors.rb +19 -0
- data/lib/mt/wall/model/address_object.rb +35 -0
- data/lib/mt/wall/model/device.rb +54 -0
- data/lib/mt/wall/model/filter_rule.rb +66 -0
- data/lib/mt/wall/model/group.rb +27 -0
- data/lib/mt/wall/model/nat_rule.rb +49 -0
- data/lib/mt/wall/model/policy.rb +27 -0
- data/lib/mt/wall/model/rule.rb +50 -0
- data/lib/mt/wall/model/service.rb +42 -0
- data/lib/mt/wall/plan.rb +304 -0
- data/lib/mt/wall/reconciler.rb +148 -0
- data/lib/mt/wall/transport/base.rb +79 -0
- data/lib/mt/wall/transport/rest_api.rb +464 -0
- data/lib/mt/wall/transport/rsc.rb +99 -0
- data/lib/mt/wall/version.rb +7 -0
- data/lib/mt/wall.rb +56 -0
- metadata +91 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mt
|
|
4
|
+
module Wall
|
|
5
|
+
module DSL
|
|
6
|
+
# Block context for the `nat` verb inside a `device` block (Layer B — the
|
|
7
|
+
# box's own NAT table). Opened by DeviceBuilder#nat. Each verb appends a
|
|
8
|
+
# Model::NatRule. IPv4-only for v1 (see Model::NatRule).
|
|
9
|
+
#
|
|
10
|
+
# device "edge-1", host: "10.0.0.1" do
|
|
11
|
+
# nat do
|
|
12
|
+
# # srcnat: hide the LAN behind the WAN address
|
|
13
|
+
# masquerade out_interface: "ether1-wan"
|
|
14
|
+
#
|
|
15
|
+
# # dstnat: publish an internal service (port-forward)
|
|
16
|
+
# dst_nat protocol: :tcp, dst_port: 443,
|
|
17
|
+
# to_addresses: "10.0.0.5", to_ports: 8443,
|
|
18
|
+
# in_interface: "ether1-wan"
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# CORE verbs only (v1): masquerade / dst_nat / src_nat — one rule each,
|
|
23
|
+
# taking native match keywords (protocol:, dst_port:, src_port:,
|
|
24
|
+
# in_interface:, out_interface:, src:, dst:, comment:) plus SPLIT
|
|
25
|
+
# translation targets (to_addresses:, to_ports:) where the action uses
|
|
26
|
+
# them. `src:` / `dst:` reference a Layer-A host/group by name; an unknown
|
|
27
|
+
# name is a fail-fast error at compile time. No `port_forward` helper and
|
|
28
|
+
# no combined "host:port" target string in v1 (YAGNI).
|
|
29
|
+
#
|
|
30
|
+
# VALIDATION (fail-fast):
|
|
31
|
+
# * SCOPING IS REQUIRED — a fully-unscoped NAT rule is dangerous and
|
|
32
|
+
# rejected: `masquerade` requires `out_interface:`; `dst_nat` requires
|
|
33
|
+
# `in_interface:` OR `dst:`. A rule lacking its required scope raises
|
|
34
|
+
# ConfigurationError.
|
|
35
|
+
# * `dst_nat`/`src_nat` require `to_addresses`; `masquerade` must NOT
|
|
36
|
+
# carry translation targets;
|
|
37
|
+
# * ports are 1..65535; addresses parse as IPv4 via IPAddr (an IPv6
|
|
38
|
+
# target is rejected in v1).
|
|
39
|
+
class NatBuilder
|
|
40
|
+
def initialize
|
|
41
|
+
@rules = []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- core verbs -----------------------------------------------------
|
|
45
|
+
|
|
46
|
+
# srcnat masquerade. Translation target is implicit (the out-interface
|
|
47
|
+
# address), so to_addresses/to_ports must be omitted. `out_interface:` is
|
|
48
|
+
# REQUIRED — an unscoped masquerade would NAT every egress path.
|
|
49
|
+
# @param out_interface [String] the egress (e.g. WAN) interface
|
|
50
|
+
# @return [void]
|
|
51
|
+
def masquerade(out_interface:, **match)
|
|
52
|
+
normalized, comment = build_match(match)
|
|
53
|
+
normalized[:out_interface] = Validators.validate_name!(out_interface, label: "out_interface")
|
|
54
|
+
|
|
55
|
+
@rules << Model::NatRule.new(chain: :srcnat, action: :masquerade,
|
|
56
|
+
match: normalized, comment: comment)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# dstnat (port-forward): redirect matched traffic to an internal host.
|
|
60
|
+
# REQUIRES scoping — `in_interface:` OR `dst:` must be present (else
|
|
61
|
+
# ConfigurationError) so the rule does not hijack arbitrary traffic.
|
|
62
|
+
# @param to_addresses [String, Array<String>] internal target address(es)
|
|
63
|
+
# @param to_ports [Integer, Array, Range, nil] internal target port(s)
|
|
64
|
+
# @return [void]
|
|
65
|
+
def dst_nat(to_addresses:, to_ports: nil, **match)
|
|
66
|
+
normalized, comment = build_match(match)
|
|
67
|
+
unless normalized.key?(:in_interface) || normalized.key?(:dst)
|
|
68
|
+
raise ConfigurationError, "dst_nat requires `in_interface:` or `dst:` scoping"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
addresses = validated_targets(to_addresses, to_ports, verb: "dst_nat")
|
|
72
|
+
@rules << Model::NatRule.new(chain: :dstnat, action: :dst_nat, match: normalized,
|
|
73
|
+
to_addresses: addresses, to_ports: to_ports, comment: comment)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# srcnat to a specific source address (static NAT / one-to-one egress).
|
|
77
|
+
# @param to_addresses [String, Array<String>]
|
|
78
|
+
# @param to_ports [Integer, Array, Range, nil]
|
|
79
|
+
# @return [void]
|
|
80
|
+
def src_nat(to_addresses:, to_ports: nil, **match)
|
|
81
|
+
normalized, comment = build_match(match)
|
|
82
|
+
addresses = validated_targets(to_addresses, to_ports, verb: "src_nat")
|
|
83
|
+
|
|
84
|
+
@rules << Model::NatRule.new(chain: :srcnat, action: :src_nat, match: normalized,
|
|
85
|
+
to_addresses: addresses, to_ports: to_ports, comment: comment)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The Model::NatRule list collected for this device.
|
|
89
|
+
# @return [Array<Model::NatRule>]
|
|
90
|
+
attr_reader :rules
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Split a match Hash into [normalized_match, comment]. NAT is IPv4-only
|
|
95
|
+
# and stateless, so `state:` is rejected by normalize_match.
|
|
96
|
+
def build_match(match)
|
|
97
|
+
match = match.dup
|
|
98
|
+
comment = match.delete(:comment)
|
|
99
|
+
[Validators.normalize_match(match, allow_state: false), comment]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Validate the (required) translation targets for dst_nat / src_nat.
|
|
103
|
+
def validated_targets(to_addresses, to_ports, verb:)
|
|
104
|
+
addresses = Array(to_addresses)
|
|
105
|
+
raise ConfigurationError, "#{verb} requires `to_addresses:`" if addresses.empty?
|
|
106
|
+
|
|
107
|
+
addresses.each { |address| Validators.validate_ipv4!(address) }
|
|
108
|
+
Validators.validate_ports!(to_ports) unless to_ports.nil?
|
|
109
|
+
addresses
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mt
|
|
4
|
+
module Wall
|
|
5
|
+
module DSL
|
|
6
|
+
# The `policy` verb: the default action for a firewall chain (its trailing
|
|
7
|
+
# rule). Shared by RootBuilder (global defaults) and DeviceBuilder
|
|
8
|
+
# (per-device overrides), so the same syntax works in both scopes.
|
|
9
|
+
#
|
|
10
|
+
# policy :forward, :drop
|
|
11
|
+
# policy :forward, :drop, log: true, log_prefix: "fwd-drop"
|
|
12
|
+
#
|
|
13
|
+
# `log:`/`log_prefix:` log packets hitting the chain default; `disabled:`
|
|
14
|
+
# keeps the default-policy rule in git but inactive. These are rule
|
|
15
|
+
# attributes (excluded from the identity tag).
|
|
16
|
+
#
|
|
17
|
+
# Includers MUST provide the storage hook:
|
|
18
|
+
# #record_policy(Model::Policy)
|
|
19
|
+
module PolicyScope
|
|
20
|
+
# @param chain [Symbol] :input, :forward or :output
|
|
21
|
+
# @param action [Symbol] :accept or :drop
|
|
22
|
+
# @return [void]
|
|
23
|
+
def policy(chain, action, comment: nil, **flags)
|
|
24
|
+
record_policy(Model::Policy.new(chain: Validators.validate_chain!(chain),
|
|
25
|
+
action: Validators.validate_policy_action!(action),
|
|
26
|
+
comment: comment, **Validators.rule_flags(**flags)))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mt
|
|
4
|
+
module Wall
|
|
5
|
+
module DSL
|
|
6
|
+
# Top-level DSL context. The public methods here are the verbs users
|
|
7
|
+
# write in firewall config files / blocks. Each verb validates its
|
|
8
|
+
# arguments and records a Model::* value object on the Configuration.
|
|
9
|
+
#
|
|
10
|
+
# `rule` (Layer-A access grants) comes from RuleScope; `policy` (chain
|
|
11
|
+
# defaults) comes from PolicyScope, shared with DeviceBuilder so the same
|
|
12
|
+
# syntax sets a global default here and overrides it inside a `device`
|
|
13
|
+
# block. The `device` block itself configures Layer-B box firewall and
|
|
14
|
+
# does NOT take `rule`.
|
|
15
|
+
#
|
|
16
|
+
# Target DSL (not yet implemented):
|
|
17
|
+
#
|
|
18
|
+
# host "web" do
|
|
19
|
+
# address "10.0.0.5"
|
|
20
|
+
# address "10.0.1.0/24"
|
|
21
|
+
# member_of "web-prod", "web-wordpress"
|
|
22
|
+
# end
|
|
23
|
+
# host "db", address: "10.0.2.10" # one-line shorthand
|
|
24
|
+
# host "web", "web-prod", "web-wordpress",
|
|
25
|
+
# address: ["10.0.0.5", "10.0.1.0/24"] # + groups
|
|
26
|
+
#
|
|
27
|
+
# group "frontend" do
|
|
28
|
+
# member "web"
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# service "mysql", protocol: :tcp, ports: [3306]
|
|
32
|
+
#
|
|
33
|
+
# rule "frontend" do # grants grouped by source
|
|
34
|
+
# to "db", "mysql" # allow (default)
|
|
35
|
+
# to "db", "mysql", :deny
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# policy :forward, :drop # global default
|
|
39
|
+
#
|
|
40
|
+
# device "edge-1", host: "10.0.0.1", transport: :rest_api do
|
|
41
|
+
# policy :input, :drop # override for this box
|
|
42
|
+
# input do # the box's own firewall
|
|
43
|
+
# allow_established
|
|
44
|
+
# drop_invalid
|
|
45
|
+
# accept protocol: :tcp, dst_port: 22, src: "admin"
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
class RootBuilder
|
|
49
|
+
include RuleScope
|
|
50
|
+
include PolicyScope
|
|
51
|
+
|
|
52
|
+
def initialize(configuration)
|
|
53
|
+
@configuration = configuration
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A named address object (host). Trailing positional args after the
|
|
57
|
+
# name are group names this host joins (same as `member_of` in the
|
|
58
|
+
# block form); addresses come from the `address:` shorthand (a single
|
|
59
|
+
# string or an array, normalized via Array()) or an `address` block.
|
|
60
|
+
#
|
|
61
|
+
# AUTO-CREATE is NARROW: a group referenced HERE (host-side membership)
|
|
62
|
+
# is created on demand, because the host contributes real addresses to
|
|
63
|
+
# it. Groups referenced from `rule` / `src:` / `dst:` are NOT
|
|
64
|
+
# auto-created — an empty/undeclared group there is a fail-fast error in
|
|
65
|
+
# the Compiler.
|
|
66
|
+
#
|
|
67
|
+
# FAIL-FAST validation:
|
|
68
|
+
# * a positional `groups` token that parses as an IP/CIDR via IPAddr
|
|
69
|
+
# raises ConfigurationError ("looks like an address — did you mean
|
|
70
|
+
# address:?") — guards the common `host "web", "10.0.0.5"` slip;
|
|
71
|
+
# * a host that ends up with NO addresses raises ConfigurationError;
|
|
72
|
+
# * `name` and every group token must match `\A[\w.-]+\z`.
|
|
73
|
+
#
|
|
74
|
+
# host "web", "web-prod", address: "10.0.0.5"
|
|
75
|
+
# host "web", "web-prod", address: ["10.0.0.5", "10.0.1.0/24"]
|
|
76
|
+
#
|
|
77
|
+
# @param groups [Array<String>] group names to fold this host into
|
|
78
|
+
# @yield a HostBuilder context
|
|
79
|
+
# @return [void]
|
|
80
|
+
def host(name, *groups, address: nil, comment: nil, &block)
|
|
81
|
+
memberships = groups.map { |token| Validators.validate_group_token!(token) }
|
|
82
|
+
|
|
83
|
+
builder = HostBuilder.new(name, comment: comment)
|
|
84
|
+
builder.instance_eval(&block) if block
|
|
85
|
+
Array(address).each { |entry| builder.address(entry) }
|
|
86
|
+
|
|
87
|
+
object = builder.to_object
|
|
88
|
+
@configuration.add_object(object)
|
|
89
|
+
|
|
90
|
+
(memberships + builder.memberships).uniq.each do |group_name|
|
|
91
|
+
@configuration.add_membership(object.name, group_name)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @yield a GroupBuilder context (declares members)
|
|
96
|
+
# @return [void]
|
|
97
|
+
def group(name, comment: nil, &block)
|
|
98
|
+
builder = GroupBuilder.new(name, comment: comment)
|
|
99
|
+
builder.instance_eval(&block) if block
|
|
100
|
+
group = builder.to_group
|
|
101
|
+
@configuration.declare_group(group.name, group.members, group.comment)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A named protocol/port definition. Accepts a single `protocol:` (legacy)
|
|
105
|
+
# OR multiple `protocols:` (e.g. DNS = tcp+udp). Ports keep their spec
|
|
106
|
+
# form (Integer/Array/Range/"a-b"), so a range round-trips as a range.
|
|
107
|
+
# @return [void]
|
|
108
|
+
def service(name, protocol: nil, protocols: nil, ports: [])
|
|
109
|
+
list = protocols || protocol
|
|
110
|
+
raise ConfigurationError, "service #{name.inspect} needs protocol: or protocols:" if list.nil?
|
|
111
|
+
|
|
112
|
+
validated = Array(list).map { |proto| Validators.validate_protocol!(proto) }
|
|
113
|
+
@configuration.add_service(
|
|
114
|
+
Model::Service.new(name: Validators.validate_name!(name, label: "service"),
|
|
115
|
+
protocols: validated,
|
|
116
|
+
ports: Validators.validate_ports!(ports))
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @yield a DeviceBuilder context (per-device policies and rules)
|
|
121
|
+
# @return [void]
|
|
122
|
+
def device(name, host:, transport: :rest_api, **options, &block)
|
|
123
|
+
builder = DeviceBuilder.new(name, host: host, transport: transport, **options)
|
|
124
|
+
builder.instance_eval(&block) if block
|
|
125
|
+
@configuration.add_device(builder.to_device)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# RuleScope storage hooks (record onto the Configuration).
|
|
129
|
+
# @return [void]
|
|
130
|
+
def record_rule(rule)
|
|
131
|
+
@configuration.add_rule(rule)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @return [void]
|
|
135
|
+
def record_policy(policy)
|
|
136
|
+
@configuration.add_global_policy(policy)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mt
|
|
4
|
+
module Wall
|
|
5
|
+
module DSL
|
|
6
|
+
# Block context for the `rule` verb. The source is fixed for the whole
|
|
7
|
+
# block (passed in); each `to` call adds one grant from that source to a
|
|
8
|
+
# destination, producing a Model::Rule.
|
|
9
|
+
#
|
|
10
|
+
# rule "admin" do
|
|
11
|
+
# to "edge-1-mgmt", "ssh" # allow (default action)
|
|
12
|
+
# to "edge-2-mgmt", "ssh", :deny # explicit action
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# `to` takes a destination host/group, an optional service name (omitted
|
|
16
|
+
# = :any), and an optional action (:allow / :deny, default :allow). Action
|
|
17
|
+
# is positional and last, so to set it you must give a service (or :any):
|
|
18
|
+
# `to "x", :any, :deny`. The DSL action maps to RouterOS accept/drop in
|
|
19
|
+
# the resulting Model::Rule.
|
|
20
|
+
class RuleBuilder
|
|
21
|
+
# DSL actions, and the tokens that trigger the service-slot footgun.
|
|
22
|
+
ACTIONS = %i[allow deny].freeze
|
|
23
|
+
ACTION_TOKENS = [:allow, :deny, "allow", "deny"].freeze
|
|
24
|
+
# DSL action -> RouterOS / Model::Rule action.
|
|
25
|
+
ACTION_MAP = { allow: :accept, deny: :drop }.freeze
|
|
26
|
+
|
|
27
|
+
def initialize(source)
|
|
28
|
+
@source = source
|
|
29
|
+
@rules = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# One grant: @source -> destination, optional service, optional action.
|
|
33
|
+
#
|
|
34
|
+
# FAIL-FAST on the positional footgun: `action` MUST be one of
|
|
35
|
+
# {:allow, :deny}. If the SERVICE slot (arg 2) is given as `:allow` or
|
|
36
|
+
# `:deny`, that is treated as the ACTION (service defaults to :any) so
|
|
37
|
+
# `to "x", :deny` cannot silently degrade into "service named :deny".
|
|
38
|
+
# Any other non-{:allow,:deny} symbol/string in the action slot raises
|
|
39
|
+
# ConfigurationError with a clear message.
|
|
40
|
+
#
|
|
41
|
+
# `log:`/`log_prefix:` enable RouterOS logging on the emitted rule(s);
|
|
42
|
+
# `disabled:` keeps the grant in git but inactive. Both are rule
|
|
43
|
+
# attributes (excluded from the identity tag, so a toggle is an :update).
|
|
44
|
+
#
|
|
45
|
+
# @param destination [String] name of the destination host/group
|
|
46
|
+
# @param service [String, Symbol] service name, or :any
|
|
47
|
+
# @param action [Symbol] :allow or :deny
|
|
48
|
+
# @param log [Boolean] log matched packets (log=yes)
|
|
49
|
+
# @param log_prefix [String, nil] optional log-prefix label
|
|
50
|
+
# @param disabled [Boolean] emit the rule(s) disabled
|
|
51
|
+
# @return [void]
|
|
52
|
+
def to(destination, service = :any, action = :allow, comment: nil, **flags)
|
|
53
|
+
Validators.validate_name!(destination, label: "destination")
|
|
54
|
+
service, action = resolve_service_and_action(service, action)
|
|
55
|
+
service_name = service == :any ? nil : Validators.validate_name!(service, label: "service")
|
|
56
|
+
|
|
57
|
+
@rules << Model::Rule.new(source: @source, destination: destination.to_s,
|
|
58
|
+
service: service_name, action: ACTION_MAP.fetch(action),
|
|
59
|
+
comment: comment, **Validators.rule_flags(**flags))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The Model::Rule list collected from this block.
|
|
63
|
+
# @return [Array<Model::Rule>]
|
|
64
|
+
attr_reader :rules
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Resolve the service-slot footgun (`to "x", :deny`) and validate the
|
|
69
|
+
# action. @return [[service, Symbol]] the (service, action) pair.
|
|
70
|
+
def resolve_service_and_action(service, action)
|
|
71
|
+
if ACTION_TOKENS.include?(service)
|
|
72
|
+
action = service
|
|
73
|
+
service = :any
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
action = action.to_sym
|
|
77
|
+
unless ACTIONS.include?(action)
|
|
78
|
+
raise ConfigurationError, "rule action must be :allow or :deny, got #{action.inspect}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[service, action]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mt
|
|
4
|
+
module Wall
|
|
5
|
+
module DSL
|
|
6
|
+
# The `rule` verb: abstract, device-agnostic access grants (Layer A).
|
|
7
|
+
# Only the global (root) scope declares these — they compile to the
|
|
8
|
+
# forward chain of every managed device. Device-local box firewall config
|
|
9
|
+
# is a different concern (see DeviceBuilder / ChainBuilder).
|
|
10
|
+
#
|
|
11
|
+
# Includers MUST provide the storage hook:
|
|
12
|
+
# #record_rule(Model::Rule)
|
|
13
|
+
#
|
|
14
|
+
# `rule` groups access grants by source: the source (a host or group) is
|
|
15
|
+
# the block header, and each `to` line inside is one grant from that
|
|
16
|
+
# source. The action defaults to :allow and may be omitted.
|
|
17
|
+
#
|
|
18
|
+
# rule "admin" do
|
|
19
|
+
# to "edge-1-mgmt", "ssh" # allow (default action)
|
|
20
|
+
# to "edge-2-mgmt", "ssh", :deny # explicit action
|
|
21
|
+
# to "edge-3-mgmt" # no service = any
|
|
22
|
+
# end
|
|
23
|
+
module RuleScope
|
|
24
|
+
# @param source [String] name of the source host/group
|
|
25
|
+
# @yield a RuleBuilder context (one `to` per grant)
|
|
26
|
+
# @return [void]
|
|
27
|
+
def rule(source, &block)
|
|
28
|
+
builder = RuleBuilder.new(Validators.validate_name!(source, label: "source"))
|
|
29
|
+
builder.instance_eval(&block) if block
|
|
30
|
+
builder.rules.each { |grant| record_rule(grant) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|