| src | ||
| vendor | ||
| .gitignore | ||
| .luarc.json | ||
| LICENSE | ||
| meson.build | ||
| README.md | ||
luaddnstool
luaddnstool is a tiny C program for managing Dynamic DNS records on various DNS hosting services using simple Lua scripts.
You use it like this:
$ luaddnstool ./myscript.lua
TODO: allow you to pass arguments to the script TODO: allow you to pass lua code as an argument
The provided Lua script runs in a Lua 5.3 environment with access to the standard library, including io and os, plus specialized DDNS API functions and various utility functions.
TODO: put a function listing here !!!!! with links !!!! https://www.lua.org/manual/5.3/contents.html#index NOTE: all standard Lua 5.3 library functions are available, including:
- pcall
- io functions, especially files
- os.clock/os.date/os.Time
- os.exit
- os.getenv
- os.execute
Here's a simple example of a script that updates a dns.he.net record whenever your public IP address changes and sends you an email if the update fails:
onipchange(300, function(oldip, newip)
print("ip changed from "..tostring(oldip).." to "..newip..", updating...")
local ok, error = pcall(function()
local changed = he_update_record("A", "paws.lua.pet", "myawesomepassword", newip)
print(changed and "he record updated" or "he record not updated")
end)
if not ok then
print(error)
print("something went wrong, sending email and exiting...")
email({
url = "smtp://mail.example.com:587",
username = "myawesomeusername",
password = "myawesomepassword",
from = "ddns@lua.pet",
to = "lua@foxgirl.dev",
subject = "DDNS update failed :(",
body = "Whoopsie!\n"..tostring(error).."\n"
})
os.exit(1)
end
end)
Installation
Prerequisites
- Meson build system
- C compiler (GCC or Clang)
- libcurl with development headers
On Debian/Ubuntu:
sudo apt install meson build-essential libcurl4-openssl-dev
On Arch Linux:
sudo pacman -S meson gcc curl
Download
Clone the repository:
git clone https://git.vixen.computer/lua/luaddnstool
cd luaddnstool
Build
Set up the build directory:
meson setup builddir --buildtype=release
Compile the project:
meson compile -C builddir
Install
After building, you can either:
-
Run directly from the build directory:
./builddir/luaddnstool ./path/to/script.lua -
Install system-wide:
sudo meson install -C builddirThen run from anywhere:
luaddnstool ./path/to/script.lua
DDNS API functions
dyn_update_record(url, username, password, domain, ips)
TODO
duck_update_record(token, record_type, domains, content)
TODO
he_update_record(record_type, domain, password, content)
Updates a Dynamic DNS record on Hurricane Electric's Hosted DNS service.
Read the docs at https://dns.he.net/.
Parameters:
- record_type - DNS record type (e.g., A, AAAA, or TXT)
- name - Fully qualified domain name to update
- password - Dynamic DNS password for the record
- content - Answer content for the DNS record (e.g., IP address)
Returns true if the update was successful or false if the record is unchanged. Raises an error if the update fails.
Example:
local current_ip = checkip()
he_update_record("A", "home.example.com", "the-ddns-password", current_ip)
porkbun_update_record(options)
Updates a DNS record on Porkbun DNS via the REST API.
Read the docs at https://porkbun.com/api/json/v3/documentation.
Supports the following parameters in the options table:
| Field | Type | Required | Description |
|---|---|---|---|
| secretapikey | string | Yes | Porkbun secret API key |
| apikey | string | Yes | Porkbun API key |
| type | string | Yes | DNS record type (e.g., A, AAAA, or TXT) |
| domain | string | Yes | Domain name to edit (eg. example.com) |
| subdomain | string | Yes | Subdomain entry to edit (eg. www) |
| content | string | Yes | Answer content for the DNS record (e.g., IP address) |
| ttl | string | No | Time to live in seconds |
| prio | string | No | Priority of the record for those that support it |
| notes | string | No | Notes to set for the record, pass empty string to clear |
Returns nothing. Raises an error if the update fails.
Example:
porkbun_update_record({
secretapikey = os.getenv("PORKBUN_SECRET_APIKEY"),
apikey = os.getenv("PORKBUN_APIKEY"),
type = "A",
domain = "example.com",
subdomain = "www",
content = checkip()
})
Utility functions
json.encode(value) / json.decode(value)
Encodes/decodes JSON using rxi's json.lua library.
Example:
local decoded_table = json.decode('{ "key": "value" }')
print(decoded_table.key) -- Prints "value"
local encoded_string = json.encode({ foo = "bar", x = 10 })
print(encoded_string) -- Prints '{"foo":"bar","x":10}'
sleep(seconds)
Pauses execution for the specified number of seconds.
Example:
sleep(30) -- Waits for 30 seconds
dig(record_type, name)
Performs a DNS lookup for the specified record type and name.
Parameters:
- record_type - DNS record type (e.g., A, AAAA, CNAME, etc.)
- name - Domain name to query
Returns the DNS record value as a string, or nil if not found
TODO: Return multiple records as a list
Example:
local a_record = dig("A", "example.com")
local txt_record = dig("TXT", "_dmarc.example.com")
fetch(url, options)
Performs a HTTP(S) request to the specified URL.
Supports the following optional parameters provided in the options table:
| Field | Type | Description |
|---|---|---|
| method | string | HTTP method (GET, POST, etc.), default GET |
| headers | table | HTTP headers as key-value pairs |
| body | string | Request body for POST/PUT |
| username | string | Username for basic auth |
| password | string | Password for basic auth |
| raise | boolean | Error on 4xx/5xx status codes, default true |
| timeout | number | Timeout in seconds, default 120s |
| verifyssl | boolean | Verify SSL certificates, default true |
| ipversion | string | IP version to use: ipv4, ipv6, or nil for default |
Returns a table with the following fields:
| Field | Type | Description |
|---|---|---|
| status | number | HTTP status code |
| headers | table | Response headers as key-value pairs, lowercase names |
| body | string | Response body |
Raises an error if the request fails, or if both:
- The response's status code is 4xx/5xx
- The raise option is not false
Example:
local response = fetch("https://api.example.com", {
method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = json.encode({ data = "value" }),
timeout = 10
})
The headers table can include multiple headers with the same name by providing a table of values:
fetch("https://api.example.com", {
headers = {
["X-Custom-Header"] = {"value1", "value2"}
}
})
If a response contains multiple headers with the same name (e.g., Set-Cookie), they are stored as a table:
local response = fetch("https://example.com")
-- If multiple Set-Cookie headers exist:
-- response.headers["set-cookie"] = {"id=123", "theme=light"}
-- Otherwise, a single value:
-- response.headers["content-type"] = "text/html"
email(options)
Sends an email via SMTP.
Supports the following parameters in the options table:
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | SMTP server (e.g., smtp://mail.example.com:587) |
| username | string | Yes | SMTP username |
| password | string | Yes | SMTP password |
| from | string | Yes | Sender email address |
| to | string/array | Yes | Recipient(s) |
| cc | string/array | No | CC recipient(s) |
| subject | string | Yes | Email subject |
| body | string | Yes | Email body |
| raise | boolean | No | Error on 4xx/5xx status codes, default true |
| timeout | number | No | Timeout in seconds, default 300s |
| verifyssl | boolean | No | Verify SSL certificates, default true |
| ipversion | string | No | IP version to use: ipv4, ipv6, or nil for default |
Returns a table with the following fields:
| Field | Type | Description |
|---|---|---|
| status | number | SMTP status code |
Raises an error if sending fails, or if both:
- the response's status code is 4xx/5xx
- the raise option is not false
Example:
email({
url = "smtp://mail.example.com:587",
username = "sender@example.com",
password = "app-password",
from = "sender@example.com",
to = {"recipient@example.com"},
subject = "Alert",
body = "Something bad happened!"
})
checkip()
Gets your current public IPv4 address using Cloudflare's canihazip.com, falling back to whatismyip.akamai.com if Cloudflare is broken.
Returns the IPv4 address as a string. Raises an error if no IPv4 address is available or if something goes wrong.
Example:
print(checkip()) -- Prints "23.215.0.138"
checkip6()
Gets your current public IPv6 address using Cloudflare's canihazip.com, falling back to whatismyip.akamai.com if Cloudflare is broken.
Returns the IPv6 address as a string. Raises an error if no IPv6 address is available or if something goes wrong.
Example:
print(checkip6()) -- Prints "2600:1408:ec00:36::1736:7f24"
onipchange(options)
TODO
onip6change(options)
TODO
urlencode(str, isformdata)
URL-encodes the provided string.
Parameters:
- str - String to encode
- isformdata - If true, spaces are encoded as '+' and newlines are normalized, default false
Example:
local encoded = urlencode("hello world!")
print(encoded) -- Prints "hello%20world%21"
print(urlencode('hello world!\ngoodbye, world.', true)) -- Prints "hello+world%21%0D%0Agoodbye%2C+world."
urldecode(str)
TODO
parseurl(url, path)
Returns a URL object.
TODO
- url.fragment
- url.host
- url.password
- url.path
- url.port
- url.query
- url.scheme
- url.user
- url.zoneid
- url:appendquery(pair, urlencode)
- getmetatable(url).__index
- getmetatable(url).__newindex
- getmetatable(url).__tostring
Contributing
To add support for a new provider:
-
Add your function to src/ddns.lua
- Follow the existing patterns
- Make your implementation clean and readable
- Raise errors with descriptive messages for failures
-
Update the README.md
- Add a new section under "DDNS API functions" documenting your function
- Include all parameters, their types, and whether they're required
- Provide a complete usage example
-
Test your implementation
- Create a small test script that exercises your function
- Verify it works correctly with the actual service
- Test error conditions
-
Submit a pull request
- Title your PR:
New DDNS function: [service name here] - In the description, include:
- Link to the service's website and documentation
- Brief explanation of how the function works
- The test script
- Title your PR:
License
Made with ❤ by Lua (foxgirl.dev). Licensed under MIT.