APID-based virtual switch for SpaceOS inter-guest routing
1open Space_net
2
3(* ============================================================================
4 Test config
5 ============================================================================ *)
6
7let camera : Config.tenant =
8 {
9 name = "camera";
10 apids = Config.apid_range 0x010 0x01F;
11 can_send_to = [ Config.apid_range 0x020 0x02F ];
12 }
13
14let processor : Config.tenant =
15 {
16 name = "processor";
17 apids = Config.apid_range 0x020 0x02F;
18 can_send_to = [ Config.apid_range 0x010 0x01F ];
19 }
20
21let test_config socket_dir : Config.t =
22 { tenants = [ camera; processor ]; socket_dir }
23
24(* ============================================================================
25 Router tests (pure, no I/O)
26 ============================================================================ *)
27
28let test_router () =
29 let config = test_config "/tmp/test-space-net" in
30 let router = Router.of_config config in
31
32 (* Owner lookup *)
33 Alcotest.(check bool)
34 "0x010 owned by camera" true
35 (match Router.owner router 0x010 with
36 | Some t -> String.equal t.name "camera"
37 | None -> false);
38 Alcotest.(check bool)
39 "0x020 owned by processor" true
40 (match Router.owner router 0x020 with
41 | Some t -> String.equal t.name "processor"
42 | None -> false);
43 Alcotest.(check bool)
44 "0x100 unowned" true
45 (Option.is_none (Router.owner router 0x100));
46
47 (* Source validation *)
48 Alcotest.(check bool)
49 "camera from 0x010 OK" true
50 (Option.is_none (Router.validate_source router ~source:camera ~apid:0x010));
51 Alcotest.(check bool)
52 "camera from 0x020 error" true
53 (Option.is_some (Router.validate_source router ~source:camera ~apid:0x020));
54
55 (* Route: camera → 0x020 = Local processor *)
56 (match Router.route router ~source:camera ~dest_apid:0x020 with
57 | Ok (Local t) -> Alcotest.(check string) "route 0x020" "processor" t.name
58 | _ -> Alcotest.fail "expected Local processor");
59
60 (* Route: camera → 0x001 = System *)
61 (match Router.route router ~source:camera ~dest_apid:0x001 with
62 | Ok System -> ()
63 | _ -> Alcotest.fail "expected System");
64
65 (* Route: camera → 0x100 = Uplink *)
66 (match Router.route router ~source:camera ~dest_apid:0x100 with
67 | Ok Uplink -> ()
68 | _ -> Alcotest.fail "expected Uplink");
69
70 (* Route: camera → 0x7FF = Drop *)
71 (match Router.route router ~source:camera ~dest_apid:0x7FF with
72 | Ok (Drop _) -> ()
73 | _ -> Alcotest.fail "expected Drop");
74
75 (* Policy: camera → 0x020 allowed *)
76 (match Router.route router ~source:camera ~dest_apid:0x020 with
77 | Ok (Local _) -> ()
78 | _ -> Alcotest.fail "expected allowed");
79
80 (* Policy: camera → 0x030 — not owned by anyone, so Uplink (no policy
81 enforcement for non-local destinations) *)
82 match Router.route router ~source:camera ~dest_apid:0x030 with
83 | Ok Uplink -> ()
84 | Error (Policy_denied _) -> ()
85 | Ok (Local _) -> Alcotest.fail "expected policy denied or uplink"
86 | _ -> ()
87
88(* ============================================================================
89 Frame helpers for switch tests
90 ============================================================================ *)
91
92(** Build a frame with source APID in [apid] and destination APID in [reserved].
93 This is the v0 switch convention. *)
94let make_routable_frame ~src_apid ~dest_apid typ payload =
95 let base = Space_wire.Msg.v typ ~apid:src_apid payload in
96 { base with reserved = dest_apid }
97
98let read_frame reader =
99 let s = Eio.Buf_read.take Space_wire.Msg.frame_size reader in
100 Wire.Codec.decode Space_wire.Msg.codec (Bytes.of_string s) 0
101
102let write_frame dst frame =
103 let buf = Bytes.make Space_wire.Msg.frame_size '\x00' in
104 Wire.Codec.encode Space_wire.Msg.codec frame buf 0;
105 Eio.Flow.copy_string (Bytes.unsafe_to_string buf) dst
106
107let with_timeout clock f =
108 match Eio.Time.with_timeout clock 2.0 (fun () -> Ok (f ())) with
109 | Ok v -> v
110 | Error `Timeout -> Alcotest.fail "timeout"
111
112let make_socket_dir prefix =
113 let socket_dir =
114 Filename.concat
115 (Filename.get_temp_dir_name ())
116 (Fmt.str "%s-%d" prefix (Unix.getpid ()))
117 in
118 (try Unix.mkdir socket_dir 0o755
119 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
120 socket_dir
121
122(** Start the switch and connect both tenants, returning the client sockets.
123 Waits for all accept fibers to complete registration before returning. *)
124let setup_switch ~sw ~net ~clock config =
125 let switch = Switch.create ~config () in
126 Switch.run switch ~sw ~net;
127
128 let cam_path = Config.socket_path config camera in
129 let proc_path = Config.socket_path config processor in
130 let cam_sock = Eio.Net.connect ~sw net (`Unix cam_path) in
131 let proc_sock = Eio.Net.connect ~sw net (`Unix proc_path) in
132 (* Let accept fibers run and register connections *)
133 Eio.Time.sleep clock 0.1;
134 (switch, cam_sock, proc_sock)
135
136(* ============================================================================
137 Switch tests (with Unix sockets)
138 ============================================================================ *)
139
140let test_inter_guest () =
141 Eio_main.run @@ fun env ->
142 Eio.Switch.run @@ fun sw ->
143 let clock = Eio.Stdenv.clock env in
144 let net = Eio.Stdenv.net env in
145 let socket_dir = make_socket_dir "space-net-inter" in
146 let config = test_config socket_dir in
147 let _switch, cam_sock, proc_sock = setup_switch ~sw ~net ~clock config in
148
149 let proc_reader =
150 Eio.Buf_read.of_flow
151 ~max_size:(Space_wire.Msg.frame_size * 10)
152 (proc_sock :> _ Eio.Flow.source)
153 in
154
155 (* Camera sends frame: src=0x010 (camera), dest=0x020 (processor) *)
156 let frame =
157 make_routable_frame ~src_apid:0x010 ~dest_apid:0x020 TM "hello processor"
158 in
159 write_frame (cam_sock :> _ Eio.Flow.sink) frame;
160
161 (* Processor should receive it *)
162 with_timeout clock (fun () ->
163 let received = read_frame proc_reader in
164 Alcotest.(check string)
165 "payload" "hello processor"
166 (Space_wire.Msg.payload_bytes received))
167
168let test_source_enforcement () =
169 Eio_main.run @@ fun env ->
170 Eio.Switch.run @@ fun sw ->
171 let clock = Eio.Stdenv.clock env in
172 let net = Eio.Stdenv.net env in
173 let socket_dir = make_socket_dir "space-net-src" in
174 let config = test_config socket_dir in
175 let _switch, cam_sock, _proc_sock = setup_switch ~sw ~net ~clock config in
176
177 let cam_reader =
178 Eio.Buf_read.of_flow
179 ~max_size:(Space_wire.Msg.frame_size * 10)
180 (cam_sock :> _ Eio.Flow.source)
181 in
182
183 (* Camera sends frame with wrong source APID (0x020 = processor's range) *)
184 let bad_frame =
185 make_routable_frame ~src_apid:0x020 ~dest_apid:0x020 TM "spoofed"
186 in
187 write_frame (cam_sock :> _ Eio.Flow.sink) bad_frame;
188
189 (* Camera should receive an ERROR back *)
190 with_timeout clock (fun () ->
191 let response = read_frame cam_reader in
192 Alcotest.(check int)
193 "error type"
194 (Space_wire.Msg.kind_to_int ERROR)
195 response.Space_wire.Msg.msg_type)
196
197let test_inject () =
198 Eio_main.run @@ fun env ->
199 Eio.Switch.run @@ fun sw ->
200 let clock = Eio.Stdenv.clock env in
201 let net = Eio.Stdenv.net env in
202 let socket_dir = make_socket_dir "space-net-inj" in
203 let config = test_config socket_dir in
204 let switch, cam_sock, _proc_sock = setup_switch ~sw ~net ~clock config in
205
206 let cam_reader =
207 Eio.Buf_read.of_flow
208 ~max_size:(Space_wire.Msg.frame_size * 10)
209 (cam_sock :> _ Eio.Flow.source)
210 in
211
212 (* Inject a frame from DTN targeting camera (dest=0x010 in reserved) *)
213 let injected =
214 { (Space_wire.Msg.v TC ~apid:0 "from DTN") with reserved = 0x010 }
215 in
216 Switch.inject switch injected;
217
218 (* Camera should receive the injected frame *)
219 with_timeout clock (fun () ->
220 let received = read_frame cam_reader in
221 Alcotest.(check string)
222 "injected payload" "from DTN"
223 (Space_wire.Msg.payload_bytes received))
224
225let test_system_handler () =
226 Eio_main.run @@ fun env ->
227 Eio.Switch.run @@ fun sw ->
228 let clock = Eio.Stdenv.clock env in
229 let net = Eio.Stdenv.net env in
230 let socket_dir = make_socket_dir "space-net-sys" in
231 let config = test_config socket_dir in
232 let system_called = ref false in
233 let on_system _frame =
234 system_called := true;
235 Some (Space_wire.Msg.v Space_wire.Msg.EVR ~apid:0x001 "system ack")
236 in
237 let switch = Switch.create ~config ~on_system () in
238 Switch.run switch ~sw ~net;
239
240 let cam_path = Config.socket_path config camera in
241 let proc_path = Config.socket_path config processor in
242 let cam_sock = Eio.Net.connect ~sw net (`Unix cam_path) in
243 let _proc_sock = Eio.Net.connect ~sw net (`Unix proc_path) in
244 Eio.Time.sleep clock 0.1;
245
246 let cam_reader =
247 Eio.Buf_read.of_flow
248 ~max_size:(Space_wire.Msg.frame_size * 10)
249 (cam_sock :> _ Eio.Flow.source)
250 in
251
252 (* Camera sends frame to system APID 0x001 (dest in reserved) *)
253 let frame =
254 make_routable_frame ~src_apid:0x010 ~dest_apid:0x001 TM "ping system"
255 in
256 write_frame (cam_sock :> _ Eio.Flow.sink) frame;
257
258 (* Should get system response back *)
259 with_timeout clock (fun () ->
260 let response = read_frame cam_reader in
261 Alcotest.(check string)
262 "system response" "system ack"
263 (Space_wire.Msg.payload_bytes response));
264 Alcotest.(check bool) "system handler called" true !system_called
265
266(* ============================================================================
267 Test suite
268 ============================================================================ *)
269
270let () =
271 Alcotest.run "space-net"
272 [
273 ("router", [ Alcotest.test_case "routing table" `Quick test_router ]);
274 ( "switch",
275 [
276 Alcotest.test_case "inter-guest" `Quick test_inter_guest;
277 Alcotest.test_case "source enforcement" `Quick test_source_enforcement;
278 Alcotest.test_case "inject" `Quick test_inject;
279 Alcotest.test_case "system handler" `Quick test_system_handler;
280 ] );
281 ]