diff --git a/Makefile b/Makefile index 28dd6fdb..46c87063 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,8 @@ examples_install: ${INSTALL} -m 0644 examples/filter/*.lua ${SCRIPTS_INSTALL_PATH}/examples/filter ${MKDIR} ${SCRIPTS_INSTALL_PATH}/examples/dnsblock ${INSTALL} -m 0644 examples/dnsblock/*.lua ${SCRIPTS_INSTALL_PATH}/examples/dnsblock + ${MKDIR} ${SCRIPTS_INSTALL_PATH}/examples/dnsdoctor + ${INSTALL} -m 0644 examples/dnsdoctor/*.lua ${SCRIPTS_INSTALL_PATH}/examples/dnsdoctor examples_uninstall: ${RM} -r ${SCRIPTS_INSTALL_PATH}/examples diff --git a/README.md b/README.md index d1de738e..10c651ce 100644 --- a/README.md +++ b/README.md @@ -1149,10 +1149,10 @@ This function receives the following arguments: * `match` : function to be called for matching packets. It receives the following arguments: * `skb` (readonly): a `data` object representing the socket buffer. * `par`: a table containing `hotdrop`, `thoff` (transport header offset) and `fragoff` (fragment offset) fields. - * `userdata` : a lua string passed from the userspace xtable module. + * `userargs` : a lua string passed from the userspace xtable module. * The function must return `true` if the packet matches the extension; otherwise, it must return `false`. - * `checkentry`: function to be called for checking the entry. This function receives `userdata` as it's argument. - * `destroy`: function to be called for destroying the xtable extension. This function receives `userdata` as it's argument. + * `checkentry`: function to be called for checking the entry. This function receives `userargs` as its argument. + * `destroy`: function to be called for destroying the xtable extension. This function receives `userargs` as its argument. #### `xtable.target(opts)` @@ -1167,10 +1167,10 @@ This function receives the following arguments: * `target` : function to be called for targeting packets. It receives the following arguments: * `skb`: a `data` object representing the socket buffer. * `par` (readonly): a table containing `hotdrop`, `thoff` (transport header offset) and `fragoff` (fragment offset) fields. - * `userdata` : a lua string passed from the userspace xtable module. + * `userargs` : a lua string passed from the userspace xtable module. * The function must return one of the values defined by the [xtable.action](https://github.com/luainkernel/lunatik#xtableaction) table. - * `checkentry`: function to be called for checking the entry. This function receives `userdata` as it's argument. - * `destroy`: function to be called for destroying the xtable extension. This function receives `userdata` as it's argument. + * `checkentry`: function to be called for checking the entry. This function receives `userargs` as its argument. + * `destroy`: function to be called for destroying the xtable extension. This function receives `userargs` as its argument. #### `xtable.family` @@ -1212,9 +1212,9 @@ netfilter hooks to Lua. ### luaxt -The `luaxt` [userspace library](usr/lib/xtable) provides support for generating userspace code for [xtable extensions](https://inai.de/projects/xtables-addons/). The user can modify the generated lua code to implement the userspace handlers for the corresponding xtable extension. +The `luaxt` [userspace library](usr/lib/xtable) provides support for generating userspace code for [xtable extensions](https://inai.de/projects/xtables-addons/). -To generate the library, the following steps are required: +To build the library, the following steps are required: 1. Go to `usr/lib/xtable` and create a `libxt_.lua` file. 2. Register your callbacks for the xtable extension by importing the library (`luaxt`) in the created file. @@ -1230,10 +1230,10 @@ This function receives the following arguments: * `revision`: integer representing the xtable extension revision (**must** be same as used in corresponding kernel extension). * `family`: address family, one of [luaxt.family](https://github.com/luainkernel/lunatik#luaxtfamily) * `help`: function to be called for displaying help message for the extension. - * `init`: function to be called for initializing the extension. This function receives an `par` table that can be used to set `userdata`. (`par.userdata = "mydata"`) - * `print`: function to be called for printing the arguments. This function recevies `userdata` set by the `init` or `parse` function. - * `save`: function to be called for saving the arguments. This function recevies `userdata` set by the `init` or `parse` function. - * `parse`: function to be called for parsing the command line arguments. This function receives an `par` table that can be used to set `userdata` and `flags`. (`par.userdata = "mydata"`) + * `init`: function to be called for initializing the extension. This function receives an `par` table that can be used to set `userargs`. (`par.userargs = "mydata"`) + * `print`: function to be called for printing the arguments. This function recevies `userargs` set by the `init` or `parse` function. + * `save`: function to be called for saving the arguments. This function recevies `userargs` set by the `init` or `parse` function. + * `parse`: function to be called for parsing the command line arguments. This function receives an `par` table that can be used to set `userargs` and `flags`. (`par.userargs = "mydata"`) * `final_check`: function to be called for final checking of the arguments. This function receives `flags` set by the `parse` function. #### `luaxt.target(opts)` @@ -1244,10 +1244,10 @@ This function receives the following arguments: * `revision`: integer representing the xtable extension revision (**must** be same as used in corresponding kernel extension). * `family`: address family, one of [luaxt.family](https://github.com/luainkernel/lunatik#luaxtfamily) * `help`: function to be called for displaying help message for the extension. - * `init`: function to be called for initializing the extension. This function receives an `par` table that can be used to set `userdata`. (`par.userdata = "mydata"`) - * `print`: function to be called for printing the arguments. This function recevies `userdata` set by the `init` or `parse` function. - * `save`: function to be called for saving the arguments. This function recevies `userdata` set by the `init` or `parse` function. - * `parse`: function to be called for parsing the command line arguments. This function receives an `par` table that can be used to set `userdata` and `flags`. (`par.userdata = "mydata"`) + * `init`: function to be called for initializing the extension. This function receives an `par` table that can be used to set `userargs`. (`par.userargs = "mydata"`) + * `print`: function to be called for printing the arguments. This function recevies `userargs` set by the `init` or `parse` function. + * `save`: function to be called for saving the arguments. This function recevies `userargs` set by the `init` or `parse` function. + * `parse`: function to be called for parsing the command line arguments. This function receives an `par` table that can be used to set `userargs` and `flags`. (`par.userargs = "mydata"`) * `final_check`: function to be called for final checking of the arguments. This function receives `flags` set by the `parse` function. #### `luaxt.family` @@ -1411,6 +1411,46 @@ 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 ``` +### dnsdoctor + +[dnsdoctor](examples/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 + +``` +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 + +# copy the userspace extension to luaxt directory +cp libxt_dnsdoctor.lua ../../usr/lib/xtable/ +cd ../../usr/lib/xtable + +# build and install the userspace extension for netfilter +LUAXTABLE_MODULE=dnsdoctor make +sudo LUAXTABLE_MODULE=dnsdoctor 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 +cd ../../../examples/dnsdoctor +cleanup.sh +``` + ## References * [Scripting the Linux Routing Table with Lua](https://netdevconf.info/0x17/sessions/talk/scripting-the-linux-routing-table-with-lua.html) diff --git a/examples/dnsdoctor/Makefile b/examples/dnsdoctor/Makefile new file mode 100644 index 00000000..e3ef173b --- /dev/null +++ b/examples/dnsdoctor/Makefile @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: (c) 2024 Mohammad Shehar Yaar Tausif +# SPDX-License-Identifier: MIT OR GPL-2.0-only + +all: + LUAXTABLE_MODULE=dnsdoctor $(MAKE) -C ../../usr/lib/xtable + +install: + sudo LUAXTABLE_MODULE=dnsdoctor $(MAKE) -C ../../usr/lib/xtable install + +uninstall: + sudo rm -f ${XTABLES_SO_DIR}/libxt_${LUAXTABLE_MODULE}.so + +clean: + LUAXTABLE_MODULE=dnsdoctor $(MAKE) -C ../../usr/lib/xtable clean + diff --git a/examples/dnsdoctor/cleanup.sh b/examples/dnsdoctor/cleanup.sh new file mode 100755 index 00000000..367129b8 --- /dev/null +++ b/examples/dnsdoctor/cleanup.sh @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: (c) 2024 Mohammad Shehar Yaar Tausif +# SPDX-License-Identifier: MIT OR GPL-2.0-only + +#!/bin/bash + +set -eux + +rm dnstest -rf + +# backup resolv config +if [[ -f /etc/resolv.conf.lunatik ]] then + echo "Restoring dns config from resolv.conf.lunatik" + sudo rm /etc/resolv.conf + sudo cp /etc/resolv.conf.lunatik /etc/resolv.conf + sudo rm /etc/resolv.conf.lunatik +fi + +# down the interfaces +sudo ip -n ns1 link set veth1 down +sudo ip -n ns2 link set veth3 down +sudo ip link set veth2 down +sudo ip link set veth4 down + +sudo ip addr delete 10.1.1.2/24 dev veth2 +sudo ip -n ns1 addr delete 10.1.1.3/24 dev veth1 +sudo ip addr delete 10.1.2.2/24 dev veth4 +sudo ip -n ns2 addr delete 10.1.2.3/24 dev veth3 + +# delete link between host and the namespaces +sudo ip -n ns1 link delete veth1 +sudo ip -n ns2 link delete veth3 + +# delete namespaces ns1 for dns server ns2 for server +sudo ip netns delete ns1 +sudo ip netns delete ns2 + diff --git a/examples/dnsdoctor/dnsdoctor.lua b/examples/dnsdoctor/dnsdoctor.lua new file mode 100644 index 00000000..dda1b2a5 --- /dev/null +++ b/examples/dnsdoctor/dnsdoctor.lua @@ -0,0 +1,68 @@ +-- +-- SPDX-FileCopyrightText: (c) 2024 Mohammad Shehar Yaar Tausif +-- SPDX-License-Identifier: MIT OR GPL-2.0-only +-- + +-- DNS Doctoring : Rewrite DNS type A record to a private address for local clients + +local xt = require("xtable") +local linux = require("linux") +local string = require("string") +local action = xt.action +local family = xt.family + +local udp = 0x11 +local dns = 0x35 + +local function nop() end + +local function get_domain(skb, off) + local _, nameoff, name = skb:getstring(off):find("([^\0]*)") + return name, nameoff + 1 +end + +local function dnsdoctor_tg(skb, par, userargs) + local target_dns, dst_ip, target_ip = string.unpack(">s4I4I4", userargs) + local thoff = par.thoff + + local packetdst = skb:getuint32(16) + if packetdst ~= linux.hton32(dst_ip) then + return action.ACCEPT + end + + local srcport = linux.ntoh16(skb:getuint16(thoff)) + if srcport == dns then + local dnsoff = thoff + 8 + local nanswers = linux.ntoh16(skb:getuint16(dnsoff + 6)) + + -- check the domain name + dnsoff = dnsoff + 12 + local domainname, nameoff = get_domain(skb, dnsoff) + + if domainname == target_dns then + dnsoff = dnsoff + nameoff + 4 -- skip over type, label fields + -- iterate over answers + for i = 1, nanswers do + local atype = linux.hton16(skb:getuint16(dnsoff + 2)) + if atype == 1 then + skb:setuint32(dnsoff + 12, linux.hton32(target_ip)) + end + dnsoff = dnsoff + 16 + end + end + end + + return action.ACCEPT +end + +xt.target{ + name = "dnsdoctor", + revision = 0, + family = family.UNSPEC, + proto = 0, + target = dnsdoctor_tg, + checkentry = nop, + destroy = nop, + hooks = 0, +} + diff --git a/examples/dnsdoctor/libxt_dnsdoctor.lua b/examples/dnsdoctor/libxt_dnsdoctor.lua new file mode 100644 index 00000000..70986904 --- /dev/null +++ b/examples/dnsdoctor/libxt_dnsdoctor.lua @@ -0,0 +1,28 @@ +local luaxt = require("luaxt") +local family = luaxt.family + +local function nop() end + +local function dnsdoctor_init(par) + local target_ip = "10.1.2.3" + local target = 0 + target_ip:gsub("%d+", function(s) target = target * 256 + tonumber(s) end) + + local src_ip = "10.1.1.2" + local src = 0 + src_ip:gsub("%d+", function(s) src = src * 256 + tonumber(s) end) + + par.userargs = string.pack(">s4I4I4", "\x07lunatik\x03com", src, target) +end + +luaxt.target{ + revision = 0, + family = family.UNSPEC, + help = nop, + init = dnsdoctor_init, + print = nop, + save = nop, + parse = nop, + final_check = nop +} + diff --git a/examples/dnsdoctor/setup.sh b/examples/dnsdoctor/setup.sh new file mode 100755 index 00000000..9c800d55 --- /dev/null +++ b/examples/dnsdoctor/setup.sh @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: (c) 2024 Mohammad Shehar Yaar Tausif +# SPDX-License-Identifier: MIT OR GPL-2.0-only + +#!/bin/bash + +set -eux + +# add namespaces ns1 for dns server ns2 for server +sudo ip netns add ns1 +sudo ip netns add ns2 + +# add link between host and the namespaces +sudo ip link add veth1 netns ns1 type veth peer name veth2 +sudo ip link add veth3 netns ns2 type veth peer name veth4 + +# add ip address to the links +# DNS IP : 10.1.1.3 +# Server IP : 10.1.2.3 +sudo ip addr add 10.1.1.2/24 dev veth2 +sudo ip -n ns1 addr add 10.1.1.3/24 dev veth1 +sudo ip addr add 10.1.2.2/24 dev veth4 +sudo ip -n ns2 addr add 10.1.2.3/24 dev veth3 + +# up the interfaces +sudo ip -n ns1 link set veth1 up +sudo ip -n ns2 link set veth3 up +sudo ip link set veth2 up +sudo ip link set veth4 up + +# make a directory to setup dns server +mkdir dnstest +cd dnstest +python -m venv .venv +source .venv/bin/activate +pip install dnserver + +# backup resolv config +echo "Backing up resolver config to /etc/resolver.conf.lunatik" +sudo cp -f /etc/resolv.conf /etc/resolv.conf.lunatik && \ +sudo sed -i 's/nameserver/#nameserver/g' /etc/resolv.conf && \ +echo "nameserver 10.1.1.3" | sudo tee -a /etc/resolv.conf && \ + +# add zone info and run dns server in ns1 +echo """ +[[zones]] +host = 'lunatik.com' +type = 'A' +answer = '192.168.10.1' + +[[zones]] +host = 'lunatik.com' +type = 'NS' +answer = 'ns1.lunatik.com.' + +[[zones]] +host = 'lunatik.com' +type = 'NS' +answer = 'ns2.lunatik.com.' +""" > zones.toml +sudo ip netns exec ns1 .venv/bin/dnserver --no-upstream zones.toml + diff --git a/lib/luaxtable.c b/lib/luaxtable.c index 9c6649f6..35eb801a 100644 --- a/lib/luaxtable.c +++ b/lib/luaxtable.c @@ -56,7 +56,7 @@ static int luaxtable_docall(lua_State *L, luaxtable_t *xtable, luaxtable_info_t lua_insert(L, base + 1); /* op */ lua_pop(L, 1); /* table */ - lua_pushstring(L, info->userdata); /* userdata */ + lua_pushlstring(L, info->userargs, LUAXTABLE_USERDATA_SIZE); /* userargs */ if (lua_pcall(L, nargs + 1, nret, 0) != LUA_OK) { pr_err("%s error: %s\n", op, lua_tostring(L, -1)); diff --git a/lib/luaxtable.h b/lib/luaxtable.h index 5ed92b82..b32821e4 100644 --- a/lib/luaxtable.h +++ b/lib/luaxtable.h @@ -6,10 +6,12 @@ #ifndef luaxtable_h #define luaxtable_h +#define LUAXTABLE_USERDATA_SIZE 256 + struct luaxtable_s; typedef struct luaxtable_info_s { - char userdata[256]; + char userargs[LUAXTABLE_USERDATA_SIZE]; /* used internally by the luaxtable kernel module */ struct luaxtable_s *data __attribute__((aligned(8))); diff --git a/usr/lib/xtable/luaxt.c b/usr/lib/xtable/luaxt.c index 11471451..ce942837 100644 --- a/usr/lib/xtable/luaxt.c +++ b/usr/lib/xtable/luaxt.c @@ -21,7 +21,7 @@ #endif #define pr_err(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__) -#define MIN(a,b) (((a)<(b))?(a):(b)) +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) typedef struct luaxt_flags_s { const char *name; @@ -48,7 +48,7 @@ static int luaxt_run(lua_State *L, const char *func_name, const char *key, int n lua_pop(L, 1); /* table */ if (lua_pcall(L, nargs, nresults, 0) != LUA_OK) { - pr_err("Failed to call Lua function %s: %s\n", func_name, + pr_err("failed to call Lua function %s: %s\n", func_name, lua_tostring(L, -1)); goto restore; } @@ -60,14 +60,14 @@ static int luaxt_run(lua_State *L, const char *func_name, const char *key, int n return ret; } -static int luaxt_doparams(lua_State *L, const char *op, const char *key, unsigned int *flags, luaxtable_info_t *info) +static int luaxt_runwithuserargs(lua_State *L, const char *op, const char *key, unsigned int *flags, luaxtable_info_t *info) { int ret; + lua_newtable(L); - lua_pushvalue(L, -1); /* stack : param param */ + lua_pushvalue(L, -1); /* stack: param, param */ - ret = luaxt_run(L, op, key, 1, 1); - if (ret == -1) + if ((ret = luaxt_run(L, op, key, 1, 1)) == -1) return 0; if (flags && (lua_getfield(L, -1, "flags") == LUA_TNUMBER)) { @@ -75,14 +75,14 @@ static int luaxt_doparams(lua_State *L, const char *op, const char *key, unsigne lua_pop(L, 1); } - if (lua_getfield(L, -1, "userdata") == LUA_TSTRING) { + if (lua_getfield(L, -1, "userargs") == LUA_TSTRING) { size_t len = 0; const char *ldata = lua_tolstring(L, -1, &len); - memset(info->userdata, 0, 256); - memcpy(info->userdata, ldata, MIN(len, 256)); + memset(info->userargs, 0, LUAXTABLE_USERDATA_SIZE); + memcpy(info->userargs, ldata, MIN(len, LUAXTABLE_USERDATA_SIZE)); lua_pop(L, 1); } - + lua_pop(L, 1); return ret; } @@ -96,14 +96,14 @@ static void luaxt_##hook##_help(void) \ #define LUAXT_INITER_CB(hook) \ static void luaxt_##hook##_init(struct xt_entry_##hook *hook) \ { \ - luaxt_doparams(L, "init", (void *)luaxt_##hook, NULL, (luaxtable_info_t *)(hook->data)); \ + luaxt_runwithuserargs(L, "init", (void *)luaxt_##hook, NULL, (luaxtable_info_t *)(hook->data)); \ } #define LUAXT_PARSER_CB(hook) \ static int luaxt_##hook##_parse(int c, char **argv, int invert, unsigned int *flags, \ const void *entry, struct xt_entry_##hook **hook) \ { \ - return luaxt_doparams(L, "parse", (void *)luaxt_##hook, flags, (luaxtable_info_t *)((*hook)->data)); \ + return luaxt_runwithuserargs(L, "parse", (void *)luaxt_##hook, flags, (luaxtable_info_t *)((*hook)->data)); \ } #define LUAXT_FINALCHECKER_CB(hook) \ @@ -117,7 +117,7 @@ static void luaxt_##hook##_finalcheck(unsigned int flags) \ static void luaxt_##hook##_print(const void *entry, const struct xt_entry_##hook *hook, int numeric) \ { \ luaxtable_info_t *info = (luaxtable_info_t *)hook->data; \ - lua_pushstring(L, info->userdata); \ + lua_pushstring(L, info->userargs); \ luaxt_run(L, "print", (void *)luaxt_##hook, 1, 0); \ } @@ -125,7 +125,7 @@ static void luaxt_##hook##_print(const void *entry, const struct xt_entry_##hook static void luaxt_##hook##_save(const void *entry, const struct xt_entry_##hook *hook) \ { \ luaxtable_info_t *info = (luaxtable_info_t *)hook->data; \ - lua_pushstring(L, info->userdata); \ + lua_pushstring(L, info->userargs); \ luaxt_run(L, "save", (void *)luaxt_##hook, 1, 0); \ } @@ -204,6 +204,7 @@ static const luaxt_flags_t luaxt_family[] = { static int luaopen_luaxt(lua_State *L) { const luaxt_flags_t *flag; + luaL_newlib(L, luaxt_lib); lua_newtable(L); for (flag = luaxt_family; flag->name; flag++) { @@ -217,14 +218,14 @@ static int luaopen_luaxt(lua_State *L) static int __attribute__((constructor)) _init(void) { if ((L = luaL_newstate()) == NULL) { - pr_err("Failed to create Lua state\n"); + pr_err("failed to create Lua state\n"); return -ENOMEM; } luaL_openlibs(L); luaL_requiref(L, "luaxt", luaopen_luaxt, 1); - if (luaL_dofile(L, "libxt_"LUAXTABLE_MODULE".lua") != LUA_OK) { - pr_err("Failed to load Lua script: %s\n", lua_tostring(L, -1)); + if (luaL_dofile(L, "libxt_" LUAXTABLE_MODULE ".lua") != LUA_OK) { + pr_err("failed to load Lua script: %s\n", lua_tostring(L, -1)); return -ENOENT; } return 0;