Skip to content

Commit 136bb36

Browse files
committed
Promote gateway real-internet mode & service_mirrors to experimental
### Motivation - The gateway nftables policies and mirrored-mode operator output are now wired, so `gateway_portal.real_internet.mode` and `gateway_portal.real_internet.service_mirrors` should be represented as `experimental` rather than `unsupported` in the feature-state registry and docs. - Tests and CLI validation must reflect that mirrored mode validates with a warning instead of failing alpha validation, while truly unsupported flags remain CI-failing. ### Description - Update feature-state registry in `netengine/spec/feature_state.py` to mark `gateway_portal.real_internet.mode` and `gateway_portal.real_internet.service_mirrors` as `experimental` and add explanatory caveats. - Update documentation rows in `docs/spec-alpha-support.md` and `docs/support-matrix.md` to document the promoted experimental state and caveats about remaining real-host integration and DNS aliasing. - Adjust CLI/tests in `tests/test_cli.py` so a spec with `gateway_portal.real_internet.mode: mirrored` validates successfully with `--explain` (shows a warning) and change the JSON CI-failure test to target `upstream_resolver_enabled` which remains `unsupported`. - Adjust spec parsing test in `tests/test_spec_parsing.py` to assert the loader logs an experimental warning for mirrored gateway mode instead of raising `SpecLoadError`. ### Testing - Ran `poetry run pytest tests/test_spec_parsing.py tests/test_cli.py tests/test_gateway_portal.py tests/test_feature_states.py` and those targeted tests passed. - Ran the full suite with `poetry run pytest`; the full run completed but showed 2 unrelated integration failures (`TestFullMVPLifecycle.test_full_mvp_lifecycle` returned `401` for reload and `TestHealthRoute.test_health_does_not_leak_last_error_detail` exposed `last_error`) that pre-existed this change. - No new regressions observed in gateway rule generation or unit tests covering gateway portal behavior.
1 parent 76b303d commit 136bb36

6 files changed

Lines changed: 99 additions & 6 deletions

File tree

docs/netengines-spec-v1.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,11 +447,13 @@ gateway_portal:
447447
# custom — explicit rule list (use when presets don't fit)
448448

449449
# Required if mode: mirrored
450+
# Alpha: in_world_service must be an IPv4 address because gateway rules
451+
# render nftables ip daddr allowlists; DNS aliasing for names is planned.
450452
# service_mirrors:
451453
# - real_hostname: s3.amazonaws.com
452-
# in_world_service: storage.platform.internal
454+
# in_world_service: 10.0.0.11
453455
# - real_hostname: github.com
454-
# in_world_service: gitea.acme.internal
456+
# in_world_service: 10.0.1.20
455457

456458
# Required if mode: custom
457459
# rules: []

docs/spec-alpha-support.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Feature states:
2727
| `pki.ocsp_enabled` | `experimental` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | step-ca OCSP config is injected and the responder URL is published in Phase 3 output; responder lifecycle/verification is hardened in CI e2e. |
2828
| `pki.rotation_policy` | `experimental` | `{enabled: true, default_interval_hours: 24, default_warning_days: 30, cert_type_overrides: {}}` | `netengine.handlers.phase_pki`, `netengine.workers.pki_cert_rotation_worker`, `netengine.api.routes` | Wired from the spec into worker registration and live-reloaded from runtime state; policy shape and cert-type semantics may change during alpha. |
2929
| `gateway_portal.real_internet.mode` | `experimental` | `isolated` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Isolated, shadowed, mirrored, and exposed nftables policies are wired; real-host integration remains alpha. |
30-
| `gateway_portal.real_internet.service_mirrors` | `experimental` | `[]` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Mirrored-mode allowlists and operator output are wired; automatic DNS aliasing for real hostnames remains alpha. |
30+
| `gateway_portal.real_internet.service_mirrors` | `experimental` | `[]` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Mirrored-mode allowlists and operator output are wired; `in_world_service` must be an IPv4 address until automatic DNS aliasing for real hostnames lands. |
3131
| `gateway_portal.real_internet.upstream_resolver_enabled` | `unsupported` | `false` | `netengine.spec.loader` | Upstream resolver forwarding is not implemented. |
3232
| `gateway_portal.cross_world.mode` | `unsupported` | `none` | `netengine.spec.loader` | Cross-world federation is not implemented. |
3333
| `gateway_portal.cross_world.peers` | `unsupported` | `[]` | `netengine.spec.loader` | Cross-world peer provisioning is not implemented. |

