re-add luci-app-wrtbwmon and update typo

This commit is contained in:
coolsnowwolf 2018-10-08 16:33:04 +08:00
parent 8383d26fbd
commit 57a769a9c2
17 changed files with 1278 additions and 2 deletions

View File

@ -10,4 +10,7 @@ uci -q batch <<-EOF >/dev/null
commit ucitrack
EOF
sed -i '/dnscache-watchdog.sh/d' /etc/crontabs/root
echo '*/60 * * * * /usr/share/dnscache-watchdog.sh' >> /etc/crontabs/root
exit 0

View File

@ -0,0 +1,9 @@
#!/bin/sh
dns_enable=$(uci flowoffload.@flow[0].dns 2>/dev/null)
if [ $dns_enable -eq 1 ]; then
if ! pidof dnscache>/dev/null; then
/etc/init.d/flowoffload restart
fi
fi

View File

@ -0,0 +1,17 @@
# Copyright (C) 2016 Openwrt.org
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for Wrtbwmon
LUCI_DEPENDS:=+luci-app-nlbwmon
LUCI_PKGARCH:=all
PKG_VERSION:=1.0
PKG_RELEASE:=5
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,562 @@
var wrt = {
// variables for auto-update, interval is in seconds
scheduleTimeout: undefined,
updateTimeout: undefined,
isScheduled: true,
interval: 5,
// option on whether to show per host sub-totals
perHostTotals: false,
// variables for sorting
sortData: {
column: 7,
elId: 'thTotal',
dir: 'desc',
cache: {}
}
};
(function () {
var oldDate, oldValues = [];
// find base path
var re = /(.*?admin\/nlbw\/[^/]+)/;
var basePath = window.location.pathname.match(re)[1];
//----------------------
// HELPER FUNCTIONS
//----------------------
/**
* Human readable text for size
* @param size
* @returns {string}
*/
function getSize(size) {
var prefix = [' ', 'k', 'M', 'G', 'T', 'P', 'E', 'Z'];
var precision, base = 1000, pos = 0;
while (size > base) {
size /= base;
pos++;
}
if (pos > 2) precision = 1000; else precision = 1;
return (Math.round(size * precision) / precision) + ' ' + prefix[pos] + 'B';
}
/**
* Human readable text for date
* @param date
* @returns {string}
*/
function dateToString(date) {
return date.toString().substring(0, 24);
}
/**
* Gets the string representation of the date received from BE
* @param value
* @returns {*}
*/
function getDateString(value) {
var tmp = value.split('_'),
str = tmp[0].split('-').reverse().join('-') + 'T' + tmp[1];
return dateToString(new Date(str));
}
/**
* Create a `tr` element with content
* @param content
* @returns {string}
*/
function createTR(content) {
var res = '<tr';
res += ' class="tr">';
res += content;
res += '</tr>';
return res;
}
/**
* Create a `th` element with content and options
* @param content
* @param opts
* @returns {string}
*/
function createTH(content, opts) {
opts = opts || {};
var res = '<th';
if (opts.right) {
res += ' align="right"';
}
if (opts.title) {
res += ' title="' + opts.title + '"';
}
if (opts.id) {
res += ' id="' + opts.id + '"';
}
res += ' class="th">';
res += content;
res += '</th>';
return res;
}
/**
* Create a `td` element with content and options
* @param content
* @param opts
* @returns {string}
*/
function createTD(content, opts) {
opts = opts || {};
var res = '<td';
if (opts.right) {
res += ' align="right"';
}
if (opts.title) {
res += ' title="' + opts.title + '"';
}
res += ' class="td">';
res += content;
res += '</td>';
return res;
}
/**
* Returns true if obj is instance of Array
* @param obj
* @returns {boolean}
*/
function isArray(obj) {
return obj instanceof Array;
}
//----------------------
// END HELPER FUNCTIONS
//----------------------
/**
* Handle the error that happened during the call to the BE
*/
function handleError() {
// TODO handle errors
// var message = 'Something went wrong...';
}
/**
* Handle the new `values` that were received from the BE
* @param values
* @returns {string}
*/
function handleValues(values) {
if (!isArray(values)) return '';
// find data and totals
var res = parseValues(values);
var data = res[0];
var totals = res[1];
// aggregate (sub-total) by hostname (or MAC address) after the global totals are computed, before sort and display
aggregateHostTotals(data);
// store them in cache for quicker re-rendering
wrt.sortData.cache.data = data;
wrt.sortData.cache.totals = totals;
renderTableData(data, totals);
}
/**
* Renders the table body
* @param data
* @param totals
*/
function renderTableData(data, totals) {
// sort data
data.sort(sortingFunction);
// display data
document.getElementById('tableBody').innerHTML = getDisplayData(data, totals);
// set sorting arrows
var el = document.getElementById(wrt.sortData.elId);
if (el) {
el.innerHTML = el.innerHTML + (wrt.sortData.dir === 'desc' ? '&#x25BC' : '&#x25B2');
}
// register table events
registerTableEventHandlers();
}
/**
* Parses the values and returns a data array, where each element in the data array is an array with two elements,
* and a totals array, that holds aggregated values for each column.
* The first element of each row in the data array, is the HTML output of the row as a `tr` element
* and the second is the actual data:
* [ result, data ]
* @param values The `values` array
* @returns {Array}
*/
function parseValues(values) {
var data = [], totals = [0, 0, 0, 0, 0];
for (var i = 0; i < values.length; i++) {
var d = parseValueRow(values[i]);
if (d[1]) {
data.push(d);
// get totals
for (var j = 0; j < totals.length; j++) {
totals[j] += d[1][3 + j];
}
}
}
return [data, totals];
}
/**
* Parse each row in the `values` array and return an array with two elements.
* The first element is the HTML output of the row as a `tr` element and the second is the actual data
* [ result, data ]
* @param data A row from the `values` array
* @returns {[ string, [] ]}
*/
function parseValueRow(data) {
// check if data is array
if (!isArray(data)) return [''];
// find old data
var oldData;
for (var i = 0; i < oldValues.length; i++) {
var cur = oldValues[i];
// compare mac addresses and ip addresses
if (oldValues[i][1] === data[1] && oldValues[i][2] === data[2]) {
oldData = cur;
break;
}
}
// find download and upload speeds
var dlSpeed = 0, upSpeed = 0;
if (oldData) {
var now = new Date(),
seconds = (now - oldDate) / 1000;
dlSpeed = (data[3] - oldData[3]) / seconds;
upSpeed = (data[4] - oldData[4]) / seconds;
}
// create rowData
var rowData = [];
for (var j = 0; j < data.length; j++) {
rowData.push(data[j]);
if (j === 2) {
rowData.push(dlSpeed, upSpeed);
}
}
// create displayData
var displayData = [
createTD(data[0] + '<br />' + data[2], {title: data[1]}),
createTD(getSize(dlSpeed) + '/s', {right: true}),
createTD(getSize(upSpeed) + '/s', {right: true}),
createTD(getSize(data[3]), {right: true}),
createTD(getSize(data[4]), {right: true}),
createTD(getSize(data[5]), {right: true}),
createTD(getDateString(data[6])),
createTD(getDateString(data[7]))
];
// display row data
var result = '';
for (var k = 0; k < displayData.length; k++) {
result += displayData[k];
}
result = createTR(result);
return [result, rowData];
}
/**
* Creates the HTML output based on the `data` and `totals` inputs
* @param data
* @param totals
* @returns {string} HTML output
*/
function getDisplayData(data, totals) {
var result =
createTH('客户端', {id: 'thClient'}) +
createTH('下载带宽', {id: 'thDownload'}) +
createTH('上传带宽', {id: 'thUpload'}) +
createTH('总下载流量', {id: 'thTotalDown'}) +
createTH('总上传流量', {id: 'thTotalUp'}) +
createTH('流量合计', {id: 'thTotal'}) +
createTH('首次上线时间', {id: 'thFirstSeen'}) +
createTH('最后上线时间', {id: 'thLastSeen'});
result = createTR(result);
for (var k = 0; k < data.length; k++) {
result += data[k][0];
}
var totalsRow = createTH('总计');
for (var m = 0; m < totals.length; m++) {
var t = totals[m];
totalsRow += createTD(getSize(t) + (m < 2 ? '/s' : ''), {right: true});
}
result += createTR(totalsRow);
return result;
}
/**
* Calculates per host sub-totals and adds them in the data input
* @param data The data input
*/
function aggregateHostTotals(data) {
if (!wrt.perHostTotals) return;
var curHost = 0, insertAt = 1;
while (curHost < data.length && insertAt < data.length) {
// grab the current hostname/mac, and walk the data looking for rows with the same host/mac
var hostName = data[curHost][1][0].toLowerCase();
for (var k = curHost + 1; k < data.length; k++) {
if (data[k][1][0].toLowerCase() === hostName) {
// this is another row for the same host, group it with any other rows for this host
data.splice(insertAt, 0, data.splice(k, 1)[0]);
insertAt++;
}
}
// if we found more than one row for the host, add a subtotal row
if (insertAt > curHost + 1) {
var hostTotals = [data[curHost][1][0], '', '', 0, 0, 0, 0, 0];
for (var i = curHost; i < insertAt && i < data.length; i++) {
for (var j = 3; j < hostTotals.length; j++) {
hostTotals[j] += data[i][1][j];
}
}
var hostTotalRow = createTH(data[curHost][1][0] + '<br/> (host total)', {title: data[curHost][1][1]});
for (var m = 3; m < hostTotals.length; m++) {
var t = hostTotals[m];
hostTotalRow += createTD(getSize(t) + (m < 5 ? '/s' : ''), {right: true});
}
hostTotalRow = createTR(hostTotalRow);
data.splice(insertAt, 0, [hostTotalRow, hostTotals]);
}
curHost = insertAt;
insertAt = curHost + 1;
}
}
/**
* Sorting function used to sort the `data`. Uses the global sort settings
* @param x first item to compare
* @param y second item to compare
* @returns {number} 1 for desc, -1 for asc, 0 for equal
*/
function sortingFunction(x, y) {
// get data from global variable
var sortColumn = wrt.sortData.column, sortDirection = wrt.sortData.dir;
var a = x[1][sortColumn];
var b = y[1][sortColumn];
if (a === b) {
return 0;
} else if (sortDirection === 'desc') {
return a < b ? 1 : -1;
} else {
return a > b ? 1 : -1;
}
}
/**
* Sets the relevant global sort variables and re-renders the table to apply the new sorting
* @param elId
* @param column
*/
function setSortColumn(elId, column) {
if (column === wrt.sortData.column) {
// same column clicked, switch direction
wrt.sortData.dir = wrt.sortData.dir === 'desc' ? 'asc' : 'desc';
} else {
// change sort column
wrt.sortData.column = column;
// reset sort direction
wrt.sortData.dir = 'desc';
}
wrt.sortData.elId = elId;
// render table data from cache
renderTableData(wrt.sortData.cache.data, wrt.sortData.cache.totals);
}
/**
* Registers the table events handlers for sorting when clicking the column headers
*/
function registerTableEventHandlers() {
// note these ordinals are into the data array, not the table output
document.getElementById('thClient').addEventListener('click', function () {
setSortColumn(this.id, 0); // hostname
});
document.getElementById('thDownload').addEventListener('click', function () {
setSortColumn(this.id, 3); // dl speed
});
document.getElementById('thUpload').addEventListener('click', function () {
setSortColumn(this.id, 4); // ul speed
});
document.getElementById('thTotalDown').addEventListener('click', function () {
setSortColumn(this.id, 5); // total down
});
document.getElementById('thTotalUp').addEventListener('click', function () {
setSortColumn(this.id, 6); // total up
});
document.getElementById('thTotal').addEventListener('click', function () {
setSortColumn(this.id, 7); // total
});
}
/**
* Fetches and handles the updated `values` from the BE
* @param once If set to true, it re-schedules itself for execution based on selected interval
*/
function receiveData(once) {
var ajax = new XMLHttpRequest();
ajax.onreadystatechange = function () {
// noinspection EqualityComparisonWithCoercionJS
if (this.readyState == 4 && this.status == 200) {
var re = /(var values = new Array[^;]*;)/,
match = ajax.responseText.match(re);
if (!match) {
handleError();
} else {
// evaluate values
eval(match[1]);
//noinspection JSUnresolvedVariable
var v = values;
if (!v) {
handleError();
} else {
handleValues(v);
// set old values
oldValues = v;
// set old date
oldDate = new Date();
document.getElementById('updated').innerHTML = '数据更新时间 ' + dateToString(oldDate);
}
}
var int = wrt.interval;
if (!once && int > 0) reschedule(int);
}
};
ajax.open('GET', basePath + '/usage_data', true);
ajax.send();
}
/**
* Registers DOM event listeners for user interaction
*/
function addEventListeners() {
document.getElementById('intervalSelect').addEventListener('change', function () {
var int = wrt.interval = this.value;
if (int > 0) {
// it is not scheduled, schedule it
if (!wrt.isScheduled) {
reschedule(int);
}
} else {
// stop the scheduling
stopSchedule();
}
});
document.getElementById('resetDatabase').addEventListener('click', function () {
if (confirm('This will delete the database file. Are you sure?')) {
var ajax = new XMLHttpRequest();
ajax.onreadystatechange = function () {
// noinspection EqualityComparisonWithCoercionJS
if (this.readyState == 4 && this.status == 204) {
location.reload();
}
};
ajax.open('GET', basePath + '/usage_reset', true);
ajax.send();
}
});
document.getElementById('perHostTotals').addEventListener('change', function () {
wrt.perHostTotals = !wrt.perHostTotals;
});
}
//----------------------
// AUTO-UPDATE
//----------------------
/**
* Stop auto-update schedule
*/
function stopSchedule() {
window.clearTimeout(wrt.scheduleTimeout);
window.clearTimeout(wrt.updateTimeout);
setUpdateMessage('');
wrt.isScheduled = false;
}
/**
* Start auto-update schedule
* @param seconds
*/
function reschedule(seconds) {
wrt.isScheduled = true;
seconds = seconds || 60;
updateSeconds(seconds);
wrt.scheduleTimeout = window.setTimeout(receiveData, seconds * 1000);
}
/**
* Sets the text of the `#updating` element
* @param msg
*/
function setUpdateMessage(msg) {
document.getElementById('updating').innerHTML = msg;
}
/**
* Updates the 'Updating in X seconds' message
* @param start
*/
function updateSeconds(start) {
setUpdateMessage('倒数 <b><font color="blue">' + start + '</font></b> 秒后刷新.');
if (start > 0) {
wrt.updateTimeout = window.setTimeout(function () {
updateSeconds(start - 1);
}, 1000);
}
}
//----------------------
// END AUTO-UPDATE
//----------------------
/**
* Check for dependency, and if all is well, run callback
* @param cb Callback function
*/
function checkForDependency(cb) {
var ajax = new XMLHttpRequest();
ajax.onreadystatechange = function () {
// noinspection EqualityComparisonWithCoercionJS
if (this.readyState == 4 && this.status == 200) {
// noinspection EqualityComparisonWithCoercionJS
if (ajax.responseText == "1") {
cb();
} else {
alert("wrtbwmon is not installed!");
}
}
};
ajax.open('GET', basePath + '/check_dependency', true);
ajax.send();
}
checkForDependency(function () {
// register events
addEventListeners();
// Main entry point
receiveData();
});
})();

View File

@ -0,0 +1,43 @@
module("luci.controller.wrtbwmon", package.seeall)
function index()
entry({"admin", "nlbw", "usage"}, alias("admin", "nlbw", "usage", "details"), _("Usage"), 60)
entry({"admin", "nlbw", "usage", "details"}, template("wrtbwmon"), _("Details"), 10).leaf=true
entry({"admin", "nlbw", "usage", "config"}, cbi("wrtbwmon/config"), _("Configuration"), 20).leaf=true
entry({"admin", "nlbw", "usage", "custom"}, form("wrtbwmon/custom"), _("User file"), 30).leaf=true
entry({"admin", "nlbw", "usage", "check_dependency"}, call("check_dependency")).dependent=true
entry({"admin", "nlbw", "usage", "usage_data"}, call("usage_data")).dependent=true
entry({"admin", "nlbw", "usage", "usage_reset"}, call("usage_reset")).dependent=true
end
function usage_database_path()
local cursor = luci.model.uci.cursor()
if cursor:get("wrtbwmon", "general", "persist") == "1" then
return "/etc/config/usage.db"
else
return "/tmp/usage.db"
end
end
function check_dependency()
local ret = "0"
if require("luci.model.ipkg").installed('iptables') then
ret = "1"
end
luci.http.prepare_content("text/plain")
luci.http.write(ret)
end
function usage_data()
local db = usage_database_path()
local publish_cmd = "wrtbwmon publish " .. db .. " /tmp/usage.htm /etc/config/wrtbwmon.user"
local cmd = "wrtbwmon update " .. db .. " && " .. publish_cmd .. " && cat /tmp/usage.htm"
luci.http.prepare_content("text/html")
luci.http.write(luci.sys.exec(cmd))
end
function usage_reset()
local db = usage_database_path()
local ret = luci.sys.call("wrtbwmon update " .. db .. " && rm " .. db)
luci.http.status(204)
end

View File

@ -0,0 +1,18 @@
local m = Map("wrtbwmon", "详细设置")
local s = m:section(NamedSection, "general", "wrtbwmon", "通用设置")
local o = s:option(Flag, "persist", "写入数据库到ROM",
"把统计数据写入 /etc/config 中避免重启或者升级后丢失 需要占用ROM空间并降低闪存寿命")
o.rmempty = false
function o.write(self, section, value)
if value == '1' then
luci.sys.call("mv /tmp/usage.db /etc/config/usage.db")
elseif value == '0' then
luci.sys.call("mv /etc/config/usage.db /tmp/usage.db")
end
return Flag.write(self, section ,value)
end
return m

View File

@ -0,0 +1,23 @@
local USER_FILE_PATH = "/etc/config/wrtbwmon.user"
local fs = require "nixio.fs"
local f = SimpleForm("wrtbwmon",
"自定义MAC地址对应的主机名",
"每一行的格式为 00:aa:bb:cc:ee:ff,username (不支持中文主机名)")
local o = f:field(Value, "_custom")
o.template = "cbi/tvalue"
o.rows = 20
function o.cfgvalue(self, section)
return fs.readfile(USER_FILE_PATH)
end
function o.write(self, section, value)
value = value:gsub("\r\n?", "\n")
fs.writefile(USER_FILE_PATH, value)
end
return f

