☀️JuNeDNS Backend

December 22, 2024 · View on GitHub

EduardoRuizM/junedns-backend

Dataclick Olimpo ☀️JuNeDNS Backend Safe, Lightweight and Full DNS Server ideal for single or hosting servers

☀️JuNeDNS Backend

🌐 Backend for JuNeDNS Server

JuNeDNS is a lightweight DNS server and backend created in Node.js with a fully functionality and easy installation and use. With templates to easily create domain zones.

Author

Eduardo Ruiz <eruiz@dataclick.es>

JuNe / JUst NEeded Philosophy

  1. Source code using less code as possible So you can understand code and find bugs easier.
  2. Few and optimized lines is better Elegant design.
  3. Avoid external dependencies abuse/bloated, and possible third-party bugs Less files size, better and faster to the interpreter.
  4. Clear and useful documentation with examples and without verbose Get to the point.
  5. Avoid showing unsolicited popups, notifications or messages in frontend For better User eXperience.
  6. Simple UI, without many menus/options and with few clicks to get to sites.
  7. Consequences of having a lot of code (and for simple things): Having to work and search through many files and folders with a lot of wasted time, successive errors due to missing unknown files, madness to move a code to another project, errors due to recursive dependencies difficult to locate, complexity or impossibility to migrate to new versions, unfeasibility to follow the trace with so much code, risk of new errors if the functionality is extended, problems not seen at the first sight, general slowness in the whole development due to excessive and unnecessary code.

Installation

1. Install and configure JuNeDNS Server

2. Add tables to JuNeDNS Server database from mysql-backend.sql

Add/Combine with existent MySQL/MariaDB JuNeDNS Server database using mysql-backend.sql For templates, users and permissions functionality.

3. Create config file

File junedns.conf used by JuNeDNS Server and JuNeDNS Backend, be sure backend_* variables are setted.

...

// For backend
backend_url=http://localhost:9053
backend_cert=
backend_key=
backend_api=false

Changes requires restart JuNeDNS Backend / You need root privileges for installation.

At the very first run it will be created for session token and encrypt users passwords in database backend_token=PRIVATE_KEYPAIR

  • 🐧Linux: For security reasons limit file access with chmod 600 junedns.conf

Use backend_cert / backend_key for SSL (Let´s Encrypt) certificates path if you want HTTPS, and remember to set URL without port backend_url=https://mybackend.tld Or maybe you prefer to use HTTP and proxy HTTPS with Nginx:

server {
	listen		443 ssl;
	listen		[::]:443 ssl; #http3?
	server_name	mybackend.tld;

	ssl_certificate		ACME_PATH/mybackend/fullchain.cer;
	ssl_certificate_key	ACME_PATH/mybackend/mybackend.key;
	ssl_protocols			TLSv1.2 TLSv1.3;

	location / {
		proxy_set_header	X-Forwarded-For $remote_addr;
		proxy_set_header	Host $http_host;
		proxy_pass		http://127.0.0.1:9053;
	}
}

4 Running

Run JuNeDNS Backend using Node.js with source code or from binary:

4.1 Running from Node.js with source code

Download or clone this repository. Install dependencies npm install (JuNe 1 dependence: MySQL).

Requirements node.js and Node Package Manager (NPM).

  • Running from command line (for example to debug) -node backend.js or npm start

  • Running as service

    • 🐧Linux:

      • Use same JuNeDNS Server config file junedns.conf, you could create a symbolic link ln -s /etc/junedns/junedns.conf /etc/junedns_backend/junedns.conf
      • Create or copy Systemctl service junedns-backend.service in folder /etc/systemd/system or /usr/lib/systemd/system use ExecStart Source code execution line and adjust path if necessary.
      • Enable and start service systemctl enable junedns-backend.service && systemctl start junedns-backend.service check if running systemctl status junedns-backend.service
    • 🪟 Windows:

      • Use same JuNeDNS Server config file junedns.conf and set correctly PATHs, you could create a symbolic link: mklink "JuNeDNS_PATH\junedns.conf" "JuNeDNS_Backend_PATH\junedns.conf"
      • Create service sc create "JuNeDNS Backend" binPath="NodeJS_PATH\node JuNeDNS_Backend_PATH\backend.js" start with net start "JuNeDNS Backend"

