Mirage

Mirage

Reference: https://ocaml.org/p/mirage/4.10.3

Install Mirage OS

Reference: https://mirage.io/docs/install

Create new switch for Mirage OS

opam switch create mirage.5.3.0 ocaml 5.3.0

eval $(opam env --switch=mirage.5.3.0)
$ opam list
# Packages matching: installed
# Name                # Installed # Synopsis
base-bigarray         base
base-domains          base
base-effects          base
base-nnp              base        Naked pointers prohibited in the OCaml heap
base-threads          base
base-unix             base
ocaml                 5.3.0       The OCaml compiler (virtual package)
ocaml-base-compiler   5.3.0       Official release of OCaml 5.3.0
ocaml-compiler        5.3.0       Official release of OCaml 5.3.0
ocaml-config          3           OCaml Switch Configuration
ocaml-options-vanilla 1           Ensure that OCaml is compiled with no special options enabled
opam update
opam upgrade

Install mirage os.

opam install mirage

Verify its installed.

mirage --help

Create unikernal with no jobs

Reference: https://mirage.io/docs/hello-world

git clone https://github.com/mirage/mirage-skeleton.git
cd mirage-skeleton

Create make file

cd tutorial/noop/
mirage configure -t unix
Successfully configured the unikernel. Now run 'make' (or more fine-grained steps: 'make all', 'make depends', or 'make lock').

Install dependencies

make depends
using overlay repository mirage: [opam-overlays, mirage-overlays] 
[opam-overlays] Initialised
[NOTE] Repository opam-overlays has been added to the selections of switch mirage.5.3.0 only.
       Run `opam repository add opam-overlays --all-switches|--set-default' to use it in all existing switches, or in
       newly created switches, respectively.

[mirage-overlays] Initialised
[NOTE] Repository mirage-overlays has been added to the selections of switch mirage.5.3.0 only.
       Run `opam repository add mirage-overlays --all-switches|--set-default' to use it in all existing switches, or in
       newly created switches, respectively.

 ↳ generate lockfile for monorepo dependencies
==> Using 1 locally scanned package as the target.
==> Found 60 opam dependencies for the target package.
==> Querying opam database for their metadata and Dune compatibility.
==> Calculating exact pins for each of them.
==> Wrote lockfile with 25 entries to mirage/noop-unix.opam.locked. You can now run opam monorepo pull to fetch their sources.
removing overlay repository [opam-overlays, mirage-overlays]
Repositories removed from the selections of switch mirage.5.3.0. Use '--all' to forget about them altogether.
Repositories removed from the selections of switch mirage.5.3.0. Use '--all' to forget about them altogether.
The lock file has been generated. Run 'make pull' to retrieve the sources, or 'make install-switch' to install the host dependencies.
 ↳ opam install switch dependencies
[WARNING] Failed checks on noop-unix package definition from source at
          file:///home/porky/ocaml/mirage/mirage-skeleton/tutorial/noop/mirage:
  warning 68: Missing field 'license'
Nothing to do.
 ↳ install external dependencies for monorepo
==> Using lockfile mirage/noop-unix.opam.locked
The dependencies have been installed. Run 'make build' to build the unikernel.
 ↳ fetch monorepo dependencies in the duniverse folder
==> Using lockfile mirage/noop-unix.opam.locked
Successfully pulled 25/25 repositories
The sources have been pulled to the duniverse folder. Run 'make build' to build the unikernel.

Compile

make build

Execute.

$ dist/noop

# see the exit status of the last executed function
$ echo $?   #
0

Create unikernal that returns log

Build hello.

cd tutorial/hello
mirage configure -t unix
make depends
dune build

Run hello.

dist/hello 
2025-12-14T09:02:01-08:00: [INFO] [application] hello
2025-12-14T09:02:02-08:00: [INFO] [application] hello
2025-12-14T09:02:03-08:00: [INFO] [application] hello
2025-12-14T09:02:04-08:00: [INFO] [application] hello

Build for KVM

mirage configure -t hvt
make depends
dune build
solo5-hvt dist/hello.hvt 
$ solo5-hvt dist/hello.hvt 
            |      ___|
  __|  _ \  |  _ \ __ \
