Pure OCaml ARP table lookup - reads /proc/net/arp on Linux and arp -a on macOS/BSD
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Squashed 'ocaml-arp/' content from commit 0343a7e git-subtree-split: 0343a7ebaa7145be267107f5d35840025ec8899f

+172
+1
.ocamlformat
··· 1 + version = 0.28.1
+25
arp.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Pure OCaml ARP table lookup" 4 + description: 5 + "Read the system ARP cache without external dependencies. Uses /proc/net/arp on Linux and arp -a on macOS/BSD." 6 + depends: [ 7 + "ocaml" {>= "4.14.0"} 8 + "dune" {>= "3.0" & >= "3.0"} 9 + "alcotest" {with-test} 10 + "odoc" {with-doc} 11 + ] 12 + build: [ 13 + ["dune" "subst"] {dev} 14 + [ 15 + "dune" 16 + "build" 17 + "-p" 18 + name 19 + "-j" 20 + jobs 21 + "@install" 22 + "@runtest" {with-test} 23 + "@doc" {with-doc} 24 + ] 25 + ]
+14
dune-project
··· 1 + (lang dune 3.0) 2 + (name arp) 3 + 4 + (generate_opam_files true) 5 + 6 + (package 7 + (name arp) 8 + (synopsis "Pure OCaml ARP table lookup") 9 + (description 10 + "Read the system ARP cache without external dependencies. Uses /proc/net/arp on Linux and arp -a on macOS/BSD.") 11 + (depends 12 + (ocaml (>= 4.14.0)) 13 + (dune (>= 3.0)) 14 + (alcotest :with-test)))
+97
lib/arp.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** ARP table lookup. 7 + 8 + Pure OCaml implementation: 9 + - Linux: reads /proc/net/arp directly 10 + - macOS/BSD: parses output of arp -a *) 11 + 12 + type entry = { ip : string; mac : string; interface : string option } 13 + 14 + let parse_proc_net_arp lines = 15 + (* /proc/net/arp format: 16 + IP address HW type Flags HW address Mask Device 17 + 192.168.1.1 0x1 0x2 aa:bb:cc:dd:ee:ff * eth0 *) 18 + match lines with 19 + | [] -> [] 20 + | _ :: data_lines -> 21 + List.filter_map 22 + (fun line -> 23 + let parts = 24 + String.split_on_char ' ' line 25 + |> List.filter (fun s -> String.length s > 0) 26 + in 27 + match parts with 28 + | ip :: _ :: flags :: mac :: _ :: iface :: _ -> 29 + (* flags 0x2 = complete entry, 0x0 = incomplete *) 30 + if flags <> "0x0" && mac <> "00:00:00:00:00:00" then 31 + Some { ip; mac; interface = Some iface } 32 + else None 33 + | _ -> None) 34 + data_lines 35 + 36 + let parse_arp_a_output lines = 37 + (* arp -a format (macOS/BSD): 38 + hostname (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 ifscope [ethernet] *) 39 + List.filter_map 40 + (fun line -> 41 + let parts = 42 + String.split_on_char ' ' line 43 + |> List.filter (fun s -> String.length s > 0) 44 + in 45 + match parts with 46 + | _ :: ip_paren :: _ :: mac :: _ :: iface :: _ 47 + when String.length ip_paren > 2 48 + && ip_paren.[0] = '(' 49 + && ip_paren.[String.length ip_paren - 1] = ')' -> 50 + let ip = String.sub ip_paren 1 (String.length ip_paren - 2) in 51 + if mac <> "(incomplete)" then Some { ip; mac; interface = Some iface } 52 + else None 53 + | _ -> None) 54 + lines 55 + 56 + let read_file path = 57 + try 58 + let ic = open_in path in 59 + let rec read_lines acc = 60 + match input_line ic with 61 + | line -> read_lines (line :: acc) 62 + | exception End_of_file -> 63 + close_in ic; 64 + List.rev acc 65 + in 66 + Some (read_lines []) 67 + with _ -> None 68 + 69 + let run_command args = 70 + let cmd = String.concat " " args in 71 + try 72 + let ic = Unix.open_process_in cmd in 73 + let rec read_lines acc = 74 + match input_line ic with 75 + | line -> read_lines (line :: acc) 76 + | exception End_of_file -> 77 + ignore (Unix.close_process_in ic); 78 + List.rev acc 79 + in 80 + Some (read_lines []) 81 + with _ -> None 82 + 83 + let get_table () = 84 + (* Try Linux /proc/net/arp first *) 85 + match read_file "/proc/net/arp" with 86 + | Some lines -> parse_proc_net_arp lines 87 + | None -> ( 88 + (* Fall back to arp -a for macOS/BSD *) 89 + match run_command [ "arp"; "-a" ] with 90 + | Some lines -> parse_arp_a_output lines 91 + | None -> []) 92 + 93 + let lookup ip = 94 + let entries = get_table () in 95 + List.find_opt (fun e -> e.ip = ip) entries 96 + 97 + let lookup_mac ip = Option.map (fun e -> e.mac) (lookup ip)
+31
lib/arp.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** ARP table lookup. 7 + 8 + Pure OCaml implementation that reads the system ARP cache: 9 + - Linux: reads [/proc/net/arp] directly 10 + - macOS/BSD: parses output of [arp -a] 11 + 12 + {2 Example} 13 + 14 + {[ 15 + let () = 16 + List.iter 17 + (fun entry -> Printf.printf "%s -> %s\n" entry.Arp.ip entry.Arp.mac) 18 + (Arp.get_table ()) 19 + ]} *) 20 + 21 + type entry = { ip : string; mac : string; interface : string option } 22 + (** An ARP table entry. *) 23 + 24 + val get_table : unit -> entry list 25 + (** [get_table ()] returns all entries from the system ARP cache. *) 26 + 27 + val lookup : string -> entry option 28 + (** [lookup ip] finds the ARP entry for the given IP address. *) 29 + 30 + val lookup_mac : string -> string option 31 + (** [lookup_mac ip] returns the MAC address for the given IP, if known. *)
+4
lib/dune
··· 1 + (library 2 + (name arp) 3 + (public_name arp) 4 + (libraries unix))