View File

@ -0,0 +1,46 @@
<%+header%>
<h2>客户端实时流量监测 (暂不兼容SFE和FLOW加速)</h2>
<p style="position:relative;">
<button id="resetDatabase" class="cbi-button" style="position:absolute;right:0;bottom:0;">重置统计数据库</button>
</p>
<p>
<small><span id="updated"></span> <span id="updating"></span></small>
<span style="float:right;text-align:right;">
<label>
<small>数据自动刷新间隔</small>
<select id="intervalSelect" style="width:90px;height:20px;font-size:11px;">
<option value="-1">禁用</option>
<option value="1">1 秒</option>
<option value="2">2 秒</option>
<option value="3">3 秒</option>
<option value="4">4 秒</option>
<option value="5" selected="selected">5 秒</option>
<option value="10">10 秒</option>
<option value="20">20 秒</option>
<option value="30">30 秒</option>
<option value="40">40 秒</option>
<option value="50">50 秒</option>
<option value="60">60 秒</option>
<option value="120">2 分钟</option>
<option value="180">3 分钟</option>
<option value="240">4 分钟</option>
<option value="360">5 分钟</option>
</select>
</label>
<br/>
<label>
<small>合并计算MAC地址相同的客户端流量</small>
<input id="perHostTotals" type="checkbox" style="vertical-align:middle;"/>
</label>
<!-- label>
<small>Show per host aggregates only</small>
<input id="showPerHostTotalsOnly" type="checkbox"/>
</label -->
</span>
</p>
<table class="table">
<tbody id="tableBody"><tr><td>Loading...</td></tr></tbody>
</table>
<!--suppress HtmlUnknownTarget -->
<script src="/luci-static/wrtbwmon.js"></script>
<%+footer%>