\__ \ (   | | (   |  ) |
____/\___/ _|\___/____/
Solo5: Bindings version v0.10.0
Solo5: Memory map: 512 MB addressable:
Solo5:   reserved @ (0x0 - 0xfffff)
Solo5:       text @ (0x100000 - 0x230fff)
Solo5:     rodata @ (0x231000 - 0x280fff)
Solo5:       data @ (0x281000 - 0x33efff)
Solo5:       heap >= 0x33f000 < stack < 0x20000000
2025-12-14T17:14:08-00:00: [INFO] [application] hello
2025-12-14T17:14:09-00:00: [INFO] [application] hello
2025-12-14T17:14:10-00:00: [INFO] [application] hello
2025-12-14T17:14:11-00:00: [INFO] [application] hello
Solo5: solo5_exit(0) called

Runtime arguments

cd tutorial/hello-key
mirage configure -t unix
make depends
$ dist/hello-key 
2025-12-14T12:25:06-08:00: [INFO] [application] Hello World!
2025-12-14T12:25:07-08:00: [INFO] [application] Hello World!
2025-12-14T12:25:08-08:00: [INFO] [application] Hello World!
2025-12-14T12:25:09-08:00: [INFO] [application] Hello World!
$ dist/hello-key --hello="Good-bye!"
2025-12-14T12:25:34-08:00: [INFO] [application] Good-bye!
2025-12-14T12:25:35-08:00: [INFO] [application] Good-bye!
2025-12-14T12:25:36-08:00: [INFO] [application] Good-bye!
2025-12-14T12:25:37-08:00: [INFO] [application] Good-bye!

…using HVT

mirage configure -t hvt
make depends
dune build
$ solo5-hvt dist/hello-key.hvt 
            |      ___|
  __|  _ \  |  _ \ __ \
\__ \ (   | | (   |  ) |
____/\___/ _|\___/____/
Solo5: Bindings version v0.10.0
Solo5: Memory map: 512 MB addressable:
Solo5:   reserved @ (0x0 - 0xfffff)
Solo5:       text @ (0x100000 - 0x231fff)
Solo5:     rodata @ (0x232000 - 0x281fff)
Solo5:       data @ (0x282000 - 0x33ffff)
Solo5:       heap >= 0x340000 < stack < 0x20000000
2025-12-14T20:29:19-00:00: [INFO] [application] Hello World!
2025-12-14T20:29:20-00:00: [INFO] [application] Hello World!
2025-12-14T20:29:21-00:00: [INFO] [application] Hello World!
2025-12-14T20:29:22-00:00: [INFO] [application] Hello World!
Solo5: solo5_exit(0) called

$ solo5-hvt dist/hello-key.hvt --hello="HVT!"
            |      ___|
  __|  _ \  |  _ \ __ \
\__ \ (   | | (   |  ) |
____/\___/ _|\___/____/
Solo5: Bindings version v0.10.0
Solo5: Memory map: 512 MB addressable:
Solo5:   reserved @ (0x0 - 0xfffff)
Solo5:       text @ (0x100000 - 0x231fff)
Solo5:     rodata @ (0x232000 - 0x281fff)
Solo5:       data @ (0x282000 - 0x33ffff)
Solo5:       heap >= 0x340000 < stack < 0x20000000
2025-12-14T20:29:44-00:00: [INFO] [application] HVT!
2025-12-14T20:29:45-00:00: [INFO] [application] HVT!
2025-12-14T20:29:46-00:00: [INFO] [application] HVT!
2025-12-14T20:29:47-00:00: [INFO] [application] HVT!
Solo5: solo5_exit(0) called
porky@macbuntu:~/ocaml/mirage/mirage-skeleton/tutorial/hello-key$ 

Access block device.

cd device-usage/block
mirage configure -t unix
make depeneds
dune build

Create disk.img

dd if=/dev/zero of=disk.img count=100000 dd if=/dev/zero of=disk.img count=100000 
100000+0 records in
100000+0 records out
51200000 bytes (51 MB, 49 MiB) copied, 0.16923 s, 303 MB/s

Execute

./dist/block_test 
2025-12-14T13:49:42-08:00: [INFO] [block] { Mirage_block.read_write = true;
                                            sector_size = 512;
                                            size_sectors = 100000L }
reading 1 sectors at 100000
reading 12 sectors at 99989
2025-12-14T13:49:42-08:00: [INFO] [block] Test sequence finished
2025-12-14T13:49:42-08:00: [INFO] [block] Total tests started: 10
2025-12-14T13:49:42-08:00: [INFO] [block] Total tests passed:  10
2025-12-14T13:49:42-08:00: [INFO] [block] Total tests failed:  0

…using hvt

$ mirage configure -t hvt
$ make depends
$ dune build
$ solo5-hvt --block:storage=disk.img ./dist/block_test.hvt
            |      ___|
  __|  _ \  |  _ \ __ \
\__ \ (   | | (   |  ) |
____/\___/ _|\___/____/
Solo5: Bindings version v0.10.0
Solo5: Memory map: 512 MB addressable:
Solo5:   reserved @ (0x0 - 0xfffff)
Solo5:       text @ (0x100000 - 0x23dfff)
Solo5:     rodata @ (0x23e000 - 0x290fff)
Solo5:       data @ (0x291000 - 0x359fff)
Solo5:       heap >= 0x35a000 < stack < 0x20000000
2025-12-14T21:53:45-00:00: [INFO] [block] { Mirage_block.read_write = true;
                                            sector_size = 512;
                                            size_sectors = 100000L }
reading 1 sectors at 100000
reading 12 sectors at 99989
2025-12-14T21:53:45-00:00: [INFO] [block] Test sequence finished
2025-12-14T21:53:45-00:00: [INFO] [block] Total tests started: 10
2025-12-14T21:53:45-00:00: [INFO] [block] Total tests passed:  10
2025-12-14T21:53:45-00:00: [INFO] [block] Total tests failed:  0
Solo5: solo5_exit(0) called

Key value store

cd device-usage/kv_ro
mirage configure -t unix
make depends
dune build
Generating Static_t.ml                   
Generating Static_t.mli

Execute…

$ dist/kv_ro 
2025-12-14T14:15:05-08:00: [INFO] [application] Contents of extremely secret vital storage confirmed!

Rebuild with --kv_ro=direct

mirage configure -t unix --kv_ro=direct
make depends
dune build
dist/kv_ro
2025-12-14T14:17:40-08:00: [INFO] [application] Contents of extremely secret vital storage confirmed!

Networking

mirage configure -t unix --net socket
make depends
dune build

Start the program

dist/network

In another console run

echo -n hello tcp world | nc -nw1 127.0.0.1 8080

…and you should see this in the command prompt

$ dist/network 
2025-12-14T14:28:37-08:00: [INFO] [tcpip-stack-socket] Dual IPv4 and IPv6 socket stack: connect
2025-12-14T14:28:40-08:00: [INFO] [application] new tcp connection from IP 127.0.0.1 on port 56654

Run program again with log set to debug

dist/network -l "*:debug"

Run the command, echo -n hello tcp world | nc -nw1 127.0.0.1 8080 and you should see the output

$ dist/network -l "*:debug"
2025-12-14T14:29:06-08:00: [INFO] [tcpip-stack-socket] Dual IPv4 and IPv6 socket stack: connect
2025-12-14T14:29:09-08:00: [INFO] [application] new tcp connection from IP 127.0.0.1 on port 45528
2025-12-14T14:29:09-08:00: [DEBUG] [application] read: 15 bytes:
hello tcp world

Unix with the ocaml network stack

mirage configure -t unix --net direct
make depends
dune build

Load the TUN (Tunnel) kernel module

To use tunctl, first install uml-utilities.

sudo apt install uml-utilities

To use ifconfig install net-tools

sudo apt install net-tools

You can see if the TUN module is loaded like this

lsmod | grep tun # this doesn't seem to do anything 

Load TUN

sudo modprobe tun 
$ sudo tunctl -u $USER -t tap0
Set 'tap0' persistent and owned by uid 1000
$ sudo ifconfig tap0 10.0.0.1 up

Run the program.

sudo dist/network -l "*:debug"

In another tab run ping 10.0.0.2 …and you’ll see the output:

$ sudo dist/network -l "*:debug"
2025-12-14T15:05:51-08:00: [DEBUG] [netif] plugging into tap0 with mac 1a:34:c7:6a:96:46 and mtu 1500
2025-12-14T15:05:51-08:00: [INFO] [netif] connect tap0 with mac 1a:34:c7:6a:96:46
2025-12-14T15:05:51-08:00: [INFO] [ethernet] Connected Ethernet interface 1a:34:c7:6a:96:46
2025-12-14T15:05:51-08:00: [INFO] [ARP] Sending gratuitous ARP for 10.0.0.2 (1a:34:c7:6a:96:46)
2025-12-14T15:05:51-08:00: [INFO] [ipv6] IP6: Starting
2025-12-14T15:05:51-08:00: [DEBUG] [ndpc6] ND6: Sending RS
2025-12-14T15:05:51-08:00: [DEBUG] [ndpc6] ND6: Sending NS src=:: dst=ff02::1:ff6a:9646 tgt=fe80::1834:c7ff:fe6a:9646
2025-12-14T15:05:51-08:00: [DEBUG] [ndpc6] IP6: Processing HOPOPT header
2025-12-14T15:05:51-08:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
2025-12-14T15:05:51-08:00: [DEBUG] [ndpc6] IP6: Processing PADN option
2025-12-14T15:05:51-08:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
2025-12-14T15:05:51-08:00: [DEBUG] [ndpc6] IP6: Processing HOPOPT header
2025-12-14T15:05:51-08:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
2025-12-14T15:05:51-08:00: [DEBUG] [ndpc6] IP6: Processing PADN option
2025-12-14T15:05:51-08:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
2025-12-14T15:05:52-08:00: [INFO] [ndpc6] ND6: Unsupported ND option in RA: ty=14 len=1
2025-12-14T15:05:52-08:00: [DEBUG] [ndpc6] ND: Received NS: src=:: dst=ff02::1:ff5a:c22d tgt=fe80::c089:f7ff:fe5a:c22d
2025-12-14T15:05:52-08:00: [DEBUG] [ndpc6] SLAAC: fe80::1834:c7ff:fe6a:9646 --> PREFERRED
2025-12-14T15:05:52-08:00: [ERROR] [netif] [read] user program requested cancellation of listen on tap0
2025-12-14T15:05:52-08:00: [INFO] [ipv6] IP6: Started with fe80::1834:c7ff:fe6a:9646
2025-12-14T15:05:52-08:00: [INFO] [tcp.pcb] TCP layer connected on 10.0.0.2/24, fe80::1834:c7ff:fe6a:9646/64
2025-12-14T15:05:52-08:00: [INFO] [udp] UDP layer connected on 10.0.0.2/24, fe80::1834:c7ff:fe6a:9646/64
2025-12-14T15:05:52-08:00: [INFO] [tcpip-stack-direct] Dual TCP/IP stack assembled: mac=1a:34:c7:6a:96:46,ip=10.0.0.2/24, fe80::1834:c7ff:fe6a:9646/64
2025-12-14T15:05:52-08:00: [DEBUG] [tcpip-stack-direct] Establishing or updating listener for stack mac=1a:34:c7:6a:96:46,ip=10.0.0.2/24, fe80::1834:c7ff:fe6a:9646/64
2025-12-14T15:05:52-08:00: [DEBUG] [tcpip-stack-direct] Establishing or updating listener for stack mac=1a:34:c7:6a:96:46,ip=10.0.0.2/24, fe80::1834:c7ff:fe6a:9646/64
2025-12-14T15:05:53-08:00: [DEBUG] [ndpc6] IP6: Processing HOPOPT header
2025-12-14T15:05:53-08:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
2025-12-14T15:05:53-08:00: [DEBUG] [ndpc6] IP6: Processing PADN option
2025-12-14T15:05:53-08:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
2025-12-14T15:05:53-08:00: [DEBUG] [ndpc6] IP6: Processing HOPOPT header
2025-12-14T15:05:53-08:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
2025-12-14T15:05:53-08:00: [DEBUG] [ndpc6] IP6: Processing PADN option
2025-12-14T15:05:53-08:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
2025-12-14T15:05:53-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e17e, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:05:53-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e17f, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:05:53-08:00: [DEBUG] [ndpc6] IP6: Processing HOPOPT header
2025-12-14T15:05:53-08:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
2025-12-14T15:05:53-08:00: [DEBUG] [ndpc6] IP6: Processing PADN option
2025-12-14T15:05:53-08:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
2025-12-14T15:05:53-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e1cb, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:05:53-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e1ef, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:05:53-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e1f0, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:05:54-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e24f, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:05:54-08:00: [DEBUG] [ndpc6] IP6: Processing HOPOPT header
2025-12-14T15:05:54-08:00: [INFO] [ndpc6] IP6: Processing unknown option, MSB 5
2025-12-14T15:05:54-08:00: [DEBUG] [ndpc6] IP6: Processing PADN option
2025-12-14T15:05:54-08:00: [INFO] [ndpc6] ICMP6: Unknown packet type: ty=143
2025-12-14T15:05:55-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e275, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:05:57-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id e8ce, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:06:16-08:00: [DEBUG] [ipv4] dropping IP fragment not for us or broadcast IPv4 packet 10.0.0.1 -> 224.0.0.251: id 1841, off 16384 proto 17, ttl 255, options 
                                          
2025-12-14T15:06:27-08:00: [DEBUG] [ARP] replying to ARP request for 10.0.0.2 from 10.0.0.1 (mac c2:89:f7:5a:c2:2d)
2025-12-14T15:06:27-08:00: [DEBUG] [icmpv4] ICMP echo-request received: ICMP type echo request, code 0, subheader [subheader: id: 63675, sequence 1] (payload 
                                            f3 42 3f 69 00 00 00 00  39 02 06 00 00 00 00 00
                                            10 11 12 13 14 15 16 17  18 19 1a 1b 1c 1d 1e 1f
                                            20 21 22 23 24 25 26 27  28 29 2a 2b 2c 2d 2e 2f
                                            30 31 32 33 34 35 36 37)
2025-12-14T15:06:27-08:00: [DEBUG] [ipv4] ip write: mtu is 1500, hdr_len is 20, size 0 payload len 64, needed_bytes 84
2025-12-14T15:06:28-08:00: [DEBUG] [icmpv4] ICMP echo-request received: ICMP type echo request, code 0, subheader [subheader: id: 63675, sequence 2] (payload 
                                            f4 42 3f 69 00 00 00 00  3b 03 06 00 00 00 00 00
                                            10 11 12 13 14 15 16 17  18 19 1a 1b 1c 1d 1e 1f
                                            20 21 22 23 24 25 26 27  28 29 2a 2b 2c 2d 2e 2f
                                            30 31 32 33 34 35 36 37)

Run it again, but use echo -n hello tcp world | nc -nw1 10.0.0.2 8080

2025-12-14T15:18:40-08:00: [DEBUG] [tcp.pcb] process-syn: [channels=0 listens=0 connects=0]
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.pcb] new-server-connection: [channels=0 listens=0 connects=0]
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Closed  - Passive_open -> Listen
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Listen  - Send_synack(3774277702) -> Syn_rcvd(3774277702)
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.tcptimer] timerloop
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.tcptimer] timerloop: sleeping for 667000000 ns
2025-12-14T15:18:40-08:00: [DEBUG] [ipv4] ip write: mtu is 1500, hdr_len is 20, size 28 payload len 0, needed_bytes 48
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.pcb] process-ack: [channels=0 listens=1 connects=0]
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.window] sequence validation: seq=2774950133 range=2774950133[262140] res=true
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Syn_rcvd(3774277702)  - Recv_ack(3774277703) -> Established
2025-12-14T15:18:40-08:00: [INFO] [application] new tcp connection from IP 10.0.0.1 on port 51776
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.window] sequence validation: seq=2774950133 range=2774950133[262140] res=true
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Established  - Recv_ack(3774277703) -> Established
2025-12-14T15:18:40-08:00: [DEBUG] [application] read: 15 bytes:
hello tcp world
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.pcb] Closing connection remote 10.0.0.1,51776 to local 10.0.0.2, 8080
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Established  - Send_fin(3774277703) -> Fin_wait_1(3774277703)
2025-12-14T15:18:40-08:00: [DEBUG] [ipv4] ip write: mtu is 1500, hdr_len is 20, size 20 payload len 0, needed_bytes 40
2025-12-14T15:18:40-08:00: [DEBUG] [ipv4] ip write: mtu is 1500, hdr_len is 20, size 20 payload len 0, needed_bytes 40
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.window] sequence validation: seq=2774950148 range=2774950148[262140] res=true
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] finwait2timer 10000000000
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Fin_wait_1(3774277703)  - Recv_ack(3774277704) -> Fin_wait_2(0)
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.window] sequence validation: seq=2774950148 range=2774950148[262140] res=true
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Fin_wait_2(0)  - Recv_ack(3774277704) -> Fin_wait_2(1)
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] timewait 2000000000
2025-12-14T15:18:40-08:00: [DEBUG] [tcp.state] 1 Fin_wait_2(1)  - Recv_fin -> Time_wait
2025-12-14T15:18:40-08:00: [DEBUG] [ipv4] ip write: mtu is 1500, hdr_len is 20, size 20 payload len 0, needed_bytes 40
2025-12-14T15:18:41-08:00: [DEBUG] [tcp.tcptimer] timerloop: stoptimer
2025-12-14T15:18:42-08:00: [DEBUG] [tcp.state] timewait on_close
2025-12-14T15:18:42-08:00: [DEBUG] [tcp.pcb] removing pcb from connection tables: [channels=1 listens=0 connects=0]
2025-12-14T15:18:42-08:00: [DEBUG] [tcp.pcb] removed remote 10.0.0.1,51776 to local 10.0.0.2, 8080 from active channels
2025-12-14T15:18:45-08:00: [DEBUG] [ARP] replying to ARP request for 10.0.0.2 from 10.0.0.1 (mac c2:89:f7:5a:c2:2d)
2025-12-14T15:18:50-08:00: [DEBUG] [tcp.state] finwait2timer: Closed