Skip to content

Commit

Permalink
Merge pull request #375 from hannesm/frag
Browse files Browse the repository at this point in the history
IPv4 fragment reassembly
  • Loading branch information
hannesm authored Nov 30, 2018
2 parents aa7b15d + 77ab0c5 commit 683fdaa
Show file tree
Hide file tree
Showing 11 changed files with 455 additions and 34 deletions.
178 changes: 178 additions & 0 deletions src/ipv4/fragments.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
(*
* Copyright (c) 2018 Hannes Mehnert <hannes@mehnert.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS l SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*)

let src = Logs.Src.create "ipv4-fragments" ~doc:"IPv4 fragmentation"
module Log = (val Logs.src_log src : Logs.LOG)

open Rresult.R.Infix

(* TODO:
current state:
lifetime is 10s max between first and last fragment
size is 1MB hardcoded
max 16 fragments for each "flow" (source ip, destrination ip, protocol, ipv4 identifier)
inserted into sorted list, checks overlaps and holes on reassembly (triggered once a fragment without "more fragments" has been received)
this has some issues:
anyone can spam (with a constant stream of fragmented packets - needs to fill 1MB in 10s) the fragment cache, leading to resource exhaustion of the cache ("valid" fragments are dropped if they're incoming too slowly)
insertion into linked list is O(n) (with n is maximal 16)
ping -s 65535 isn't answered with MTU=1500 (doesn't fit into 16 fragments)
what we could do instead
maximum storage per source ip
use a bitmask or tree data structure for the segments (offset is on 8byte boundaries)
may lead to verification of overlaps at insertion time --> can drop immediately
*)

(* IP Fragmentation using a LRU cache:
The key of our cache is source ip * destination ip * protocol * identifier.
The value is a quintuple consisting of first segment received. IP options
(which are usually sent only in the first IP segment), "last segment
received" (i.e. an IPv4 segment without the more fragment bit set), a counter
of the length of items, and a list of pairs, which contain an offset and
payload. The list is sorted by offset in descending order. *)

module V = struct
type t = int64 * Cstruct.t * bool * int * (int * Cstruct.t) list

let weight (_, _, _, _, v) = Cstruct.lenv (List.map snd v)
end

module K = struct
type t = Ipaddr.V4.t * Ipaddr.V4.t * int * int

let compare (src, dst, proto, id) (src', dst', proto', id') =
let (&&&) a b = match a with 0 -> b | x -> x in
let int_cmp : int -> int -> int = compare in
Ipaddr.V4.compare src src' &&&
Ipaddr.V4.compare dst dst' &&&
int_cmp proto proto' &&&
int_cmp id id'
end

module Cache = Lru.F.Make(K)(V)

