├── .merlin ├── README.md ├── bootvar.ml ├── config.ml ├── cubietruck.md ├── multibridge.sh ├── multibridge.xl └── simple_nat.ml /.merlin: -------------------------------------------------------------------------------- 1 | EXT lwt 2 | PKG irmin 3 | PKG cohttp 4 | PKG lwt 5 | PKG lwt.syntax 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What Is This? 2 | 3 | `simple-nat` is a [MirageOS](https://mirage.io) project that provides a NAT device with storage backed by [Irmin](https://github.com/mirage/irmin). The NAT table is exposed by an Irmin HTTP server and can be manipulated by Irmin's command-line tools, in addition to the automatic updates which are triggered by normal operation of the NAT device. 4 | 5 | For more background, see the [irmin-arp](https://github.com/yomimono/irmin-arp) documentation. 6 | 7 | ### Prepare 8 | 9 | Currently, pinned versions of `dolog` and `bin_prot` are necessary to run applications using Irmin as unikernels. Get them here: 10 | 11 | ``` 12 | opam pin add dolog https://github.com/unixjunkie/dolog.git#no_unix 13 | opam pin add bin_prot https://github.com/samoht/bin_prot.git#112.35.00+xen 14 | ``` 15 | 16 | Additionally, there are some dependencies which are not available in the main OPAM repository. They can be installed as follows: 17 | 18 | ``` 19 | opam pin add irmin-network-datastores https://github.com/yomimono/irmin-network-datastores.git 20 | opam pin add mirage-nat https://github.com/yomimono/mirage-nat.git 21 | opam pin add irmin-arp https://github.com/yomimono/irmin-arp.git 22 | ``` 23 | 24 | (optional) 25 | ``` 26 | opam pin add lwt https://github.com/mirage/lwt.git#tracing 27 | ``` 28 | 29 | You'll need `mirage` itself as well: 30 | 31 | ``` 32 | opam install mirage 33 | ``` 34 | 35 | ### Compile 36 | 37 | ``` 38 | mirage configure --xen # for a virtual machine 39 | make 40 | ``` 41 | 42 | ### Run 43 | 44 | For an example configuration and invocation of the NAT device, see `multibridge.xl` and `multibridge.sh`. 45 | -------------------------------------------------------------------------------- /bootvar.ml: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2014-2015 Magnus Skjegstad 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | * 16 | *) 17 | open V1_LWT 18 | open Lwt 19 | open Ipaddr 20 | open String 21 | open Re 22 | 23 | (* Based on mirage-skeleton/xen/static_website+ip code for reading boot parameters *) 24 | type t = { cmd_line : string; 25 | parameters : (string * string) list } 26 | 27 | (* read boot parameter line and store in assoc list - expected format is "key1=val1 key2=val2" *) 28 | let create = 29 | OS.Xs.make () >>= fun client -> 30 | OS.Xs.(immediate client (fun x -> read x "vm")) >>= fun vm -> 31 | OS.Xs.(immediate client (fun x -> read x (vm^"/image/cmdline"))) >>= fun cmd_line -> 32 | (*let cmd_line = OS.Start_info.((get ()).cmd_line) in*) 33 | Printf.printf "OS cmd_line is %s\n" cmd_line; 34 | let entries = Re_str.(split (regexp_string " ") cmd_line) in 35 | let vartuples = 36 | List.map (fun x -> 37 | match Re_str.(split (regexp_string "=") x) with 38 | | [a;b] -> Printf.printf "%s=%s\n" a b ; (a,b) 39 | | _ -> raise (Failure "malformed boot parameters")) entries 40 | in 41 | Lwt.return { cmd_line = cmd_line; parameters = vartuples} 42 | 43 | (* get boot parameter *) 44 | let get t parameter = 45 | try 46 | List.assoc parameter t.parameters 47 | with 48 | Not_found -> Printf.printf "Boot parameter %s not found\n" parameter; raise Not_found 49 | 50 | -------------------------------------------------------------------------------- /config.ml: -------------------------------------------------------------------------------- 1 | open Mirage 2 | 3 | let main = foreign "Simple_nat.Main" (console @-> random @-> clock @-> network @-> network @-> 4 | http @-> job) 5 | 6 | let console = default_console 7 | 8 | (* 0 is usually the bridge with other stuff on it *) 9 | (* so the "first" vif offered to us will be a "management" interface *) 10 | let stack = direct_stackv4_with_dhcp console (netif "0") 11 | let port = `Tcp (`Port 80) 12 | let http = http_server (conduit_direct stack) 13 | 14 | (* netif actually needs an integer, shoved 15 | into a string, which maps to a device ID number assigned by Xen, to do anything 16 | helpful when xen is the target. Stuff that can't be turned into an int 17 | is silently dropped in that case and we just get the first Xen network iface. *) 18 | 19 | let primary_netif = (netif "1") 20 | let secondary_netif = (netif "2") 21 | 22 | let fs = crunch "static" 23 | 24 | let () = 25 | add_to_opam_packages 26 | ["mirage-nat";"irmin";"re";"tcpip";"mirage-profile";"irmin-arp"]; 27 | add_to_ocamlfind_libraries ["mirage-nat";"re.str";"irmin.http"; 28 | "irmin-arp";"tcpip.ethif";"tcpip.ipv4";"mirage-profile"]; 29 | register "simple-nat" [ 30 | main $ console $ default_random $ default_clock $ primary_netif $ secondary_netif $ http 31 | ] 32 | -------------------------------------------------------------------------------- /cubietruck.md: -------------------------------------------------------------------------------- 1 | # To NAT Traffic Through This Unikernel on a CubieBoard2 or CubieTruck 2 | 3 | These instructions assume you are using an external USB WiFi dongle with a CubieBoard2 or CubieTruck running Xen on a Linux Dom0, although it's also possible to use the same strategy with an x86 Xen machine or a device with two Ethernet interfaces. 4 | 5 | ## Preconditions 6 | * follow the instructions at [xen-arm-builder](http://github.com/mirage/xen-arm-builder) to get a Xen hypervisor and OCaml toolchain set up on your CubieThing, then ssh into it with the default credentials 7 | * make a personal account and set its password with `passwd`, then add it to sudoers with `visudo` 8 | * get the Cubie access to the internet via Ethernet by your favorite method (if your favorite method doesn't involve the Ethernet interface, be sure to add the interface which does have access to bridge `br0`) 9 | 10 | ## Dom0 Setup 11 | 12 | * install `hostapd` (for wireless access point) and `dnsmasq` (for DHCP server) 13 | ``` 14 | sudo apt-get install hostapd dnsmasq 15 | ``` 16 | * `apt` will start `dnsmasq` right after it's installed. You don't want it to be running yet as it's not properly configured, so kill it: 17 | 18 | ``` 19 | sudo kill `pidof dnsmasq` 20 | ``` 21 | 22 | * create the file `/etc/hostapd.conf` and add these lines to it (assuming you want to offer an open WiFi access point named "free wifi for humans not a trap" on channel 11; if you want something else, change these values): 23 | 24 | ``` 25 | interface=wlan0 26 | ssid=free wifi for humans not a trap 27 | channel=11 28 | ``` 29 | 30 | * initialize the wireless USB dongle: 31 | 32 | ``` 33 | sudo ip link set wlan0 up 34 | ``` 35 | 36 | * start hostapd: 37 | 38 | ``` 39 | sudo hostapd -B /etc/hostapd.conf 40 | ``` 41 | 42 | Output should look something like this (the important part being "AP-ENABLED"): 43 | 44 | ``` 45 | Configuration file: /etc/hostapd.conf 46 | Using interface wlan0 with hwaddr 00:04:15:7a:bc:a4 and ssid "free wifi for humans not a trap" 47 | VLAN: vlan_set_name_type: SET_VLAN_NAME_TYPE_CMD name_type=2 failed: Package not installed 48 | wlan0: interface state UNINITIALIZED->ENABLED 49 | wlan0: AP-ENABLED 50 | ``` 51 | 52 | * create a new network bridge: 53 | 54 | ``` 55 | sudo brctl addbr br1 56 | ``` 57 | 58 | * add the wireless interface to the new network bridge and set its link to be up: 59 | 60 | ``` 61 | sudo brctl addif br1 wlan0 62 | sudo ip link set br1 up 63 | ``` 64 | 65 | * set up a static network on the new bridge. I chose a 192.168 network that's unlikely to collide with other networks in use, but you may wish to use something else (particularly if you happen to be using a network with the same addressing to configure the CubieBoard!); if so, you'll need to change it here and in the step below where you configure `dnsmasq.conf`. 66 | 67 | ``` 68 | sudo ip addr add 192.168.252.1/24 dev br1 69 | ``` 70 | 71 | * pick a MAC address for your unikernel. In this example, I'll use the default mirage autoconfigured one, `c0:ff:ee:c0:ff:ee`. 72 | 73 | * configure `dnsmasq`, the DHCP server, by adding the following lines to `/etc/dnsmasq.conf`: 74 | 75 | ``` 76 | interface=br1 77 | dhcp-range=192.168.252.2,192.168.252.250,30m 78 | dhcp-option=3,192.168.252.2 79 | dhcp-host=c0:ff:ee:c0:ff:ee,192.168.252.2 80 | ``` 81 | 82 | This will give each client a 30 minute lease. If you need more IPs, you can reconfigure the bridge to have a larger network and expand the dhcp range accordingly. A unikernel with a vif having mac address c0:ff:ee:c0:ff:ee will always get the address 192.168.252.2 . (Note that currently `simple_nat.ml` configures its addresses statically using variables set at boot time, so as of this writing the `dhcp-host` entry above is purely advisory.) 83 | 84 | The `dhcp-host` line instructs `dnsmasq` to hand out DHCP leases that specify the default gateway as 192.168.252.2, the IP address of the NAT unikernel. 85 | 86 | * start `dnsmasq` 87 | 88 | ``` 89 | sudo dnsmasq 90 | ``` 91 | 92 | * at this point you should be able to connect to the wifi and be given an IP, although you won't be able to browse anything (unless another device is answering to the IP address you chose for the unikernel!). 93 | 94 | ## Mirage and Unikernel Config 95 | 96 | * follow the instructions in README.md for compiling simple-nat and initiating a unikernel build 97 | 98 | * get a cup of your preferred beverage, respond to your e-mail, etc while the unikernel and its dependencies compile 99 | 100 | * once that's complete, you'll have a generated `simple-nat.xl` configuration file in the `simple-nat` directory. Edit it to contain the following line: 101 | 102 | ``` 103 | vif = [ 'mac=00:16:3e:00:00:00,bridge=br0','mac=c0:ff:ee:c0:ff:ee,bridge=br1'] 104 | ``` 105 | 106 | The MAC address you set to receive a constant IP in `dnsmasq.conf` should be the same as the MAC address you set the interface with `bridge=br1` to. The MAC address for the interface on `br0` can be anything that doesn't conflict with something else on your network. 107 | 108 | Last, you'll need to send some network parameters to `simple-nat` when it boots, so it knows how to properly configure its network. (For the curious, this is done through [bootvars](https://github.com/MagnusS/mirage-bootvar-xen).) For a unikernel that boots with an "external" (on br0) IP of 192.168.2.99/24, gateway 192.168.3.1, and an "internal" IP of 192.168.252.2/24 (agreeing with what we set in `dnsmasq.conf`, boot the unikernel this way: 109 | 110 | ``` 111 | sudo xl create simple_nat.xl -c 'extra="external_ip=192.168.3.99 external_netmask=255.255.255.0 external_gateway=192.168.3.1 internal_ip=192.168.252.2 internal_netmask=255.255.255.0"' 112 | ``` 113 | 114 | (an example is included in the repository as `nat_setup.sh`) 115 | 116 | The unikernel should then come up and begin listening for traffic on br1 to send off to br0. If you watch outgoing traffic on the Cubie's command line via `tcpdump`, you'll see the rewritten packets going out and replies to be rewritten coming back. 117 | -------------------------------------------------------------------------------- /multibridge.sh: -------------------------------------------------------------------------------- 1 | sudo xl create multibridge.xl -c 'extra="external_ip=192.168.3.99 external_netmask=255.255.255.0 external_gateway=192.168.3.1 internal_ip=192.168.252.2 internal_netmask=255.255.255.0 "' 2 | -------------------------------------------------------------------------------- /multibridge.xl: -------------------------------------------------------------------------------- 1 | # Generated by Mirage (Wed, 21 Jan 2015 17:28:00 GMT). 2 | 3 | name = 'simple_nat' 4 | kernel = 'mir-simple-nat.xen' 5 | builder = 'linux' 6 | memory = 256 7 | on_crash = 'preserve' 8 | 9 | # "external" network on xenbr0, "internal" on xenbr1, management on xenbr0 10 | vif = [ 'mac=00:16:3e:01:01:01,bridge=xenbr0', 'bridge=xenbr0', 'bridge=xenbr1'] 11 | 12 | # "external" network on xenbr0, "internal" on xenbr1, management on xenbr1 13 | # vif = [ 'mac=00:16:3e:01:01:01,bridge=xenbr1', 'bridge=xenbr0', 'bridge=xenbr1'] 14 | -------------------------------------------------------------------------------- /simple_nat.ml: -------------------------------------------------------------------------------- 1 | open V1_LWT 2 | open Lwt 3 | 4 | module Date = struct 5 | let pretty date = Int64.to_string date 6 | end 7 | 8 | module Main (C: CONSOLE) (Random: V1.RANDOM) (Clock : V1.CLOCK) 9 | (PRI: NETWORK) (SEC: NETWORK) 10 | (HTTP: Cohttp_lwt.Server) = struct 11 | 12 | module Nat_clock = struct 13 | let now () = Int64.of_float (Clock.time ()) 14 | end 15 | 16 | module Backend = Irmin_mem.Make 17 | module Nat = Nat_rewrite.Make(Backend)(Nat_clock)(OS.Time) 18 | 19 | module ETH = Ethif.Make(PRI) 20 | module A = Irmin_arp.Arp.Make(ETH)(Clock)(OS.Time)(Random)(Backend) 21 | module IPV4 = Ipv4.Make(ETH)(A) 22 | type direction = Nat_types.direction 23 | 24 | let listen nf arp push = 25 | (* ingest packets *) 26 | PRI.listen nf 27 | (fun frame -> 28 | match (Wire_structs.get_ethernet_ethertype frame) with 29 | | 0x0806 -> A.input arp (Cstruct.shift frame 14) 30 | | _ -> return (push (Some frame))) 31 | 32 | let allow_nat_traffic table frame (ip : Ipaddr.t) = 33 | let rec stubborn_insert table frame ip port = 34 | match port with 35 | (* TODO: in the unlikely event that no port is available, this 36 | function will never terminate (this is really a tcpip todo) *) 37 | | n when n < 1024 -> 38 | stubborn_insert table frame ip (Random.int 65535) 39 | | n -> 40 | let open Nat in 41 | let endpoint : Nat_types.endpoint = (ip, n) in 42 | add_nat table frame endpoint >>= function 43 | | Ok -> Lwt.return (Some ()) 44 | | Unparseable -> Lwt.return None 45 | | Overlap -> stubborn_insert table frame ip (Random.int 65535) 46 | in 47 | (* TODO: connection tracking logic *) 48 | stubborn_insert table frame ip (Random.int 65535) 49 | 50 | (* other_ip means the IP held by the NAT device on the interface which *isn't* 51 | the one that received this traffic *) 52 | let allow_rewrite_traffic table frame other_ip client_ip fwd_port = 53 | let rec stubborn_insert table frame other_ip client_ip fwd_port xl_port = 54 | match xl_port with 55 | | n when n < 1024 -> stubborn_insert table frame other_ip client_ip 56 | fwd_port (Random.int 65535) 57 | | n -> 58 | let open Nat in 59 | add_redirect table frame (other_ip, n) (client_ip, fwd_port) >>= function 60 | | Ok -> Lwt.return (Some ()) 61 | | Unparseable -> Lwt.return None 62 | | Overlap -> stubborn_insert table frame other_ip client_ip 63 | fwd_port (Random.int 65535) 64 | in 65 | stubborn_insert table frame other_ip client_ip fwd_port (Random.int 65535) 66 | 67 | let nat translation_ip nat_table (direction : direction) 68 | in_queue out_push = 69 | let rec frame_wrapper frame = 70 | let open Nat_types in 71 | (* typical NAT logic: traffic from the internal "trusted" interface gets 72 | new mappings by default; traffic from other interfaces gets dropped if 73 | no mapping exists (which it doesn't, since we already checked) *) 74 | Nat.translate nat_table direction frame >>= fun result -> 75 | match direction, result with 76 | | Source, Translated | Destination, Translated -> return (out_push (Some frame)) 77 | | Destination, Untranslated -> Lwt.return_unit (* nothing in the table, drop it *) 78 | | Source, Untranslated -> 79 | (* mutate nat_table to include entries for the frame *) 80 | allow_nat_traffic nat_table frame translation_ip >>= function 81 | | Some () -> 82 | (* try rewriting again; we should now have an entry for this packet *) 83 | frame_wrapper frame 84 | | None -> 85 | (* this frame is hopeless! *) 86 | return_unit 87 | in 88 | while_lwt true do 89 | Lwt_stream.next in_queue >>= frame_wrapper 90 | done 91 | 92 | let send_packets c nf i out_queue = 93 | while_lwt true do 94 | lwt frame = Lwt_stream.next out_queue in 95 | 96 | match Nat_decompose.layers frame with 97 | | None -> raise (Invalid_argument "NAT transformation rendered packet unparseable") 98 | | Some (ether, ip, tx, _payload) -> 99 | try_lwt 100 | let ether = Nat_rewrite.set_smac ether (PRI.mac nf) in 101 | let (just_headers, higherlevel_data) = 102 | Nat_rewrite.recalculate_transport_checksum (IPV4.checksum) (ether, ip, tx) 103 | in 104 | IPV4.writev i just_headers [ higherlevel_data ] 105 | with 106 | | IPV4.Routing.No_route_to_destination_address addr -> 107 | (* clients may go offline with connections still in process; this 108 | shouldn't cause the NAT device to go offline *) 109 | C.log c ("ARP resolution failed - dropping packet for " ^ 110 | (Ipaddr.V4.to_string addr)); 111 | return_unit 112 | done 113 | 114 | let start c _random _clock pri sec http = 115 | let module Http_server = struct 116 | include HTTP 117 | 118 | let listen given_http ?timeout _uri = 119 | http (`TCP 80) given_http 120 | 121 | end 122 | in 123 | 124 | let module Nat_server = Irmin_http_server.Make(Http_server)(Date)(Nat.I) in 125 | 126 | let (pri_in_queue, pri_in_push) = Lwt_stream.create () in 127 | let (pri_out_queue, pri_out_push) = Lwt_stream.create () in 128 | let (sec_in_queue, sec_in_push) = Lwt_stream.create () in 129 | let (sec_out_queue, sec_out_push) = Lwt_stream.create () in 130 | 131 | let arp_config = Irmin_mem.config () in 132 | 133 | (* or_error brazenly stolen from netif-forward *) 134 | let or_error c name fn t = 135 | fn t 136 | >>= function 137 | | `Error e -> fail (Failure ("error starting " ^ name)) 138 | | `Ok t -> C.log_s c (Printf.sprintf "%s connected." name) >> 139 | return t 140 | in 141 | 142 | (* get network configuration from bootvars *) 143 | Bootvar.create >>= fun bootvar -> 144 | let try_bootvar key = Ipaddr.V4.of_string_exn (Bootvar.get bootvar key) in 145 | let internal_ip = try_bootvar "internal_ip" in 146 | let internal_netmask = try_bootvar "internal_netmask" in 147 | let external_ip = try_bootvar "external_ip" in 148 | let external_netmask = try_bootvar "external_netmask" in 149 | let external_gateway = try_bootvar "external_gateway" in 150 | 151 | (* initialize interfaces *) 152 | lwt nf1 = or_error c "primary interface" ETH.connect pri in 153 | lwt nf2 = or_error c "secondary interface" ETH.connect sec in 154 | 155 | A.connect nf1 arp_config ~pull:[] ~node:["arp";"primary"] >>= function 156 | | `Error e -> fail (Failure ("error starting arp")) 157 | | `Ok arp1 -> 158 | A.connect nf2 arp_config ~pull:[] ~node:["arp";"secondary"] >>= function 159 | | `Error e -> fail (Failure ("error starting arp")) 160 | | `Ok arp2 -> 161 | 162 | (* set up ipv4 on interfaces so ARP will be answered *) 163 | lwt ext_i = or_error c "ip for primary interface" (IPV4.connect nf1) arp1 in 164 | lwt int_i = or_error c "ip for secondary interface" (IPV4.connect nf2) arp2 in 165 | IPV4.set_ip ext_i external_ip >>= fun () -> 166 | IPV4.set_ip_netmask ext_i external_netmask >>= fun () -> 167 | IPV4.set_ip int_i internal_ip >>= fun () -> 168 | IPV4.set_ip_netmask int_i internal_netmask >>= fun () -> 169 | IPV4.set_ip_gateways ext_i [ external_gateway ] >>= fun () -> 170 | 171 | Nat.empty (Irmin_mem.config ()) >>= fun nat_t -> 172 | 173 | Lwt.choose [ 174 | (* packet intake *) 175 | (listen pri arp1 pri_in_push); 176 | (listen sec arp2 sec_in_push); 177 | 178 | (* TODO: ICMP, at least on our own behalf *) 179 | 180 | (* address translation *) 181 | 182 | (* for packets received on xenbr1 ("internal"), rewrite source address/port 183 | before sending packets out the primary interface *) 184 | (nat (Ipaddr.V4 external_ip) nat_t Source sec_in_queue pri_out_push); 185 | 186 | (* for packets received on the first interface (xenbr0/br0 in examples, 187 | which is an "external" world-facing interface), 188 | rewrite destination addresses/ports before sending packet out the second 189 | interface *) 190 | (nat (Ipaddr.V4 external_ip) nat_t Destination pri_in_queue sec_out_push); 191 | 192 | (* packet output *) 193 | (send_packets c pri ext_i pri_out_queue); 194 | (send_packets c sec int_i sec_out_queue); 195 | 196 | (* Expose the NAT table via an Irmin HTTP server on the fully-configured 197 | stack passed to `start`. By default this will be exposed on the 198 | "external" bridge, but can be changed to the internal bridge to more fully 199 | mimic a typical edge network by moving the third vif to xenbr1 -- see 200 | multibridge.xl for a fuller example. *) 201 | Nat_server.listen (Nat.store_of_t nat_t) (Uri.of_string "http://localhost:80"); 202 | ] 203 | 204 | end 205 | --------------------------------------------------------------------------------