View File

@ -0,0 +1,30 @@
msgid "Usage"
msgstr "实时流量监测"
msgid "Details"
msgstr "详细信息"
msgid "Configuration"
msgstr "配置"
msgid "User file"
msgstr "自定义主机信息"
msgid "Usage - Configuration"
msgstr "详细设置"
msgid "General settings"
msgstr "通用设置"
msgid "Persist database"
msgstr "写入数据库到硬盘"
msgid "Check this to persist the database file under /etc/config. "
msgstr "把统计数据写入 /etc/config 中避免重启或者升级后丢失 需要占用空间并降低ROM寿命"
msgid "Usage - Custom User File"
msgstr "自定义MAC地址对应的主机名"
msgid "This file is used to match users with MAC addresses and it must have the following format: 00:aa:bb:cc:ee:ff,username"
msgstr "每一行的格式为 00:aa:bb:cc:ee:ff,username (不支持中文主机名)"

View File

@ -0,0 +1,4 @@
config wrtbwmon 'general'
option persist '0'

View File

@ -0,0 +1,22 @@
#!/bin/sh /etc/rc.common
#
# start/stop wrtbwmon bandwidth monitor
### BEGIN INIT INFO
# Provides: wrtbwmon
# Required-Start: $network $local_fs $remote_fs
# Required-Stop: $local_fs $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: iptables-based bandwidth monitor
### END INIT INFO
START=91
start(){
/usr/sbin/wrtbwmon setup /tmp/usage.db
}
stop(){
/usr/sbin/wrtbwmon remove
}

View File

@ -0,0 +1,5 @@
#!/bin/sh
/etc/init.d/wrtbwmon enable
/etc/init.d/wrtbwmon start
exit 0

View File

@ -0,0 +1,157 @@
#!/usr/bin/awk
function inInterfaces(host){
return(interfaces ~ "(^| )"host"($| )")
}
function newRule(arp_ip,
ipt_cmd){
# checking for existing rules shouldn't be necessary if newRule is
# always called after db is read, arp table is read, and existing
# iptables rules are read.
ipt_cmd="iptables -t mangle -j RETURN -s " arp_ip
system(ipt_cmd " -C RRDIPT_FORWARD 2>/dev/null || " ipt_cmd " -A RRDIPT_FORWARD")
ipt_cmd="iptables -t mangle -j RETURN -d " arp_ip
system(ipt_cmd " -C RRDIPT_FORWARD 2>/dev/null || " ipt_cmd " -A RRDIPT_FORWARD")
}
function total(i){
return(bw[i "/in"] + bw[i "/out"])
}
function date( cmd, d){
cmd="date +%d-%m-%Y_%H:%M:%S"
cmd | getline d
close(cmd)
#!@todo could start a process with "while true; do date ...; done"
return(d)
}
BEGIN {
od=""
fid=1
debug=0
rrd=0
}
/^#/ { # get DB filename
FS=","
dbFile=FILENAME
next
}
# data from database; first file
FNR==NR { #!@todo this doesn't help if the DB file is empty.
if($2 == "NA")
#!@todo could get interface IP here
n=$1
else
n=$2
hosts[n] = "" # add this host/interface to hosts
mac[n] = $1
ip[n] = $2
inter[n] = $3
bw[n "/in"] = $4
bw[n "/out"] = $5
firstDate[n] = $7
lastDate[n] = $8
next
}
# not triggered on the first file
FNR==1 {
FS=" "
fid++ #!@todo use fid for all files; may be problematic for empty files
next
}
# arp: ip hw flags hw_addr mask device
fid==2 {
#!@todo regex match IPs and MACs for sanity
arp_ip = $1
arp_flags = $3
arp_mac = $4
arp_dev = $6
if(arp_flags != "0x0" && !(arp_ip in ip)){
if(debug)
print "new host:", arp_ip, arp_flags > "/dev/stderr"
hosts[arp_ip] = ""
mac[arp_ip] = arp_mac
ip[arp_ip] = arp_ip
inter[arp_ip] = arp_dev
bw[arp_ip "/in"] = bw[arp_ip "/out"] = 0
firstDate[arp_ip] = lastDate[arp_ip] = date()
}
next
}
#!@todo could use mangle chain totals or tailing "unnact" rules to
# account for data for new hosts from their first presence on the
# network to rule creation. The "unnact" rules would have to be
# maintained at the end of the list, and new rules would be inserted
# at the top.
# skip line
# read the chain name and deal with the data accordingly
fid==3 && $1 == "Chain"{
rrd=$2 ~ /RRDIPT_.*/
next
}
fid==3 && rrd && (NF < 9 || $1=="pkts"){ next }
fid==3 && rrd { # iptables input
if($6 != "*"){
m=$6
n=m "/out"
} else if($7 != "*"){
m=$7
n=m "/in"
} else if($8 != "0.0.0.0/0"){
m=$8
n=m "/out"
} else { # $9 != "0.0.0.0/0"
m=$9
n=m "/in"
}
# remove host from array; any hosts left in array at END get new
# iptables rules
#!@todo this deletes a host if any rule exists; if only one
# directional rule is removed, this will not remedy the situation
delete hosts[m]
if($2 > 0){ # counted some bytes
if(mode == "diff" || mode == "noUpdate")
print n, $2
if(mode!="noUpdate"){
if(inInterfaces(m)){ # if label is an interface
if(!(m in mac)){ # if label was not in db (also not in
# arp table, but interfaces won't be
# there anyway)
firstDate[m] = date()
mac[m] = inter[m] = m
ip[m] = "NA"
bw[m "/in"]=bw[m "/out"]= 0
}
}
bw[n]+=$2
lastDate[m] = date()
}
}
}
END {
if(mode=="noUpdate") exit
close(dbFile)
system("rm -f " dbFile)
print "#mac,ip,iface,in,out,total,first_date,last_date" > dbFile
OFS=","
for(i in mac)
print mac[i], ip[i], inter[i], bw[i "/in"], bw[i "/out"], total(i), firstDate[i], lastDate[i] > dbFile
close(dbFile)
# for hosts without rules
for(host in hosts) if(!inInterfaces(host)) newRule(host)
}