4.2 Running from binaries (x64 bits)

Download and decompress your version: 🐧Linux (20 Mb), 🪟 Windows (17 Mb) or 🍎MacOS (20 Mb). Create junedns.conf

  • Running from command line (for example to debug with log=3) -./junedns-backend or junedns-backend

  • Running as service

    • 🐧Linux:

      • Use same JuNeDNS Server path /etc/junedns and copy file in it.
      • Add executable permission chmod +x /etc/junedns/junedns-backend
      • Use same JuNeDNS Server config file junedns.conf
      • Create or copy Systemctl service junedns-backend.service in folder /etc/systemd/system or /usr/lib/systemd/system use ExecStart Binary execution line and adjust path if necessary.
      • Enable and start service systemctl enable junedns-backend.service && systemctl start junedns-backend.service check if running systemctl status junedns-backend.service
    • 🪟 Windows:

      • Use same JuNeDNS Server path C:\Users\[USER]\AppData\Roaming\JuNeDNS and copy file in it.
      • Use same JuNeDNS Server config file junedns.conf in C:\Users\[USER]\AppData\Roaming\JuNeDNS\junedns.conf
      • Create service sc create "JuNeDNS Backend" binPath="C:\Users\[USER]\AppData\Roaming\JuNeDNS\junedns-backend.exe" start with net start "JuNeDNS Backend"

🐧Linux: Uncomment the ExecStart line you need for junedns-backend.service

[Unit]
Description=JuNeDNS Backend
After=network.target

[Service]
Type=simple
# Select only 1 ExecStart
#ExecStart=/etc/junedns/junedns-backend									#Binary execution
#ExecStart=/usr/bin/node /etc/junedns_backend/backend.js		#Source code execution

Restart=always
TimeoutStartSec=0

[Install]
WantedBy=default.target

5 Create admin user

Create the admin user to login and first steps, using createuser param in command line: node backend.js createuser USER PASSWORD

Endpoints

  • JSON Content-Type requests.
  • POST variables in BODY in JSON format.
  • You can send lang GET parameter for language /login?lang=es just when changed, token updated.
  • Returns variables in JSON.
  • Token must be in header x-access-token except for login, noip or API.
  • Users/templates management or create/delete domains only if current user is_admin=1
  • Domain and Records changes only if users.is_admin=1 or permissions.readonly=0
EndpointActionMethodPOST VariablesReturn variables
/loginLogin user (all users)POSTuser, passwduser, types
/usersRetrieve usersGET-users array
/usersCreate userPOSTcode, passwd, name, is_admin, domains-
/users/:idGet userGET--
/users/:idChange userPOSTcode, passwd, name, is_admin, domains-
/users/:idDelete userDELETE--
/templatesRetrieve templates with records countGET-templates array
/templatesCreate templatePOSTname, description, is_default-
/templates/:idGet templateGET--
/templates/:idChange templatePOSTname, description, is_default, records-
/templates/:idDelete templateDELETE--
/templates/:id/recordsCreate template recordPOSTname, type, content, ttl, prio-
/templates/:id/records/:ridChange template recordPOSTname, type, content, ttl, prio-
/templates/:id/records/:ridDelete template recordDELETE--
/domainsRetrieve domains with records count (all users)GET-domains array (according permissions)
/domainsCreate domainPOSTname, template, users-
/domains/:nameGet domainGET (all users)-domain and records
/domains/:nameChange domain templatePOSTtemplate, users-
/domains/:nameDelete domainDELETE--
/domains/:name/recordsCreate recordPOSTname, type, content, ttl, prio, disabled, no_ip-
/domains/:name/records/:ridChange recordPOSTname, type, content, ttl, prio, disabled, no_ip-
/domains/:name/records/:ridDelete recordDELETE--
/noip/:tokenChange No-IP valueGET--
/api/:apikey/:domainCreate/change record from APIPOSTname, type, content-
/api/:apikey/:name/:typeDelete record from APIDELETE--

JuNeDNS No-IP uses /noip/:token endpoint to update IP value. /api/:apikey/... API endpoints for Let´s Encrypt or other service you need.

