Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/netengines-spec-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -447,11 +447,13 @@ gateway_portal:
# custom — explicit rule list (use when presets don't fit)

# Required if mode: mirrored
# Alpha: in_world_service must be an IPv4 address because gateway rules
# render nftables ip daddr allowlists; DNS aliasing for names is planned.
# service_mirrors:
# - real_hostname: s3.amazonaws.com
# in_world_service: storage.platform.internal
# in_world_service: 10.0.0.11
# - real_hostname: github.com
# in_world_service: gitea.acme.internal
# in_world_service: 10.0.1.20

# Required if mode: custom
# rules: []
Expand Down
4 changes: 2 additions & 2 deletions docs/spec-alpha-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Feature states:
| `pki.crl_enabled` | `experimental` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | step-ca CRL generation is enabled in `ca.json` and the distribution URL is published in Phase 3 output; client-validation coverage is hardened in CI e2e. |
| `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. |
| `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. |
| `gateway_portal.real_internet.mode` | `unsupported` | `isolated` | `netengine.spec.loader` | Real-internet gateway policies are not implemented. |
| `gateway_portal.real_internet.service_mirrors` | `unsupported` | `[]` | `netengine.spec.loader` | Service mirror provisioning is not implemented. |
| `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. |
| `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. |
| `gateway_portal.real_internet.upstream_resolver_enabled` | `unsupported` | `false` | `netengine.spec.loader` | Upstream resolver forwarding is not implemented. |
| `gateway_portal.cross_world.mode` | `unsupported` | `none` | `netengine.spec.loader` | Cross-world federation is not implemented. |
| `gateway_portal.cross_world.peers` | `unsupported` | `[]` | `netengine.spec.loader` | Cross-world peer provisioning is not implemented. |
Expand Down
4 changes: 2 additions & 2 deletions docs/support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Feature states:
| `pki.crl_enabled` | `unsupported` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | Handler code can inject a step-ca CRL config and report a URL, but CRL publication/distribution-point plumbing is incomplete and active use is unsupported in alpha validation. |
| `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. |
| `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. |
| `gateway_portal.real_internet.mode` | `unsupported` | `isolated` | `netengine.spec.loader` | Real-internet gateway policies are not implemented. |
| `gateway_portal.real_internet.service_mirrors` | `unsupported` | `[]` | `netengine.spec.loader` | Service mirror provisioning is not implemented. |
| `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. |
| `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. |
| `gateway_portal.real_internet.upstream_resolver_enabled` | `unsupported` | `false` | `netengine.spec.loader` | Upstream resolver forwarding is not implemented. |
| `gateway_portal.cross_world.mode` | `unsupported` | `none` | `netengine.spec.loader` | Cross-world federation is not implemented. |
| `gateway_portal.cross_world.peers` | `unsupported` | `[]` | `netengine.spec.loader` | Cross-world peer provisioning is not implemented. |
Expand Down
14 changes: 10 additions & 4 deletions netengine/spec/feature_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,21 @@ class FeatureStateEntry:
),
FeatureStateEntry(
path="gateway_portal.real_internet.mode",
state="unsupported",
state="experimental",
stage="alpha",
reason="real-internet gateway policies are not implemented",
reason=(
"gateway policies for isolated, shadowed, mirrored, and exposed modes "
"are wired through nftables, but real-host integration remains alpha"
),
),
FeatureStateEntry(
path="gateway_portal.real_internet.service_mirrors",
state="unsupported",
state="experimental",
stage="alpha",
reason="service mirror provisioning is not implemented",
reason=(
"mirrored-mode nftables allowlists and operator output are wired; "
"automatic DNS aliasing for real hostnames remains alpha"
),
),
FeatureStateEntry(
path="gateway_portal.real_internet.upstream_resolver_enabled",
Expand Down
27 changes: 27 additions & 0 deletions netengine/spec/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from netengine.config.loader import ConfigLoader
from netengine.spec.models import SUPPORTED_SPEC_SCHEMA_VERSIONS, NetEngineSpec
from netengine.spec.types import GatewayRealInternetMode

logger = logging.getLogger(__name__)

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

# 5. Mirrored real-internet mode needs explicit IP allowlist targets.
# The alpha gateway implementation renders nftables `ip daddr` rules from
# service_mirrors, so hostnames would generate invalid nftables policy until
# DNS aliasing/resolution is implemented.
real_internet = spec.gateway_portal.real_internet
if real_internet.mode == GatewayRealInternetMode.MIRRORED:
if not real_internet.service_mirrors:
errors.append(
"gateway_portal.real_internet.service_mirrors: "
"at least one service mirror is required when mode is 'mirrored'"
)
elif real_internet.service_mirrors:
errors.append(
"gateway_portal.real_internet.service_mirrors: "
"service mirrors require real_internet.mode: 'mirrored'"
)

for idx, mirror in enumerate(real_internet.service_mirrors):
try:
ipaddress.IPv4Address(mirror.in_world_service)
except ValueError:
errors.append(
f"gateway_portal.real_internet.service_mirrors[{idx}].in_world_service: "
"must be an IPv4 address for alpha mirrored gateway nftables rules"
)

if errors:
raise SpecLoadError(
"Spec cross-validation failed:\n" + "\n".join(f" - {e}" for e in errors)
Expand Down
27 changes: 18 additions & 9 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,15 +433,22 @@ def test_validate_valid_spec_exits_zero() -> None:
assert "Spec validation succeeded: minimal-example" in result.output


def test_validate_unsupported_enabled_feature_exits_nonzero(tmp_path: Path) -> None:
"""Unsupported active feature gates should fail validation precisely."""
spec_file = _write_cli_validate_spec(tmp_path, gateway_portal__real_internet__mode="mirrored")
def test_validate_experimental_gateway_mode_exits_zero_with_explanation(tmp_path: Path) -> None:
"""Mirrored gateway mode is wired but remains experimental during alpha."""
spec_file = _write_cli_validate_spec(
tmp_path,
gateway_portal__real_internet__mode="mirrored",
gateway_portal__real_internet__service_mirrors=[
{"real_hostname": "github.com", "in_world_service": "10.0.1.20"}
],
)

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

assert result.exit_code == 1
assert "Unsupported spec features enabled" in result.output
assert "gateway_portal.real_internet.mode is unsupported" in result.output
assert result.exit_code == 0, result.output
assert "Spec validation succeeded" in result.output
assert "WARNING:" in result.output
assert "gateway_portal.real_internet.mode: experimental" in result.output


def test_validate_experimental_enabled_feature_exits_zero_with_explanation(tmp_path: Path) -> None:
Expand Down Expand Up @@ -480,14 +487,16 @@ def test_validate_json_reports_active_feature_states(tmp_path: Path) -> None:

def test_validate_json_unsupported_active_feature_exits_nonzero(tmp_path: Path) -> None:
"""Unsupported active fields should be machine-readable and fail CI."""
spec_file = _write_cli_validate_spec(tmp_path, pki__crl_enabled=True)
spec_file = _write_cli_validate_spec(
tmp_path, gateway_portal__real_internet__upstream_resolver_enabled=True
)

result = CliRunner().invoke(cli_main.cli, ["validate", str(spec_file), "--format", "json"])

assert result.exit_code == 1
payload = json.loads(result.output)
assert payload["ok"] is False
assert payload["feature_states"][0]["path"] == "pki.crl_enabled"
assert payload["feature_states"][0]["path"] == "gateway_portal.real_internet.upstream_resolver_enabled"
assert payload["feature_states"][0]["state"] == "unsupported"
assert payload["feature_states"][0]["current_value"] is True
assert payload["feature_states"][0]["default_value"] is False
Expand Down
73 changes: 68 additions & 5 deletions tests/test_spec_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,80 @@ def test_experimental_pki_field_logs_warning_not_error(self, tmp_path, caplog) -
"pki.dnssec_enabled is experimental in alpha" in r.message for r in caplog.records
)

def test_unsupported_non_default_active_field_raises_spec_load_error(self, tmp_path) -> None:
def test_experimental_gateway_mode_logs_warning_not_raises(self, tmp_path, caplog) -> None:
import logging

spec_file = self._write_spec(
tmp_path,
{
"gateway_portal": {
"real_internet": {
"mode": "mirrored",
"service_mirrors": [
{"real_hostname": "github.com", "in_world_service": "10.0.1.20"}
],
}
}
},
)

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

assert spec.gateway_portal.real_internet.mode.value == "mirrored"
assert spec.gateway_portal.real_internet.service_mirrors[0].in_world_service == "10.0.1.20"
assert any(
"gateway_portal.real_internet.mode is experimental in alpha" in r.message
for r in caplog.records
)

def test_mirrored_gateway_requires_service_mirrors(self, tmp_path) -> None:
spec_file = self._write_spec(
tmp_path, {"gateway_portal": {"real_internet": {"mode": "mirrored"}}}
)

with pytest.raises(
SpecLoadError,
match="gateway_portal\\.real_internet\\.mode is unsupported in alpha",
):
with pytest.raises(SpecLoadError, match="at least one service mirror is required"):
load_spec(spec_file)

def test_service_mirrors_require_mirrored_gateway_mode(self, tmp_path) -> None:
spec_file = self._write_spec(
tmp_path,
{
"gateway_portal": {
"real_internet": {
"service_mirrors": [
{"real_hostname": "github.com", "in_world_service": "10.0.1.20"}
]
}
}
},
)

with pytest.raises(SpecLoadError, match="service mirrors require"):
load_spec(spec_file)

def test_service_mirror_targets_must_be_ipv4_for_alpha_nftables(self, tmp_path) -> None:
spec_file = self._write_spec(
tmp_path,
{
"gateway_portal": {
"real_internet": {
"mode": "mirrored",
"service_mirrors": [
{
"real_hostname": "github.com",
"in_world_service": "gitea.acme.internal",
}
],
}
}
},
)

with pytest.raises(SpecLoadError, match="must be an IPv4 address"):
load_spec(spec_file)


def test_experimental_profile_field_logs_warning_not_raises(self, tmp_path, caplog) -> None:
import logging

Expand Down