(* insert_sorted inserts a fragment in a list, sort is by frag_start, descending *)
let rec insert_sorted ((frag_start, _) as frag) = function
| [] -> [ frag ]
| ((frag'_start, _) as frag')::tl ->
if frag'_start <= frag_start
then frag::frag'::tl
else frag'::insert_sorted frag tl

(* attempt_reassemble takes a list of fragments, and returns either
- Ok payload when the payload was completed
- Error Hole if some fragment is still missing
- Error Bad if the list of fragments was bad: it contains overlapping
segments. This is an indication for malicious activity, and we drop the
IP fragment
There are various attacks (and DoS) on IP reassembly, most prominent use
overlapping segments (and selection thereof), we just drop overlapping segments
(similar as Linux does since https://git.kernel.org/pub/scm/linux/kernel/git/davem/net-next.git/commit/?id=c30f1fc041b74ecdb072dd44f858750414b8b19f).
*)

type r = Bad | Hole

let attempt_reassemble fragments =
Log.debug (fun m -> m "reassemble %a"
Fmt.(list ~sep:(unit "; ") (pair ~sep:(unit ", len ") int int))
(List.map (fun (off, data) -> off, Cstruct.len data) fragments)) ;
(* input: list of (offset, fragment) with decreasing offset *)
(* output: maybe a cstruct.t if there are no gaps *)
let len =
(* List.hd is safe here, since we are never called with an empty list *)
let off, data = List.hd fragments in
off + Cstruct.len data
in
let rec check until = function
| [] -> if until = 0 then Ok () else Error Hole
| (start, d)::tl ->
let until' = start + (Cstruct.len d) in
if until = until'
then check start tl
else if until' > until
then Error Bad
else Error Hole
in
check len fragments >>= fun () ->
let buf = Cstruct.create_unsafe len in
List.iter (fun (off, data) ->
Cstruct.blit data 0 buf off (Cstruct.len data))
fragments ;
Ok buf

let max_number_of_fragments = 16

let max_duration = Duration.of_sec 10

let process cache ts (packet : Ipv4_packet.t) payload =
Log.debug (fun m -> m "process called with off %x" packet.off) ;
if packet.off land 0x3FFF = 0 then (* ignore reserved and don't fragment *)
(* fastpath *)
cache, Some (packet, payload)
else
let offset, more =
(packet.off land 0x1FFF) lsl 3, (* of 8 byte blocks *)
packet.off land 0x2000 = 0x2000
and key = (packet.src, packet.dst, packet.proto, packet.id)
in
match Cache.find key cache with
| None ->
Log.debug (fun m -> m "%a none found, inserting into cache" Ipv4_packet.pp packet) ;
Cache.add key (ts, packet.options, not more, 1, [(offset, payload)]) cache, None
| Some ((ts', options, finished, cnt, frags), cache') ->
if Int64.sub ts ts' >= max_duration then begin
Log.warn (fun m -> m "%a found some, but timestamp exceeded duration %a, dropping old segments and inserting new segment into cache" Ipv4_packet.pp packet Duration.pp max_duration) ;
Cache.add key (ts, packet.options, not more, 1, [(offset, payload)]) cache, None
end else
let all_frags = insert_sorted (offset, payload) frags
and try_reassemble = finished || not more
and options' = if offset = 0 then packet.options else options
in
Log.debug (fun m -> m "%d found, finished %b more %b try_reassemble %b"
cnt finished more try_reassemble) ;
let maybe_add_to_cache c =
if cnt < max_number_of_fragments then
Cache.add key (ts', options', try_reassemble, succ cnt, all_frags) c
else
(Log.warn (fun m -> m "%a dropping from cache, maximum number of fragments exceeded"
Ipv4_packet.pp packet) ;
Cache.remove key c)
in
if try_reassemble then
match attempt_reassemble all_frags with
| Ok p ->
Log.debug (fun m -> m "%a reassembled to payload %d" Ipv4_packet.pp packet (Cstruct.len p)) ;
let packet' = { packet with options = options' ; off = 0 } in
Cache.remove key cache', Some (packet', p)
| Error Bad ->
Log.warn (fun m -> m "%a dropping from cache, bad fragments (%a)"
Ipv4_packet.pp packet
Fmt.(list ~sep:(unit "; ") (pair ~sep:(unit ", ") int int))
(List.map (fun (s, d) -> (s, Cstruct.len d)) all_frags)) ;
Logs.debug (fun m -> m "full fragments: %a"
Fmt.(list ~sep:(unit "@.") Cstruct.hexdump_pp)
(List.map snd all_frags)) ;

Cache.remove key cache', None
| Error Hole -> maybe_add_to_cache cache', None
else
maybe_add_to_cache cache', None
36 changes: 36 additions & 0 deletions src/ipv4/fragments.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
(*
* Copyright (c) 2018 Hannes Mehnert <hannes@mehnert.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS l SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*)

module V : sig
type t = int64 * Cstruct.t * bool * int * (int * Cstruct.t) list

val weight : t -> int
end

module K : sig
type t = Ipaddr.V4.t * Ipaddr.V4.t * int * int
val compare : t -> t -> int
end

module Cache : sig
include Lru.F.S with type k = K.t and type v = V.t
end

val max_number_of_fragments : int
val max_duration : int64

val process : Cache.t -> int64 -> Ipv4_packet.t -> Cstruct.t ->
Cache.t * (Ipv4_packet.t * Cstruct.t) option
3 changes: 2 additions & 1 deletion src/ipv4/ipv4_common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ let allocate_frame ~src ~source ~(dst:Ipaddr.V4.t) ~(proto : [`ICMP | `TCP | `UD
let buf = Cstruct.shift ethernet_frame Ethif_wire.sizeof_ethernet in
(* TODO: why 38 for TTL? *)
let ipv4_header = Ipv4_packet.({options = Cstruct.create 0;
src; dst; ttl = 38;
src; dst; ttl = 38;
off = 0 ; id = 0x0000 ;
proto = Ipv4_packet.Marshal.protocol_to_int proto; }) in
(* set the payload_len to 0, since we don't know what it'll be yet *)
(* the caller needs to then use [writev] or [write] to output the buffer;
Expand Down
21 changes: 14 additions & 7 deletions src/ipv4/ipv4_packet.ml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
type t = {
src : Ipaddr.V4.t;
dst : Ipaddr.V4.t;
proto : Cstruct.uint8;
id : Cstruct.uint16;
off : Cstruct.uint16;
ttl : Cstruct.uint8;
proto : Cstruct.uint8;
options : Cstruct.t;
}

Expand All @@ -12,14 +14,16 @@ type protocol = [
| `UDP ]

let pp fmt t =
Format.fprintf fmt "IPv4 packet %a -> %a: proto %d, ttl %d, options %a"
Ipaddr.V4.pp_hum t.src Ipaddr.V4.pp_hum t.dst t.proto t.ttl Cstruct.hexdump_pp t.options
Format.fprintf fmt "IPv4 packet %a -> %a: id %04x, off %d proto %d, ttl %d, options %a"
Ipaddr.V4.pp_hum t.src Ipaddr.V4.pp_hum t.dst t.id t.off t.proto t.ttl Cstruct.hexdump_pp t.options

let equal {src; dst; proto; ttl; options} q =
let equal {src; dst; id; off; ttl; proto; options} q =
src = q.src &&
dst = q.dst &&
proto = q.proto &&
id = q.id &&
off = q.off &&
ttl = q.ttl &&
proto = q.proto &&
Cstruct.equal options q.options

module Marshal = struct
Expand All @@ -33,6 +37,7 @@ module Marshal = struct
| `UDP -> 17

let pseudoheader ~src ~dst ~proto len =
(* should we do sth about id or off (assert false?) *)
let proto = protocol_to_int proto in
let ph = Cstruct.create 12 in
let numify = Ipaddr.V4.to_int32 in
Expand Down Expand Up @@ -120,8 +125,10 @@ module Unmarshal = struct
let payload_len = (get_ipv4_len buf) - options_end in
let src = Ipaddr.V4.of_int32 (get_ipv4_src buf) in
let dst = Ipaddr.V4.of_int32 (get_ipv4_dst buf) in
let proto = get_ipv4_proto buf in
let id = get_ipv4_id buf in
let off = get_ipv4_off buf in
let ttl = get_ipv4_ttl buf in
let proto = get_ipv4_proto buf in
let options =
if options_end > sizeof_ipv4 then (Cstruct.sub buf sizeof_ipv4 (options_end - sizeof_ipv4))
else (Cstruct.create 0)
Expand All @@ -131,7 +138,7 @@ module Unmarshal = struct
Error (Printf.sprintf "Payload buffer (%d bytes) too small to contain payload (of size %d from header)" payload_available payload_len)
) else (
let payload = Cstruct.sub buf options_end payload_len in
Ok ({src; dst; proto; ttl; options;}, payload)
Ok ({src; dst; id; off; ttl; proto; options;}, payload)
)
in
size_check buf >>= check_version >>= get_header_length >>= parse buf
Expand Down
4 changes: 3 additions & 1 deletion src/ipv4/ipv4_packet.mli
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
type t = {
src : Ipaddr.V4.t;
dst : Ipaddr.V4.t;
proto : Cstruct.uint8;
id : Cstruct.uint16;
off : Cstruct.uint16;
ttl : Cstruct.uint8;
proto : Cstruct.uint8;
options : Cstruct.t;
}

Expand Down
2 changes: 1 addition & 1 deletion src/ipv4/jbuild
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
((name tcpip_ipv4)
(public_name tcpip.ipv4)
(libraries (logs io-page mirage-protocols-lwt ipaddr cstruct rresult
tcpip tcpip.ethif tcpip.udp mirage-random mirage-clock randomconv))
tcpip tcpip.ethif tcpip.udp mirage-random mirage-clock randomconv lru))
(preprocess (pps (ppx_cstruct)))
(wrapped false)))
45 changes: 27 additions & 18 deletions src/ipv4/static_ipv4.ml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ module Make (R: Mirage_random.C) (C: Mirage_clock.MCLOCK) (Ethif: Mirage_protoco
type t = {
ethif : Ethif.t;
arp : Arpv4.t;
clock : C.t;
mutable ip: Ipaddr.V4.t;
network: Ipaddr.V4.Prefix.t;
mutable gateway: Ipaddr.V4.t option;
mutable cache: Fragments.Cache.t;
}

let adjust_output_header = Ipv4_common.adjust_output_header ~rng:R.generate
Expand Down Expand Up @@ -74,36 +76,43 @@ module Make (R: Mirage_random.C) (C: Mirage_clock.MCLOCK) (Ethif: Mirage_protoco
writev t frame [buf]

(* TODO: ought we to check to make sure the destination is relevant here? currently we'll process all incoming packets, regardless of destination address *)
let input _t ~tcp ~udp ~default buf =
let open Ipv4_packet in
match Unmarshal.of_cstruct buf with
let input t ~tcp ~udp ~default buf =
match Ipv4_packet.Unmarshal.of_cstruct buf with
| Error s ->
Log.info (fun f -> f "IP.input: unparseable header (%s): %S" s (Cstruct.to_string buf));
Log.info (fun m -> m "error %s while parsing IPv4 frame %a" s Cstruct.hexdump_pp buf);
Lwt.return_unit
| Ok (packet, payload) ->
match Unmarshal.int_to_protocol packet.proto, Cstruct.len payload with
| Some _, 0 ->
(* Don't pass on empty buffers as payloads to known protocols, as they have no relevant headers *)
Lwt.return_unit
| None, 0 -> (* we don't know anything about the protocol; an empty
payload may be meaningful somehow? *)
default ~proto:packet.proto ~src:packet.src ~dst:packet.dst payload
| Some `TCP, _ -> tcp ~src:packet.src ~dst:packet.dst payload
| Some `UDP, _ -> udp ~src:packet.src ~dst:packet.dst payload
| Some `ICMP, _ | None, _ ->
default ~proto:packet.proto ~src:packet.src ~dst:packet.dst payload
if Cstruct.len payload = 0 then
(Log.info (fun m -> m "dropping zero length IPv4 frame %a" Ipv4_packet.pp packet) ;
Lwt.return_unit)
else
let ts = C.elapsed_ns t.clock in
let cache, res = Fragments.process t.cache ts packet payload in
t.cache <- cache ;
match res with
| None -> Lwt.return_unit
| Some (packet, payload) ->
let src, dst = packet.src, packet.dst in
match Ipv4_packet.Unmarshal.int_to_protocol packet.proto with
| Some `TCP -> tcp ~src ~dst payload
| Some `UDP -> udp ~src ~dst payload
| Some `ICMP | None -> default ~proto:packet.proto ~src ~dst payload

let connect
?(ip=Ipaddr.V4.any)
?(network=Ipaddr.V4.Prefix.make 0 Ipaddr.V4.any)
?(gateway=None) _clock ethif arp =
?(gateway=None) clock ethif arp =
match Ipaddr.V4.Prefix.mem ip network with
| false ->
Log.warn (fun f -> f "IPv4: ip %a is not in the prefix %a" Ipaddr.V4.pp_hum ip Ipaddr.V4.Prefix.pp_hum network);
Log.warn (fun f -> f "IPv4: ip %a is not in the prefix %a"
Ipaddr.V4.pp_hum ip Ipaddr.V4.Prefix.pp_hum network);
Lwt.fail_with "given IP is not in the network provided"
| true ->
Arpv4.set_ips arp [ip] >>= fun () ->
let t = { ethif; arp; ip; network; gateway } in
(* TODO currently hardcoded to 4MB, should be configurable
and maybe limited per-src/dst-ip as well? *)
let cache = Fragments.Cache.empty (1024 * 1024 * 4) in
let t = { ethif; arp; ip; clock; network; gateway ; cache } in
Lwt.return t

let disconnect _ = Lwt.return_unit
Expand Down
1 change: 1 addition & 0 deletions tcpip.opam
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ depends: [
"pcap-format" {with-test}
"mirage-clock-unix" {with-test & >= "1.2.0"}
"mirage-random-test" {with-test}
"lru"
]
synopsis: "OCaml TCP/IP networking stack, used in MirageOS"
description: """
Expand Down
2 changes: 1 addition & 1 deletion test/test_checksums.ml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ let udp_ipv4_zero_checksum () =
let ipv4_header = Ipv4_packet.{
src; dst;
proto = Ipv4_packet.Marshal.protocol_to_int proto;
ttl; options } in
ttl; id = 0 ; off = 0 ; options } in
let pseudoheader = Ipv4_packet.Marshal.pseudoheader
~src
~dst
Expand Down
Loading

0 comments on commit 683fdaa

Please sign in to comment.