Definition of managements fields and types (if bool then value 0 or 1).

Required fields are not, for POST variables if default value.

User fields

FieldTypeRequiredDefaultDefinition
idInteger--User Id
codeString-User code
passwdString-User password
nameString--User name
is_adminBool0User is admin (for users or full management)
domainsArray--For no-admin users, for create/change actions with domains access and if readonly
domains sample [{"domain_id": 1, "readonly": 0}, {"domain_id": 3, "readonly": 1}]

Domain fields

FieldTypeRequiredDefaultDefinition
idInteger--Domain Id
nameString-Domain name in Punycode
nopunycodeString--Domain name
templateInteger-1Template to use to create zone
usersArray--For no-admin users, for create/change actions with users access and if readonly
users Only is taken if admin user, sample [{"user_id": 1, "readonly": 0}, {"user_id": 3, "readonly": 1}]

Record fields

FieldTypeRequiredDefaultDefinition
idInteger--Record Id
nameString-Record name in Punycode
typeString-DNS type (from login available types)
contentString-Record content
ttlInteger259200Record Time To Live / Expiry (seconds)
prioInteger--Priority only for MX
disabledBool0Record is disabled
no_ipString--No-IP automatized token

Although domain name is inside record name, for security reasons don´t allow users to change it and show it as read only (like Frontend does), JuNeDNS Server checks Root Zone with record, and JuNeDNS Backend parses this value to avoid spoofing.

Templates to insert predefined records automatically, management only for admin users

Template fields

FieldTypeRequiredDefaultDefinition
idInteger--Template Id
nameString-Template name
descriptionString--Template description
is_defaultBool0Is default selected template for new domains
recordsArray--Template records

Template records fields

FieldTypeRequiredDefaultDefinition
idInteger--Template record Id
nameString-Template record name
typeString-Template record type
contentString-Template record content
ttlInteger259200Template record Time To Live / Expiry (seconds)
prioInteger--Template record priority only for MX

Wildcards used in template records (name or content)

Records in templates using wildcards that are replaced with real values:

WildcardDescriptionSample
%d%Domain namemydomain.tld
%m%Main domain where NS point to, setted in config main_domainnsdomain.tld
%ip4%IPv4 setted in config ipv41.2.3.4
%ip6%IPv6 setted in config ipv6 ignored if not IPv6i:want:an:ipv6:address:so:change:me

Have a look to some template records as SOA or SPF to check that are correct values (ns1, info...).

Predefined templates

Two templates to create domain DNS zones, you can create your owns in database tables:

idNameDescriptionDefault
1DefaultNormal DNS zone with MX on server
2With Google WorkspaceDNS zone with MX for Google Workspace-

HTTP status response

With translated JSON message as message variable in return BODY, or content response if 200:

StatusResponseSample message
200Ok-
201CreatedCreated
400Bad RequestAlready exists
401UnauthorizedYou must login
403ForbiddenYou have not permissions to create domains
404Not foundNot found mydomain.tld

🏳Languages

Availables for lang GET parameter. Help us to translate JuNeDNS in your language 📩 info@dataclick.es

CodeShort codeLanguage
en-USen🇬🇧 English
es-ESes🇪🇸 Español
fr-FRfr🇫🇷 Français
de-DEde🇩🇪 Deutsch
it-ITit🇮🇹 Italiano
pt-PTpt🇵🇹 Português
zh-CNch🇨🇳 中文

First try ´code´ next ´Short code´ from window.navigator.language (default en-US)

Types available on login

Return all available DNS records, with name and type (str, int8, int16, int32, ipv4, ipv6 or array). Sample:

const types = {
	SOA: {primary: 'str', admin: 'str', serial: 'int32', refresh: 'int32', retry: 'int32', expiration: 'int32', minimum: 'int32'},
	 A: {address: 'ipv4'},
	 CAA: {flags: [0, 1], tag: ['issue', 'issuewild', 'iodef'], value: 'str'},
	 ...
};

Permissions

is_admin per user, readonly per user+domain:

Actionsis_admin=1is_admin=0, readonly=0is_admin=0, readonly=1is_admin=0
Users/templates management
Domains create/delete
Domains list
Records create/change/delete
Records list

Test

