APID-based virtual switch for SpaceOS inter-guest routing
at main 281 lines 9.8 kB view raw
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 ]