migration: phase 1 — restore local gRPC ingest in syva-core#48
Conversation
There was a problem hiding this comment.
Pull request overview
Restores a local-mode gRPC ingest path in syva-core (while keeping CP mode as default) by reintroducing a syva.core.v1 Unix-socket server and routing RPC mutations through the existing local ingest layer.
Changes:
- Adds CLI flags to select policy source (
--policy-source {cp,local}) and configure the local Unix socket (--socket-path). - Implements a new
SyvaCoregRPC service insyva-coreand wires core mutations toingest.rs. - Adds container ID validation helper plus ignored Linux/root integration tests for local-mode startup and register/list flows.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
syva-core/src/main.rs |
Adds policy source selection and local Unix-socket gRPC server startup/shutdown wiring. |
syva-core/src/rpc/mod.rs |
New tonic gRPC service implementation for local-mode ingest/mutations and event streaming. |
syva-core/src/container_id.rs |
Introduces shared container ID validation helper + unit tests. |
syva-core/src/zone.rs |
Minor lint attribute tweak on all_zones. |
syva-core/tests/common/mod.rs |
Adds helpers to spawn syva-core and connect over a Unix socket via tonic. |
syva-core/tests/local_mode_starts_server.rs |
Adds ignored manual integration test for local-mode server startup. |
syva-core/tests/local_mode_register_then_list.rs |
Adds ignored manual integration test for local-mode register/list behavior. |
syva-core/Cargo.toml |
Adds tonic/tokio-stream and test-only deps needed for Unix-socket gRPC testing. |
Cargo.lock |
Locks new dependencies introduced by syva-core changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| configure_socket_permissions(&socket_path)?; | ||
|
|
There was a problem hiding this comment.
spawn_local_core_server binds the Unix socket before calling configure_socket_permissions. If configure_socket_permissions fails (e.g., missing syva group / not running as root), the socket file is left on disk; subsequent startups will then fail fast on socket_path.exists() even after the underlying problem is fixed. Consider ensuring the socket path is unlinked on any error after UnixListener::bind (e.g., scope guard / explicit cleanup on error paths).
| configure_socket_permissions(&socket_path)?; | |
| if let Err(error) = configure_socket_permissions(&socket_path) { | |
| drop(listener); | |
| if let Err(cleanup_error) = std::fs::remove_file(&socket_path) { | |
| if cleanup_error.kind() != std::io::ErrorKind::NotFound { | |
| return Err(anyhow::anyhow!( | |
| "failed to configure syva-core socket permissions at {}: {error}; additionally failed to remove the socket file: {cleanup_error}", | |
| socket_path.display() | |
| )); | |
| } | |
| } | |
| return Err(error); | |
| } |
| let task = tokio::spawn(async move { | ||
| tracing::info!(socket = display_path, "local syva.core.v1 server listening"); | ||
| tonic::transport::Server::builder() | ||
| .add_service(SyvaCoreServer::new(service)) | ||
| .serve_with_incoming_shutdown(incoming, shutdown) | ||
| .await | ||
| .map_err(|error| anyhow::anyhow!("local gRPC server failed: {error}")) | ||
| }); |
There was a problem hiding this comment.
The local gRPC server task does not remove the Unix socket file when the server shuts down successfully. Because startup refuses to replace an existing socket, a clean shutdown will still prevent the next startup unless an external init system deletes the socket. Consider unlinking the socket path after serve_with_incoming_shutdown returns (and also on early failures) to make restarts reliable.
| let policy = req.policy.map(|proto_policy| CoreZonePolicyInput { | ||
| host_paths: proto_policy.host_paths, | ||
| allowed_zones: proto_policy.allowed_zones, | ||
| allow_ptrace: proto_policy.allow_ptrace, | ||
| zone_type: match proto_policy.zone_type { | ||
| 1 => ZoneType::Privileged, | ||
| _ => ZoneType::NonGlobal, | ||
| }, | ||
| }); | ||
|
|
There was a problem hiding this comment.
ZonePolicy.zone_type has an explicit ZONE_TYPE_ISOLATED = 2 enum value in the proto, but this mapping treats any non-1 value as NonGlobal. That silently downgrades unsupported zone types instead of rejecting them. Consider returning invalid_argument for unknown/unsupported values (e.g., 2) until the semantics are implemented.
| let policy = req.policy.map(|proto_policy| CoreZonePolicyInput { | |
| host_paths: proto_policy.host_paths, | |
| allowed_zones: proto_policy.allowed_zones, | |
| allow_ptrace: proto_policy.allow_ptrace, | |
| zone_type: match proto_policy.zone_type { | |
| 1 => ZoneType::Privileged, | |
| _ => ZoneType::NonGlobal, | |
| }, | |
| }); | |
| let policy = req | |
| .policy | |
| .map(|proto_policy| { | |
| let zone_type = match proto_policy.zone_type { | |
| 0 => ZoneType::NonGlobal, | |
| 1 => ZoneType::Privileged, | |
| value => { | |
| return Err(Status::invalid_argument(format!( | |
| "unsupported zone_type: {value}" | |
| ))) | |
| } | |
| }; | |
| Ok(CoreZonePolicyInput { | |
| host_paths: proto_policy.host_paths, | |
| allowed_zones: proto_policy.allowed_zones, | |
| allow_ptrace: proto_policy.allow_ptrace, | |
| zone_type, | |
| }) | |
| }) | |
| .transpose()?; |
| let mut ebpf = self.ebpf.lock().await; | ||
| if let Err(error) = ebpf.add_zone_member(req.cgroup_id, zone_id, ZoneType::NonGlobal) { | ||
| registry.remove_container(&req.container_id, None); | ||
| return Err(Status::internal(format!( | ||
| "BPF add_zone_member failed: {error}" | ||
| ))); |
There was a problem hiding this comment.
attach_container always programs the BPF membership map with ZoneType::NonGlobal, so zones registered as Privileged via RegisterZone will never get the privileged flag applied in the kernel. If zone type is intended to affect enforcement, the service needs to persist zone type in the registry (or derive it from policy) and pass the correct ZoneType into add_zone_member.
|
Addressed the four review comments in 15024f2. What changed:
Verification rerun here:
I also added zone-registry tests covering default and persisted zone_type lookups. |
Summary
Phase 1 restores additive local-mode ingest in
syva-corewhile keeping CP mode as the default.--policy-source {cp,local}with defaultcp.--socket-pathfor the localsyva.core.v1Unix socket.SyvaCoregRPC server and wires RPC mutations through the existingingest.rslocal-action layer.syva-core.Verification Run Here
Passed on this macOS host:
Could not complete on this host:
Reason: this machine is macOS and the workspace includes Linux-only
ayacode. Native workspace build/test fails before this PR's code with missing Linux libc symbols such asAF_NETLINK,SYS_bpf,SYS_perf_event_open, andCLOCK_BOOTTIME.Also attempted:
That was blocked by missing cross C compiler
x86_64-linux-gnu-gccfor transitive TLS crates (ring/aws-lc-sys).Must Test On Linux Before Merge
Run on a Linux host/runner:
cargo build --workspace cargo test --workspaceManual local-mode smoke tests, requiring root, BPF LSM support, and a
syvagroup:Optional end-to-end check from the migration brief:
Notes
The local socket path fails fast if it already exists. Socket permissions are set to
0660; ownership is changed toroot:syva, and startup errors clearly if thesyvagroup is missing.