Skip to content

Commit

Permalink
Add parallel processing mac2ip DHCP server.
Browse files Browse the repository at this point in the history
Uses the node:cluster module to launch parallel workers processes (based
on `--processes CNT` parameter). The primary process does the arg
parsing, spawns the workers, waits for them to start, and then sends
them to full user configuration (file and CLI parameters).

Also add the capability (in dhcp.node-server/create-server) to listen on
all interfaces.  This precludes answering with a broadcast message so
this is more useful for unicast DHCP server mode (e.g. such as a DHCP
relay situation).

Add a separate `test/docker-compose-mac3ip.yaml` compose test to
specifically test the mac2ip server.
  • Loading branch information
kanaka committed Sep 20, 2023
1 parent bf1dc34 commit 5bcf3de
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 16 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ RUN cd /app && shadow-cljs info
ADD src/ /app/src/
ADD test/ /app/test/
RUN cd /app && \
shadow-cljs compile dhcp-client pool-server ping-client read-pcap test && \
shadow-cljs compile dhcp-client pool-server mac2ip-server ping-client read-pcap test && \
chmod +x build/*.js


Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,12 @@ cljs.user=> (prn (platform/buf->vec buf2 0))

[//]: # (This should be kept in sync with docs/examples.md)

The project includes four working examples:
The project includes five working examples:

* A DHCP client
* A DHCP server that uses an IP lease pool
* A DHCP server that uses direct MAC to IP mapping and that supports
multiprocess parallel workers
* An ICMP/ping client
* A pcap file parser

Expand Down Expand Up @@ -144,6 +146,14 @@ npx shadow-cljs compile dhcp-client dhcp-server ping-client read-pcap
sudo node ./build/pool-server.js eth0
```

* **DHCP mac2ip server** - Run a DHCP server on eth0 that does direct
MAC to IP assignments (defined in the config file) and runs
5 parallel worker processes.

```
sudo node ./build/mac2-ip-server.js -processes 5 --if-name eth0 --config-file mac2ip.json
```

* **ICMP/ping client** - Use the ping client to demonstrate ICMP
protocl reading/writing. Elevated permissions are required to
send/receive ICMP packets.
Expand Down
13 changes: 11 additions & 2 deletions docs/examples.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# Example programs

The project includes four working examples:
The project includes five working examples:

* A DHCP client
* A DHCP pool server
* A DHCP mac2ip server
* An ICMP/ping client
* A pcap file parser

Build the examples:

```
npx shadow-cljs compile dhcp-client pool-server ping-client read-pcap
npx shadow-cljs compile dhcp-client pool-server mac2ip-server ping-client read-pcap
```

## Usage:
Expand All @@ -32,6 +33,14 @@ npx shadow-cljs compile dhcp-client pool-server ping-client read-pcap
sudo node ./build/pool-server.js eth0
```

* **DHCP mac2ip server** - Run a DHCP server on eth0 that calculates
the IP assignment based on the client's MAC address. The MAC to IP
mappings are defined in a config file.

```
sudo node ./build/mac2ip-server.js --if-name eth0 --config-file mac2ip.json
```

* **ICMP/ping client** - Use the ping client to demonstrate ICMP
protocl reading/writing. Elevated permissions are required to
send/receive ICMP packets.
Expand Down
10 changes: 10 additions & 0 deletions shadow-cljs.edn
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
{:optimizations :simple
:source-map-use-fs-paths true}}

:mac2ip-server
{:target :node-script
:main dhcp.mac2ip-server/main
:output-to "build/mac2ip-server.js"
;; Don't try and connect back to shadow-cljs process
:devtools {:enabled false :console-support false}
:compiler-options
{:optimizations :simple
:source-map-use-fs-paths true}}

:ping-client
{:target :node-script
:main icmp.ping/main
Expand Down
136 changes: 136 additions & 0 deletions src/dhcp/mac2ip_server.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
;; Copyright (c) 2023, Viasat, Inc
;; Licensed under EPL 2.0

(ns dhcp.mac2ip-server
"Multiprocess DHCP server that does direct mapping of MAC to IP addresses."
(:require [cljs-bean.core :refer [->clj ->js]]
[protocol.fields :as fields]
[protocol.addrs :as addrs]
[dhcp.core :as dhcp]
[dhcp.util :as util]
[dhcp.logging :as logging]
[dhcp.node-server :as server]
[clojure.walk :refer [postwalk]]))

(def cluster (js/require "node:cluster"))
(def minimist (js/require "minimist"))

(def USAGE "Usage: mac2ip-server [options]
Options:
--processes CNT - Number of worker processes
[default: 1]
--config-file CFG - Load json/edn config from CFG
[default: mac2ip.json]
--if-name IF-NAME - Bind to interface IF-NAME
[default: all]
--log-level LEVEL - Set logging level to LEVEL:
[default: 1]
0 - none
1 - one character per event
2 - full log line for each event
")

(def arg-defaults {:processes 1
:if-name "all"
:config-file "mac2ip.json"
:log-level 1})

(defn load-config
[config-file]
(let [cfg (util/load-config config-file)
ranges (for [r (:ranges cfg)]
{:ip-start (-> r :ip-start addrs/ip->int)
:ip-end (-> r :ip-end addrs/ip->int)
:mac-start (-> r :mac-start addrs/mac->int)
:mac-end (-> r :mac-end addrs/mac->int)})]
(assoc cfg :ranges ranges)))

(defn mac->ip [msg-map ranges]
(let [mac-int (addrs/mac->int (:chaddr msg-map))
r (first (filter #(and (>= mac-int (:mac-start %))
(<= mac-int (:mac-end %)))
ranges))]
(when r
(addrs/int->ip (+ (:ip-start r) (- mac-int (:mac-start r)))))))

(defn mac2ip-handler [{:keys [ranges server-info log-msg
log-level] :as cfg} msg-map]
(let [ip (mac->ip msg-map ranges)]
(if (not ip)
(do
(log-msg :error (str "MAC " (:chaddr msg-map) "is out of range"))
nil)
(do
(condp = log-level
2 (log-msg :info (str "Assigning " ip " to " (:chaddr msg-map)))
nil)
(merge
(dhcp/default-response msg-map server-info)
(select-keys msg-map [:giaddr :opt/relay-agent-info])
(:fields cfg) ;; config file field/option overrides
{:yiaddr ip})))))

(defn worker [user-cfg]
(let [log-msg logging/log-message
cfg (merge
user-cfg
{:message-handler mac2ip-handler
:error-handler #(util/fatal 1 "Could not create server:" %)
:log-msg log-msg})]

(logging/start-logging cfg)
(log-msg :info "Starting DHCP Server...")
(server/create-server cfg)))

(defn parse-args
[& args]
(let [minimist-opts {:default arg-defaults }
opts (->clj (minimist (apply array args) (->js minimist-opts)))
{:keys [h help if-name config-file log-level]} opts
_ (when (or h help) (util/fatal 2 USAGE))
_ (when-not (util/existsSync config-file)
(util/fatal 2 "Config file" config-file "does not exist"))
file-cfg (load-config config-file)
_ (when (and (= "all" if-name)
(not (:server-info file-cfg)))
(util/fatal 2 "--if-name or config server-info required"))
if-info (util/get-if-ipv4 if-name)
_ (when (and (not= "all" if-name)
(not if-info))
(util/fatal 2 "Interface" if-name "not found"))
;; precedence: CLI opts, file config, discovered interface info
user-cfg (util/deep-merge {:server-info if-info
:buffsz (* 16 1024 1024)}
file-cfg
(dissoc opts :_))]

(when (and (= "all" if-name)
(not (:disable-broadcast user-cfg)))
(util/fatal 2 "--if-name or --disable-broadcast must be specified"))

user-cfg))

(defn main
"Start mac2ip DHCP server worker processes listening on `if-name`
using `config-file`"
[& args]
(if cluster.isPrimary
(let [{:keys [processes log-level] :as cfg} (apply parse-args args)]
(when (= 2 log-level) (println "User config:" cfg))
(println "Forking" processes "workers")
(doseq [i (range processes)
:let [cfg (assoc cfg :log-prefix (str "worker-" i))
worker (cluster.fork)]]
^object
(.on worker "message"
#(let [msg (->clj %)]
(condp = (:type msg)
"ready" (.send worker (->js {:type "start"
:cfg cfg})))))))
(do
(.on js/process "message"
#(let [msg (->clj %)]
(if (= "start" (:type msg))
(worker (:cfg msg)))))
(.send js/process (->js {:type "ready"
:pid js/process.pid})))))
24 changes: 13 additions & 11 deletions src/dhcp/node_server.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
(ns dhcp.node-server
"Framework for creating DHCP server implementations. Actual "
(:require [clojure.string :as string]
[cljs-bean.core :refer [->clj]]
[protocol.socket :as socket]
[dhcp.core :as dhcp]
["dgram" :as dgram]))
Expand Down Expand Up @@ -31,25 +32,26 @@
(str resp-addr ":" (:port rinfo))))))
(log-msg :error "No msg-map from message-handler, ignoring"))))


(defn create-server
"Create a DHCP server listening on `if-name` that will call
`message-handler` to get a response message for a client message."
[{:keys [if-name buffsz log-msg error-handler] :as cfg
[{:keys [if-name buffsz disable-broadcast log-msg error-handler] :as cfg
:or {log-msg #(apply println %&)
error-handler #(prn :err %)}}]
(let [sock (dgram/createSocket #js {:type "udp4" :reuseAddr true})
cfg (assoc cfg :sock sock)]
(doto sock
(.on "error" error-handler)
(.on "message" (fn [buf rinfo]
(server-message-handler
cfg buf (js->clj rinfo :keywordize-keys true))))
(.on "listening" (fn []
(socket/set-reuse-port sock)
(.setBroadcast sock true)
(when buffsz (socket/set-rcvbuf sock buffsz))
(socket/bind-to-device sock if-name)
(log-msg :info (str "Listening on interface " if-name
" port " dhcp/RECV-PORT))))
(.on "message"
(fn [buf rinfo]
(server-message-handler cfg buf (->clj rinfo))))
(.on "listening"
(fn []
(when-not disable-broadcast (.setBroadcast sock true))
(when (not= "all" if-name) (socket/bind-to-device sock if-name))
(when buffsz (socket/set-rcvbuf sock buffsz))
(log-msg :info (str "Listening to port " dhcp/RECV-PORT
" on " if-name))))
(.bind dhcp/RECV-PORT))
cfg))
6 changes: 6 additions & 0 deletions src/protocol/addrs.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
(defn int->ip "Convert IPv4 uint32 value to IPv4 string" [num]
(octet->ip (fields/int->octet num 4)))

(defn mac->int "Convert MAC string to int value" [ip]
(fields/octet->int (mac->octet ip)))

(defn int->mac "Convert MAC int value to IPv4 string" [num]
(octet->mac (fields/int->octet num 6)))

(defn first-ip
"Return first IPv4 addr based on `ip` and `netmask`"
[ip netmask]
Expand Down
70 changes: 70 additions & 0 deletions test/docker-compose-mac2ip.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
version: "2.4"

x-service-base: &service-base
cap_add: [NET_ADMIN, NET_RAW, NET_BROADCAST, NET_BIND_SERVICE, SYS_ADMIN]
security_opt: [ 'apparmor:unconfined' ] # needed on Ubuntu 18.04
network_mode: none
image: clj-protocol-test
build:
dockerfile: test/Dockerfile.node
context: ../
volumes:
- ./:/test:ro
- ../build:/root/app/build:ro
- ../.shadow-cljs:/root/app/.shadow-cljs:ro

services:
net:
image: lonocloud/conlink:20210825_165410-g9c367bc6fc24
pid: host
network_mode: none
cap_add: [SYS_ADMIN, SYS_NICE, NET_ADMIN, NET_BROADCAST]
security_opt: [ 'apparmor:unconfined' ] # needed on Ubuntu 18.04
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker:/var/lib/docker
- ./:/test
command: /sbin/conlink.py --compose-file /test/docker-compose-mac2ip.yaml
x-network:
links:
- left: {container: svr_1, intf: eth0, ip: 10.1.0.1/16}
right: {container: net_1, intf: svr}

- {left: {container: ucli_1, intf: eth0, ip: 10.1.1.1/16, mac: "00:00:10:01:07:01"}, right: {container: net_1, intf: uc1}}
- {left: {container: ucli_2, intf: eth0, ip: 10.1.1.2/16, mac: "00:00:10:01:07:02"}, right: {container: net_1, intf: uc2}}
- {left: {container: ucli_3, intf: eth0, ip: 10.1.1.3/16, mac: "00:00:10:01:07:03"}, right: {container: net_1, intf: uc3}}
- {left: {container: ucli_4, intf: eth0, ip: 10.1.1.4/16, mac: "00:00:10:01:07:04"}, right: {container: net_1, intf: uc4}}
- {left: {container: ucli_5, intf: eth0, ip: 10.1.1.5/16, mac: "00:00:10:01:07:05"}, right: {container: net_1, intf: uc5}}
- {left: {container: ucli_6, intf: eth0, ip: 10.1.1.6/16, mac: "00:00:10:01:07:06"}, right: {container: net_1, intf: uc6}}
- {left: {container: ucli_7, intf: eth0, ip: 10.1.1.7/16, mac: "00:00:10:01:07:07"}, right: {container: net_1, intf: uc7}}
- {left: {container: ucli_8, intf: eth0, ip: 10.1.1.8/16, mac: "00:00:10:01:07:08"}, right: {container: net_1, intf: uc8}}
- {left: {container: ucli_9, intf: eth0, ip: 10.1.1.9/16, mac: "00:00:10:01:07:09"}, right: {container: net_1, intf: uc9}}
- {left: {container: ucli_10, intf: eth0, ip: 10.1.1.10/16, mac: "00:00:10:01:07:0a"}, right: {container: net_1, intf: uc10}}

mininet-cfg:
switches:
- name: s1
interfaces:
- {name: svr, node: s1}

- {name: uc1, node: s1}
- {name: uc2, node: s1}
- {name: uc3, node: s1}
- {name: uc4, node: s1}
- {name: uc5, node: s1}
- {name: uc6, node: s1}
- {name: uc7, node: s1}
- {name: uc8, node: s1}
- {name: uc9, node: s1}
- {name: uc10, node: s1}

svr:
<<: *service-base
command: /root/app/init.sh /root/app/build/mac2ip-server.js --processes 2 --config-file /test/mac2ip.json --if-name eth0 --log-level 2

ucli:
<<: *service-base
scale: 10
#scale: 1
command: /root/app/init.sh /root/app/build/dhcp-client.js --unicast --server-ip 10.1.0.1 --if-name eth0 --log-level 2
6 changes: 5 additions & 1 deletion test/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ wait_for_if eth0
#echo "Starting tcpdump"
#tcpdump -enli eth0 &

echo "Running: ${@}"
# Give servers a chance to fully start
case "${*}" in
*client*) sleep 2 ;;
esac

echo "Running: ${@}"
${@}
11 changes: 11 additions & 0 deletions test/mac2ip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"disable-broadcast": true,
"ranges": [
{
"ip-start": "10.0.0.0",
"ip-end": "11.0.0.0",
"mac-start": "00:00:10:00:00:00",
"mac-end": "00:00:11:00:00:00"
}
]
}

0 comments on commit 5bcf3de

Please sign in to comment.