With cURL (you don´t need Postman):

  • Login: curl -v -X POST -H "Content-Type: application/json" -d "{\"user\": \"USER\", \"passwd\": \"\"}" http://localhost:9053/login
  • Create domain without template: curl -v -X POST -H "Content-Type: application/json" -H "x-access-token: TOKEN" -d "{\"name\": \"mydomain.tld\", \"template\": 0}" http://localhost:9053/domains
  • Delete record 22 from domain mydomain.tld: curl -v -X DELETE -H "Content-Type: application/json" -H "x-access-token: TOKEN" http://localhost:9053/domains/mydomain.tld/records/22

Logs

You can use log to share parameter with JuNeDNS Server or use backend_log For security, maximum log size per file will be 50 Mb or truncate.

Reading logs

Line formats in junedns-backend.log when log=2 (or 3) in junedns.conf

  • LGN = Login, NOI = No-IP, API, or ERR = Error. yyyy-mm-dd hh:mm:ss [LGN|NOI|API|ERR] IP: <ip> message
  • Sample of login: 2024-01-01 09:45:05 [LGN] IP: <1.2.3.4> USER user
  • Sample of No-IP / API change request (token with 10 first chars): 2024-01-01 09:45:05 [NOI] IP: <1.2.3.4> TOKEN token 2024-01-01 09:45:05 [API] IP: <1.2.3.4> APIKEY apikey
  • Error sample if log is not 0 in junedns.conf maybe USER if login: 2024-01-01 09:45:05 [ERR] IP: <1.2.3.4> USER user 2024-01-01 09:45:05 [ERR] IP: <1.2.3.4> API api
  • ...or if No-IP request invalid: 2024-01-01 09:45:05 [ERR] IP: <1.2.3.4> NOIP token User or No-IP token will be truncated to 20 first characters due security.

Rotate logs

🐧Linux: Add rotate log functionality and keep in mind that log file will be increase over time.

  • Your could change log file path to /var/log/junedns-backend.log and changing const flog value in backend.js
  • Create file /etc/logrotate.d/junedns-backend and set correctly JuNeDNS_PATH
JuNeDNS_PATH/junedns-backend.log {
        daily
        missingok
        rotate 4
        compress
        copytruncate
        create 600 root root
}

Compile JuNeDNS Backend

Get executable compiled in folder dist/ (x64 bits) for: 🐧Linux, 🪟 Windows and 🍎MacOS.

  • Rename package.json to package.bak.json
  • Then rename package.compile.json to package.json to use Package your Node.js (pkg)
  • And run npm run build or by platform npm run build-linux (or build-win, build-macos)

Let´s Encrypt

Using acme.sh to create/renew SSL certificates for HTTPS, and create TXT domain challenge. Different way to create/change record using Backend internal API with cURL, instead of dns_junedns.sh from JuNeDNS Server that uses MySQL/MariaDB client.

  • Copy or replace dns_junednsapi.sh to folder ACME_PATH/dnsapi
  • Be sure cURL installed on server sudo apt install curl
  • Change backend_api=true in junedns.conf then restart and you see a new backend_apikey value.
  • Add this API key and backend URL values to ACME_PATH/account.conf
JUNEDNSAPI_URL='http://localhost:9053'
JUNEDNSAPI_APIKEY='SAME_APIKEY_AS_junedns.conf_backend_apikey'

Then to create a new certificate: ACME_PATH/acme.sh --issue --dns dns_junednsapi -d "mydomain.tld" --server letsencrypt

Let´s Encrypt is free but only 3 months validity. Add cron task to automatically renew SSL certificates: ACME_PATH/acme.sh --home "ACME_PATH" --renew-all --stopRenewOnError --server letsencrypt --cron Use --server letsencrypt to allow * wildcard domains (default ZeroSSL not supported).

✔️JuNeDNS Backend detects if certificates are renewed (different datetime) and restarts automatically.

Fail2ban

If you want to prevent DDoS / brute force attacks you can use Fail2ban, in case of login or No-IP.

  • Set log=2 in junedns.conf
  • Create /etc/fail2ban/jail.d/junednsbackend.conf
