Lua for Network Engineers

Lua scripting for network tools, configuration, and embedded systems.

Why Lua?

  • Embedded everywhere - 200KB footprint, runs on anything

  • Network tool scripting - Wireshark, Nmap, Snort

  • Config language - Neovim, Nginx/OpenResty, Redis

  • Cisco IOS-XE - On-box EEM/scripting

  • Fast - LuaJIT rivals C performance

Neovim Configuration

Your future ~/.config/nvim/init.lua:

-- Set leader key
vim.g.mapleader = " "

-- Line numbers
vim.opt.number = true
vim.opt.relativenumber = true

-- Tabs
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true

-- Keybinding
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })

Wireshark Dissector

Custom protocol parser:

-- Register custom protocol
local myproto = Proto("myproto", "My Custom Protocol")

-- Define fields
myproto.fields.command = ProtoField.uint8("myproto.cmd", "Command", base.HEX)
myproto.fields.length = ProtoField.uint16("myproto.len", "Length", base.DEC)

-- Dissector function
function myproto.dissector(buffer, pinfo, tree)
    pinfo.cols.protocol = "MYPROTO"
    local subtree = tree:add(myproto, buffer(), "My Protocol Data")
    subtree:add(myproto.fields.command, buffer(0,1))
    subtree:add(myproto.fields.length, buffer(1,2))
end

-- Register on TCP port
local tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(9999, myproto)

Nmap NSE Script

Custom vulnerability check:

description = [[
Checks for default IPMI credentials
]]

author = "EvanusModestus"
categories = {"auth", "default"}

local ipmi = require "ipmi"
local shortport = require "shortport"

portrule = shortport.port_or_service(623, "asf-rmcp", "udp")

action = function(host, port)
    local default_users = {"ADMIN", "admin", "root"}
    local default_pass = {"ADMIN", "admin", "password"}

    for _, user in ipairs(default_users) do
        for _, pass in ipairs(default_pass) do
            local status, err = ipmi.login(host.ip, user, pass)
            if status then
                return string.format("DEFAULT CREDS: %s/%s", user, pass)
            end
        end
    end
    return "No default credentials found"
end

Cisco IOS-XE EEM Script

On-box automation:

-- Triggered when interface goes down
-- File: flash:eem_interface_down.lua

function main()
    local cli = require("cli")
    local syslog = require("syslog")

    -- Get interface from event
    local intf = eem.event_data("interface")

    -- Log event
    syslog.info("Interface " .. intf .. " went down - running recovery")

    -- Attempt recovery
    cli.exec("configure terminal")
    cli.exec("interface " .. intf)
    cli.exec("shutdown")
    cli.exec("no shutdown")
    cli.exec("end")

    syslog.info("Recovery attempt complete for " .. intf)
end

main()

Nginx/OpenResty Request Handling

Dynamic routing:

-- /etc/nginx/lua/auth.lua
local jwt = require "resty.jwt"

local function authenticate()
    local token = ngx.var.http_authorization
    if not token then
        ngx.status = 401
        ngx.say("Missing token")
        return ngx.exit(401)
    end

    local verified = jwt:verify("secret", token:gsub("Bearer ", ""))
    if not verified.verified then
        ngx.status = 403
        ngx.say("Invalid token")
        return ngx.exit(403)
    end

    -- Set user for downstream
    ngx.var.authenticated_user = verified.payload.sub
end

return { authenticate = authenticate }

Variables and Types

-- Variables (global by default, use 'local')
local name = "kvm-02"
local ip = "10.50.1.111"
local port = 22
local enabled = true
local nothing = nil

-- Tables (arrays AND dictionaries)
local hosts = {"kvm-01", "kvm-02", "kvm-03"}
local config = {
    hostname = "kvm-02",
    ip = "10.50.1.111",
    ports = {22, 443, 5900}
}

-- Access
print(hosts[1])           -- "kvm-01" (1-indexed!)
print(config.hostname)    -- "kvm-02"
print(config["ip"])       -- "10.50.1.111"

Control Flow

-- If/else
if status == "up" then
    print("Online")
elseif status == "degraded" then
    print("Warning")
else
    print("Down")
end

-- For loops
for i = 1, 10 do
    print(i)
end

-- Iterate table
for key, value in pairs(config) do
    print(key .. " = " .. tostring(value))
end

-- Iterate array
for index, host in ipairs(hosts) do
    print(index .. ": " .. host)
end

Functions

-- Basic function
local function greet(name)
    return "Hello, " .. name
end

-- Multiple returns
local function get_host_info(hostname)
    return "10.50.1.111", 22, "Rocky Linux"
end

local ip, port, os = get_host_info("kvm-02")

-- Anonymous function
local process = function(x) return x * 2 end

-- Closures
local function counter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end

local c = counter()
print(c())  -- 1
print(c())  -- 2

String Operations

local host = "kvm-02.inside.domusdigitalis.dev"

-- Concatenation
local msg = "Host: " .. host

-- Length
print(#host)  -- 33

-- Pattern matching (like regex-lite)
local name = host:match("^([^.]+)")  -- "kvm-02"
local domain = host:match("%.(.+)$")  -- "inside.domusdigitalis.dev"

-- Split (manual, no built-in)
local function split(str, sep)
    local result = {}
    for part in str:gmatch("([^" .. sep .. "]+)") do
        table.insert(result, part)
    end
    return result
end

local parts = split(host, ".")  -- {"kvm-02", "inside", "domusdigitalis", "dev"}

Lua vs Python vs Bash

Task Bash Python Lua

Embedded scripting

No

Heavy

Yes (200KB)

Network tool integration

Limited

Good

Native (Wireshark, Nmap)

Config language

No

No

Yes (Neovim, Nginx)

Performance

Slow

Medium

Fast (LuaJIT)

Learning curve

Medium

Easy

Easy

On-device (Cisco)

No

Limited

Yes

Learning Path

  1. Now: Master bash/awk/sed (current focus)

  2. Next: Neovim + Lua config

  3. Then: Wireshark Lua dissectors

  4. Later: Nmap NSE scripting

  5. Advanced: OpenResty/Nginx Lua