diff --git a/package/lean/luci-app-wrtbwmon/Makefile b/package/lean/luci-app-wrtbwmon/Makefile
new file mode 100644
index 0000000000..dbf91c76d0
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/Makefile
@@ -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:=1
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
+
diff --git a/package/lean/luci-app-wrtbwmon/htdocs/luci-static/wrtbwmon.js b/package/lean/luci-app-wrtbwmon/htdocs/luci-static/wrtbwmon.js
new file mode 100644
index 0000000000..36812e723b
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/htdocs/luci-static/wrtbwmon.js
@@ -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 = '
';
+ res += content;
+ res += '
';
+
+ return res;
+ }
+
+ /**
+ * Create a `th` element with content and options
+ * @param content
+ * @param opts
+ * @returns {string}
+ */
+ function createTH(content, opts) {
+ opts = opts || {};
+ var res = '';
+ res += content;
+ res += ' | ';
+ return res;
+ }
+
+ /**
+ * Create a `td` element with content and options
+ * @param content
+ * @param opts
+ * @returns {string}
+ */
+ function createTD(content, opts) {
+ opts = opts || {};
+ var res = '';
+ res += content;
+ res += ' | ';
+ 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' ? '▼' : '▲');
+ }
+
+ // 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] + '
' + 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] + '
(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('倒数 ' + start + ' 秒后刷新.');
+ 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();
+ });
+
+})();
diff --git a/package/lean/luci-app-wrtbwmon/luasrc/controller/wrtbwmon.lua b/package/lean/luci-app-wrtbwmon/luasrc/controller/wrtbwmon.lua
new file mode 100644
index 0000000000..4211100b5f
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/luasrc/controller/wrtbwmon.lua
@@ -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"}, cbi("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/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
diff --git a/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua
new file mode 100644
index 0000000000..10edc41cf8
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/config.lua
@@ -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
diff --git a/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua
new file mode 100644
index 0000000000..cd882d4c88
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/luasrc/model/cbi/wrtbwmon/custom.lua
@@ -0,0 +1,23 @@
+local USER_FILE_PATH = "/etc/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
diff --git a/package/lean/luci-app-wrtbwmon/luasrc/view/wrtbwmon.htm b/package/lean/luci-app-wrtbwmon/luasrc/view/wrtbwmon.htm
new file mode 100644
index 0000000000..c1bf439524
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/luasrc/view/wrtbwmon.htm
@@ -0,0 +1,46 @@
+<%+header%>
+客户端实时流量监测
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<%+footer%>
diff --git a/package/lean/luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po b/package/lean/luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po
new file mode 100644
index 0000000000..47e8ea29d9
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/po/zh-cn/wrtbwmon.po
@@ -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 (不支持中文主机名)"
+
diff --git a/package/lean/luci-app-wrtbwmon/root/etc/config/wrtbwmon b/package/lean/luci-app-wrtbwmon/root/etc/config/wrtbwmon
new file mode 100644
index 0000000000..419270dbab
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/root/etc/config/wrtbwmon
@@ -0,0 +1,4 @@
+
+config wrtbwmon 'general'
+ option persist '0'
+
diff --git a/package/lean/luci-app-wrtbwmon/root/etc/init.d/wrtbwmon b/package/lean/luci-app-wrtbwmon/root/etc/init.d/wrtbwmon
new file mode 100755
index 0000000000..d57c09ae1d
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/root/etc/init.d/wrtbwmon
@@ -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
+}
diff --git a/package/lean/luci-app-wrtbwmon/root/etc/uci-defaults/luci-wrtbwmon b/package/lean/luci-app-wrtbwmon/root/etc/uci-defaults/luci-wrtbwmon
new file mode 100755
index 0000000000..c86001a6f5
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/root/etc/uci-defaults/luci-wrtbwmon
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+/etc/init.d/wrtbwmon enable
+/etc/init.d/wrtbwmon start
+exit 0
diff --git a/package/lean/luci-app-wrtbwmon/root/usr/sbin/readDB.awk b/package/lean/luci-app-wrtbwmon/root/usr/sbin/readDB.awk
new file mode 100755
index 0000000000..fe67e4ae8b
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/root/usr/sbin/readDB.awk
@@ -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)
+}
diff --git a/package/lean/luci-app-wrtbwmon/root/usr/sbin/wrtbwmon b/package/lean/luci-app-wrtbwmon/root/usr/sbin/wrtbwmon
new file mode 100755
index 0000000000..b2c0b9a07a
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/root/usr/sbin/wrtbwmon
@@ -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
diff --git a/package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm1 b/package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm1
new file mode 100644
index 0000000000..1f0c342a7c
--- /dev/null
+++ b/package/lean/luci-app-wrtbwmon/root/usr/share/wrtbwmon/usage.htm1
@@ -0,0 +1,23 @@
+Traffic
+
+Total Usage:
+
+
+| User |
+Download |
+Upload |
+Total |
+First seen |
+Last seen |
+
+
+
This page was generated on (date)
+