docs/support-matrix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Feature states:
2727
| `pki.ocsp_enabled` | `unsupported` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | Handler code can inject OCSP-related step-ca config and report a URL, but responder deployment/verification is incomplete and active use is unsupported in alpha validation. |
2828
| `pki.rotation_policy` | `experimental` | `{enabled: true, default_interval_hours: 24, default_warning_days: 30, cert_type_overrides: {}}` | `netengine.handlers.phase_pki`, `netengine.workers.pki_cert_rotation_worker`, `netengine.api.routes` | Wired from the spec into worker registration and live-reloaded from runtime state; policy shape and cert-type semantics may change during alpha. |
2929
| `gateway_portal.real_internet.mode` | `experimental` | `isolated` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Isolated, shadowed, mirrored, and exposed nftables policies are wired; real-host integration remains alpha. |
30-
| `gateway_portal.real_internet.service_mirrors` | `experimental` | `[]` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Mirrored-mode allowlists and operator output are wired; automatic DNS aliasing for real hostnames remains alpha. |
30+
| `gateway_portal.real_internet.service_mirrors` | `experimental` | `[]` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Mirrored-mode allowlists and operator output are wired; `in_world_service` must be an IPv4 address until automatic DNS aliasing for real hostnames lands. |
3131
| `gateway_portal.real_internet.upstream_resolver_enabled` | `unsupported` | `false` | `netengine.spec.loader` | Upstream resolver forwarding is not implemented. |
3232
| `gateway_portal.cross_world.mode` | `unsupported` | `none` | `netengine.spec.loader` | Cross-world federation is not implemented. |
3333
| `gateway_portal.cross_world.peers` | `unsupported` | `[]` | `netengine.spec.loader` | Cross-world peer provisioning is not implemented. |

netengine/spec/loader.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from netengine.config.loader import ConfigLoader
1313
from netengine.spec.models import SUPPORTED_SPEC_SCHEMA_VERSIONS, NetEngineSpec
14+
from netengine.spec.types import GatewayRealInternetMode
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -155,6 +156,32 @@ def _cross_validate(spec: NetEngineSpec) -> None:
155156
if net_a.overlaps(net_b):
156157
errors.append(f"subnet overlap: {name_a} ({net_a}) overlaps {name_b} ({net_b})")
157158

