Skip to content

Instantly share code, notes, and snippets.

@duncansmart
Last active March 28, 2024 15:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save duncansmart/ed19dab6780e3f4db88c048c25b2b7d3 to your computer and use it in GitHub Desktop.
Save duncansmart/ed19dab6780e3f4db88c048c25b2b7d3 to your computer and use it in GitHub Desktop.
Continuously grabs the traffic stats from an EdgeRouter-X router and dumps them in CSV format to stdout.

Usage:

  • Download both files: edgemax-dump-traffic-stats.js and package.json to the same directory.
  • Run npm install in that dir to get all the dependencies.
  • Update the URL/username/password in edgemax-dump-traffic-stats.js for your environment
  • Run node edgemax-dump-traffic-stats.js

To write to a file, pipe to a file like so:

node edgemax-dump-traffic-stats.js > edgerouter-stats.csv

Only tested on EdgeRouter X v2.0.8-hotfix.1

const url = require('url');
const axios = require('axios');
const qs = require('querystring')
const WebSocket = require('ws');
// disable certificate verification
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const baseUrl = 'https://192.168.1.1';
const username = 'ubnt';
const password = 'Pa55w0rd';
(async () => {
//console.error('Start');
const sessionCookie = await logon(baseUrl, username, password);
//console.error('Logged on');
//console.error({ sessionCookie });
await refreshHostNames(sessionCookie)
const { protocol, hostname } = url.parse(baseUrl)
const wsproto = protocol == 'https:' ? 'wss:' : 'ws:';
const hostnameIsIp = (/\d+\.\d+\.\d+\.\d+/).test(hostname)
const ws = new WebSocket(`${wsproto}//${hostname}/ws/stats`, {
servername: hostnameIsIp ? '' : undefined // suppress "[DEP0123] DeprecationWarning: Setting the TLS ServerName to an IP address is not permitted by RFC 6066. This will be ignored in a future version."
});
ws.on('open', function open(x) {
//console.error("Opening websocket");
const initMessage = JSON.stringify({
SUBSCRIBE: [{ name: "export" }],
SESSION_ID: sessionCookie
});
//console.error("Sending: ", initMessage);
ws.send(initMessage.length + '\n' + initMessage, function (e) {
if (e)
console.error('init message error', e);
});
//console.error('sent init message');
});
let messageLength = 0;
let messageContent = '';
ws.on('message', async (data) => {
if (messageLength == 0) {
//console.log('... new msg');
const newlinepos = data.indexOf('\n');
messageLength = ~~data.slice(0, newlinepos);
messageContent = data.slice(newlinepos + 1);
}
else
{
//... append
messageContent += data;
}
if (messageContent.length < messageLength) {
// incomplete, wait for next part
return;
}
await handleMessage(messageContent);
try {
await refreshHostNames(sessionCookie);
} catch (error) {
console.error('ERROR: refreshHostNames ', error);
}
messageLength = 0;
messageContent = "";
});
ws.on('error', (code, reason) => {
console.error('WS ERROR', { code, reason });
})
ws.on('close', (code, reason) => {
console.error('WS CLOSE', { code, reason });
})
})();
const _hostnames = {};
let _messageCount = 0
async function handleMessage(messageContent) {
// header row
if (_messageCount++ == 0) {
console.log('time,categoryName,appName,hostname,ip,tx_bytes,tx_rate,rx_bytes,rx_rate');
}
/* {
"export": {
"192.168.1.66": {
"Google Static Content(SSL)|Network protocols": {
"tx_bytes": "2355",
"tx_rate": "0",
"rx_bytes": "1500",
"rx_rate": "0"
},
"Youtube|Media streaming services": {
"tx_bytes": "85079",
"tx_rate": "0",
"rx_bytes": "688716",
"rx_rate": "0"
},
},
...
*/
//console.error(messageContent);
const message = JSON.parse(messageContent);
if (!message["export"])
return;
const time = new Date().toISOString().substring(0, 19).replace('T', ' ');
const exportItems = message['export'];
for (const ip in exportItems) {
const exportItem = exportItems[ip];
for (const appAndCategory in exportItem) {
const stat = exportItem[appAndCategory]
const hostname = _hostnames[ip] || ip;
const [appName, categoryName] = appAndCategory.split(/\|/);
console.log(`${time},${csvEncode(categoryName)},${csvEncode(appName)},${csvEncode(hostname)},${ip},${stat.tx_bytes},${stat.tx_rate},${stat.rx_bytes},${stat.rx_rate}`);
}
}
}
function csvEncode(str) {
if (str == null)
return '';
if (str.indexOf(",") > -1 || str.indexOf("\"") > -1)
str = '"' + str.replace(/"/g, '""') + '"';
return str;
}
async function logon(baseUrl, username, password) {
const form = { username: username, password: password };
const response = await axios.post(baseUrl, qs.stringify(form), {
maxRedirects: 0,
validateStatus: () => true // accept all certs
});
if (!response.headers['set-cookie']){
throw new Error('Logon failed, please check username/password')
}
const cookies = response.headers['set-cookie'].reduce((obj, item) => {
const [name, value] = item.split(/=/);
obj[name] = value.split(/;/)[0];
return obj;
}, {});
//console.log({cookies});
const sessionCookie = cookies['beaker.session.id'];
return sessionCookie;
}
async function refreshHostNames(sessionCookie) {
// only run every minute
if ((new Date() - (refreshHostNames.lastUpdated || 0)) / 1000 < 60)
return;
//console.error('Refresh host names')
const response = await axios.get(baseUrl.replace(/\/$/, '') + "/api/edge/data.json?data=dhcp_leases", {
headers: { "Cookie": `beaker.session.id=${sessionCookie}` }
});
//console.log("refreshHostNames:", response.data.output['dhcp-server-leases'].LAN);
const leases = response.data.output['dhcp-server-leases'].LAN;
for (const ip in leases) {
_hostnames[ip] = leases[ip]['client-hostname'].replace(/\?/g, '');
}
refreshHostNames.lastUpdated = new Date();
}
{
"dependencies": {
"axios": "^0.19.2",
"ws": "^7.3.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment