// 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();