mdns-sync/main.js
git dfdaff628f removes the local-file persistence
removes the local-file persistence (saveDbToFile, loadDbFromFile and related fs/database.json bits) and relies solely on MariaDB for storing and fetching both local and remote mDNS records
2025-06-26 10:01:19 +01:00

199 lines
5.2 KiB
JavaScript

// mdns-sync.js
// A single-file mDNS sync tool acting as client or server
// Uses mdns-server for mDNS and Express for HTTP
/*********************************
*
* 1. LIBRARY FUNCTIONS
*
*********************************/
const os = require('os');
const mysql = require('mysql2/promise');
const mdns = require('mdns-server'); // we'll initialize below
// 0. HELPER FUNCTIONS
// Extract password from ~/.ssh/id_rsa.pub (last 6 characters of base64)
function getDbPassword() {
const pubKeyContent = require('fs').readFileSync(
`${os.homedir()}/.ssh/id_rsa.pub`,
'utf8'
);
const base64Part = pubKeyContent.split(' ')[1];
return base64Part.slice(-7, -1);
}
// Insert/update data into MariaDB
async function updateMariaDB(record) {
const { Type, Name } = record;
if (!['A','PTR','SRV','TXT'].includes(Type)) return;
const host = DB_USER;
const dataStr = typeof record.Data === 'object'
? JSON.stringify(record.Data)
: record.Data;
try {
const sql = `
INSERT INTO ${DB_TABLE} (Host, Type, Name, Data)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE Data = VALUES(Data)
`;
await dbPool.execute(sql, [host, Type, Name, dataStr]);
} catch (err) {
console.error('DB Upsert Error:', err);
}
}
// 1. Network functions
function getLocalNetworkAddressesIPs() {
const interfaces = os.networkInterfaces();
return Object.values(interfaces)
.flat()
.filter(iface => iface.family === 'IPv4' && !iface.internal)
.map(iface => iface.address);
}
function sniffmDNSLocalPackets() {
mdns.on('response', async response => {
const answers = response.answers.concat(response.additionals);
for (const ans of answers) {
// only care about standard types
if (!['A','PTR','SRV','TXT'].includes(ans.type)) continue;
// For each answer, immediately upsert to MariaDB
await updateMariaDB({
Type: ans.type,
Name: ans.name,
Data: ans.data
});
}
});
mdns.on('destroyed', () => {
console.log('Server destroyed.'); process.exit(0);
});
mdns.on('ready', () => {
console.log('mDNS server is ready...');
});
}
function replyLocallyWithRemoteDevicesData(name, type) {
const answers = [];
if (type === 'PTR' && db.remote.PTR[name]) {
db.remote.PTR[name].forEach(ptrData =>
answers.push({ name, type:'PTR', class:'IN', ttl:120, data: ptrData })
);
}
if (type === 'A' && db.remote.A[name]) {
answers.push({ name, type:'A', class:'IN', ttl:120, data: db.remote.A[name] });
}
if (type === 'SRV' && db.remote.services[name]?.SRV) {
answers.push({ name, type:'SRV', class:'IN', ttl:120, data: db.remote.services[name].SRV });
}
if (type === 'TXT' && db.remote.services[name]?.TXT) {
answers.push({
name, type:'TXT', class:'IN', ttl:120,
data: db.remote.services[name].TXT.map(e => Buffer.from(e.data, 'utf8'))
});
}
if (type === 'ANY') {
// same as above but for ANY
['PTR','A','SRV','TXT'].forEach(t =>
replyLocallyWithRemoteDevicesData(name, t)
);
}
if (answers.length) {
console.log(`Responding with ${answers.length} answer(s) for ${name}`);
mdns.respond({ answers });
}
}
async function fetchRemoteDevicesData() {
try {
const [rows] = await dbPool.execute(
`SELECT Type, Name, Data
FROM ${DB_TABLE}
WHERE Host <> ?
AND MakeAvailableEverywhere = 1`,
[DB_USER]
);
// reinitialize remote cache
db.remote = { A: {}, PTR: {}, services: {} };
for (const { Type, Name, Data } of rows) {
const parsed = ['[','{'].includes(Data[0]) ? JSON.parse(Data) : Data;
if (Type === 'A') {
db.remote.A[Name] = parsed;
} else if (Type === 'PTR') {
db.remote.PTR[Name] = parsed;
} else { // SRV or TXT
db.remote.services[Name] ||= { TXT:null, SRV:null };
db.remote.services[Name][Type] = parsed;
}
}
console.log(
`Remote DB updated. A: ${Object.keys(db.remote.A).length}, PTR: ${Object.keys(db.remote.PTR).length}`
);
} catch (err) {
console.error('Error fetching remote DB:', err);
}
}
/*********************************
*
* 100. MAIN / IMPERATIVE CODE
*
*********************************/
// MariaDB Configuration
const DB_HOST = '10.10.8.1';
const DB_NAME = 'NETWORK';
const DB_TABLE = 'mDNS';
const DB_USER = os.hostname();
// Initialize in-memory cache
const db = { remote: { A: {}, PTR: {}, services: {} } };
// Pick first non-internal v4 address
const LOCAL_IP_ADDR = getLocalNetworkAddressesIPs()[0];
const mdnsServer = mdns({
interface: LOCAL_IP_ADDR,
reuseAddr: true,
loopback: false,
noInit: true
});
// Init MariaDB connection pool
const dbPool = mysql.createPool({
host: DB_HOST,
user: DB_USER,
password: getDbPassword(),
database: DB_NAME,
connectionLimit: 5
});
console.log(
`Connecting to SQL ${DB_HOST} as ${DB_USER}, DB=${DB_NAME}`
);
sniffmDNSLocalPackets();
mdnsServer.on('query', ({ questions }) => {
for (const { name, type } of questions) {
console.log(`Query for ${name} (${type})`);
replyLocallyWithRemoteDevicesData(name, type);
}
});
// fetch remote every 15s
setInterval(fetchRemoteDevicesData, 15_000);
fetchRemoteDevicesData();
// now start the server
mdnsServer.initServer();