227 lines
8.8 KiB
OCaml
227 lines
8.8 KiB
OCaml
open Common
|
|
|
|
let src = Logs.Src.create "test_icmpv4" ~doc:"ICMP tests"
|
|
module Log = (val Logs.src_log src : Logs.LOG)
|
|
|
|
module B = Basic_backend.Make
|
|
module V = Vnetif.Make(B)
|
|
module E = Ethernet.Make(V)
|
|
module Static_arp = Static_arp.Make(E)
|
|
|
|
open Lwt.Infix
|
|
|
|
type decomposed = {
|
|
ipv4_payload : Cstruct.t;
|
|
ipv4_header : Ipv4_packet.t;
|
|
ethernet_payload : Cstruct.t;
|
|
ethernet_header : Ethernet.Packet.t;
|
|
}
|
|
|
|
module Ip = Static_ipv4.Make(E)(Static_arp)
|
|
module Icmp = Icmpv4.Make(Ip)
|
|
|
|
module Udp = Udp.Make(Ip)
|
|
|
|
type stack = {
|
|
backend : B.t;
|
|
netif : V.t;
|
|
ethif : E.t;
|
|
arp : Static_arp.t;
|
|
ip : Ip.t;
|
|
icmp : Icmp.t;
|
|
udp : Udp.t;
|
|
}
|
|
|
|
let testbind x y =
|
|
match x with
|
|
| Ok p -> y p
|
|
| Error s -> Alcotest.fail s
|
|
let (>>=?) = testbind
|
|
|
|
(* some default addresses which will be on the same class C *)
|
|
let listener_address = Ipaddr.V4.of_string_exn "192.168.222.1"
|
|
let speaker_address = Ipaddr.V4.of_string_exn "192.168.222.10"
|
|
|
|
let header_size = Ethernet.Packet.sizeof_ethernet
|
|
|
|
let get_stack ?(backend = B.create ~use_async_readers:true
|
|
~yield:(fun() -> Lwt.pause ()) ())
|
|
ip =
|
|
let cidr = Ipaddr.V4.Prefix.make 24 ip in
|
|
V.connect backend >>= fun netif ->
|
|
E.connect netif >>= fun ethif ->
|
|
Static_arp.connect ethif >>= fun arp ->
|
|
Ip.connect ~cidr ethif arp >>= fun ip ->
|
|
Icmp.connect ip >>= fun icmp ->
|
|
Udp.connect ip >>= fun udp ->
|
|
Lwt.return { backend; netif; ethif; arp; ip; icmp; udp }
|
|
|
|
let icmp_listen stack fn =
|
|
let noop = fun ~src:_ ~dst:_ _buf -> Lwt.return_unit in
|
|
V.listen stack.netif ~header_size (* some buffer -> (unit, error) result io *)
|
|
( E.input stack.ethif ~arpv4:(Static_arp.input stack.arp)
|
|
~ipv6:(fun _ -> Lwt.return_unit)
|
|
~ipv4:
|
|
( Ip.input stack.ip
|
|
~tcp:noop ~udp:noop
|
|
~default:(fun ~proto -> match proto with | 1 -> fn | _ -> noop))) >|= fun _ -> ()
|
|
|
|
|
|
let inform_arp stack = Static_arp.add_entry stack.arp
|
|
let mac_of_stack stack = E.mac stack.ethif
|
|
|
|
let short_read () =
|
|
let too_short = Cstruct.create 4 in
|
|
match Icmpv4_packet.Unmarshal.of_cstruct too_short with
|
|
| Ok (icmp, _) ->
|
|
Alcotest.fail (Format.asprintf "processed something too short to be real: %a produced %a"
|
|
Cstruct.hexdump_pp too_short Icmpv4_packet.pp icmp)
|
|
| Error str -> Printf.printf "short packet rejected successfully! msg: %s\n" str;
|
|
Lwt.return_unit
|
|
|
|
let echo_request () =
|
|
let seq_no = 0x01 in
|
|
let id_no = 0x1234 in
|
|
let request_payload = Cstruct.of_string "plz reply i'm so lonely" in
|
|
get_stack speaker_address >>= fun speaker ->
|
|
get_stack ~backend:speaker.backend listener_address >>= fun listener ->
|
|
inform_arp speaker listener_address (mac_of_stack listener);
|
|
inform_arp listener speaker_address (mac_of_stack speaker);
|
|
let req = Icmpv4_packet.({code = 0x00; ty = Icmpv4_wire.Echo_request;
|
|
subheader = Id_and_seq (id_no, seq_no)}) in
|
|
let echo_request = Cstruct.create 2048 in
|
|
Icmpv4_packet.Marshal.into_cstruct req echo_request ~payload:request_payload >>=? fun () ->
|
|
Cstruct.blit request_payload 0 echo_request (Icmpv4_wire.sizeof_icmpv4) (Cstruct.length request_payload);
|
|
let echo_request = Cstruct.sub echo_request 0 (Icmpv4_wire.sizeof_icmpv4 + Cstruct.length request_payload) in
|
|
let check buf =
|
|
let open Icmpv4_packet in
|
|
Log.debug (fun f -> f "Incoming ICMP message: %a" Cstruct.hexdump_pp buf);
|
|
Cstruct.hexdump buf;
|
|
Unmarshal.of_cstruct buf >>=? fun (reply, payload) ->
|
|
match reply.subheader with
|
|
| Next_hop_mtu _ | Pointer _ | Address _ | Unused ->
|
|
Alcotest.fail "received an ICMP message which wasn't an echo-request or reply"
|
|
| Id_and_seq (id, seq) ->
|
|
Alcotest.(check int) "icmp response type" 0x00 (Icmpv4_wire.ty_to_int reply.ty); (* expect an icmp echo reply *)
|
|
Alcotest.(check int) "icmp echo-reply code" 0x00 reply.code; (* should be code 0 *)
|
|
Alcotest.(check int) "icmp echo-reply id" id_no id;
|
|
Alcotest.(check int) "icmp echo-reply seq" seq_no seq;
|
|
Alcotest.(check cstruct) "icmp echo-reply payload" payload request_payload;
|
|
Lwt.return_unit
|
|
in
|
|
Lwt.async (fun () -> Lwt.pick [
|
|
icmp_listen listener (fun ~src ~dst buf ->
|
|
Logs.debug (fun f -> f "listener's ICMP listener invoked");
|
|
Icmp.input listener.icmp ~src ~dst buf);
|
|
icmp_listen speaker (fun ~src:_ ~dst:_ -> check)
|
|
]);
|
|
Icmp.write speaker.icmp ~dst:listener_address echo_request >>= function
|
|
| Error e -> Alcotest.failf "ICMP echo request write: %a" Icmp.pp_error e
|
|
| Ok () -> Lwt.return_unit
|
|
|
|
let echo_silent () =
|
|
let open Icmpv4_packet in
|
|
get_stack speaker_address >>= fun speaker ->
|
|
get_stack ~backend:speaker.backend listener_address >>= fun listener ->
|
|
let req = ({code = 0x00; ty = Icmpv4_wire.Echo_request;
|
|
subheader = Id_and_seq (0xff, 0x4341)}) in
|
|
let echo_request = Marshal.make_cstruct req ~payload:Cstruct.(create 0) in
|
|
let check buf =
|
|
Unmarshal.of_cstruct buf >>=? fun (message, _) ->
|
|
match message.ty with
|
|
| Icmpv4_wire.Echo_reply ->
|
|
Alcotest.fail "received an ICMP echo reply even though we shouldn't have"
|
|
| msg_ty ->
|
|
Printf.printf "received an unexpected ICMP message (type %s); ignoring it"
|
|
(Icmpv4_wire.ty_to_string msg_ty);
|
|
Lwt.return_unit
|
|
in
|
|
let nobody_home = Ipaddr.V4.of_string_exn "192.168.222.90" in
|
|
inform_arp speaker listener_address (mac_of_stack listener);
|
|
inform_arp listener speaker_address (mac_of_stack speaker);
|
|
(* set up an ARP mapping so the listener is more likely to see the echo-request *)
|
|
inform_arp speaker nobody_home (mac_of_stack listener);
|
|
Lwt.async (fun () ->
|
|
Lwt.pick [
|
|
icmp_listen listener (fun ~src ~dst buf -> Icmp.input listener.icmp ~src ~dst buf);
|
|
icmp_listen speaker (fun ~src:_ ~dst:_ -> check);
|
|
]);
|
|
Icmp.write speaker.icmp ~dst:nobody_home echo_request >>= function
|
|
| Error e -> Alcotest.failf "ICMP echo request write: %a" Icmp.pp_error e
|
|
| Ok () -> Lwt.return_unit
|
|
|
|
let write_errors () =
|
|
let decompose buf =
|
|
let open Ethernet.Packet in
|
|
let* ethernet_header, ethernet_payload = of_cstruct buf in
|
|
match ethernet_header.ethertype with
|
|
| `IPv6 | `ARP -> Error "not an ipv4 packet"
|
|
| `IPv4 ->
|
|
let* ipv4_header, ipv4_payload =
|
|
Ipv4_packet.Unmarshal.of_cstruct ethernet_payload
|
|
in
|
|
Ok { ethernet_header; ethernet_payload; ipv4_header; ipv4_payload }
|
|
in
|
|
(* for any incoming packet, reject it with would_fragment *)
|
|
let reject_all stack =
|
|
let reject buf =
|
|
match decompose buf with
|
|
| Error s -> Alcotest.fail s
|
|
| Ok decomposed ->
|
|
let reply = Icmpv4_packet.({
|
|
ty = Icmpv4_wire.Destination_unreachable;
|
|
code = Icmpv4_wire.(unreachable_reason_to_int Would_fragment);
|
|
subheader = Next_hop_mtu 576;
|
|
}) in
|
|
let header = Icmpv4_packet.Marshal.make_cstruct reply
|
|
~payload:decomposed.ethernet_payload in
|
|
let header_and_payload = Cstruct.concat ([header ; decomposed.ethernet_payload]) in
|
|
let open Ipv4_packet in
|
|
Icmp.write stack.icmp ~dst:decomposed.ipv4_header.src header_and_payload >|= Result.get_ok
|
|
in
|
|
V.listen stack.netif ~header_size reject >|= fun _ -> ()
|
|
in
|
|
let check_packet buf : unit Lwt.t =
|
|
let aux buf =
|
|
let open Icmpv4_packet in
|
|
let* icmp, icmp_payload = Unmarshal.of_cstruct buf in
|
|
Alcotest.check Alcotest.int "ICMP message type" 0x03 (Icmpv4_wire.ty_to_int icmp.ty);
|
|
Alcotest.check Alcotest.int "ICMP message code" 0x04 icmp.code;
|
|
match Cstruct.length icmp_payload with
|
|
| 0 -> Alcotest.fail "Error message should've had a payload"
|
|
| _n ->
|
|
(* TODO: packet should have an IP header in it *)
|
|
Alcotest.(check int) "Payload first byte" 0x45 (Cstruct.get_uint8 icmp_payload 0);
|
|
Ok ()
|
|
in
|
|
match aux buf with
|
|
| Error s -> Alcotest.fail s
|
|
| Ok () -> Lwt.return_unit
|
|
in
|
|
let check_rejection stack dst =
|
|
let payload = Cstruct.of_string "!@#$" in
|
|
Lwt.pick [
|
|
icmp_listen stack (fun ~src:_ ~dst:_ buf -> check_packet buf >>= fun () ->
|
|
V.disconnect stack.netif);
|
|
Mirage_sleep.ns (Duration.of_ms 500) >>= fun () ->
|
|
Udp.write stack.udp ~dst ~src_port:1212 ~dst_port:123 payload
|
|
>|= Result.get_ok >>= fun () ->
|
|
Mirage_sleep.ns (Duration.of_sec 1) >>= fun () ->
|
|
Alcotest.fail "writing thread completed first";
|
|
]
|
|
in
|
|
get_stack speaker_address >>= fun speaker ->
|
|
get_stack ~backend:speaker.backend listener_address >>= fun listener ->
|
|
inform_arp speaker listener_address (mac_of_stack listener);
|
|
inform_arp listener speaker_address (mac_of_stack speaker);
|
|
Lwt.pick [
|
|
reject_all listener;
|
|
check_rejection speaker listener_address;
|
|
]
|
|
|
|
let suite = [
|
|
"short read", `Quick, short_read;
|
|
"echo requests elicit an echo reply", `Quick, echo_request;
|
|
"echo requests for other ips don't elicit an echo reply", `Quick, echo_silent;
|
|
"error messages are written", `Quick, write_errors;
|
|
]
|