mdns-sync/main.js

302 lines
9.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');
// 0. HELPER FUNCTIONS
// Extract password from ~/.ssh/id_rsa.pub (last 6 characters of base64)
function getDbPassword() {
const pubKeyContent = fs.readFileSync(`${os.homedir()}/.ssh/id_rsa.pub`, "utf8");
const base64Part = pubKeyContent.split(" ")[1];
return base64Part.slice(-7,-1);
}
// Insert data into MariaDB
async function insertToMariaDB(record) {
if (!['A', 'PTR', 'SRV','TXT'].includes(record.Type)) return;
const dataStr = typeof record.Data === 'object' ? JSON.stringify(record.Data) : record.Data;
try {
await dbPool.execute(
`REPLACE INTO ${DB_TABLE} (Host, Type, Name, Data) VALUES (?, ?, ?, ?);`,
[DB_USER, record.Type, record.Name, dataStr]
);
} catch (err) {
console.error("DB Insert Error:", err);
}
}
// 1. Database functions
function saveDbToFile(data, filename) {
return fs.writeFileSync(filename,typeof data === 'object'?JSON.stringify(data):data);
}
function loadDbFromFile(filename) {if (!fs.existsSync(filename)) return {local:{},remote:{}}; else return JSON.parse(fs.readFileSync(filename));}
// 2. Network functions
function getLocalNetworkAddressesIPs() {
const interfaces = os.networkInterfaces();
const addresses = [];
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === 'IPv4' && !iface.internal) {
addresses.push(iface.address);
}
}
}
return addresses;
}
function sniffmDNSLocalPackets() {
// 1. Listen for responses
mdns.on('response', async function(response) {
// console.log("got a new mDNS response.",response);
let all_answers = response.answers.concat(response.additionals);
for (let k in all_answers) {
let answer = all_answers[k];
// 1. Handle DEVICES for a given SERVICE
if (answer.type == "PTR") {
// 1.1 Filter by RELEVANT SERVICE TYPES
if (!["in-addr.arpa","_googlecast._tcp.local","_tcp.local"].some(suffix => answer.name.endsWith(suffix))) continue;
// 1.2 Initialize POINTERS (services) sub-database
if (!db.local.PTR) db.local.PTR = {};
// 1.3 Initialize this SERVICE TYPE (e.g. _googlecast._tcp.local), if needed
if (!db.local.PTR[answer.name]) {
db.local.PTR[answer.name] = [answer.data];
} else {
// Add a new DEVICE to this SERVICE TYPE if needed
if (!db.local.PTR[answer.name].includes(answer.data))
db.local.PTR[answer.name].push(answer.data);
}
}
// 2. Handle DEVICE IP resolution
if (answer.type == "A") {
// 2.1 Initialize ADDRESSES sub-database
if (!db.local.A) db.local.A = {};
db.local.A[answer.name] = answer.data;
}
if (answer.type == "SRV") {
// Initialize this device / entry, if needed
if (db.local[answer.name] === undefined) db.local[answer.name] = {"TXT":null,"SRV":null};
db.local[answer.name][answer.type] = answer.data;
}
if (answer.type == "TXT") {
// Initialize this device / entry, if needed
if (db.local[answer.name] === undefined) db.local[answer.name] = {"TXT":null,"SRV":null};
db.local[answer.name][answer.type] = answer.data;
}
await insertToMariaDB({
Type: answer.type,
Name: answer.name,
Data: answer.data
});
saveDbToFile(db,DB_FILENAME);
}
})
// 2. Handle the server being destroyed
mdns.on('destroyed', function () {console.log('Server destroyed.');process.exit(0);});
// 3. Handle the onReady event
mdns.on('ready', function () {
console.log("mDNS server is ready...");
// mdns.query({questions:[{ name: '_:googlecast._tcp.local', type: 'PTR', class: 'IN'}]} );
})
}
function injectmDNSPacketLocally(type,name,data,flush = true,_class = 'IN',ttl = 120) {
console.log(`sending packet ${name} of ${type} with data: ${data}`);
mdns.respond({answers:[
{
name: name,
type: type,
ttl: ttl,
class: _class,
flush: flush,
data: data
}
]});}
function replyLocallyWithRemoteDevicesData(name,type,query) {
const answers = [];
if (!db.remote) db.remote = {};
if (!db.remote.PTR) db.remote.PTR = {};
if (!db.remote.A) db.remote.A = {};
// 1. If the query is asking for a PTR (pointer to a service)
if (type === 'PTR' && db.remote.PTR && db.remote.PTR[name]) {
for (let ptrData of db.remote.PTR[name]) {
answers.push({
name, // The service name being queried
type: 'PTR', // Type of DNS record
class: 'IN', // Internet class
ttl: 120, // Time-to-live (how long to cache)
data: ptrData // The actual pointer data (e.g. device instance name)
});
}
}
// 2. If the query is for an A record (IPv4 address of a device)
if (type === 'A' && db.remote.A && db.remote.A[name]) {
answers.push({
name,
type: 'A',
class: 'IN',
ttl: 120,
data: db.remote.A[name] // The IPv4 address
});
}
// 3. If the query is for an SRV record (hostname + port of a service)
if (type === 'SRV' && db.remote.services[name] && db.remote.services[name].SRV !== null) {
answers.push({
name,
type: 'SRV',
class: 'IN',
ttl: 120,
data: db.remote.services[name].SRV // Must be an object like { port, target, priority, weight }
});
}
// 4. If the query is for a TXT record (extra metadata)
if (type === 'TXT' && db.remote.services[name] && db.remote.services[name].TXT !== null) {
answers.push({
name,
type: 'TXT',
class: 'IN',
ttl: 120,
// Convert plain string to buffer; required by mdns-server
data: Buffer.from(db.remote.services[name].TXT, 'utf8')
});
}
// 5. Many mDNS tools (like Avahi or Bonjour) send type: 'ANY' queries to discover all records for a name.
if (type === 'ANY') {
// Respond with everything you know about this name
if (db.remote.PTR && db.remote.PTR[name]) {
for (let ptrData of db.remote.PTR[name]) {
answers.push({ name, type: 'PTR', class: 'IN', ttl: 120, data: ptrData });
}
}
if (db.remote.A && db.remote.A[name]) {
answers.push({ name, type: 'A', class: 'IN', ttl: 120, data: db.remote.A[name] });
}
if (db.remote.services[name]) {
const r = db.remote.services[name];
if (r.SRV) answers.push({ name, type: 'SRV', class: 'IN', ttl: 120, data: r.SRV });
if (r.TXT) answers.push({ name, type: 'TXT', class: 'IN', ttl: 120, data: r.TXT.map(entry=>Buffer.from(entry.data)) });
}
}
// 6. If we prepared any answers, respond to the query
if (answers.length > 0) {
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]
);
db.remote = { A: {}, PTR: {}, services: {} };
rows.forEach(row => {
const { Type, Name, Data } = row;
if (Type === 'A') {
db.remote.A[Name] = Data;
}
else if (Type === 'PTR') {
if (!db.remote.PTR[Name]) db.remote.PTR[Name] = [];
if (!db.remote.PTR[Name].includes(Data)) db.remote.PTR[Name].push(Data);
}
else if (['TXT', 'SRV'].includes(Type)) {
if (!db.remote.services[Name]) db.remote.services[Name] = { TXT: null, SRV: null };
db.remote.services[Name][Type] = (Data.substr(0,1)=="{"?JSON.parse(Data):Data);
}
});
console.log(`Remote DB updated. A records: ${Object.keys(db.remote.A).length} PTR records: ${Object.keys(db.remote.PTR).length}`);
} catch (err) {
console.error('Error fetching remote DB:', err);
}
}
/*********************************
*
* 100. MAIN / IMPERATIVE CODE
*
*********************************/
const fs = require("fs");
const DB_FILENAME = 'database.json';
const db = loadDbFromFile(DB_FILENAME);
const LOCAL_IP_ADDR = getLocalNetworkAddressesIPs()[0];
const mdns = require('mdns-server')({interface: LOCAL_IP_ADDR,reuseAddr: true,loopback: false,noInit: true});
// MariaDB Configuration
const DB_HOST = "10.10.8.1";
const DB_NAME = "NETWORK";
const DB_TABLE = "mDNS";
const DB_USER = os.hostname();
// Initialize MariaDB connection pool
const dbPool = mysql.createPool({
host: DB_HOST,
user: DB_USER,
password: getDbPassword(),
database: DB_NAME,
connectionLimit: 5
});
sniffmDNSLocalPackets();
mdns.on('query', function (query) {
// Loop through each question in the mDNS query
query.questions.forEach(q => {
const { name, type } = q;
console.log(`Received mDNS query for ${name} (${type})`);
replyLocallyWithRemoteDevicesData(name,type,q);
});
});
// every 15 seconds
setInterval(fetchRemoteDevicesData, 15000);
fetchRemoteDevicesData(); // initial immediate call
// initialize the server now that we are watching for events
mdns.initServer()