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)
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
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.
sudo lunatik # execute Lunatik REPL
Lunatik 3.5 Copyright (C) 2023-2024 ring-0 Ltda.
> return 42 -- execute this line in the kernel
42
usage: lunatik [load|unload|reload|status|list] [run|spawn|stop <script>]
load
: load Lunatik kernel modulesunload
: unload Lunatik kernel modulesreload
: reload Lunatik kernel modulesstatus
: show which Lunatik kernel modules are currently loadedlist
: show which runtime environments are currently runningrun
: 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)
Lunatik 3.4 is based on Lua 5.4 adapted to run in the kernel.
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.
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.
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.
#include <lunatik.h>
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
.
-- /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);
}
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
.
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.
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;
}
void lunatik_getobject(lunatik_object_t *object);
lunatik_getobject() increments the
reference counter
of this object
(e.g., runtime
environment).
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_object_t *lunatik_toruntime(lua_State *L);
lunatik_toruntime() returns the runtime
environment referenced by the L
's
extra space.
The lunatik
library provides support to load and run scripts and manage runtime environments from Lua.
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 returns a Lunatik object representing the runtime
environment.
If sleep
is true or omitted, it will use a mutex
and
GFP_KERNEL;
otherwise, it will use a spinlock and GFP_ATOMIC.
lunatik.runtime() opens the Lua standard libraries
present on Lunatik.
runtime:stop()
stops
the runtime
environment and clear its reference from the runtime object.
runtime:resume()
resumes the execution of a runtime
.
The values obj1, ...
are passed as the arguments to the function returned on the runtime
creation.
If the runtime
has yielded, resume()
restarts it; the values obj1, ...
are passed as the results from the yield.
The device
library provides support for writting
character device drivers
in Lua.
device.new() returns a new device
object
and installs its driver
in the system.
The driver
must be defined as a table containing the following field:
name
: string defining the device name; it is used for creating the device file (e.g.,/dev/<name>
).
The driver
table might optionally contain the following fields:
read
: callback function to handle the read operation on the device file. It receives thedriver
table as the first argument followed by two integers, thelength
to be read and the fileoffset
. It should return a string and, optionally, theupdated offset
. If the length of the returned string is greater than the requestedlength
, the string will be corrected to thatlength
. If theupdated offset
is not returned, theoffset
will be updated withoffset + length
.write
: callback function to handle the write operation on the device file. It receives thedriver
table as the first argument followed by the string to be written and an integer as the fileoffset
. It might return optionally the writtenlength
followed by theupdated offset
. If the returned length is greater than the requestedlength
, the returned length will be corrected. If theupdated offset
is not returned, theoffset
will be updated withoffset + length
.open
: callback function to handle the open operation on the device file. It receives thedriver
table and it is expected to return nothing.release
: callback function to handle the release operation on the device file. It receives thedriver
table and it is expected to return nothing.mode
: an integer specifying the device file mode.
If an operation callback is not defined, the device
returns -ENXIO
to VFS on its access.
device.stop() removes a device driver
specified by the dev
object from the system.
The linux
library provides support for some Linux kernel facilities.
linux.random() mimics the behavior of math.random, but binding <linux/random.h>'s get_random_u32() and get_random_u64() APIs.
When called without arguments,
produces an integer with all bits (pseudo)random.
When called with two integers m
and n
,
linux.random() returns a pseudo-random integer with uniform distribution in the range [m, n]
.
The call math.random(n)
, for a positive n
, is equivalent to math.random(1, n)
.
linux.stat is a table that exports <linux/stat.h> integer flags to Lua.
"IRWXUGO"
: permission to read, write and execute for user, group and other."IRUGO"
: permission only to read for user, group and other."IWUGO"
: permission only to write for user, group and other."IXUGO"
: permission only to execute for user, group and other.
linux.schedule() sets the current task state
and makes the it sleep until timeout
milliseconds have elapsed.
If timeout
is omitted, it uses MAX_SCHEDULE_TIMEOUT
.
If state
is omitted, it uses task.INTERRUPTIBLE
.
linux.task is a table that exports task state flags to Lua.
"RUNNING"
: task is executing on a CPU or waiting to be executed."INTERRUPTIBLE"
: task is waiting for a signal or a resource (sleeping)."UNINTERRUPTIBLE"
: behaves like "INTERRUPTIBLE" with the exception that signal will not wake up the task."KILLABLE"
: behaves like "UNINTERRUPTIBLE" with the exception that fatal signals will wake up the task."IDLE"
: behaves like "UNINTERRUPTIBLE" with the exception that it avoids the loadavg accounting.
linux.time() returns the current time in nanoseconds since epoch.
linux.errno is a table that exports <uapi/asm-generic/errno-base.h> flags to Lua.
"PERM"
: Operation not permitted."NOENT"
: No such file or directory."SRCH"
: No such process."INTR"
: Interrupted system call."IO"
: I/O error."NXIO"
:No such device or address."2BIG"
:, Argument list too long."NOEXEC"
: Exec format error."BADF"
: Bad file number."CHILD"
: No child processes."AGAIN"
: Try again."NOMEM"
: Out of memory."ACCES"
: Permission denied."FAULT"
: Bad address."NOTBLK"
: Block device required."BUSY"
: Device or resource busy."EXIST"
: File exists."XDEV"
: Cross-device link."NODEV"
: No such device."NOTDIR"
: Not a directory."ISDIR"
: Is a directory."INVAL"
: Invalid argument."NFILE"
: File table overflow."MFILE"
: Too many open files."NOTTY"
: Not a typewriter."TXTBSY"
: Text file busy."FBIG"
: File too large."NOSPC"
: No space left on device."SPIPE"
: Illegal seek."ROFS"
: Read-only file system."MLINK"
: Too many links."PIPE"
: Broken pipe."DOM"
: Math argument out of domain of func."RANGE"
: Math result not representable.
linux.hton16() converts the host byte order to network byte order for a 16-bit integer.
linux.hton32() converts the host byte order to network byte order for a 32-bit integer.
linux.hton64() converts the host byte order to network byte order for a 64-bit integer.
linux.ntoh16() converts the network byte order to host byte order for a 16-bit integer.
linux.ntoh32() converts the network byte order to host byte order for a 32-bit integer.
linux.ntoh64() converts the network byte order to host byte order for a 64-bit integer.
linux.htobe16() converts the host byte order to big-endian byte order for a 16-bit integer.
linux.htobe32() converts the host byte order to big-endian byte order for a 32-bit integer.
linux.htobe64() converts the host byte order to big-endian byte order for a 64-bit integer.
linux.be16toh() converts the big-endian byte order to host byte order for a 16-bit integer.
linux.be32toh() converts the big-endian byte order to host byte order for a 32-bit integer.
linux.be64toh() converts the big-endian byte order to host byte order for a 64-bit integer.
linux.htole16() converts the host byte order to little-endian byte order for a 16-bit integer.
linux.htole32() converts the host byte order to little-endian byte order for a 32-bit integer.
linux.htole64() converts the host byte order to little-endian byte order for a 64-bit integer.
linux.le16toh() converts the little-endian byte order to host byte order for a 16-bit integer.
linux.le32toh() converts the little-endian byte order to host byte order for a 32-bit integer.
linux.le64toh() converts the little-endian byte order to host byte order for a 64-bit integer.
The notifier
library provides support for the kernel
notifier chains.
notifier.keyboard() returns a new keyboard notifier
object and installs it in the system.
The callback
function is called whenever a console keyboard event happens
(e.g., a key has been pressed or released).
This callback
receives the following arguments:
event
: the available events are defined by the notifier.kbd table.down
:true
, if the key is pressed;false
, if it is released.shift
:true
, if the shift key is held;false
, otherwise.key
: keycode or keysym depending onevent
.
The callback
function might return the values defined by the
notifier.notify table.
notifier.kbd is a table that exports KBD flags to Lua.
"KEYCODE"
: keyboard keycode, called before any other."UNBOUND_KEYCODE"
: keyboard keycode which is not bound to any other."UNICODE"
: keyboard unicode."KEYSYM"
: keyboard keysym."POST_KEYSYM"
: called after keyboard keysym interpretation.
notifier.netdevice() returns a new netdevice notifier
object and installs it in the system.
The callback
function is called whenever a console netdevice event happens
(e.g., a network interface has been connected or disconnected).
This callback
receives the following arguments:
event
: the available events are defined by the notifier.netdev table.name
: the device name.
The callback
function might return the values defined by the
notifier.notify table.
notifier.netdev is a table that exports NETDEV flags to Lua.
notifier.notify is a table that exports NOTIFY flags to Lua.
"DONE"
: don't care."OK"
: suits me."BAD"
: bad/veto action."STOP"
: clean way to return from the notifier and stop further calls.
notfr:delete() removes a notifier
specified by the notfr
object from the system.
The socket
library provides support for the kernel
networking handling.
This library was inspired by
Chengzhi Tan's
GSoC project.
socket.new() creates a new socket
object.
This function receives the following arguments:
family
: the available address families are defined by the socket.af table.sock
: the available types are present on the socket.sock table.protocol
: the available protocols are defined by the socket.ipproto table.
socket.af is a table that exports address families (AF) to Lua.
"UNSPEC"
: Unspecified."UNIX"
: Unix domain sockets."LOCAL"
: POSIX name for AF_UNIX."INET"
: Internet IP Protocol."AX25"
: Amateur Radio AX.25."IPX"
: Novell IPX."APPLETALK"
: AppleTalk DDP."NETROM"
: Amateur Radio NET/ROM."BRIDGE"
: Multiprotocol bridge."ATMPVC"
: ATM PVCs."X25"
: Reserved for X.25 project."INET6"
: IP version 6."ROSE"
: Amateur Radio X.25 PLP."DEC"
: Reserved for DECnet project."NETBEUI"
: Reserved for 802.2LLC project."SECURITY"
: Security callback pseudo AF."KEY"
: PF_KEY key management API."NETLINK"
: Netlink."ROUTE"
: Alias to emulate 4.4BSD."PACKET"
: Packet family."ASH"
: Ash."ECONET"
: Acorn Econet."ATMSVC"
: ATM SVCs."RDS"
: RDS sockets."SNA"
: Linux SNA Project (nutters!)."IRDA"
: IRDA sockets."PPPOX"
: PPPoX sockets."WANPIPE"
: Wanpipe API Sockets."LLC"
: Linux LLC."IB"
: Native InfiniBand address."MPLS"
: MPLS."CAN"
: Controller Area Network."TIPC"
: TIPC sockets."BLUETOOTH"
: Bluetooth sockets."IUCV"
: IUCV sockets."RXRPC"
: RxRPC sockets."ISDN"
: mISDN sockets."PHONET"
: Phonet sockets."IEEE802154"
: IEEE802154 sockets."CAIF"
: CAIF sockets."ALG"
: Algorithm sockets."NFC"
: NFC sockets."VSOCK"
: vSockets."KCM"
: Kernel Connection Multiplexor."QIPCRTR"
: Qualcomm IPC Router."SMC"
: reserve number for PF_SMC protocol family that reuses AF_INET address family."XDP"
: XDP sockets."MCTP"
: Management component transport protocol."MAX"
: Maximum.
socket.sock is a table that exports socket types (SOCK):
"STREAM"
: stream (connection) socket."DGRAM"
: datagram (conn.less) socket."RAW"
: raw socket."RDM"
: reliably-delivered message."SEQPACKET"
: sequential packet socket."DCCP"
: Datagram Congestion Control Protocol socket."PACKET"
: linux specific way of getting packets at the dev level.
and flags (SOCK):
"CLOEXEC"
: n/a."NONBLOCK"
: n/a.
socket.ipproto is a table that exports IP protocols (IPPROTO) to Lua.
"IP"
: Dummy protocol for TCP."ICMP"
: Internet Control Message Protocol."IGMP"
: Internet Group Management Protocol."IPIP"
: IPIP tunnels (older KA9Q tunnels use 94)."TCP"
: Transmission Control Protocol."EGP"
: Exterior Gateway Protocol."PUP"
: PUP protocol."UDP"
: User Datagram Protocol."IDP"
: XNS IDP protocol."TP"
: SO Transport Protocol Class 4."DCCP"
: Datagram Congestion Control Protocol."IPV6"
: IPv6-in-IPv4 tunnelling."RSVP"
: RSVP Protocol."GRE"
: Cisco GRE tunnels (rfc 1701,1702)."ESP"
: Encapsulation Security Payload protocol."AH"
: Authentication Header protocol."MTP"
: Multicast Transport Protocol."BEETPH"
: IP option pseudo header for BEET."ENCAP"
: Encapsulation Header."PIM"
: Protocol Independent Multicast."COMP"
: Compression Header Protocol."SCTP"
: Stream Control Transport Protocol."UDPLITE"
: UDP-Lite (RFC 3828)."MPLS"
: MPLS in IP (RFC 4023)."ETHERNET"
: Ethernet-within-IPv6 Encapsulation."RAW"
: Raw IP packets."MPTCP"
: Multipath TCP connection.
sock:close() removes sock
object from the system.
sock:send() sends a string message
through the socket sock
.
If the sock
address family is af.INET
, then it expects the following arguments:
addr
:integer
describing the destination IPv4 address.port
:integer
describing the destination IPv4 port.
Otherwise:
addr
: packed string describing the destination address.
sock:receive() receives a string with up to length
bytes through the socket sock
.
The available message flags are defined by the
socket.msg table.
If from
is true
, it returns the received message followed by the peer's address.
Otherwise, it returns only the received message.
socket.msg is a table that exports message flags to Lua.
"OOB"
: n/a."PEEK"
: n/a."DONTROUTE"
: n/a."TRYHARD"
: Synonym for"DONTROUTE"
for DECnet."CTRUNC"
: n/a."PROBE"
: Do not send. Only probe path f.e. for MTU."TRUNC"
: n/a."DONTWAIT"
: Nonblocking io."EOR"
: End of record."WAITALL"
: Wait for a full request."FIN"
: n/a."SYN"
: n/a."CONFIRM"
: Confirm path validity."RST"
: n/a."ERRQUEUE"
: Fetch message from error queue."NOSIGNAL"
: Do not generate SIGPIPE."MORE"
: Sender will send more."WAITFORONE"
: recvmmsg(): block until 1+ packets avail."SENDPAGE_NOPOLICY"
: sendpage() internal: do no apply policy."SENDPAGE_NOTLAST"
: sendpage() internal: not the last page."BATCH"
: sendmmsg(): more messages coming."EOF"
: n/a."NO_SHARED_FRAGS"
: sendpage() internal: page frags are not shared."SENDPAGE_DECRYPTED"
: sendpage() internal: page may carry plain text and require encryption."ZEROCOPY"
: Use user data in kernel path."FASTOPEN"
: Send data in TCP SYN."CMSG_CLOEXEC"
: Set close_on_exec for file descriptor received through SCM_RIGHTS.
sock:bind() binds the socket sock
to a given address.
If the sock
address family is af.INET
, then it expects the following arguments:
addr
:integer
describing host IPv4 address.port
:integer
describing host IPv4 port.
Otherwise:
addr
: packed string describing host address.
sock:listen() moves the socket sock
to listening state.
backlog
: pending connections queue size. If omitted, it uses SOMAXCONN as default.
sock:accept() accepts a connection on socket sock
.
It returns a new socket
object.
The available flags are present on the
socket.sock table.
sock:connect() connects the socket sock
to the address addr
.
If the sock
address family is af.INET
, then it expects the following arguments:
addr
:integer
describing the destination IPv4 address.port
:integer
describing the destination IPv4 port.
Otherwise:
addr
: packed string describing the destination address.
The available flags are present on the socket.sock table.
For datagram sockets, addr
is the address to which datagrams are sent
by default, and the only address from which datagrams are received.
For stream sockets, attempts to connect to addr
.
sock:getsockname() get the address which the socket sock
is bound.
If the sock
address family is af.INET
, then it returns the following:
addr
:integer
describing the bounded IPv4 address.port
:integer
describing the bounded IPv4 port.
Otherwise:
addr
: packed string describing the bounded address.
sock:getpeername() get the address which the socket sock
is connected.
If the sock
address family is af.INET
, then it returns the following:
addr
:integer
describing the peer's IPv4 address.port
:integer
describing the peer's IPv4 port.
Otherwise:
addr
: packed string describing the peer's address.
The socket.inet
library provides support for high-level IPv4 sockets.
inet.tcp() creates a new socket
using
af.INET address family,
sock.STREAM type
and
ipproto.TCP protocol.
It overrides socket
methods to use addresses as numbers-and-dots notation
(e.g., "127.0.0.1"
), instead of integers.
inet.udp() creates a new socket
using
af.INET address family,
sock.DGRAM type
and
ipproto.UDP protocol.
It overrides socket
methods to use addresses as numbers-and-dots notation
(e.g., "127.0.0.1"
), instead of integers.
udp:receivefrom() is just an alias to sock:receive(length, flags, true)
.
The rcu
library provides support for the kernel
Read-copy update (RCU)
synchronization mechanism.
This library was inspired by
Caio Messias'
GSoC project.
rcu.table() creates a new rcu.table
object
which binds the kernel generic hash table.
This function receives as argument the number of buckets rounded up to the next power of 2.
The default size is 1024
.
Key must be a string and value must be a Lunatik object or nil.
The thread
library provides support for the
kernel thread primitives.
thread.run() creates a new thread
object and wakes it up.
This function receives the following arguments:
runtime
: the runtime environment for running a task in the created kernel thread. The task must be specified by returning a function on the script loaded in theruntime
environment.name
: string representing the name for the thread (e.g., as shown onps
).
thread.shouldstop() returns true
if
thread.stop()
was called; otherwise, it returns false
.
thread.current() returns a thread
object representing the current task.
thrd:stop() sets
thread.shouldstop()
on the thread thrd
to return true, wakes thrd
, and waits for it to exit.
thrd:task() returns a table containing the task information of this thread
(e.g., "cpu", "command", "pid" and "tgid").
The fib
library provides support for the
kernel Forwarding Information Base.
fib.newrule() binds the kernel fib_nl_newrule API; it creates a new FIB rule that matches the specified routing table with the specified priorioty. This function is similar to the user-space command ip rule add provided by iproute2.
fib.delrule() binds the kernel fib_nl_delrule API; it removes a FIB rule that matches the specified routing table with the specified priorioty. This function is similar to the user-space command ip rule del provided by iproute2.
The data
library provides support for binding the system memory to Lua.
data.new() creates a new data
object which allocates size
bytes.
d:getnumber() extracts a lua_Integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setnumber() insert a lua_Integer
number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getbyte() extracts a byte
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setbyte() insert a byte
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getstring() extracts a string with length
bytes
from the memory referenced by a data
object and a byte offset
,
starting from zero. If length
is omitted, it extracts all bytes
from offset
to the end of the data
.
d:setstring() insert the string s
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getint8(d, offset) extracts a signed 8-bit integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setint8() inserts a signed 8-bit number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getuint8() extracts an unsigned 8-bit integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setuint8() inserts an unsigned 8-bit number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getint16() extracts a signed 16-bit integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setint16() inserts a signed 16-bit number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getuint16() extracts an unsigned 16-bit integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setuint16() inserts an unsigned 16-bit number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getint32() extracts a signed 32-bit integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setint32() inserts a signed 32-bit number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getuint32() extracts an unsigned 32-bit integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setuint32() inserts an unsigned 32-bit number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
d:getint64() extracts a signed 64-bit integer
from the memory referenced by a data
object and a byte offset
,
starting from zero.
d:setint64() inserts a signed 64-bit number
into the memory referenced by a data
object and a byte offset
,
starting from zero.
The probe
library provides support for
kernel probes.
probe.new() returns a new probe
object for monitoring a kernel symbol
(string) or address
(light userdata)
and installs its handlers
in the system.
The handler
must be defined as a table containing the following field:
pre
: function to be called before the probed instruction. It receives thesymbol
oraddress
, followed by a closure that may be called to show the CPU registers and stack in the system log.post
: function to be called after the probed instruction. It receives thesymbol
oraddress
, followed by a closure that may be called to show the CPU registers and stack in the system log.
p:stop() removes the probe
handlers from the system.
p:enable() enables or disables the probe
handlers, accordingly to bool
.
The syscall
library provides support for system call addresses and numbers.
syscall.address() returns the system call address (light userdata) referenced by the given number
.
syscall.number() returns the system call number referenced by the given name
.
The syscall.table
library provides support for translating system call names to addresses (light userdata).
The xdp
library provides support for the kernel
eXpress Data Path (XDP)
subsystem.
This library was inspired by
Victor Nogueira's
GSoC project.
xdp.attach() registers a callback
function to the current runtime
to be called from an XDP/eBPF program whenever it calls
bpf_luaxdp_run
kfunc.
This callback
receives the following arguments:
buffer
: adata
object representing the network buffer.argument
: adata
object containing the argument passed by the XDP/eBPF program.
The callback
function might return the values defined by the
xdp.action table.
xdp.detach() unregisters the callback
associated with the current runtime
, if any.
xdp.action is a table that exports xdp_action flags to Lua.
"ABORTED"
: Indicates that the XDP program aborted, typically due to an error."DROP"
: Specifies that the packet should be dropped, discarding it entirely."PASS"
: Allows the packet to pass through to the Linux network stack."TX"
: Transmits the packet back out on the same interface it was received."REDIRECT"
: Redirects the packet to another interface or processing context.
The xtable
library provides support for developing netfilter xtable extensions.
xtable.match() returns a new xtable object for match extensions. This function receives the following arguments:
opts
: a table containing the following fields:name
: string representing the xtable extension name.revision
: integer representing the xtable extension revision.family
: address family, one of netfilter.family.proto
: protocol number, one of socket.ipproto.hooks
: hook to attach the extension to, one value from either of the hooks table - netfilter.inet_hooks, netfilter.bridge_hooks and netfilter.arp_hooks (Note: netfilter.netdev_hooks is not available for legacy x_tables). (E.g -1 << inet_hooks.LOCAL_OUT
).match
: function to be called for matching packets. It receives the following arguments:skb
(readonly): adata
object representing the socket buffer.par
: a table containinghotdrop
,thoff
(transport header offset) andfragoff
(fragment offset) fields.userargs
: a lua string passed from the userspace xtable module.- The function must return
true
if the packet matches the extension; otherwise, it must returnfalse
.
checkentry
: function to be called for checking the entry. This function receivesuserargs
as its argument.destroy
: function to be called for destroying the xtable extension. This function receivesuserargs
as its argument.
xtable.target() returns a new xtable object for target extension. This function receives the following arguments:
opts
: a table containing the following fields:name
: string representing the xtable extension name.revision
: integer representing the xtable extension revision.family
: address family, one of netfilter.family.proto
: protocol number, one of socket.ipproto.hooks
: hook to attach the extension to, one value from either of the hooks table - netfilter.inet_hooks, netfilter.bridge_hooks and netfilter.arp_hooks (Note: netfilter.netdev_hooks is not available for legacy x_tables). (E.g -1 << inet_hooks.LOCAL_OUT
).target
: function to be called for targeting packets. It receives the following arguments:skb
: adata
object representing the socket buffer.par
(readonly): a table containinghotdrop
,thoff
(transport header offset) andfragoff
(fragment offset) fields.userargs
: a lua string passed from the userspace xtable module.- The function must return one of the values defined by the netfilter.action table.
checkentry
: function to be called for checking the entry. This function receivesuserargs
as its argument.destroy
: function to be called for destroying the xtable extension. This function receivesuserargs
as its argument.
The netfilter
library provides support for the new netfilter hook system.
netfilter.register() registers a new netfilter hook with the given ops
table.
This function receives the following arguments:
ops
: a table containing the following fields:pf
: protocol family, one of netfilter.familyhooknum
: hook to attach the filter to, one value from either of the hooks table - netfilter.inet_hooks, netfilter.bridge_hooks, netfilter.arp_hooks and netfilter.netdev_hooks. (E.g -inet_hooks.LOCAL_OUT + 11
).priority
: priority of the hook. One of the values from the netfilter.ip_priority or netfilter.bridge_priority tables.hook
: function to be called for the hook. It receives the following arguments:skb
: adata
object representing the socket buffer.- The function must return one of the values defined by the netfilter.action.
netfilter.family is a table that exports address families to Lua.
"UNSPEC"
: Unspecified."INET"
: Internet Protocol version 4."IPV4"
: Internet Protocol version 4."IPV6"
: Internet Protocol version 6."ARP"
: Address Resolution Protocol."NETDEV"
: Device ingress and egress path"BRIDGE"
: Ethernet Bridge.
netfilter.action is a table that exports netfilter actions to Lua.
"DROP"
:NF_DROP
. The packet is dropped. It is not forwarded, processed, or seen by any other network layer."ACCEPT"
:NF_ACCEPT
. The packet is accepted and passed to the next step in the network processing chain."STOLEN"
:NF_STOLEN
. The packet is taken by the handler, and processing stops."QUEUE"
:NF_QUEUE
. The packet is queued for user-space processing."REPEAT"
:NF_REPEAT
. The packet is sent through the hook chain again."STOP"
:NF_STOP
. Processing of the packet stops."CONTINUE"
:XT_CONTINUE
. Return the packet should continue traversing the rules within the same table."RETURN"
:XT_RETURN
. Return the packet to the previous chain.
netfilter.inet_hooks is a table that exports inet netfilter hooks to Lua.
"PRE_ROUTING"
:NF_INET_PRE_ROUTING
. The packet is received by the network stack."LOCAL_IN"
:NF_INET_LOCAL_IN
. The packet is destined for the local system."FORWARD"
:NF_INET_FORWARD
. The packet is to be forwarded to another host."LOCAL_OUT"
:NF_INET_LOCAL_OUT
. The packet is generated by the local system."POST_ROUTING"
:NF_INET_POST_ROUTING
. The packet is about to be sent out.
netfilter.bridge_hooks is a table that exports bridge netfilter hooks to Lua.
"PRE_ROUTING"
:NF_BR_PRE_ROUTING
. First hook invoked, runs before forward database is consulted."LOCAL_IN"
:NF_BR_LOCAL_IN
. Invoked for packets destined for the machine where the bridge was configured on."FORWARD"
:NF_BR_FORWARD
. Called for frames that are bridged to a different port of the same logical bridge device."LOCAL_OUT"
:NF_BR_LOCAL_OUT
. Called for locally originating packets that will be transmitted via the bridge."POST_ROUTING"
:NF_BR_POST_ROUTING
. Called for all locally generated packets and all bridged packets
netfilter.arp_hooks is a table that exports arp netfilter hooks to Lua.
"IN"
:NF_ARP_IN
. The packet is received by the network stack."OUT"
:NF_ARP_OUT
. The packet is generated by the local system."FORWARD"
:NF_ARP_FORWARD
. The packet is to be forwarded to another host.
netfilter.netdev_hooks is a table that exports netdev netfilter hooks to Lua.
"INGRESS"
:NF_NETDEV_INGRESS
. The packet is received by the network stack."EGRESS"
:NF_NETDEV_EGRESS
. The packet is generated by the local system.
netfilter.ip_priority is a table that exports netfilter IPv4/IPv6 priority levels to Lua.
"FIRST"
:NF_IP_PRI_FIRST
"RAW_BEFORE_DEFRAG"
:NF_IP_PRI_RAW_BEFORE_DEFRAG
"CONNTRACK_DEFRAG"
:NF_IP_PRI_CONNTRACK_DEFRAG
"RAW"
:NF_IP_PRI_RAW
"SELINUX_FIRST"
:NF_IP_PRI_SELINUX_FIRST
"CONNTRACK"
:NF_IP_PRI_CONNTRACK
"MANGLE"
:NF_IP_PRI_MANGLE
"NAT_DST"
:NF_IP_PRI_NAT_DST
"FILTER"
:NF_IP_PRI_FILTER
"SECURITY"
:NF_IP_PRI_SECURITY
"NAT_SRC"
:NF_IP_PRI_NAT_SRC
"SELINUX_LAST"
:NF_IP_PRI_SELINUX_LAST
"CONNTRACK_HELPER"
:NF_IP_PRI_CONNTRACK_HELPER
"LAST"
:NF_IP_PRI_LAST
netfilter.bridge_priority is a table that exports netfilter bridge priority levels to Lua.
"FIRST"
:NF_BR_PRI_FIRST
"NAT_DST_BRIDGED"
:NF_BR_PRI_NAT_DST_BRIDGED
"FILTER_BRIDGED"
:NF_BR_PRI_FILTER_BRIDGED
"BRNF"
:NF_BR_PRI_BRNF
"NAT_DST_OTHER"
:NF_BR_PRI_NAT_DST_OTHER
"FILTER_OTHER"
:NF_BR_PRI_FILTER_OTHER
"NAT_SRC"
:NF_BR_PRI_NAT_SRC
"LAST"
:NF_BR_PRI_LAST
The luaxt
userspace library provides support for generating userspace code for xtable extensions.
To build the library, the following steps are required:
- Go to
usr/lib/xtable
and create alibxt_<ext_name>.lua
file. - Register your callbacks for the xtable extension by importing the library (
luaxt
) in the created file. - Run
LUAXTABLE_MODULE=<ext_name> make
to build the extension andLUAXTABLE_MODULE=<ext_name> make install
(as root) to install the userspace plugin to the system.
Now load the extension normally using iptables
.
luaxt.match() returns a new luaxt object for match extensions. This function receives the following arguments:
opts
: a table containing the following fields:revision
: integer representing the xtable extension revision (must be same as used in corresponding kernel extension).family
: address family, one of luaxt.familyhelp
: function to be called for displaying help message for the extension.init
: function to be called for initializing the extension. This function receives anpar
table that can be used to setuserargs
. (par.userargs = "mydata"
)print
: function to be called for printing the arguments. This function receviesuserargs
set by theinit
orparse
function.save
: function to be called for saving the arguments. This function receviesuserargs
set by theinit
orparse
function.parse
: function to be called for parsing the command line arguments. This function receives anpar
table that can be used to setuserargs
andflags
. (par.userargs = "mydata"
)final_check
: function to be called for final checking of the arguments. This function receivesflags
set by theparse
function.
luaxt.target() returns a new luaxt object for target extensions. This function receives the following arguments:
opts
: a table containing the following fields:revision
: integer representing the xtable extension revision (must be same as used in corresponding kernel extension).family
: address family, one of luaxt.familyhelp
: function to be called for displaying help message for the extension.init
: function to be called for initializing the extension. This function receives anpar
table that can be used to setuserargs
. (par.userargs = "mydata"
)print
: function to be called for printing the arguments. This function receviesuserargs
set by theinit
orparse
function.save
: function to be called for saving the arguments. This function receviesuserargs
set by theinit
orparse
function.parse
: function to be called for parsing the command line arguments. This function receives anpar
table that can be used to setuserargs
andflags
. (par.userargs = "mydata"
)final_check
: function to be called for final checking of the arguments. This function receivesflags
set by theparse
function.
luaxt.family is a table that exports address families to Lua.
"UNSPEC"
: Unspecified."INET"
: Internet Protocol version 4."IPV4"
: Internet Protocol version 4."IPV6"
: Internet Protocol version 6."ARP"
: Address Resolution Protocol."NETDEV"
: Device ingress and egress path"BRIDGE"
: Ethernet Bridge.
The completion
library provides support for the kernel completion primitives.
Task completion is a synchronization mechanism used to coordinate the execution of multiple threads, similar to pthread_barrier
, it allows threads to wait for a specific event to occur before proceeding, ensuring certain tasks are complete in a race-free manner.
completion.new() creates a new completion
object.
c:complete() signals a single thread waiting on this completion.
c:wait() waits for completion of a task until the specified timeout expires.
The timeout is specified in milliseconds. If the timeout
parameter is omitted, it waits indefinitely. Passing a timeout value less than zero results in undefined behavior.
Threads waiting for events can be interrupted by signals, for example, such as when thread.stop
is invoked.
Therefore, this function can return in three ways:
- If it succeeds, it returns
true
- If the timeout is reached, it returns
nil, "timeout"
- If the task is interrupted, it returns
nil, "interrupt"
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
).
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
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.
sudo make examples_install # installs examples
sudo lunatik run examples/keylocker # runs keylocker
<↑> <↑> <↓> <↓> <←> <→> <←> <→> <LCTRL> <LALT> # locks keyboard
<↑> <↑> <↓> <↓> <←> <→> <←> <→> <LCTRL> <LALT> # unlocks keyboard
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.
sudo make examples_install # installs examples
sudo lunatik run examples/tap # runs tap
cat /dev/tap
shared is a kernel script that implements an in-memory key-value store using rcu, data, socket and thread.
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 is an echo server implemented as kernel scripts.
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 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.
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 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.
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
.
This other sni filter uses netfilter api.
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.
- 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
- Using new netfilter framework (luanetfilter)
sudo make examples_install # installs examples
sudo lunatik run examples/dnsblock/nf_dnsblock false # runs the Lua kernel script
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.
- 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
- 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
- Scripting the Linux Routing Table with Lua
- Lua no Núcleo (Portuguese)
- Linux Network Scripting with Lua
- Scriptables Operating Systems with Lua
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.