View File

@ -0,0 +1,301 @@
#!/bin/sh
#
# wrtbwmon: traffic logging tool for routers
#
# Peter Bailey (peter.eldridge.bailey+wrtbwmon AT gmail.com)
#
# Based on work by:
# Emmanuel Brucy (e.brucy AT qut.edu.au)
# Fredrik Erlandsson (erlis AT linux.nu)
# twist - http://wiki.openwrt.org/RrdTrafficWatch
trap "rm -f /tmp/*_$$.tmp; kill $$" INT
binDir=/usr/sbin
dataDir=/usr/share/wrtbwmon
lockDir=/tmp/wrtbwmon.lock
pidFile=$lockDir/pid
networkFuncs=/lib/functions/network.sh
uci=`which uci 2>/dev/null`
nslookup=`which nslookup 2>/dev/null`
nvram=`which nvram 2>/dev/null`
chains='INPUT OUTPUT FORWARD'
DEBUG=
interfaces='eth0 tun0' # in addition to detected WAN
DB=$2
mode=
# DNS server for reverse lookups provided in "DNS".
# don't perform reverse DNS lookups by default
DO_RDNS=${DNS-}
header="#mac,ip,iface,in,out,total,first_date,last_date"
createDbIfMissing()
{
[ ! -f "$DB" ] && echo $header > "$DB"
}
checkDbArg()
{
[ -z "$DB" ] && echo "ERROR: Missing argument 2 (database file)" && exit 1
}
checkDB()
{
[ ! -f "$DB" ] && echo "ERROR: $DB does not exist" && exit 1
[ ! -w "$DB" ] && echo "ERROR: $DB is not writable" && exit 1
}
checkWAN()
{
[ -z "$wan" ] && echo "Warning: failed to detect WAN interface."
}
lookup()
{
MAC=$1
IP=$2
userDB=$3
for USERSFILE in $userDB /tmp/dhcp.leases /tmp/dnsmasq.conf /etc/dnsmasq.conf /etc/hosts; do
[ -e "$USERSFILE" ] || continue
case $USERSFILE in
/tmp/dhcp.leases )
USER=$(grep -i "$MAC" $USERSFILE | cut -f4 -s -d' ')
;;
/etc/hosts )
USER=$(grep "^$IP " $USERSFILE | cut -f2 -s -d' ')
;;
* )
USER=$(grep -i "$MAC" "$USERSFILE" | cut -f2 -s -d,)
;;
esac
[ "$USER" = "*" ] && USER=
[ -n "$USER" ] && break
done
if [ -n "$DO_RDNS" -a -z "$USER" -a "$IP" != "NA" -a -n "$nslookup" ]; then
USER=`$nslookup $IP $DNS | awk '!/server can/{if($4){print $4; exit}}' | sed -re 's/[.]$//'`
fi
[ -z "$USER" ] && USER=${MAC}
echo $USER
}
detectIF()
{
if [ -f "$networkFuncs" ]; then
IF=`. $networkFuncs; network_get_device netdev $1; echo $netdev`
[ -n "$IF" ] && echo $IF && return
fi
if [ -n "$uci" -a -x "$uci" ]; then
IF=`$uci get network.${1}.ifname 2>/dev/null`
[ $? -eq 0 -a -n "$IF" ] && echo $IF && return
fi
if [ -n "$nvram" -a -x "$nvram" ]; then
IF=`$nvram get ${1}_ifname 2>/dev/null`
[ $? -eq 0 -a -n "$IF" ] && echo $IF && return
fi
}
detectLAN()
{
[ -e /sys/class/net/br-lan ] && echo br-lan && return
lan=$(detectIF lan)
[ -n "$lan" ] && echo $lan && return
}
detectWAN()
{
[ -n "$WAN_IF" ] && echo $WAN_IF && return
wan=$(detectIF wan)
[ -n "$wan" ] && echo $wan && return
wan=$(ip route show 2>/dev/null | grep default | sed -re '/^default/ s/default.*dev +([^ ]+).*/\1/')
[ -n "$wan" ] && echo $wan && return
[ -f "$networkFuncs" ] && wan=$(. $networkFuncs; network_find_wan wan; echo $wan)
[ -n "$wan" ] && echo $wan && return
}
lock()
{
attempts=0
while [ $attempts -lt 10 ]; do
mkdir $lockDir 2>/dev/null && break
attempts=$((attempts+1))
pid=`cat $pidFile 2>/dev/null`
if [ -n "$pid" ]; then
if [ -d "/proc/$pid" ]; then
[ -n "$DEBUG" ] && echo "WARNING: Lockfile detected but process $(cat $pidFile) does not exist !"
rm -rf $lockDir
else
sleep 1
fi
fi
done
mkdir $lockDir 2>/dev/null
echo $$ > $pidFile
[ -n "$DEBUG" ] && echo $$ "got lock after $attempts attempts"
trap '' INT
}
unlock()
{
rm -rf $lockDir
[ -n "$DEBUG" ] && echo $$ "released lock"
trap "rm -f /tmp/*_$$.tmp; kill $$" INT
}
# chain
newChain()
{
chain=$1
# Create the RRDIPT_$chain chain (it doesn't matter if it already exists).
iptables -t mangle -N RRDIPT_$chain 2> /dev/null
# Add the RRDIPT_$chain CHAIN to the $chain chain if not present
iptables -t mangle -C $chain -j RRDIPT_$chain 2>/dev/null
if [ $? -ne 0 ]; then
[ -n "$DEBUG" ] && echo "DEBUG: iptables chain misplaced, recreating it..."
iptables -t mangle -I $chain -j RRDIPT_$chain
fi
}
# chain tun
newRuleIF()
{
chain=$1
IF=$2
#!@todo test
if [ "$chain" = "OUTPUT" ]; then
cmd="iptables -t mangle -o $IF -j RETURN"
eval $cmd " -C RRDIPT_$chain 2>/dev/null" || eval $cmd " -A RRDIPT_$chain"
elif [ "$chain" = "INPUT" ]; then
cmd="iptables -t mangle -i $IF -j RETURN"
eval $cmd " -C RRDIPT_$chain 2>/dev/null" || eval $cmd " -A RRDIPT_$chain"
fi
}
update()
{
#!@todo could let readDB.awk handle this; that would place header
#!info in fewer places
createDbIfMissing
checkDB
checkWAN
> /tmp/iptables_$$.tmp
lock
# only zero our own chains
for chain in $chains; do
iptables -nvxL RRDIPT_$chain -t mangle -Z >> /tmp/iptables_$$.tmp
done
# the iptables and readDB commands have to be separate. Otherwise,
# they will fight over iptables locks
awk -v mode="$mode" -v interfaces=\""$interfaces"\" -f $binDir/readDB.awk \
$DB \
/proc/net/arp \
/tmp/iptables_$$.tmp
unlock
}
############################################################
case $1 in
"dump" )
checkDbArg
lock
tr ',' '\t' < "$DB"
unlock
;;
"update" )
checkDbArg
wan=$(detectWAN)
interfaces="$interfaces $wan"
update
rm -f /tmp/*_$$.tmp
exit
;;
"publish" )
checkDbArg
[ -z "$3" ] && echo "ERROR: Missing argument 3 (output html file)" && exit 1
# sort DB
lock
# busybox sort truncates numbers to 32 bits
grep -v '^#' $DB | awk -F, '{OFS=","; a=sprintf("%f",$4/1e6); $4=""; print a,$0}' | tr -s ',' | sort -rn | awk -F, '{OFS=",";$1=sprintf("%f",$1*1e6);print}' > /tmp/sorted_$$.tmp
# create HTML page
rm -f $3.tmp
cp $dataDir/usage.htm1 $3.tmp
#!@todo fix publishing
while IFS=, read PEAKUSAGE_IN MAC IP IFACE PEAKUSAGE_OUT TOTAL FIRSTSEEN LASTSEEN
do
echo "
new Array(\"$(lookup $MAC $IP $4)\",\"$MAC\",\"$IP\",
$PEAKUSAGE_IN,$PEAKUSAGE_OUT,$TOTAL,\"$FIRSTSEEN\",\"$LASTSEEN\")," >> $3.tmp
done < /tmp/sorted_$$.tmp
echo "0);" >> $3.tmp
sed "s/(date)/`date`/" < $dataDir/usage.htm2 >> $3.tmp
mv $3.tmp $3
unlock
#Free some memory
rm -f /tmp/*_$$.tmp
;;
"setup" )
checkDbArg
[ -w "$DB" ] && echo "Warning: using existing $DB"
createDbIfMissing
for chain in $chains; do
newChain $chain
done
#lan=$(detectLAN)
wan=$(detectWAN)
checkWAN
interfaces="$interfaces $wan"
# track local data
for chain in INPUT OUTPUT; do
for interface in $interfaces; do
[ -n "$interface" ] && [ -e "/sys/class/net/$interface" ] && newRuleIF $chain $interface
done
done
# this will add rules for hosts in arp table
update
rm -f /tmp/*_$$.tmp
;;
"remove" )
iptables-save | grep -v RRDIPT | iptables-restore
rm -rf "$lockDir"
;;
*)
echo \
"Usage: $0 {setup|update|publish|remove} [options...]
Options:
$0 setup database_file
$0 update database_file
$0 publish database_file path_of_html_report [user_file]
Examples:
$0 setup /tmp/usage.db
$0 update /tmp/usage.db
$0 publish /tmp/usage.db /www/user/usage.htm /jffs/users.txt
$0 remove
Note: [user_file] is an optional file to match users with MAC addresses.
Its format is \"00:MA:CA:DD:RE:SS,username\", with one entry per line."
;;
esac

View File

@ -0,0 +1,23 @@
<html><head><title>Traffic</title>
<script type="text/javascript">
function getSize(size) {
var prefix=new Array("","k","M","G","T","P","E","Z"); var base=1000;
var pos=0;
while (size>base) {
size/=base; pos++;
}
if (pos > 2) precision=1000; else precision = 1;
return (Math.round(size*precision)/precision)+' '+prefix[pos];}
</script></head>
<body><h1>Total Usage:</h1>
<table border="1">
<tr bgcolor=silver>
<th>User</th>
<th>Download</th>
<th>Upload</th>
<th>Total</th>
<th>First seen</th>
<th>Last seen</th>
</tr>
<script type="text/javascript">
var values = new Array(

View File

@ -0,0 +1,14 @@
var totalIn = 0;
var totalOut = 0;
for (i=0; i < values.length-1; i++) {
totalIn += values[i][3];
totalOut += values[i][4];
document.write("<tr><td><div title=\"" + values[i][1] + " (" + values[i][2] + ")" + "\">" + values[i][0] + "</div></td>");
for (j=3; j < 6; j++)
document.write("<td>" + getSize(values[i][j]) + "</td>");
document.write("<td>" + values[i][6] + "</td><td>" + values[i][7] + "</td></tr>");
}
document.write("<tr><td>TOTAL</td><td>" + getSize(totalIn) + "</td><td>" + getSize(totalOut) + "</td><td>" + getSize(totalIn + totalOut) + "</td><td></td><td></td></tr>");
</script></table>
<br /><small>This page was generated on (date)</small>
</body></html>

View File

@ -23,8 +23,7 @@ DEFAULT_PACKAGES += partx-utils mkf2fs fdisk e2fsprogs wpad kmod-usb-hid \
kmod-ath5k kmod-ath9k kmod-ath9k-htc kmod-ath10k kmod-rt2800-usb kmod-e1000e kmod-igb kmod-igbvf kmod-ixgbe kmod-pcnet32 kmod-tulip kmod-vmxnet3 htop lm-sensors autocore automount autosamba \
ath10k-firmware-qca988x ath10k-firmware-qca9888 ath10k-firmware-qca9984 brcmfmac-firmware-43602a1-pcie intel-microcode amd64-microcode\
alsa-utils kmod-ac97 kmod-sound-hda-core kmod-sound-hda-codec-realtek kmod-sound-hda-codec-via kmod-sound-via82xx kmod-usb-audio \
kmod-usb-net kmod-usb-net-asix kmod-usb-net-asix-ax88179 kmod-usb-net-rtl8150 kmod-usb-net-rtl8152 \
luci-app-zerotier luci-app-v2ray-pro
kmod-usb-net kmod-usb-net-asix kmod-usb-net-asix-ax88179 kmod-usb-net-rtl8150 kmod-usb-net-rtl8152
$(eval $(call BuildTarget))