Merge pull request #1285 from pi-hole/new/mac_clients

Allow defining clients by their MAC address
This commit is contained in:
DL6ER
2020-08-25 20:19:48 +02:00
committed by GitHub
12 changed files with 192 additions and 48 deletions
+17 -8
View File
@@ -62,18 +62,27 @@ if(isset($_GET["network"]) && $auth)
while($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC))
{
$id = $res["id"];
// Empty array for holding the IP addresses
$id = intval($res["id"]);
// Get IP addresses and host names for this device
$res["ip"] = array();
// Get IP addresses for this device
$network_addresses = $db->query("SELECT ip FROM network_addresses WHERE network_id = $id ORDER BY lastSeen DESC");
while($network_addresses !== false && $ip = $network_addresses->fetchArray(SQLITE3_ASSOC))
array_push($res["ip"],$ip["ip"]);
// UTF-8 encode host name and vendor
$res["name"] = utf8_encode($res["name"]);
$res["name"] = array();
$network_addresses = $db->query("SELECT ip,name FROM network_addresses WHERE network_id = $id ORDER BY lastSeen DESC");
while($network_addresses !== false && $network_address = $network_addresses->fetchArray(SQLITE3_ASSOC))
{
array_push($res["ip"],$network_address["ip"]);
if($network_address["name"] !== null)
array_push($res["name"],utf8_encode($network_address["name"]));
else
array_push($res["name"],"");
}
$network_addresses->finalize();
// UTF-8 encode vendor
$res["macVendor"] = utf8_encode($res["macVendor"]);
array_push($network, $res);
}
$results->finalize();
$data = array_merge($data, array('network' => $network));
}
+14 -3
View File
@@ -30,14 +30,25 @@
<label for="select">Known clients:</label>
<select id="select" class="form-control" placeholder="">
<option disabled selected>Loading...</option>
</select><br>
<input id="ip-custom" type="text" class="form-control" disabled placeholder="Client IP address (IPv4 or IPv6, CIDR subnetting available, optional)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</select>
</div>
<div class="form-group col-md-6">
<label for="new_comment">Comment:</label>
<input id="new_comment" type="text" class="form-control" placeholder="Client description (optional)">
</div>
</div>
<div class="row">
<div class="col-md-12">
<p>You can select an existing client or add a custom one by typing into the field above and confirming your entry with <kbd>&#x23CE;</kbd>.</p>
<p>Clients may be described either by their IP addresses (IPv4 and IPv6 are supported),
IP subnets (CIDR notation, like <code>192.168.2.0/24</code>),
their MAC addresses (like <code>12:34:56:78:9A:BC</code>),
by their hostnames (like <code>localhost</code>), or by the interface they are connected to (prefaced with a colon, like <code>:eth0</code>).</p>
<p>Note that client recognition by IP addresses (incl. subnet ranges) are prefered over MAC address, host name or interface recognition as
the two latter will only be available after some time.
Furthermore, MAC address recognition only works for devices at most one networking hop away from your Pi-hole.</p>
</div>
</div>
</div>
<div class="box-footer clearfix">
<button type="button" id="btnAdd" class="btn btn-primary pull-right">Add</button>
@@ -59,7 +70,7 @@
<thead>
<tr>
<th>ID</th>
<th>IP address</th>
<th title="Acceptable values are: IP address, subnet (CIDR notation), MAC address (AA:BB:CC:DD:EE:FF format) or host names.">Client</th>
<th>Comment</th>
<th>Group assignment</th>
<th>Action</th>
+42 -27
View File
@@ -17,28 +17,35 @@ function reloadClientSuggestions() {
{ action: "get_unconfigured_clients", token: token },
function (data) {
var sel = $("#select");
var customWasSelected = sel.val() === "custom";
sel.empty();
// In order for the placeholder value to appear, we have to have a blank
// <option> as the first option in our <select> control. This is because
// the browser tries to select the first option by default. If our first
// option were non-empty, the browser would display this instead of the
// placeholder.
sel.append($("<option />"));
// Add data obtained from API
for (var key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
var text = key;
var keyPlain = key;
if (key.startsWith("IP-")) {
// Mock MAC address for address-only devices
keyPlain = key.substring(3);
text = keyPlain;
}
// Append host name if available
if (data[key].length > 0) {
text += " (" + data[key] + ")";
}
sel.append($("<option />").val(key).text(text));
}
if (data.length === 0) {
$("#ip-custom").prop("disabled", false);
}
sel.append($("<option />").val("custom").text("Custom, specified below..."));
if (customWasSelected) {
sel.val("custom");
sel.append($("<option />").val(keyPlain).text(text));
}
},
"json"
@@ -59,6 +66,11 @@ function getGroups() {
$(function () {
$("#btnAdd").on("click", addClient);
$("select").select2({
tags: true,
placeholder: "Select client...",
allowClear: true
});
reloadClientSuggestions();
utils.setBsSelectDefaults();
@@ -221,6 +233,7 @@ function initTable() {
return data;
}
});
// Disable autocorrect in the search box
var input = document.querySelector("input[type=search]");
if (input !== null) {
@@ -238,6 +251,7 @@ function initTable() {
$("#resetButton").addClass("hidden");
}
});
$("#resetButton").on("click", function () {
table.order([[0, "asc"]]).draw();
$("#resetButton").addClass("hidden");
@@ -245,33 +259,34 @@ function initTable() {
}
function addClient() {
var ip = $("#select").val();
var ip = $("#select").val().trim();
var comment = utils.escapeHtml($("#new_comment").val());
if (ip === "custom") {
ip = utils.escapeHtml($("#ip-custom").val().trim());
}
utils.disableAll();
utils.showAlert("info", "", "Adding client...", ip);
if (ip.length === 0) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Please specify a client IP address");
utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address");
return;
}
// Validate IP address (may contain CIDR details)
var ipv6format = ip.includes(":");
if (!ipv6format && !utils.validateIPv4CIDR(ip)) {
// Validate input, can be:
// - IPv4 address (with and without CIDR)
// - IPv6 address (with and without CIDR)
// - MAC address (in the form AA:BB:CC:DD:EE:FF)
// - host name (arbitrary form, we're only checking against some reserved charaters)
if (utils.validateIPv4CIDR(ip) || utils.validateIPv6CIDR(ip) || utils.validateMAC(ip)) {
// Convert input to upper case (important for MAC addresses)
ip = ip.toUpperCase();
} else if (!utils.validateHostname(ip)) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Invalid IPv4 address!");
return;
}
if (ipv6format && !utils.validateIPv6CIDR(ip)) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Invalid IPv6 address!");
utils.showAlert(
"warning",
"",
"Warning",
"Input is neither a valid IP or MAC address nor a valid host name!"
);
return;
}
+8 -3
View File
@@ -5,12 +5,17 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
// This code has been adapted from
// This code has been taken from
// https://datatables.net/plug-ins/sorting/ip-address
// and was modified by the Pi-hole team to support
// CIDR notation and be more robust against invalid
// input data (like empty IP addresses)
$.extend($.fn.dataTableExt.oSort, {
"ip-address-pre": function (a) {
if (!a) {
return 0;
// Skip empty fields (IP address might have expired or
// reassigned to a differenct device)
if (!a || a.length === 0) {
return Infinity;
}
var i, item;
+37 -2
View File
@@ -69,6 +69,8 @@ $(function () {
tableApi = $("#network-entries").DataTable({
rowCallback: function (row, data) {
var color;
var index;
var maxiter;
var iconClasses;
var lastQuery = parseInt(data.lastQuery, 10);
var diff = getTimestamp() - lastQuery;
@@ -109,14 +111,47 @@ $(function () {
// Set hostname to "unknown" if not available
if (!data.name || data.name.length === 0) {
$("td:eq(3)", row).html("<em>unknown</em>");
} else {
var names = [];
var name = "";
maxiter = Math.min(data.name.length, MAXIPDISPLAY);
index = 0;
for (index = 0; index < maxiter; index++) {
name = data.name[index];
if (name.length === 0) continue;
names.push('<a href="queries.php?client=' + name + '">' + name + "</a>");
}
if (data.name.length > MAXIPDISPLAY) {
// We hit the maximum above, add "..." to symbolize we would
// have more to show here
names.push("...");
}
maxiter = Math.min(data.ip.length, data.name.length);
var allnames = [];
for (index = 0; index < maxiter; index++) {
name = data.name[index];
if (name.length > 0) {
allnames.push(name + " (" + data.ip[index] + ")");
} else {
allnames.push("No host name for " + data.ip[index] + " known");
}
}
$("td:eq(3)", row).html(names.join("<br>"));
$("td:eq(3)", row).hover(function () {
this.title = allnames.join("\n");
});
}
// Set number of queries to localized string (add thousand separators)
$("td:eq(6)", row).html(data.numQueries.toLocaleString());
var ips = [];
var maxiter = Math.min(data.ip.length, MAXIPDISPLAY);
for (var index = 0; index < maxiter; index++) {
maxiter = Math.min(data.ip.length, MAXIPDISPLAY);
index = 0;
for (index = 0; index < maxiter; index++) {
var ip = data.ip[index];
ips.push('<a href="queries.php?client=' + ip + '">' + ip + "</a>");
}
+13 -1
View File
@@ -167,6 +167,16 @@ function validateIPv6CIDR(ip) {
return ipv6validator.test(ip);
}
function validateMAC(mac) {
var macvalidator = new RegExp(/^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/);
return macvalidator.test(mac);
}
function validateHostname(name) {
var namevalidator = new RegExp(/[^<>;"]/);
return namevalidator.test(name);
}
// set bootstrap-select defaults
function setBsSelectDefaults() {
var bsSelectDefaults = $.fn.selectpicker.Constructor.DEFAULTS;
@@ -234,6 +244,8 @@ window.utils = (function () {
setBsSelectDefaults: setBsSelectDefaults,
stateSaveCallback: stateSaveCallback,
stateLoadCallback: stateLoadCallback,
getGraphType: getGraphType
getGraphType: getGraphType,
validateMAC: validateMAC,
validateHostname: validateHostname
};
})();
+47 -3
View File
@@ -184,7 +184,7 @@ if ($_POST['action'] == 'get_groups') {
throw new Exception('Error while querying gravity\'s client_by_group table: ' . $db->lastErrorMsg());
}
$stmt = $FTLdb->prepare('SELECT name FROM network WHERE id = (SELECT network_id FROM network_addresses WHERE ip = :ip);');
$stmt = $FTLdb->prepare('SELECT name FROM network_addresses WHERE ip = :ip;');
if (!$stmt) {
throw new Exception('Error while preparing network table statement: ' . $db->lastErrorMsg());
}
@@ -206,6 +206,7 @@ if ($_POST['action'] == 'get_groups') {
while ($gres = $group_query->fetchArray(SQLITE3_ASSOC)) {
array_push($groups, $gres['group_id']);
}
$group_query->finalize();
$res['groups'] = $groups;
array_push($data, $res);
}
@@ -221,7 +222,7 @@ if ($_POST['action'] == 'get_groups') {
$QUERYDB = getQueriesDBFilename();
$FTLdb = SQLite3_connect($QUERYDB);
$query = $FTLdb->query('SELECT DISTINCT ip,network.name FROM network_addresses AS name LEFT JOIN network ON network.id = network_id ORDER BY ip ASC;');
$query = $FTLdb->query('SELECT DISTINCT id,hwaddr,macVendor FROM network ORDER BY firstSeen DESC;');
if (!$query) {
throw new Exception('Error while querying FTL\'s database: ' . $db->lastErrorMsg());
}
@@ -229,7 +230,47 @@ if ($_POST['action'] == 'get_groups') {
// Loop over results
$ips = array();
while ($res = $query->fetchArray(SQLITE3_ASSOC)) {
$ips[$res['ip']] = $res['name'] !== null ? $res['name'] : '';
$id = intval($res["id"]);
// Get possibly associated IP addresses and hostnames for this client
$query_ips = $FTLdb->query("SELECT ip,name FROM network_addresses WHERE network_id = $id ORDER BY lastSeen DESC;");
$addresses = [];
$names = [];
while ($res_ips = $query_ips->fetchArray(SQLITE3_ASSOC)) {
array_push($addresses, utf8_encode($res_ips["ip"]));
if($res_ips["name"] !== null)
array_push($names,utf8_encode($res_ips["name"]));
}
$query_ips->finalize();
// Prepare extra information
$extrainfo = "";
// Add list of associated host names to info string (if available)
if(count($names) === 1)
$extrainfo .= "hostname: ".$names[0];
else if(count($names) > 0)
$extrainfo .= "hostnames: ".implode(", ", $names);
// Add device vendor to info string (if available)
if (strlen($res["macVendor"]) > 0) {
if (count($names) > 0)
$extrainfo .= "; ";
$extrainfo .= "vendor: ".htmlspecialchars($res["macVendor"]);
}
// Add list of associated host names to info string (if available and if this is not a mock device)
if (stripos($res["hwaddr"], "ip-") === FALSE) {
if ((count($names) > 0 || strlen($res["macVendor"]) > 0) && count($addresses) > 0)
$extrainfo .= "; ";
if(count($addresses) === 1)
$extrainfo .= "address: ".$addresses[0];
else if(count($addresses) > 0)
$extrainfo .= "addresses: ".implode(", ", $addresses);
}
$ips[strtoupper($res['hwaddr'])] = $extrainfo;
}
$FTLdb->close();
@@ -243,6 +284,9 @@ if ($_POST['action'] == 'get_groups') {
if (isset($ips[$res['ip']])) {
unset($ips[$res['ip']]);
}
if (isset($ips["IP-".$res['ip']])) {
unset($ips["IP-".$res['ip']]);
}
}
header('Content-type: application/json');
+2
View File
@@ -191,6 +191,7 @@
<link rel="stylesheet" href="style/vendor/datatables.min.css">
<link rel="stylesheet" href="style/vendor/daterangepicker.min.css">
<link rel="stylesheet" href="style/vendor/AdminLTE.min.css">
<link rel="stylesheet" href="style/vendor/select2.min.css">
<?php if (in_array($scriptname, array("groups.php", "groups-adlists.php", "groups-clients.php", "groups-domains.php"))){ ?>
<link rel="stylesheet" href="style/vendor/animate.min.css">
@@ -205,6 +206,7 @@
<script src="style/vendor/bootstrap/js/bootstrap.min.js"></script>
<script src="scripts/vendor/adminlte.min.js"></script>
<script src="scripts/vendor/bootstrap-notify.min.js"></script>
<script src="scripts/vendor/select2.min.js"></script>
<script src="scripts/vendor/datatables.min.js"></script>
<script src="scripts/vendor/moment.min.js"></script>
<script src="scripts/vendor/Chart.min.js"></script>
+5 -1
View File
@@ -738,7 +738,11 @@ function addStaticDHCPLease($mac, $ip, $hostname) {
// Flush network table
case "flusharp":
$output = pihole_execute("arpflush quiet");
$error = implode("<br>", $output);
$error = "";
if(is_array($output))
{
$error = implode("<br>", $output);
}
if(strlen($error) == 0)
{
$success .= "The network table has been flushed";
+2
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -300,3 +300,7 @@
width: 100%;
vertical-align: middle;
}
.select2-container--default .select2-results > .select2-results__options {
max-height: 400px;
}
+1
View File
File diff suppressed because one or more lines are too long