Paste-in import: one CIDR/IP per line, optional comment after '#', blank/comment-only lines ignored - matches the format used by common public blocklist feeds (Spamhaus DROP, blocklist.de, etc.) so those can mostly be pasted in directly. Whole batch gets one list_type (allow or block). Reuses the existing single-entry validation, skips duplicates (by list_type+CIDR, including within the same paste), caps at 5000 lines, and reports imported/skipped/invalid counts plus per-line errors. New route: POST /firewall-lists/entries/import (admin-only). UI: a collapsible "Bulk import" section on the Global Firewall Lists page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
240 lines
10 KiB
HTML
240 lines
10 KiB
HTML
{{define "title"}}
|
|
Global Firewall Lists
|
|
{{end}}
|
|
|
|
{{define "top_css"}}
|
|
{{end}}
|
|
|
|
{{define "username"}}
|
|
{{.username}}
|
|
{{end}}
|
|
|
|
{{define "page_title"}}
|
|
Global Firewall Lists
|
|
{{end}}
|
|
|
|
{{define "page_content"}}
|
|
<section class="content">
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="card card-warning">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Host-wide Allow / Block Lists</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="text-muted">
|
|
These entries are NOT scoped to a single WireGuard server - they apply to
|
|
<strong>all traffic on this host</strong>, evaluated before every per-server
|
|
firewall table (nftables priority -10). Allow entries always win over block entries.
|
|
Nothing is applied until you press "Apply now (live)".
|
|
</p>
|
|
|
|
<table class="table table-sm" id="_iplist_table">
|
|
<thead>
|
|
<tr><th>Type</th><th>CIDR / IP</th><th>Comment</th><th></th></tr>
|
|
</thead>
|
|
<tbody id="_iplist_tbody"></tbody>
|
|
</table>
|
|
|
|
<form id="frm_iplist_entry" class="form-inline">
|
|
<select class="form-control form-control-sm mr-1 mb-1" id="_iplist_type">
|
|
<option value="block">block</option>
|
|
<option value="allow">allow</option>
|
|
</select>
|
|
<input type="text" class="form-control form-control-sm mr-1 mb-1" id="_iplist_cidr" placeholder="e.g. 203.0.113.0/24" style="width:14em">
|
|
<input type="text" class="form-control form-control-sm mr-1 mb-1" id="_iplist_comment" placeholder="comment" style="width:14em">
|
|
<button type="submit" class="btn btn-primary btn-sm mb-1">Add entry</button>
|
|
</form>
|
|
|
|
<hr>
|
|
<p><a data-toggle="collapse" href="#collapse_bulk_import" role="button" aria-expanded="false" aria-controls="collapse_bulk_import">Bulk import ▼</a></p>
|
|
<div class="collapse" id="collapse_bulk_import">
|
|
<div class="card card-body">
|
|
<form id="frm_iplist_import">
|
|
<div class="form-group">
|
|
<label for="_iplist_import_type">List type for this batch</label>
|
|
<select class="form-control form-control-sm" id="_iplist_import_type" style="width:10em">
|
|
<option value="block">block</option>
|
|
<option value="allow">allow</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="_iplist_import_text">Entries (one CIDR/IP per line)</label>
|
|
<textarea class="form-control" id="_iplist_import_text" rows="10"
|
|
placeholder="One CIDR or IP per line. Optional comment after '#'. Blank lines and lines starting with '#' are ignored. 203.0.113.0/24 198.51.100.5 # known scanner # this whole line is a comment"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary btn-sm">Import</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
<p class="text-muted mb-1">Ruleset preview:</p>
|
|
<pre id="_iplist_preview_text" style="max-height: 30vh; overflow:auto;"></pre>
|
|
|
|
<button type="button" class="btn btn-danger" id="btn_apply_global_firewall">Apply now (live)</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{define "bottom_js"}}
|
|
<script>
|
|
function refreshGlobalPreview() {
|
|
$("#_iplist_preview_text").text("Loading...");
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: '{{.basePath}}/firewall-lists/preview',
|
|
dataType: 'text',
|
|
success: function (data) { $("#_iplist_preview_text").text(data); },
|
|
error: function () { $("#_iplist_preview_text").text("Could not load preview."); }
|
|
});
|
|
}
|
|
|
|
function renderIPList(entries) {
|
|
const tbody = $("#_iplist_tbody");
|
|
tbody.empty();
|
|
$.each(entries, function (i, entry) {
|
|
const safeCidr = $('<div>').text(entry.cidr).html();
|
|
const safeComment = $('<div>').text(entry.comment || "").html();
|
|
const row = `<tr>
|
|
<td>${entry.list_type}</td>
|
|
<td>${safeCidr}</td>
|
|
<td>${safeComment}</td>
|
|
<td><button type="button" class="btn btn-outline-danger btn-sm btn-delete-iplist" data-id="${entry.id}">Delete</button></td>
|
|
</tr>`;
|
|
tbody.append(row);
|
|
});
|
|
}
|
|
|
|
function loadIPList() {
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'GET',
|
|
url: '{{.basePath}}/firewall-lists/entries',
|
|
dataType: 'json',
|
|
success: function (entries) { renderIPList(entries); },
|
|
error: function (jqXHR) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
loadIPList();
|
|
refreshGlobalPreview();
|
|
|
|
$("#frm_iplist_entry").on('submit', function (e) {
|
|
e.preventDefault();
|
|
const data = {
|
|
list_type: $("#_iplist_type").val(),
|
|
cidr: $("#_iplist_cidr").val(),
|
|
comment: $("#_iplist_comment").val()
|
|
};
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/firewall-lists/entries',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
data: JSON.stringify(data),
|
|
success: function () {
|
|
toastr.success("Entry added");
|
|
$("#frm_iplist_entry")[0].reset();
|
|
loadIPList();
|
|
refreshGlobalPreview();
|
|
},
|
|
error: function (jqXHR) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
});
|
|
|
|
$("#frm_iplist_import").on('submit', function (e) {
|
|
e.preventDefault();
|
|
const data = {
|
|
list_type: $("#_iplist_import_type").val(),
|
|
text: $("#_iplist_import_text").val()
|
|
};
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/firewall-lists/entries/import',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
data: JSON.stringify(data),
|
|
success: function (result) {
|
|
let summary = "Imported " + result.imported + ", skipped " + result.skipped_duplicates + " duplicates";
|
|
if (result.invalid_count > 0) {
|
|
summary += ", " + result.invalid_count + " invalid lines";
|
|
console.warn("Bulk import invalid lines:", result.invalid_lines);
|
|
toastr.warning(summary);
|
|
} else {
|
|
toastr.success(summary);
|
|
}
|
|
$("#_iplist_import_text").val("");
|
|
loadIPList();
|
|
refreshGlobalPreview();
|
|
},
|
|
error: function (jqXHR) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message'] || "Import failed");
|
|
}
|
|
});
|
|
});
|
|
|
|
$("#_iplist_tbody").on('click', '.btn-delete-iplist', function () {
|
|
const id = $(this).data('id');
|
|
if (!confirm("Delete this entry?")) return;
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/firewall-lists/entries/' + id + '/delete',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function () {
|
|
toastr.success("Entry deleted");
|
|
loadIPList();
|
|
refreshGlobalPreview();
|
|
},
|
|
error: function (jqXHR) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message']);
|
|
}
|
|
});
|
|
});
|
|
|
|
$("#btn_apply_global_firewall").click(function () {
|
|
if (!confirm("Apply the global allow/block lists to the live firewall now?\n" +
|
|
"This runs 'nft -f' on the server, scoped to the dedicated wireguard_ui_global table only, " +
|
|
"but it affects ALL traffic on this host, not just WireGuard.")) {
|
|
return;
|
|
}
|
|
$.ajax({
|
|
cache: false,
|
|
method: 'POST',
|
|
url: '{{.basePath}}/firewall-lists/apply',
|
|
dataType: 'json',
|
|
contentType: "application/json",
|
|
success: function (data) {
|
|
toastr.success(data.message);
|
|
if (data.output) { $("#_iplist_preview_text").text(data.output); }
|
|
},
|
|
error: function (jqXHR) {
|
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
|
toastr.error(responseJson['message'] || "Failed to apply");
|
|
if (responseJson['output']) { $("#_iplist_preview_text").text(responseJson['output']); }
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{{end}}
|