159+
# 5. Mirrored real-internet mode needs explicit IP allowlist targets.
160+
# The alpha gateway implementation renders nftables `ip daddr` rules from
161+
# service_mirrors, so hostnames would generate invalid nftables policy until
162+
# DNS aliasing/resolution is implemented.
163+
real_internet = spec.gateway_portal.real_internet
164+
if real_internet.mode == GatewayRealInternetMode.MIRRORED:
165+
if not real_internet.service_mirrors:
166+
errors.append(
167+
"gateway_portal.real_internet.service_mirrors: "
168+
"at least one service mirror is required when mode is 'mirrored'"
169+
)
170+
elif real_internet.service_mirrors:
171+
errors.append(
172+
"gateway_portal.real_internet.service_mirrors: "
173+
"service mirrors require real_internet.mode: 'mirrored'"
174+
)
175+
176+
for idx, mirror in enumerate(real_internet.service_mirrors):
177+
try:
178+
ipaddress.IPv4Address(mirror.in_world_service)
179+
except ValueError:
180+
errors.append(
181+
f"gateway_portal.real_internet.service_mirrors[{idx}].in_world_service: "
182+
"must be an IPv4 address for alpha mirrored gateway nftables rules"
183+
)
184+
158185
if errors:
159186
raise SpecLoadError(
160187
"Spec cross-validation failed:\n" + "\n".join(f" - {e}" for e in errors)

tests/test_cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,13 @@ def test_validate_valid_spec_exits_zero() -> None:
435435

436436
def test_validate_experimental_gateway_mode_exits_zero_with_explanation(tmp_path: Path) -> None:
437437
"""Mirrored gateway mode is wired but remains experimental during alpha."""
438-
spec_file = _write_cli_validate_spec(tmp_path, gateway_portal__real_internet__mode="mirrored")
438+
spec_file = _write_cli_validate_spec(
439+
tmp_path,
440+
gateway_portal__real_internet__mode="mirrored",
441+
gateway_portal__real_internet__service_mirrors=[
442+
{"real_hostname": "github.com", "in_world_service": "10.0.1.20"}
443+
],
444+
)
439445

440446
result = CliRunner().invoke(cli_main.cli, ["validate", str(spec_file), "--explain"])
441447

tests/test_spec_parsing.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,18 +184,76 @@ def test_experimental_gateway_mode_logs_warning_not_raises(self, tmp_path, caplo
184184
import logging
185185

186186
spec_file = self._write_spec(
187-
tmp_path, {"gateway_portal": {"real_internet": {"mode": "mirrored"}}}
187+
tmp_path,
188+
{
189+
"gateway_portal": {
190+
"real_internet": {
191+
"mode": "mirrored",
192+
"service_mirrors": [
193+
{"real_hostname": "github.com", "in_world_service": "10.0.1.20"}
194+
],
195+
}
196+
}
197+
},
188198
)
189199

190200
with caplog.at_level(logging.WARNING, logger="netengine.spec.loader"):
191201
spec = load_spec(spec_file)
192202

193203
assert spec.gateway_portal.real_internet.mode.value == "mirrored"
204+
assert spec.gateway_portal.real_internet.service_mirrors[0].in_world_service == "10.0.1.20"
194205
assert any(
195206
"gateway_portal.real_internet.mode is experimental in alpha" in r.message
196207
for r in caplog.records
197208
)
198209

210+
def test_mirrored_gateway_requires_service_mirrors(self, tmp_path) -> None:
211+
spec_file = self._write_spec(
212+
tmp_path, {"gateway_portal": {"real_internet": {"mode": "mirrored"}}}
213+
)
214+
215+
with pytest.raises(SpecLoadError, match="at least one service mirror is required"):
216+
load_spec(spec_file)
217+
218+
def test_service_mirrors_require_mirrored_gateway_mode(self, tmp_path) -> None:
219+
spec_file = self._write_spec(
220+
tmp_path,
221+
{
222+
"gateway_portal": {
223+
"real_internet": {
224+
"service_mirrors": [
225+
{"real_hostname": "github.com", "in_world_service": "10.0.1.20"}
226+
]
227+
}
228+
}
229+
},
230+
)
231+
232+
with pytest.raises(SpecLoadError, match="service mirrors require"):
233+
load_spec(spec_file)
234+
235+
def test_service_mirror_targets_must_be_ipv4_for_alpha_nftables(self, tmp_path) -> None:
236+
spec_file = self._write_spec(
237+
tmp_path,
238+
{
239+
"gateway_portal": {
240+
"real_internet": {
241+
"mode": "mirrored",
242+
"service_mirrors": [
243+
{
244+
"real_hostname": "github.com",
245+
"in_world_service": "gitea.acme.internal",
246+
}
247+
],
248+
}
249+
}
250+
},
251+
)
252+
253+
with pytest.raises(SpecLoadError, match="must be an IPv4 address"):
254+
load_spec(spec_file)
255+
256+
199257
def test_experimental_profile_field_logs_warning_not_raises(self, tmp_path, caplog) -> None:
200258
import logging
201259

0 commit comments

Comments
 (0)