[junednsbackend-iptables]
enabled = true
filter = junednsbackendfilter
action = iptables[name=junednsbackend, port=9053, protocol=http]
logpath = /etc/junedns/junedns-backend.log
maxretry = 10
  • Create /etc/fail2ban/filter.d/junednsbackendfilter.conf
[Definition]
failregex = ^.+ERR.+IP: <HOST>.+$
ignoreregex =
  • Unban IP fail2ban-client set junednsbackend-iptables unbanip 1.2.3.4

JuNe BackServer for tokens

When you create a client ↔ server system, now renamed backend ↔ frontend, you need a link between both for an identification. This used to be done with a session token saved in a database table along the data as a JSON on the server (I mean, backend), and on the client (I mean, frontend) it´s saved as a cookie. Now a token is made in a JSON with encrypted data, which is sent in the HTTP header each time by the frontend and verified by the backend. JSONWebToken, a module of 1170 files, 21 folders and 1,6 MB... is used as standard, included it´s ´shhhhh´ secret. But with JuNe Token the same goal is achieved with a few lines of code. A private key is generated and stored in junedns.conf if it was not already created backend_token= then a JSON is encoded with the data and the expiration of the token, and sent as HTTP header. Frontend stores it in sessionStorage, and never sends by GET parameters to avoid relogin if back.

🗜 You can see that this code is just a few lines in 930 bytes, a 0,05% from JSONWebToken (that is x1800):

const crypto = require('crypto');

// Generate key pair to export to config
token = crypto.generateKeyPairSync('rsa', {modulusLength: 1024}).privateKey.export({type: 'pkcs1', format: 'der'}).toString('base64');

// Create public and private keys
const pubk = crypto.createPublicKey({key: Buffer.from(token, 'base64'), type: 'pkcs1', format: 'der'});
const prik = crypto.createPrivateKey({key: Buffer.from(token, 'base64'), type: 'pkcs1', format: 'der'});

// Session token variable
let session = {};

// Set header x-access-token an expiry 15 minutes
function setHeader(expiry = 900) {
  session._exp = Math.round((new Date()).getTime() / 1000) + expiry;
  return crypto.publicEncrypt({key: pubk, padding: crypto.constants.RSA_PKCS1_PADDING}, Buffer.from(JSON.stringify(session))).toString('base64');
}

// Get header if not expired
function getHeader(xhdr) {
  session = JSON.parse(crypto.privateDecrypt({key: prik, padding: crypto.constants.RSA_PKCS1_PADDING}, Buffer.from(xhdr, 'base64')).toString('utf-8'));
  if(session._exp < Math.round((new Date()).getTime() / 1000))
    session = {};
}

** What is safer? ** In database token system, it´s very complicated to discover the token, which would not exist for more than a few hours, and also brute force attacks can be controlled. In JSON token system if the private key is compromised, then it would be easier to perform spoofing.

Included to make this project JuNe BackServer

JuNeDNS Server & Frontend & No-IP

https://github.com/EduardoRuizM/junedns-server

https://github.com/EduardoRuizM/junedns-frontend

https://github.com/EduardoRuizM/junedns-noip

Trademarks©️

Dataclick Olimpo JuNeDNS

  • Dataclick.es is a software development company since 2006.
  • Olimpo is a whole solution software to manage all domains services such as hosting services and to create Webs in a server.
  • JuNe / JUst NEeded Philosophy, available software and development solutions.
  • JuNeDNS is a part of Dataclick Olimpo domains management for DNS service, released to Internet community.
  • Feel free to use JuNeDNS according MIT license respecting the brand and image logotype that you can use.

Files

FileDescription
backend.jsJuNeDNS Backend main file, just 25 Kb (JuNe Philosophy)
dns_junedns.shLet´s Encrypt API using cURL for acme.sh to create/renew SSL certificates
junedns.confConfiguration file for JuNeDNS Server and Backend
junedns-backend.serviceSystemctl service for Backend binary or source code execution
logo.pngJuNeDNS Backend Logo free to use
mysql-backend.sqlMySQL/MariaDB database to combine with JuNeDNS Server database
package.compile.jsonpackage.json file to compile Backend binaries in folder dist/
package.jsonOriginal Backend package.json
texts.jsTexts in languages
backserver.jsJuNe BackServer for routing and web token