Lunatik

Lunatik is a framework for scripting the Linux kernel with Lua. It is composed by the Lua interpreter modified to run in the kernel; a device driver (written in Lua =)) and a command line tool to load and run scripts and manage runtime environments from the user space; a C API to load and run scripts and manage runtime environments from the kernel; and Lua APIs for binding kernel facilities to Lua scripts.

Here is an example of a character device driver written in Lua using Lunatik to generate random ASCII printable characters:

-- /lib/modules/lua/passwd.lua
--
-- implements /dev/passwd for generate passwords
-- usage: $ sudo lunatik run passwd
--        $ head -c <width> /dev/passwd

local device = require("device")
local linux  = require("linux")

local function nop() end -- do nothing

local s = linux.stat
local driver = {name = "passwd", open = nop, release = nop, mode = s.IRUGO}

function driver:read() -- read(2) callback
 -- generate random ASCII printable characters
 return string.char(linux.random(32, 126))
end

-- creates a new character device
device.new(driver)

Setup

Install dependencies (here for Debian/Ubuntu, to be adapted to one's distribution):

sudo apt install git build-essential lua5.4 dwarves clang llvm libelf-dev linux-headers-$(uname -r) linux-tools-common linux-tools-$(uname -r) pkg-config libpcap-dev m4

Install dependencies (here for Arch Linux):

sudo pacman -S git lua clang llvm m4 libpcap pkg-config build2 linux-tools linux-headers

Compile and install lunatik:

LUNATIK_DIR=~/lunatik  # to be adapted
mkdir "${LUNATIK_DIR}" ; cd "${LUNATIK_DIR}"
git clone --depth 1 --recurse-submodules https://github.com/luainkernel/lunatik.git
cd lunatik
make
sudo make install

Once done, the debian_kernel_postinst_lunatik.sh script from tools/ may be copied into /etc/kernel/postinst.d/: this ensures lunatik (and also the xdp needed libs) will get compiled on kernel upgrade.

OpenWRT

Install Lunatik from our package feed.

Usage

 sudo lunatik # execute Lunatik REPL
 Lunatik 3.6  Copyright (C) 2023-2025 ring-0 Ltda.
 > return 42 -- execute this line in the kernel
 42

lunatik

usage: lunatik [load|unload|reload|status|list] [run|spawn|stop <script>]
  • load: load Lunatik kernel modules
  • unload: unload Lunatik kernel modules
  • reload: reload Lunatik kernel modules
  • status: show which Lunatik kernel modules are currently loaded
  • list: show which runtime environments are currently running
  • run: create a new runtime environment to run the script /lib/modules/lua/<script>.lua
  • spawn: create a new runtime environment and spawn a thread to run the script /lib/modules/lua/<script>.lua
  • stop: stop the runtime environment created to run the script <script>
  • default: start a REPL (Read–Eval–Print Loop)

Lua Version

Lunatik 3.6 is based on Lua 5.4 adapted to run in the kernel.

Floating-point numbers

Lunatik does not support floating-point arithmetic, thus it does not support __div nor __pow metamethods and the type number has only the subtype integer.

Lua API

Lunatik does not support both io and os libraries, and the given identifiers from the following libraries: * debug.debug, math.acos, math.asin, math.atan, math.ceil, math.cos, math.deg, math.exp, math.floor, math.fmod, math.huge. math.log, math.modf, math.pi, math.rad, math.random, math.randomseed, math.sin, math.sqrt, math.tan, math.type, package.cpath.

Lunatik modifies the following identifiers: * _VERSION: is defined as "Lua 5.4-kernel". * collectgarbage("count"): returns the total memory in use by Lua in bytes, instead of Kbytes. * package.path: is defined as "/lib/modules/lua/?.lua;/lib/modules/lua/?/init.lua". * require: only supports built-in or already linked C modules, that is, Lunatik cannot load kernel modules dynamically.

C API

Lunatik does not support luaL_Stream, luaL_execresult, luaL_fileresult, luaopen_io and luaopen_os.

Lunatik modifies luaL_openlibs to remove luaopen_io and luaopen_os.

Lunatik C API

#include <lunatik.h>

lunatik_runtime

int lunatik_runtime(lunatik_object_t **pruntime, const char *script, bool sleep);

lunatik_runtime() creates a new runtime environment then loads and runs the script /lib/modules/lua/<script>.lua as the entry point for this environment. It must only be called from process context. The runtime environment is a Lunatik object that holds a Lua state. Lunatik objects are special Lua userdata which also hold a lock type and a reference counter. If sleep is true, lunatik_runtime() will use a mutex for locking the runtime environment and the GFP_KERNEL flag for allocating new memory later on on lunatik_run() calls. Otherwise, it will use a spinlock and GFP_ATOMIC. lunatik_runtime() opens the Lua standard libraries present on Lunatik. If successful, lunatik_runtime() sets the address pointed by pruntime and Lua's extra space with a pointer for the new created runtime environment, sets the reference counter to 1 and then returns 0. Otherwise, it returns -ENOMEM, if insufficient memory is available; or -EINVAL, if it fails to load or run the script.

Example
-- /lib/modules/lua/mydevice.lua
function myread(len, off)
    return "42"
end
static lunatik_object_t *runtime;

static int __init mydevice_init(void)
{
    return lunatik_runtime(&runtime, "mydevice", true);
}

lunatik_stop

int lunatik_stop(lunatik_object_t *runtime);

lunatik_stop() closes the Lua state created for this runtime environment and decrements the reference counter. Once the reference counter is decremented to zero, the lock type and the memory allocated for the runtime environment are released. If the runtime environment has been released, it returns 1; otherwise, it returns 0.

lunatik_run

void lunatik_run(lunatik_object_t *runtime, <inttype> (*handler)(...), <inttype> &ret, ...);

lunatik_run() locks the runtime environment and calls the handler passing the associated Lua state as the first argument followed by the variadic arguments. If the Lua state has been closed, ret is set with -ENXIO; otherwise, ret is set with the result of handler(L, ...) call. Then, it restores the Lua stack and unlocks the runtime environment. It is defined as a macro.

Example
static int l_read(lua_State *L, char *buf, size_t len, loff_t *off)
{
    size_t llen;
    const char *lbuf;

    lua_getglobal(L, "myread");
    lua_pushinteger(L, len);
    lua_pushinteger(L, *off);
    if (lua_pcall(L, 2, 2, 0) != LUA_OK) { /* calls myread(len, off) */
        pr_err("%s\n", lua_tostring(L, -1));
        return -ECANCELED;
    }

    lbuf = lua_tolstring(L, -2, &llen);
    llen = min(len, llen);
    if (copy_to_user(buf, lbuf, llen) != 0)
        return -EFAULT;

    *off = (loff_t)luaL_optinteger(L, -1, *off + llen);
    return (ssize_t)llen;
}

static ssize_t mydevice_read(struct file *f, char *buf, size_t len, loff_t *off)
{
    ssize_t ret;
    lunatik_object_t *runtime = (lunatik_object_t *)f->private_data;

    lunatik_run(runtime, l_read, ret, buf, len, off);
    return ret;
}

lunatik_getobject

void lunatik_getobject(lunatik_object_t *object);

lunatik_getobject() increments the reference counter of this object (e.g., runtime environment).

lunatik_put

int lunatik_putobject(lunatik_object_t *object);

lunatik_putobject() decrements the reference counter of this object (e.g., runtime environment). If the object has been released, it returns 1; otherwise, it returns 0.

lunatik_toruntime

lunatik_object_t *lunatik_toruntime(lua_State *L);

lunatik_toruntime() returns the runtime environment referenced by the L's extra space.

Lunatik Lua APIs

Lua APIs are documented thanks to LDoc. This documentation can be read here: https://luainkernel.github.io/lunatik/, and in the source files.

Examples

spyglass

spyglass is a kernel script that implements a keylogger inspired by the spy kernel module. This kernel script logs the keysym of the pressed keys in a device (/dev/spyglass). If the keysym is a printable character, spyglass logs the keysym itself; otherwise, it logs a mnemonic of the ASCII code, (e.g., <del> stands for 127).

Usage

 sudo make examples_install          # installs examples
 sudo lunatik run examples/spyglass  # runs spyglass
 sudo tail -f /dev/spyglass          # prints the key log
 sudo sh -c "echo 'enable=false' > /dev/spyglass"       # disable the key logging
 sudo sh -c "echo 'enable=true' > /dev/spyglass"        # enable the key logging
 sudo sh -c "echo 'net=127.0.0.1:1337' > /dev/spyglass" # enable network support
 nc -lu 127.0.0.1 1337 &             # listen to UDP 127.0.0.1:1337
 sudo tail -f /dev/spyglass          # sends the key log through the network

keylocker

keylocker is a kernel script that implements Konami Code for locking and unlocking the console keyboard. When the user types ↑ ↑ ↓ ↓ ← → ← → LCTRL LALT, the keyboard will be locked; that is, the system will stop processing any key pressed until the user types the same key sequence again.

Usage

 sudo make examples_install                     # installs examples
 sudo lunatik run examples/keylocker            # runs keylocker
 <↑> <↑> <↓> <↓> <←> <→> <←> <→> <LCTRL> <LALT> # locks keyboard
 <↑> <↑> <↓> <↓> <←> <→> <←> <→> <LCTRL> <LALT> # unlocks keyboard

tap

tap is a kernel script that implements a sniffer using AF_PACKET socket. It prints destination and source MAC addresses followed by Ethernet type and the frame size.

Usage

 sudo make examples_install    # installs examples
 sudo lunatik run examples/tap # runs tap
 cat /dev/tap

shared

shared is a kernel script that implements an in-memory key-value store using rcu, data, socket and thread.

Usage

 sudo make examples_install         # installs examples
 sudo lunatik spawn examples/shared # spawns shared
 nc 127.0.0.1 90                    # connects to shared
 foo=bar                            # assigns "bar" to foo
 foo                                # retrieves foo
 bar
 ^C                                 # finishes the connection

echod

echod is an echo server implemented as kernel scripts.

Usage

 sudo make examples_install               # installs examples
 sudo lunatik spawn examples/echod/daemon # runs echod
 nc 127.0.0.1 1337
 hello kernel!
 hello kernel!

systrack

systrack is a kernel script that implements a device driver to monitor system calls. It prints the amount of times each system call was called since the driver has been installed.

Usage

 sudo make examples_install         # installs examples
 sudo lunatik run examples/systrack # runs systracker
 cat /dev/systrack
 writev: 0
 close: 1927
 write: 1085
 openat: 2036
 read: 4131
 readv: 0

filter

filter is a kernel extension composed by a XDP/eBPF program to filter HTTPS sessions and a Lua kernel script to filter SNI TLS extension. This kernel extension drops any HTTPS request destinated to a blacklisted server.

Usage

Compile and install libbpf, libxdp and xdp-loader:

mkdir -p "${LUNATIK_DIR}" ; cd "${LUNATIK_DIR}"  # LUNATIK_DIR must be set to the same value as above (Setup section)
git clone --depth 1 --recurse-submodules https://github.com/xdp-project/xdp-tools.git
cd xdp-tools/lib/libbpf/src
make
sudo DESTDIR=/ make install
cd ../../../
make libxdp
cd xdp-loader
make
sudo make install

Come back to this repository, install and load the filter:

cd ${LUNATIK_DIR}/lunatik                    # cf. above
sudo make btf_install                        # needed to export the 'bpf_luaxdp_run' kfunc
sudo make examples_install                   # installs examples
make ebpf                                    # builds the XDP/eBPF program
sudo make ebpf_install                       # installs the XDP/eBPF program
sudo lunatik run examples/filter/sni false   # runs the Lua kernel script
sudo xdp-loader load -m skb <ifname> https.o # loads the XDP/eBPF program

For example, testing is easy thanks to docker. Assuming docker is installed and running:

  • in a terminal:
    sudo xdp-loader load -m skb docker0 https.o
    sudo journalctl -ft kernel
    
  • in another one:
    docker run --rm -it alpine/curl https://ebpf.io
    

The system logs (in the first terminal) should display filter_sni: ebpf.io DROP, and the docker run… should return curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to ebpf.io:443.

filter in MoonScript

This other sni filter uses netfilter api.

dnsblock

dnsblock is a kernel script that uses the lunatik xtable library to filter DNS packets. This script drops any outbound DNS packet with question matching the blacklist provided by the user. By default, it will block DNS resolutions for the domains github.com and gitlab.com.

Usage

  1. Using legacy iptables sudo make examples_install # installs examples cd examples/dnsblock make # builds the userspace extension for netfilter sudo make install # installs the extension to Xtables directory sudo lunatik run examples/dnsblock/dnsblock false # runs the Lua kernel script sudo iptables -A OUTPUT -m dnsblock -j DROP # this initiates the netfilter framework to load our extension

  2. Using new netfilter framework (luanetfilter)

    sudo make examplesinstall # installs examples sudo lunatik run examples/dnsblock/nfdnsblock false # runs the Lua kernel script

dnsdoctor

dnsdoctor is a kernel script that uses the lunatik xtable library to change the DNS response from Public IP to a Private IP if the destination IP matches the one provided by the user. For example, if the user wants to change the DNS response from 192.168.10.1 to 10.1.2.3 for the domain lunatik.com if the query is being sent to 10.1.1.2 (a private client), this script can be used.

Usage

  1. Using legacy iptables sudo make examples_install # installs examples cd examples/dnsdoctor setup.sh # sets up the environment

    # test the setup, a response with IP 192.168.10.1 should be returned dig lunatik.com

    # run the Lua kernel script sudo lunatik run examples/dnsdoctor/dnsdoctor false

    # build and install the userspace extension for netfilter make sudo make install

    # add rule to the mangle table sudo iptables -t mangle -A PREROUTING -p udp --sport 53 -j dnsdoctor

    # test the setup, a response with IP 10.1.2.3 should be returned dig lunatik.com

    # cleanup sudo iptables -t mangle -D PREROUTING -p udp --sport 53 -j dnsdoctor # remove the rule sudo lunatik unload cleanup.sh

  2. Using new netfilter framework (luanetfilter) sudo make examples_install # installs examples examples/dnsdoctor/setup.sh # sets up the environment

    # test the setup, a response with IP 192.168.10.1 should be returned dig lunatik.com

    # run the Lua kernel script sudo lunatik run examples/dnsdoctor/nf_dnsdoctor false

    # test the setup, a response with IP 10.1.2.3 should be returned dig lunatik.com

    # cleanup sudo lunatik unload examples/dnsdoctor/cleanup.sh

References

License

Lunatik is dual-licensed under MIT or GPL-2.0-only.

Lua submodule is licensed under MIT. For more details, see its Copyright Notice.

Klibc submodule is dual-licensed under BSD 3-Clause or GPL-2.0-only. For more details, see its LICENCE file.

generated by LDoc 1.5.0 Last updated 2025-06-27 17:53:55