empeira 0.1.0.pre.alpha.4 → 0.1.0.pre.alpha.6
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 +4 -4
- data/config/defaults.yaml +10 -4
- data/docs/addons.md +10 -6
- data/docs/configuration.md +45 -22
- data/docs/hiera-and-eyaml.md +2 -2
- data/docs/networking.md +16 -7
- data/docs/proxy.md +62 -19
- data/docs/troubleshooting.md +2 -2
- data/lib/empeira/build.json +3 -3
- data/lib/empeira/cli/proxy.rb +2 -2
- data/lib/empeira/configuration/schema.rb +27 -2
- data/lib/empeira/control_plane/controller.rb +15 -2
- data/lib/empeira/control_plane/files.rb +1 -8
- data/lib/empeira/control_plane/plan.rb +16 -14
- data/lib/empeira/network/direct_egress.rb +0 -7
- data/lib/empeira/network/gateway.rb +9 -1
- data/lib/empeira/network/host_policy.rb +1 -1
- data/lib/empeira/network/peer/layout.rb +8 -0
- data/lib/empeira/network/proxy_bindings.rb +27 -10
- data/lib/empeira/node/runtime_proxy.rb +2 -2
- data/lib/empeira/runtime/service_arguments.rb +7 -1
- data/resources/gateway/configure.rb +21 -11
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a6bd2bd179b9a3fa789f2342e0646d9e5d681941cd2df234770cf8a94a01f31
|
|
4
|
+
data.tar.gz: 8925fd4ded29c7206cfe6c1d2d072f7a41a649582504dbf1a29232543525aacf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 988ab09ce7e604a6bddfe1f5acad7f2847939b96e33ad6b73d06a03d49a6aafc7048222f5bb2a26eddb412dd75442b6d57addddd9558cb974a675603c11a1fb6
|
|
7
|
+
data.tar.gz: 6d5c737b4f18f0d400f279c59b0b10f7e68bb3b564c649b42207e0f447ba7b68a2acd93b34258ca7d025252d91de4c2befcfc741b85195f23a8b8feecaa99e46
|
data/config/defaults.yaml
CHANGED
|
@@ -50,12 +50,18 @@ puppetdb:
|
|
|
50
50
|
bootstrap_config: /etc/puppetlabs/puppetdb/bootstrap.cfg
|
|
51
51
|
data: /opt/puppetlabs/server/data/puppetdb
|
|
52
52
|
network:
|
|
53
|
-
egress:
|
|
53
|
+
egress:
|
|
54
|
+
nodes: []
|
|
55
|
+
infrastructure: []
|
|
54
56
|
redirects: []
|
|
55
57
|
proxy:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
nodes:
|
|
59
|
+
enabled: false
|
|
60
|
+
global: []
|
|
61
|
+
rules: []
|
|
62
|
+
infrastructure:
|
|
63
|
+
enabled: false
|
|
64
|
+
allow: []
|
|
59
65
|
vm:
|
|
60
66
|
disk: 30
|
|
61
67
|
interfaces: []
|
data/docs/addons.md
CHANGED
|
@@ -208,8 +208,12 @@ Array elements can be mappings, arrays or scalars. For example:
|
|
|
208
208
|
|
|
209
209
|
```yaml
|
|
210
210
|
proxy:
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
nodes:
|
|
212
|
+
enabled: true
|
|
213
|
+
global: [forge.puppet.com, github.com]
|
|
214
|
+
infrastructure:
|
|
215
|
+
enabled: false
|
|
216
|
+
allow: []
|
|
213
217
|
server:
|
|
214
218
|
mounts:
|
|
215
219
|
- source: /home/user/data
|
|
@@ -217,10 +221,10 @@ server:
|
|
|
217
221
|
readonly: true
|
|
218
222
|
```
|
|
219
223
|
|
|
220
|
-
produces `
|
|
221
|
-
`
|
|
222
|
-
`
|
|
223
|
-
`
|
|
224
|
+
produces `EMPEIRA_CONFIG_PROXY_NODES_ENABLED=true`,
|
|
225
|
+
`EMPEIRA_CONFIG_PROXY_NODES_GLOBAL_COUNT=2`,
|
|
226
|
+
`EMPEIRA_CONFIG_PROXY_NODES_GLOBAL_0=forge.puppet.com`,
|
|
227
|
+
`EMPEIRA_CONFIG_PROXY_NODES_GLOBAL_1=github.com`,
|
|
224
228
|
`EMPEIRA_CONFIG_SERVER_MOUNTS_COUNT=1`, and the variables
|
|
225
229
|
`EMPEIRA_CONFIG_SERVER_MOUNTS_0_SOURCE`, `_TARGET`, `_READONLY`.
|
|
226
230
|
|
data/docs/configuration.md
CHANGED
|
@@ -107,9 +107,15 @@ runtime:
|
|
|
107
107
|
server:
|
|
108
108
|
environment: production
|
|
109
109
|
network:
|
|
110
|
-
egress:
|
|
110
|
+
egress:
|
|
111
|
+
nodes: []
|
|
112
|
+
infrastructure: []
|
|
111
113
|
proxy:
|
|
112
|
-
|
|
114
|
+
nodes:
|
|
115
|
+
enabled: false
|
|
116
|
+
infrastructure:
|
|
117
|
+
enabled: false
|
|
118
|
+
allow: []
|
|
113
119
|
node_defaults:
|
|
114
120
|
os: ubuntu
|
|
115
121
|
version: "24.04"
|
|
@@ -150,11 +156,14 @@ release version. A partial project mapping may omit it and inherit the default.
|
|
|
150
156
|
| `server.timeout` | Positive readiness timeout in seconds per component, `240` |
|
|
151
157
|
| `server.mounts` | Additional server-only host bind mounts, `[]`; `source`, `target`, optional boolean `readonly` (default `true`) |
|
|
152
158
|
| `puppetdb.memory` | Positive integer MiB, `768` |
|
|
153
|
-
| `network.egress` |
|
|
159
|
+
| `network.egress.nodes` | Node-only direct TCP host/port grants, `[]`; arrays replace |
|
|
160
|
+
| `network.egress.infrastructure` | Infrastructure-only direct TCP host/port grants, `[]`; arrays replace |
|
|
154
161
|
| `network.redirects` | Exact IPv4/protocol/port endpoints redirected to enabled internal services, `[]` |
|
|
155
|
-
| `proxy.enabled` | Boolean, `false`;
|
|
156
|
-
| `proxy.global` | DNS destination array, `[]` |
|
|
157
|
-
| `proxy.rules` | Additive hostname-glob rules with `hosts` and `allow` arrays, `[]` |
|
|
162
|
+
| `proxy.nodes.enabled` | Boolean, `false`; enables normal node HTTP/HTTPS proxy access |
|
|
163
|
+
| `proxy.nodes.global` | DNS destination array, `[]` |
|
|
164
|
+
| `proxy.nodes.rules` | Additive node hostname-glob rules with `hosts` and `allow` arrays, `[]` |
|
|
165
|
+
| `proxy.infrastructure.enabled` | Boolean, `false`; enables infrastructure HTTP/HTTPS proxy access |
|
|
166
|
+
| `proxy.infrastructure.allow` | Independent infrastructure DNS destination array, `[]` |
|
|
158
167
|
| `vm.disk` | New VM virtual disk capacity in GiB, integer `1`–`2048`, default `30`; must accommodate the base image |
|
|
159
168
|
| `vm.interfaces` | VM-only hostname rules defining dummy/VLAN devices with explicit IPv4 CIDR host addresses, `[]` |
|
|
160
169
|
| `vm.console.root_password` | Initial console password, `empeira`; a string overrides it and `null` disables it |
|
|
@@ -488,14 +497,20 @@ The numeric gem packaging fallback is never used to bypass this check.
|
|
|
488
497
|
```yaml
|
|
489
498
|
network:
|
|
490
499
|
egress:
|
|
491
|
-
|
|
492
|
-
|
|
500
|
+
nodes:
|
|
501
|
+
- host: api.example.com
|
|
502
|
+
ports: [443]
|
|
503
|
+
infrastructure: []
|
|
493
504
|
proxy:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
505
|
+
nodes:
|
|
506
|
+
enabled: true
|
|
507
|
+
global: [forgeapi.puppet.com]
|
|
508
|
+
rules:
|
|
509
|
+
- hosts: ["*-web-*"]
|
|
510
|
+
allow: [github.com, "*.github.com"]
|
|
511
|
+
infrastructure:
|
|
512
|
+
enabled: false
|
|
513
|
+
allow: []
|
|
499
514
|
hiera:
|
|
500
515
|
mounts:
|
|
501
516
|
- source: ../hieradata
|
|
@@ -507,7 +522,12 @@ vm:
|
|
|
507
522
|
root_password: null
|
|
508
523
|
```
|
|
509
524
|
|
|
510
|
-
When `proxy.enabled` is true, every node may use its matching destination policy.
|
|
525
|
+
When `proxy.nodes.enabled` is true, every node may use its matching destination policy.
|
|
526
|
+
Infrastructure access is separately controlled by `proxy.infrastructure.enabled`
|
|
527
|
+
and `proxy.infrastructure.allow`; both scopes default to disabled. Disabled
|
|
528
|
+
infrastructure has no configured proxy endpoint even when nodes use the proxy.
|
|
529
|
+
Keep its direct-egress list empty for internal-only infrastructure. See the
|
|
530
|
+
[migration instructions](proxy.md#migrating-shared-settings) for former shared keys.
|
|
511
531
|
After bootstrap cleanup, Empeira reconciles normal proxy settings on running,
|
|
512
532
|
provisioned nodes during `up` and `node start`. Repeated Puppet runs use the
|
|
513
533
|
existing guest configuration. No
|
|
@@ -719,21 +739,24 @@ desired state; Puppet owns package state after the first catalog.
|
|
|
719
739
|
|
|
720
740
|
## Direct egress
|
|
721
741
|
|
|
722
|
-
`network.egress`
|
|
723
|
-
|
|
742
|
+
`network.egress.nodes` and `network.egress.infrastructure` contain separate
|
|
743
|
+
destination TCP grants, independent of HTTP/HTTPS proxy policy. A node grant never
|
|
744
|
+
applies to infrastructure or conversely. Entries have exactly one `host` or IPv4 `ip` and a nonempty unique `ports`
|
|
724
745
|
array (1–65535). `up` resolves all A records and replaces the complete policy;
|
|
725
746
|
resolution/apply failure blocks egress. IPv6 and external UDP grants are unsupported.
|
|
726
747
|
|
|
727
748
|
```yaml
|
|
728
749
|
network:
|
|
729
750
|
egress:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
751
|
+
nodes:
|
|
752
|
+
- host: repository.example.net
|
|
753
|
+
ports: [443]
|
|
754
|
+
- ip: 203.0.113.10
|
|
755
|
+
ports: [443, 8443]
|
|
756
|
+
infrastructure: []
|
|
734
757
|
```
|
|
735
758
|
|
|
736
|
-
|
|
759
|
+
Direct destinations enter only their own scope's generated `NO_PROXY`; applications choose DIRECT
|
|
737
760
|
or proxy themselves. See [networking](networking.md#one-gateway-one-direct-egress-policy)
|
|
738
761
|
for routing, revocation and privilege boundaries.
|
|
739
762
|
|
|
@@ -765,7 +788,7 @@ vm:
|
|
|
765
788
|
network: 192.0.2.20/32
|
|
766
789
|
```
|
|
767
790
|
|
|
768
|
-
Patterns use the same `*`/`?` glob matcher as `proxy.rules`, against the full stored
|
|
791
|
+
Patterns use the same `*`/`?` glob matcher as `proxy.nodes.rules`, against the full stored
|
|
769
792
|
node hostname, case-insensitively. Patterns within a rule are alternatives. All
|
|
770
793
|
matching rules contribute devices; identical definitions merge once. A conflicting
|
|
771
794
|
definition names the hostname, device and both configuration paths. No match creates
|
data/docs/hiera-and-eyaml.md
CHANGED
|
@@ -207,8 +207,8 @@ The obsolete `images.modules` key is rejected.
|
|
|
207
207
|
The disposable installer belongs to the update plane. It uses the selected
|
|
208
208
|
runtime's normal bridge network (`bridge` on Docker, `podman` on Podman), with
|
|
209
209
|
normal runtime DNS and external connectivity. It never joins an Empeira workspace
|
|
210
|
-
network and needs neither CoreDNS nor Squid. Workspace `network.egress`, `proxy.enabled`,
|
|
211
|
-
`proxy.global` and `proxy.rules` do not affect
|
|
210
|
+
network and needs neither CoreDNS nor Squid. Workspace `network.egress`, `proxy.nodes.enabled`,
|
|
211
|
+
`proxy.nodes.global` and `proxy.nodes.rules` do not affect
|
|
212
212
|
Forge or Git acquisition. Supplied `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` and their
|
|
213
213
|
lowercase equivalents are inherited unchanged; Empeira generates no proxy URL.
|
|
214
214
|
The host/runtime's normal network and corporate-proxy configuration must permit
|
data/docs/networking.md
CHANGED
|
@@ -102,17 +102,26 @@ Every peer uses the owned gateway as its IPv4 default route. Only the gateway ha
|
|
|
102
102
|
an external uplink; DNS, proxies, server, database and nodes stay isolated.
|
|
103
103
|
The fixed browser UI relay has a separate loopback access path with forwarding off.
|
|
104
104
|
|
|
105
|
-
`network.egress`
|
|
105
|
+
`network.egress.nodes` and `network.egress.infrastructure` grant destination
|
|
106
|
+
IPv4/TCP-port pairs independently:
|
|
106
107
|
|
|
107
108
|
```yaml
|
|
108
109
|
network:
|
|
109
110
|
egress:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
nodes:
|
|
112
|
+
- host: repository.example.net
|
|
113
|
+
ports: [443]
|
|
114
|
+
- ip: 203.0.113.10
|
|
115
|
+
ports: [443, 8443]
|
|
116
|
+
infrastructure: []
|
|
114
117
|
```
|
|
115
118
|
|
|
119
|
+
The gateway restricts node grants to the reserved VM/container node address ranges
|
|
120
|
+
and infrastructure grants to its separate runtime allocation pool. Both directions
|
|
121
|
+
of direct connections are source-bound. The shared workspace subnet is never used
|
|
122
|
+
as a blanket source for direct grants. Custom gateway images must advertise
|
|
123
|
+
`scoped-egress-v1`; an older image fails closed before receiving external access.
|
|
124
|
+
|
|
116
125
|
Entries have exactly one `host` or `ip`. Hostnames match exactly, without wildcards;
|
|
117
126
|
`up` resolves every current A record and replaces the complete policy. IP selectors
|
|
118
127
|
bypass DNS and may intentionally permit private LAN/VPN destinations. A different
|
|
@@ -128,9 +137,9 @@ CoreDNS alone receives TCP/UDP 53 to its selected resolvers. Proxies alone recei
|
|
|
128
137
|
TCP 80/443 and enforce their destination allowlists. Normal proxy allow rules apply
|
|
129
138
|
regardless of target IP range; the temporary authenticated bootstrap proxy retains
|
|
130
139
|
its separate private/reserved-address restrictions.
|
|
131
|
-
`proxy.global` does not grant direct egress. Applications choose DIRECT/proxy using
|
|
140
|
+
`proxy.nodes.global` does not grant direct egress. Applications choose DIRECT/proxy using
|
|
132
141
|
their own settings and `NO_PROXY`; direct/proxy selection has no automatic fallback.
|
|
133
|
-
Both explicit hostnames and IPv4 selectors
|
|
142
|
+
Both explicit hostnames and IPv4 selectors are included only in their own scope's
|
|
134
143
|
normal proxy bypass settings. The gateway still permits only their configured TCP ports.
|
|
135
144
|
See [proxy policy](proxy.md) for hostname rules and bootstrap destinations.
|
|
136
145
|
|
data/docs/proxy.md
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
# Proxy policy
|
|
2
2
|
|
|
3
|
-
Normal external HTTP/HTTPS access stays behind Squid.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Normal external HTTP/HTTPS access stays behind Squid. Nodes and infrastructure
|
|
4
|
+
have independent switches and destination lists, both disabled by default. Enabling
|
|
5
|
+
the node scope makes its global and hostname rules available to nodes:
|
|
6
6
|
|
|
7
7
|
```yaml
|
|
8
8
|
proxy:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
nodes:
|
|
10
|
+
enabled: true
|
|
11
|
+
global:
|
|
12
|
+
- forgeapi.puppet.com
|
|
13
|
+
rules:
|
|
14
|
+
- hosts: ["lab-*"]
|
|
15
|
+
allow: ["*.github.com"]
|
|
16
|
+
- hosts: ["*-web-*"]
|
|
17
|
+
allow: [github.com, nginx.org]
|
|
18
|
+
infrastructure:
|
|
19
|
+
enabled: false
|
|
20
|
+
allow: []
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
```console
|
|
@@ -46,7 +50,7 @@ infrastructure has already been reconciled.
|
|
|
46
50
|
|
|
47
51
|
## Matching and destinations
|
|
48
52
|
|
|
49
|
-
Defaults are `proxy.global: []` and `proxy.rules: []`. Every node receives
|
|
53
|
+
Defaults are `proxy.nodes.global: []` and `proxy.nodes.rules: []`. Every node receives
|
|
50
54
|
all global destinations plus `allow` from **every** matching rule. Duplicate entries
|
|
51
55
|
are removed while preserving configuration order. A rule matches when any of its
|
|
52
56
|
`hosts` patterns matches the complete lowercase hostname. Patterns must be lowercase.
|
|
@@ -66,14 +70,14 @@ Destination rules are separate from hostname globs:
|
|
|
66
70
|
Only HTTP port 80 and HTTPS CONNECT port 443 are allowed. Allowed DNS destinations
|
|
67
71
|
are reachable regardless of their target IP range, including private, loopback,
|
|
68
72
|
link-local and reserved addresses. Domains without a matching allow rule remain
|
|
69
|
-
denied. There is no TLS interception.
|
|
70
|
-
|
|
73
|
+
denied. There is no TLS interception. Node destinations never grant infrastructure
|
|
74
|
+
access, and infrastructure destinations never grant node access.
|
|
71
75
|
|
|
72
76
|
## Node policy and reconciliation
|
|
73
77
|
|
|
74
|
-
When `proxy.enabled` is true, all nodes are authorized as proxy clients. Nodes get
|
|
78
|
+
When `proxy.nodes.enabled` is true, all nodes are authorized as proxy clients. Nodes get
|
|
75
79
|
upper/lowercase proxy variables and internal `NO_PROXY` entries, including explicitly
|
|
76
|
-
configured `network.egress` hosts. Those named hosts use the common workspace
|
|
80
|
+
configured `network.egress.nodes` hosts. Those named hosts use the common workspace
|
|
77
81
|
gateway for their configured ports. VM access uses the same destination policy bound
|
|
78
82
|
directly to the VM instance's reserved peer address. The proxy is reached at
|
|
79
83
|
`proxy.empeira.internal:3128` on the isolated network. A request without a matching
|
|
@@ -90,17 +94,56 @@ Container and VM nodes consume the new policy without a restart; open proxy
|
|
|
90
94
|
connections can end during reconciliation. Enabling/disabling proxy mode is a
|
|
91
95
|
separate topology change and may require explicit node cleanup as reported by `up`.
|
|
92
96
|
|
|
93
|
-
Changing
|
|
94
|
-
environment. `up` replaces a server whose environment definition changes while
|
|
97
|
+
Changing `network.egress.infrastructure` destinations also changes infrastructure
|
|
98
|
+
`NO_PROXY`. Changing node destinations does not change the server environment. `up` replaces a server whose environment definition changes while
|
|
95
99
|
retaining CA storage and node identities. Changing only destination ports or
|
|
96
100
|
transparent redirect rules preserves the server when its definition is unchanged.
|
|
97
101
|
|
|
102
|
+
## Infrastructure policy
|
|
103
|
+
|
|
104
|
+
`proxy.infrastructure.enabled` controls the Puppet/OpenVox server, PostgreSQL,
|
|
105
|
+
PuppetDB backend/relay and `containers.additional` services. Its independent
|
|
106
|
+
`allow` list uses the same exact-domain and domain-family syntax as node grants.
|
|
107
|
+
The browser, DNS, Squid and gateway transports do not receive these application
|
|
108
|
+
proxy settings.
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
proxy:
|
|
112
|
+
infrastructure:
|
|
113
|
+
enabled: true
|
|
114
|
+
allow: [api.example.net]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
When disabled, infrastructure definitions contain empty proxy values, overriding
|
|
118
|
+
any image defaults. They contain no configured proxy endpoint and receive no Squid
|
|
119
|
+
client grants, even if the node proxy is enabled. No startup wrapper removes an
|
|
120
|
+
otherwise configured proxy. Internal names and mocks remain directly reachable.
|
|
121
|
+
When enabled, only infrastructure's own direct destinations enter its `NO_PROXY`.
|
|
122
|
+
Explicit environment settings in additional-service definitions remain application
|
|
123
|
+
configuration; they do not add Squid permissions.
|
|
124
|
+
|
|
125
|
+
`up` creates the shared normal proxy when either scope is enabled and removes it
|
|
126
|
+
when both are disabled. Changing infrastructure's switch updates affected service
|
|
127
|
+
definitions while retaining storage and node identities. Allowlist-only edits reload
|
|
128
|
+
Squid without replacing unrelated services. Direct grants remain independent of
|
|
129
|
+
both proxy switches; keep `network.egress.infrastructure: []` for internal-only
|
|
130
|
+
infrastructure.
|
|
131
|
+
|
|
132
|
+
## Migrating shared settings
|
|
133
|
+
|
|
134
|
+
The former `proxy.enabled`, `proxy.global` and `proxy.rules` keys are rejected with
|
|
135
|
+
a migration diagnostic. Move them under `proxy.nodes`; configure infrastructure
|
|
136
|
+
explicitly with its own switch and `allow` list. Old shared `network.egress` arrays
|
|
137
|
+
and legacy `mode` values are also rejected. Move grants to `network.egress.nodes`
|
|
138
|
+
or `network.egress.infrastructure` according to the intended clients. No existing
|
|
139
|
+
grant is silently copied to infrastructure.
|
|
140
|
+
|
|
98
141
|
## Managed bootstrap
|
|
99
142
|
|
|
100
143
|
Managed bootstrap destinations are separate from normal user policy. Reviewed
|
|
101
144
|
agent and distribution endpoints in `agent.install` and `bootstrap.guests` allow
|
|
102
145
|
Puppet/OpenVox and required guest packages to be installed without duplicating them
|
|
103
|
-
in `proxy.global`. The temporary authenticated bootstrap proxy uses workspace DNS
|
|
146
|
+
in `proxy.nodes.global`. The temporary authenticated bootstrap proxy uses workspace DNS
|
|
104
147
|
and retains its private, loopback, link-local and reserved destination-address
|
|
105
148
|
restrictions independently of normal proxy policy. Its credential is not stored in
|
|
106
149
|
the guest seed, JSON inventory or logs. A /32 source ACL admits only the provisioning
|
data/docs/troubleshooting.md
CHANGED
|
@@ -132,7 +132,7 @@ or restart of that same VM conflicts with the shared instance guard: detach with
|
|
|
132
132
|
## Proxy destination denied
|
|
133
133
|
|
|
134
134
|
Run `empeira proxy show HOSTNAME` and check every matching rule. Verify the node
|
|
135
|
-
exists in the workspace, `proxy.enabled` is true, and `up` has reconciled the
|
|
135
|
+
exists in the workspace, `proxy.nodes.enabled` is true, and `up` has reconciled the
|
|
136
136
|
configuration and current node addresses. Allowed DNS destinations may resolve to
|
|
137
137
|
private or reserved IP addresses; only HTTP port 80 and HTTPS CONNECT port 443 are
|
|
138
138
|
permitted. After updating Empeira, run `empeira up` to replace a proxy still using
|
|
@@ -367,6 +367,6 @@ external runtime network as a workaround. Docker workspace bridge attachment
|
|
|
367
367
|
errors require an accessible Linux-engine host namespace and an identifiable
|
|
368
368
|
iptables backend. The helper changes only owned bridge rules. Do not disable the
|
|
369
369
|
host firewall globally. Check Docker Desktop host-network restrictions separately.
|
|
370
|
-
Proxy errors require checking Squid source bindings and `proxy.global`/`proxy.rules`,
|
|
370
|
+
Proxy errors require checking Squid source bindings and `proxy.nodes.global`/`proxy.nodes.rules`,
|
|
371
371
|
not adding unrelated direct grants. Corporate DNS still follows host discovery;
|
|
372
372
|
IPv6-only upstreams must be replaced with reachable IPv4 resolvers for this backend.
|
data/lib/empeira/build.json
CHANGED
data/lib/empeira/cli/proxy.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Empeira
|
|
|
5
5
|
class Proxy < Base
|
|
6
6
|
desc 'show HOSTNAME', 'Explain the normal proxy destination policy for a hostname'
|
|
7
7
|
def show(hostname)
|
|
8
|
-
config = application.context.configuration.
|
|
8
|
+
config = application.context.configuration.dig('proxy', 'nodes')
|
|
9
9
|
resolver = Network::HostPolicy.new(config)
|
|
10
10
|
say "Effective proxy policy for #{hostname.downcase}"
|
|
11
11
|
say access_message(config)
|
|
@@ -19,7 +19,7 @@ module Empeira
|
|
|
19
19
|
return "All workspace nodes may use this policy after 'empeira up' reconciles the proxy." if
|
|
20
20
|
config.fetch('enabled')
|
|
21
21
|
|
|
22
|
-
'The normal
|
|
22
|
+
'The normal node proxy is disabled.'
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def display_matches(rules)
|
|
@@ -20,8 +20,10 @@ module Empeira
|
|
|
20
20
|
'environment' => :environment,
|
|
21
21
|
'memory' => :positive_integer, 'cpus' => :positive_integer, 'timeout' => :positive_integer },
|
|
22
22
|
'puppetdb' => { 'enabled' => :boolean, 'memory' => :positive_integer, 'runtime' => :puppetdb_runtime },
|
|
23
|
-
'network' => { 'egress' => :network_egress, '
|
|
24
|
-
|
|
23
|
+
'network' => { 'egress' => { 'nodes' => :network_egress, 'infrastructure' => :network_egress },
|
|
24
|
+
'redirects' => :network_redirects },
|
|
25
|
+
'proxy' => { 'nodes' => { 'enabled' => :boolean, 'global' => :allowlist, 'rules' => :proxy_rules },
|
|
26
|
+
'infrastructure' => { 'enabled' => :boolean, 'allow' => :allowlist } },
|
|
25
27
|
'vm' => { 'disk' => :vm_disk, 'interfaces' => :vm_interfaces,
|
|
26
28
|
'console' => { 'root_password' => :optional_string } },
|
|
27
29
|
'dns' => { 'upstream' => { 'mode' => :dns_mode, 'servers' => :dns_servers },
|
|
@@ -116,12 +118,35 @@ module Empeira
|
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
def validate_fragment!(data)
|
|
121
|
+
validate_scope_migration!(data)
|
|
119
122
|
validate_mapping!(data, RULES, [])
|
|
120
123
|
data
|
|
121
124
|
end
|
|
122
125
|
|
|
123
126
|
private
|
|
124
127
|
|
|
128
|
+
def validate_scope_migration!(data)
|
|
129
|
+
return unless data.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
validate_proxy_scope_migration!(data['proxy'])
|
|
132
|
+
network = data['network']
|
|
133
|
+
validate_egress_scope_migration!(network['egress']) if network.is_a?(Hash)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def validate_proxy_scope_migration!(proxy)
|
|
137
|
+
return unless proxy.is_a?(Hash) && proxy.keys.intersect?(%w[enabled global rules])
|
|
138
|
+
|
|
139
|
+
raise ConfigurationError, 'Shared proxy settings have moved to proxy.nodes; configure ' \
|
|
140
|
+
'proxy.infrastructure separately'
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_egress_scope_migration!(egress)
|
|
144
|
+
return unless egress.is_a?(Array) || (egress.is_a?(Hash) && egress.key?('mode'))
|
|
145
|
+
|
|
146
|
+
raise ConfigurationError, 'Shared network.egress grants must be moved explicitly to ' \
|
|
147
|
+
'network.egress.nodes or network.egress.infrastructure'
|
|
148
|
+
end
|
|
149
|
+
|
|
125
150
|
def validate_operational!(data)
|
|
126
151
|
validate_images!(data.fetch('images'))
|
|
127
152
|
NodeImages.effective!(data.fetch('images'))
|
|
@@ -254,8 +254,10 @@ module Empeira
|
|
|
254
254
|
end
|
|
255
255
|
|
|
256
256
|
def resolve_direct_egress
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
entries = @plan.config.dig('network', 'egress').flat_map do |scope, grants|
|
|
258
|
+
@direct_resolver.resolve(grants).entries.map { |entry| entry.merge('scope' => scope) }
|
|
259
|
+
end
|
|
260
|
+
Network::DirectEgress::Resolved.new(entries)
|
|
259
261
|
end
|
|
260
262
|
|
|
261
263
|
def bootstrap_dns?(upstreams:, routes:)
|
|
@@ -636,6 +638,7 @@ module Empeira
|
|
|
636
638
|
end
|
|
637
639
|
reconcile('gateway')
|
|
638
640
|
lockdown_gateway(@observed.fetch('gateway'))
|
|
641
|
+
require_scoped_egress_capability
|
|
639
642
|
require_redirect_capability
|
|
640
643
|
attach_gateway('gateway')
|
|
641
644
|
apply_gateway_policy
|
|
@@ -668,6 +671,16 @@ module Empeira
|
|
|
668
671
|
'images.direct_egress image'
|
|
669
672
|
end
|
|
670
673
|
|
|
674
|
+
def require_scoped_egress_capability
|
|
675
|
+
return if @gateway_plan.fetch('entries').empty?
|
|
676
|
+
|
|
677
|
+
result = @runtime.service_exec(@observed.fetch('gateway'),
|
|
678
|
+
[Network::Gateway::EXECUTABLE, 'scoped-egress-capability'])
|
|
679
|
+
return if result.success? && result.stdout.strip == 'scoped-egress-v1'
|
|
680
|
+
|
|
681
|
+
raise Error, 'Gateway does not support scoped direct egress; configure a compatible images.direct_egress image'
|
|
682
|
+
end
|
|
683
|
+
|
|
671
684
|
def apply_gateway_policy
|
|
672
685
|
# An uncertain apply must force fresh lockdown during failure recovery.
|
|
673
686
|
@gateway_locked_id = nil
|
|
@@ -116,10 +116,7 @@ module Empeira
|
|
|
116
116
|
bindings = Network::ProxyBindings.new(context: @context, resources: resources, network: network, nodes: nodes)
|
|
117
117
|
# The workspace parent is private; Squid's mapped service user must read the bind mount.
|
|
118
118
|
rules_changed = write('proxy-rules.conf', bindings.configuration)
|
|
119
|
-
|
|
120
|
-
addresses = resources.slice(*allowed).values.filter_map do |resource|
|
|
121
|
-
resource&.dig('networks', network, 'IPAddress')
|
|
122
|
-
end
|
|
119
|
+
addresses = bindings.addresses
|
|
123
120
|
clients_changed = write('proxy-clients', "#{(['127.0.0.1/32'] + addresses.reject(&:empty?).sort).join("\n")}\n")
|
|
124
121
|
rules_changed || clients_changed
|
|
125
122
|
end
|
|
@@ -131,10 +128,6 @@ module Empeira
|
|
|
131
128
|
Infrastructure::Definition.fingerprint(content)
|
|
132
129
|
end
|
|
133
130
|
|
|
134
|
-
def proxy_allowed(nodes)
|
|
135
|
-
['server', *nodes.keys]
|
|
136
|
-
end
|
|
137
|
-
|
|
138
131
|
private
|
|
139
132
|
|
|
140
133
|
def corefile_content(upstreams:, routes:, bootstrap: false)
|
|
@@ -27,8 +27,7 @@ module Empeira
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def proxy?
|
|
30
|
-
|
|
31
|
-
config.dig('proxy', 'enabled') || (egress.is_a?(Hash) && egress['mode'] == 'proxy')
|
|
30
|
+
%w[nodes infrastructure].any? { |scope| config.dig('proxy', scope, 'enabled') }
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
def database?
|
|
@@ -63,7 +62,7 @@ module Empeira
|
|
|
63
62
|
def additional_services(common = {})
|
|
64
63
|
config.dig('containers', 'additional').each_with_index.to_h do |entry, index|
|
|
65
64
|
options = { image: image_reference(entry.fetch('image')), memory: 256,
|
|
66
|
-
environment:
|
|
65
|
+
environment: infrastructure_proxy_environment.merge(entry.fetch('environment', {})),
|
|
67
66
|
command: entry.fetch('command', []) }
|
|
68
67
|
location = "containers.additional[#{index}].configuration"
|
|
69
68
|
generated = additional_configurations.options(entry, mounts: common.fetch(:mounts, []), location: location)
|
|
@@ -175,9 +174,9 @@ module Empeira
|
|
|
175
174
|
end
|
|
176
175
|
|
|
177
176
|
def node_proxy_environment
|
|
178
|
-
return {} unless proxy
|
|
177
|
+
return {} unless config.dig('proxy', 'nodes', 'enabled')
|
|
179
178
|
|
|
180
|
-
proxy_environment_for("http://#{naming.hostname('proxy')}:3128")
|
|
179
|
+
proxy_environment_for("http://#{naming.hostname('proxy')}:3128", scope: 'nodes')
|
|
181
180
|
end
|
|
182
181
|
|
|
183
182
|
private
|
|
@@ -209,9 +208,10 @@ module Empeira
|
|
|
209
208
|
def database_services(common)
|
|
210
209
|
return {} unless database?
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
'puppetdb' => service('puppetdb', **
|
|
211
|
+
options = common.merge(environment: infrastructure_proxy_environment)
|
|
212
|
+
{ 'postgres' => service('postgres', **options, **postgres),
|
|
213
|
+
'puppetdb-backend' => service('puppetdb-backend', **options, **puppetdb),
|
|
214
|
+
'puppetdb' => service('puppetdb', **options, **puppetdb_relay(dns: common[:dns])) }
|
|
215
215
|
end
|
|
216
216
|
|
|
217
217
|
def server
|
|
@@ -271,15 +271,16 @@ module Empeira
|
|
|
271
271
|
reports: database? ? 'puppetdb' : 'log',
|
|
272
272
|
# Upstream entrypoints otherwise rewrite puppetdb.conf from their default URL.
|
|
273
273
|
puppetdb_server_urls: ''
|
|
274
|
-
).merge(
|
|
274
|
+
).merge(infrastructure_proxy_environment)
|
|
275
275
|
end
|
|
276
276
|
|
|
277
|
-
def
|
|
278
|
-
|
|
277
|
+
def infrastructure_proxy_environment
|
|
278
|
+
enabled = config.dig('proxy', 'infrastructure', 'enabled')
|
|
279
|
+
proxy_environment_for(enabled ? "http://#{naming.hostname('proxy')}:3128" : '', scope: 'infrastructure')
|
|
279
280
|
end
|
|
280
281
|
|
|
281
|
-
def proxy_environment_for(url)
|
|
282
|
-
bypass = Network::DirectEgress.destinations(config.dig('network', 'egress'))
|
|
282
|
+
def proxy_environment_for(url, scope:)
|
|
283
|
+
bypass = Network::DirectEgress.destinations(config.dig('network', 'egress', scope))
|
|
283
284
|
no_proxy = naming.no_proxy(bypass)
|
|
284
285
|
{ 'http_proxy' => url, 'https_proxy' => url, 'HTTP_PROXY' => url, 'HTTPS_PROXY' => url,
|
|
285
286
|
'all_proxy' => '', 'ALL_PROXY' => '', 'no_proxy' => no_proxy, 'NO_PROXY' => no_proxy }
|
|
@@ -289,7 +290,8 @@ module Empeira
|
|
|
289
290
|
{ image: image_reference(config.dig('images', 'postgres')), memory: 256,
|
|
290
291
|
env_file: files.secret_path,
|
|
291
292
|
configuration: files.configuration.fetch('database-setup.sql'),
|
|
292
|
-
environment:
|
|
293
|
+
environment: infrastructure_proxy_environment.merge('POSTGRES_USER' => 'postgres',
|
|
294
|
+
'POSTGRES_DB' => 'puppetdb'),
|
|
293
295
|
mounts: [storage('postgres-data', '/var/lib/postgresql/data'),
|
|
294
296
|
bind(files.path('database-setup.sql'), '/empeira/setup.sql')],
|
|
295
297
|
command: ['postgres', '-c', 'shared_buffers=64MB', '-c', 'max_connections=50'] }
|
|
@@ -24,7 +24,6 @@ module Empeira
|
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
26
|
def validate!(value, path = 'network.egress')
|
|
27
|
-
return legacy!(value, path) if value.is_a?(Hash)
|
|
28
27
|
unless value.is_a?(Array) && value.size <= MAX_HOSTS
|
|
29
28
|
raise ConfigurationError, "#{path} must be an array with at most #{MAX_HOSTS} destination entries"
|
|
30
29
|
end
|
|
@@ -33,12 +32,6 @@ module Empeira
|
|
|
33
32
|
raise ConfigurationError, "#{path} contains duplicate destinations" unless hosts.uniq.size == hosts.size
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
def legacy!(value, path)
|
|
37
|
-
return if value.keys == ['mode'] && %w[none proxy].include?(value['mode'])
|
|
38
|
-
|
|
39
|
-
raise ConfigurationError, "#{path} must be a host array (legacy mode accepts only none or proxy)"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
35
|
def destinations(value)
|
|
43
36
|
return [] unless value.is_a?(Array)
|
|
44
37
|
|
|
@@ -22,7 +22,15 @@ module Empeira
|
|
|
22
22
|
{ 'version' => 1, 'subnet' => subnet, 'gateway' => address(subnet),
|
|
23
23
|
'dns' => address(subnet, 'dns'), 'resolvers' => resolvers,
|
|
24
24
|
'proxies' => %w[proxy bootstrap-proxy].map { |key| address(subnet, key) },
|
|
25
|
-
'blocked' => blocked_addresses(state), 'entries' => resolved
|
|
25
|
+
'blocked' => blocked_addresses(state), 'entries' => scoped_entries(resolved, subnet),
|
|
26
|
+
'redirects' => redirects }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.scoped_entries(resolved, subnet)
|
|
30
|
+
layout = Peer::Layout.new(subnet)
|
|
31
|
+
resolved.entries.map do |entry|
|
|
32
|
+
entry.except('scope').merge('sources' => layout.egress_sources(entry.fetch('scope')))
|
|
33
|
+
end
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def self.blocked_addresses(state)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Empeira
|
|
4
4
|
module Network
|
|
5
5
|
class HostPolicy
|
|
6
|
-
def self.validate!(rules, path = 'proxy.rules')
|
|
6
|
+
def self.validate!(rules, path = 'proxy.nodes.rules')
|
|
7
7
|
raise ConfigurationError, "#{path} must be an array" unless rules.is_a?(Array)
|
|
8
8
|
|
|
9
9
|
rules.each_with_index { |rule, index| validate_rule!(rule, "#{path}.#{index}") }
|
|
@@ -28,6 +28,14 @@ module Empeira
|
|
|
28
28
|
"#{address(128)}/25"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def egress_sources(scope)
|
|
32
|
+
case scope
|
|
33
|
+
when 'nodes' then ["#{address(32)}/27", "#{address(64)}/26"]
|
|
34
|
+
when 'infrastructure' then [container_pool]
|
|
35
|
+
else raise Error, 'Unknown direct-egress scope'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
def lease(nodes)
|
|
32
40
|
used = nodes.values.filter_map { |record| record.dig('peer', 'ip') }
|
|
33
41
|
ip = (32..95).map { |offset| address(offset) }.find { |candidate| !used.include?(candidate) }
|
|
@@ -12,22 +12,39 @@ module Empeira
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def configuration
|
|
15
|
-
entries =
|
|
16
|
-
rules(
|
|
15
|
+
entries = bindings.filter_map do |label, hostname, domains|
|
|
16
|
+
rules(label, address(hostname), domains)
|
|
17
17
|
end
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
"#{entries.join("\n")}\n"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def addresses
|
|
22
|
+
bindings.filter_map { |_, hostname, _| address(hostname) }.uniq
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
private
|
|
25
26
|
|
|
26
|
-
def
|
|
27
|
-
|
|
27
|
+
def bindings
|
|
28
|
+
infrastructure_bindings + node_bindings
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def infrastructure_bindings
|
|
32
|
+
scope = @config.fetch('infrastructure')
|
|
33
|
+
return [] unless scope.fetch('enabled')
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
names = %w[server postgres puppetdb-backend puppetdb] +
|
|
36
|
+
@context.configuration.dig('containers', 'additional').map { |service| service.fetch('name') }
|
|
37
|
+
names.map { |name| [name.tr('-', '_'), name, scope.fetch('allow')] }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def node_bindings
|
|
41
|
+
scope = @config.fetch('nodes')
|
|
42
|
+
return [] unless scope.fetch('enabled')
|
|
43
|
+
|
|
44
|
+
policy = HostPolicy.new(scope)
|
|
45
|
+
@nodes.sort.each_with_index.filter_map do |(hostname, record), index|
|
|
46
|
+
["node_#{index}", hostname, policy.resolve(hostname)] unless record['network_phase'] == 'bootstrap'
|
|
47
|
+
end
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
def address(name)
|
|
@@ -63,9 +63,9 @@ module Empeira
|
|
|
63
63
|
|
|
64
64
|
naming = Network::Naming.new
|
|
65
65
|
internal = naming.reserved(@context.configuration).flat_map { |name| naming.aliases(name) }
|
|
66
|
+
bypass = Network::DirectEgress.destinations(@context.configuration.dig('network', 'egress', 'nodes'))
|
|
66
67
|
{ 'url' => environment.fetch('http_proxy'), 'no_proxy' => environment.fetch('no_proxy'),
|
|
67
|
-
'direct' => ['localhost', '127.0.0.1', *internal,
|
|
68
|
-
*Network::DirectEgress.destinations(@context.configuration.dig('network', 'egress'))].uniq }
|
|
68
|
+
'direct' => ['localhost', '127.0.0.1', *internal, *bypass].uniq }
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def execute(desired, previous)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Empeira
|
|
4
4
|
module Runtime
|
|
5
5
|
class ServiceArguments
|
|
6
|
+
PROXY_VARIABLES = %w[http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY no_proxy NO_PROXY].freeze
|
|
6
7
|
SCALARS = { 'dns' => '--dns', 'user' => '--user', 'entrypoint' => '--entrypoint',
|
|
7
8
|
'env_file' => '--env-file', 'shm_size' => '--shm-size', 'mac_address' => '--mac-address',
|
|
8
9
|
'ip' => '--ip', 'runtime_ip' => '--ip' }.freeze
|
|
@@ -20,7 +21,7 @@ module Empeira
|
|
|
20
21
|
'--network', @options.fetch('network'), '--memory', "#{@options.fetch('memory')}m",
|
|
21
22
|
'--cpus', @options.fetch('cpus', 1).to_s, '--no-healthcheck',
|
|
22
23
|
*init_options, *security_options, *pairs('--label', @definition.labels), *scalar_options,
|
|
23
|
-
*pairs('--env',
|
|
24
|
+
*pairs('--env', environment),
|
|
24
25
|
*pairs('--sysctl', @options.fetch('sysctls', {})),
|
|
25
26
|
*@options.fetch('mounts', []).flat_map { |mount| ['--mount', mount] },
|
|
26
27
|
*@options.fetch('ports', []).flat_map { |port| ['--publish', port] },
|
|
@@ -29,6 +30,11 @@ module Empeira
|
|
|
29
30
|
|
|
30
31
|
private
|
|
31
32
|
|
|
33
|
+
def environment
|
|
34
|
+
PROXY_VARIABLES.to_h { |key| [key, ''] }.merge(@options.fetch('environment', {}))
|
|
35
|
+
.merge(@options.fetch('runtime_environment', {}))
|
|
36
|
+
end
|
|
37
|
+
|
|
32
38
|
def security_options
|
|
33
39
|
values = { 'cap_drop' => '--cap-drop', 'cap_add' => '--cap-add', 'devices' => '--device',
|
|
34
40
|
'security_options' => '--security-opt' }
|
|
@@ -111,17 +111,13 @@ module EmpeiraGateway
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def direct_rules(plan, internal, external)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"-d #{ip}/32 -p tcp --dport #{port} -j ACCEPT"
|
|
120
|
-
filter << reply(internal, external, destination: ip, port: port)
|
|
121
|
-
end
|
|
114
|
+
plan.fetch('entries').flat_map do |entry|
|
|
115
|
+
entry.fetch('sources').product(entry.fetch('addresses'), entry.fetch('ports')).flat_map do |source, ip, port|
|
|
116
|
+
["-A FORWARD -i #{internal} -o #{external} -s #{source} " \
|
|
117
|
+
"-d #{ip}/32 -p tcp --dport #{port} -j ACCEPT",
|
|
118
|
+
reply(internal, external, source: source, destination: ip, port: port)]
|
|
122
119
|
end
|
|
123
120
|
end
|
|
124
|
-
filter
|
|
125
121
|
end
|
|
126
122
|
|
|
127
123
|
def reply(internal, external, port:, source: nil, destination: nil, protocol: 'tcp')
|
|
@@ -145,7 +141,7 @@ module EmpeiraGateway
|
|
|
145
141
|
raise 'Invalid workspace subnet' unless subnet.ipv4? && subnet.private? && subnet.prefix == 24
|
|
146
142
|
|
|
147
143
|
validate_addresses(plan, subnet)
|
|
148
|
-
validate_entries(plan.fetch('entries'))
|
|
144
|
+
validate_entries(plan.fetch('entries'), subnet)
|
|
149
145
|
validate_redirects(plan.fetch('redirects', []), subnet)
|
|
150
146
|
end
|
|
151
147
|
|
|
@@ -197,14 +193,27 @@ module EmpeiraGateway
|
|
|
197
193
|
%w[resolvers proxies blocked].each { |key| plan.fetch(key, []).each { |ip| ipv4(ip) } }
|
|
198
194
|
end
|
|
199
195
|
|
|
200
|
-
def validate_entries(entries)
|
|
196
|
+
def validate_entries(entries, subnet)
|
|
201
197
|
entries.each do |entry|
|
|
198
|
+
validate_sources(entry.fetch('sources'), subnet)
|
|
202
199
|
entry.fetch('addresses').each { |ip| ipv4(ip) }
|
|
203
200
|
valid = entry.fetch('ports').all? { |port| port.is_a?(Integer) && port.between?(1, 65_535) }
|
|
204
201
|
raise 'Invalid gateway ports' unless valid
|
|
205
202
|
end
|
|
206
203
|
end
|
|
207
204
|
|
|
205
|
+
def validate_sources(sources, subnet)
|
|
206
|
+
valid = sources.is_a?(Array) && !sources.empty? && sources.all? { |source| valid_source?(source, subnet) }
|
|
207
|
+
raise 'Invalid direct-egress source ranges' unless valid
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def valid_source?(source, subnet)
|
|
211
|
+
range = IPAddr.new(source)
|
|
212
|
+
range.ipv4? && range.prefix > subnet.prefix && subnet.include?(range) && "#{range}/#{range.prefix}" == source
|
|
213
|
+
rescue IPAddr::InvalidAddressError, TypeError
|
|
214
|
+
false
|
|
215
|
+
end
|
|
216
|
+
|
|
208
217
|
def ipv4_interfaces
|
|
209
218
|
Socket.getifaddrs.select { |entry| entry.addr&.ipv4? && !entry.addr.ipv4_loopback? }
|
|
210
219
|
end
|
|
@@ -301,6 +310,7 @@ module EmpeiraGateway
|
|
|
301
310
|
when 'redirects-capability'
|
|
302
311
|
command('conntrack', '--version')
|
|
303
312
|
puts 'tcp-udp-redirects-v2'
|
|
313
|
+
when 'scoped-egress-capability' then puts 'scoped-egress-v1'
|
|
304
314
|
when 'lockdown'
|
|
305
315
|
restore(lockdown)
|
|
306
316
|
when 'apply' then apply(arguments.fetch(1))
|