From c1d3c5be0431af9ced839f2e33acaf864e48e15b Mon Sep 17 00:00:00 2001 From: Yang Min Sheng Date: Sun, 13 Sep 2026 04:45:08 +0800 Subject: [PATCH] luci-app-geoguard: add GeoGuard country allow-list IPS builder GeoGuard builds firewall4 ipset sets from IP-geolocation feeds for selected countries plus a custom whitelist, with a DDNS guard exemption list and a LuCI/SSH login guard. - 217 countries, dual feeds with auto-failover, daily/weekly/monthly refresh - Per-helper rpcd ACL entries; LUCI_DEPENDS on luci-base, firewall4, wget-ssl and jsonfilter - Guard and schedule generated on first UI save (fresh install is inert); English source strings with source references in the .pot template - State in package-owned /etc/geoguard (upgrade migration, conffiles, sysupgrade backup), single-flight lock on tmpfs, reboot-safe ban persistence, atomic live refresh, missing-state restore on upgrade, EN-only search placeholder, no double _(), ASCII punctuation, English comments throughout Signed-off-by: Yang Min Sheng --- applications/luci-app-geoguard/LICENSE | 21 + applications/luci-app-geoguard/Makefile | 24 + .../luci-static/resources/view/geoguard.js | 860 ++++++++++++ .../po/templates/geoguard.pot | 1208 +++++++++++++++++ .../root/etc/config/geoguard | 28 + .../root/etc/geoguard/cc-en.txt | 217 +++ .../root/etc/init.d/geoguard-ban | 34 + .../root/etc/nftables.d/10-geoguard-guard.nft | 3 + .../etc/uci-defaults/40-luci-app-geoguard | 23 + .../etc/uci-defaults/41-luci-app-geoguard-ban | 17 + .../root/lib/upgrade/keep.d/luci-app-geoguard | 1 + .../root/usr/bin/geoguard-ban | 133 ++ .../root/usr/bin/geoguard-ban-guard | 60 + .../root/usr/bin/geoguard-ban-status | 26 + .../root/usr/bin/geoguard-ban-unban | 23 + .../root/usr/bin/geoguard-clear-history | 6 + .../root/usr/bin/geoguard-counts | 23 + .../root/usr/bin/geoguard-cron | 52 + .../root/usr/bin/geoguard-ddns | 84 ++ .../root/usr/bin/geoguard-fetch | 82 ++ .../root/usr/bin/geoguard-status | 56 + .../root/usr/bin/geoguard-update | 284 ++++ .../share/luci/menu.d/luci-app-geoguard.json | 13 + .../share/rpcd/acl.d/luci-app-geoguard.json | 33 + 24 files changed, 3311 insertions(+) create mode 100644 applications/luci-app-geoguard/LICENSE create mode 100644 applications/luci-app-geoguard/Makefile create mode 100644 applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js create mode 100644 applications/luci-app-geoguard/po/templates/geoguard.pot create mode 100644 applications/luci-app-geoguard/root/etc/config/geoguard create mode 100644 applications/luci-app-geoguard/root/etc/geoguard/cc-en.txt create mode 100755 applications/luci-app-geoguard/root/etc/init.d/geoguard-ban create mode 100644 applications/luci-app-geoguard/root/etc/nftables.d/10-geoguard-guard.nft create mode 100755 applications/luci-app-geoguard/root/etc/uci-defaults/40-luci-app-geoguard create mode 100755 applications/luci-app-geoguard/root/etc/uci-defaults/41-luci-app-geoguard-ban create mode 100644 applications/luci-app-geoguard/root/lib/upgrade/keep.d/luci-app-geoguard create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-ban create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-ban-guard create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-ban-status create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-ban-unban create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-clear-history create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-counts create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-cron create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-ddns create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-fetch create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-status create mode 100755 applications/luci-app-geoguard/root/usr/bin/geoguard-update create mode 100644 applications/luci-app-geoguard/root/usr/share/luci/menu.d/luci-app-geoguard.json create mode 100644 applications/luci-app-geoguard/root/usr/share/rpcd/acl.d/luci-app-geoguard.json diff --git a/applications/luci-app-geoguard/LICENSE b/applications/luci-app-geoguard/LICENSE new file mode 100644 index 000000000000..2528301be4c4 --- /dev/null +++ b/applications/luci-app-geoguard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 chinoyan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/applications/luci-app-geoguard/Makefile b/applications/luci-app-geoguard/Makefile new file mode 100644 index 000000000000..e6199a8438d8 --- /dev/null +++ b/applications/luci-app-geoguard/Makefile @@ -0,0 +1,24 @@ +# Copyright 2026 Yang Min Sheng +# This is free software, licensed under the MIT License. + +include $(TOPDIR)/rules.mk + +PKG_VERSION:=2.2.0 +PKG_RELEASE:=1 +PKG_LICENSE:=MIT +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=Yang Min Sheng + +LUCI_TITLE:=LuCI support for GeoGuard (country allow-list IPS with login guard) +LUCI_DESCRIPTION:=GeoGuard builds country allow-list ipsets for firewall4 from IP-geolocation feeds, with a custom whitelist, a DDNS no-ban list and a LuCI/SSH login guard. +LUCI_DEPENDS:=+luci-base +firewall4 +wget-ssl +jsonfilter +LUCI_PKGARCH:=all + +define Package/luci-app-geoguard/conffiles +/etc/config/geoguard +/etc/nftables.d/10-geoguard-guard.nft +endef + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js b/applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js new file mode 100644 index 000000000000..205889f01d12 --- /dev/null +++ b/applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js @@ -0,0 +1,860 @@ +'use strict'; +'require view'; +'require form'; +'require fs'; +'require ui'; +'require uci'; + +var VERSION = '2.2.0'; +var fmt = function(s) { + var args = Array.prototype.slice.call(arguments, 1); + var i = 0; + return String(s).replace(/%s/g, function() { return (i < args.length) ? args[i++] : ''; }); +}; +var CONTINENTS = [ + ['asia', [ + ['af', _('AFGHANISTAN')], ['am', _('ARMENIA')], ['az', _('AZERBAIJAN')], + ['bd', _('BANGLADESH')], ['bh', _('BAHRAIN')], ['bn', _('BRUNEI')], + ['bt', _('BHUTAN')], ['cn', _('CHINA')], ['cy', _('CYPRUS')], + ['ge', _('GEORGIA')], ['hk', _('HONG KONG')], ['id', _('INDONESIA')], + ['il', _('ISRAEL')], ['in', _('INDIA')], ['iq', _('IRAQ')], + ['ir', _('IRAN')], ['jo', _('JORDAN')], ['jp', _('JAPAN')], + ['kg', _('KYRGYZSTAN')], ['kh', _('CAMBODIA')], ['kp', _('NORTH KOREA')], + ['kr', _('SOUTH KOREA')], ['kw', _('KUWAIT')], ['kz', _('KAZAKHSTAN')], + ['la', _('LAOS')], ['lb', _('LEBANON')], ['lk', _('SRI LANKA')], + ['mm', _('MYANMAR')], ['mn', _('MONGOLIA')], ['mo', _('MACAO')], + ['my', _('MALAYSIA')], ['np', _('NEPAL')], ['om', _('OMAN')], + ['ph', _('PHILIPPINES')], ['pk', _('PAKISTAN')], ['ps', _('PALESTINE')], + ['qa', _('QATAR')], ['sa', _('SAUDI ARABIA')], ['sg', _('SINGAPORE')], + ['sy', _('SYRIA')], ['th', _('THAILAND')], ['tj', _('TAJIKISTAN')], + ['tl', _('TIMOR-LESTE')], ['tm', _('TURKMENISTAN')], ['tr', _('TURKEY')], + ['tw', _('TAIWAN')], ['uz', _('UZBEKISTAN')], ['vn', _('VIETNAM')], + ['ye', _('YEMEN')] + ]], + ['europe', [ + ['ad', _('ANDORRA')], ['al', _('ALBANIA')], ['at', _('AUSTRIA')], + ['ax', _('ALAND ISLANDS')], ['ba', _('BOSNIA AND HERZEGOVINA')], ['be', _('BELGIUM')], + ['bg', _('BULGARIA')], ['by', _('BELARUS')], ['ch', _('SWITZERLAND')], + ['cz', _('CZECHIA')], ['de', _('GERMANY')], ['dk', _('DENMARK')], + ['ee', _('ESTONIA')], ['es', _('SPAIN')], ['fi', _('FINLAND')], + ['fo', _('FAROE ISLANDS')], ['fr', _('FRANCE')], ['gb', _('UNITED KINGDOM')], + ['gg', _('GUERNSEY')], ['gi', _('GIBRALTAR')], ['gr', _('GREECE')], + ['hr', _('CROATIA')], ['hu', _('HUNGARY')], ['ie', _('IRELAND')], + ['im', _('ISLE OF MAN')], ['is', _('ICELAND')], ['it', _('ITALY')], + ['je', _('JERSEY')], ['li', _('LIECHTENSTEIN')], ['lt', _('LITHUANIA')], + ['lu', _('LUXEMBOURG')], ['lv', _('LATVIA')], ['mc', _('MONACO')], + ['md', _('MOLDOVA')], ['me', _('MONTENEGRO')], ['mk', _('NORTH MACEDONIA')], + ['mt', _('MALTA')], ['nl', _('NETHERLANDS')], ['no', _('NORWAY')], + ['pl', _('POLAND')], ['pt', _('PORTUGAL')], ['ro', _('ROMANIA')], + ['rs', _('SERBIA')], ['ru', _('RUSSIA')], ['se', _('SWEDEN')], + ['si', _('SLOVENIA')], ['sk', _('SLOVAKIA')], ['sm', _('SAN MARINO')], + ['ua', _('UKRAINE')], ['va', _('VATICAN CITY')] + ]], + ['africa', [ + ['dz', _('ALGERIA')], ['ao', _('ANGOLA')], ['bj', _('BENIN')], + ['bw', _('BOTSWANA')], ['bf', _('BURKINA FASO')], ['bi', _('BURUNDI')], + ['cm', _('CAMEROON')], ['cv', _('CABO VERDE')], ['cf', _('CENTRAL AFRICAN REPUBLIC')], + ['td', _('CHAD')], ['km', _('COMOROS')], ['cg', _('CONGO')], + ['cd', _('DEMOCRATIC REPUBLIC OF THE CONGO')], ['dj', _('DJIBOUTI')], ['eg', _('EGYPT')], + ['gq', _('EQUATORIAL GUINEA')], ['er', _('ERITREA')], ['et', _('ETHIOPIA')], + ['ga', _('GABON')], ['gm', _('GAMBIA')], ['gh', _('GHANA')], + ['gn', _('GUINEA')], ['gw', _('GUINEA-BISSAU')], ['ke', _('KENYA')], + ['lr', _('LIBERIA')], ['ls', _('LESOTHO')], ['ly', _('LIBYA')], + ['ma', _('MOROCCO')], ['mg', _('MADAGASCAR')], ['ml', _('MALI')], + ['mr', _('MAURITANIA')], ['mu', _('MAURITIUS')], ['mw', _('MALAWI')], + ['mz', _('MOZAMBIQUE')], ['na', _('NAMIBIA')], ['ne', _('NIGER')], + ['ng', _('NIGERIA')], ['rw', _('RWANDA')], ['sc', _('SEYCHELLES')], + ['sd', _('SUDAN')], ['sh', _('SAINT HELENA')], ['sl', _('SIERRA LEONE')], + ['sn', _('SENEGAL')], ['so', _('SOMALIA')], ['ss', _('SOUTH SUDAN')], + ['st', _('SAO TOME AND PRINCIPE')], ['sz', _('ESWATINI')], ['tg', _('TOGO')], + ['tn', _('TUNISIA')], ['tz', _('TANZANIA')], ['ug', _('UGANDA')], + ['za', _('SOUTH AFRICA')], ['zm', _('ZAMBIA')], ['zw', _('ZIMBABWE')] + ]], + ['northamerica', [ + ['ag', _('ANTIGUA AND BARBUDA')], ['ai', _('ANGUILLA')], ['bs', _('BAHAMAS')], + ['bb', _('BARBADOS')], ['bz', _('BELIZE')], ['bm', _('BERMUDA')], + ['ca', _('CANADA')], ['cr', _('COSTA RICA')], ['cu', _('CUBA')], + ['dm', _('DOMINICA')], ['do', _('DOMINICAN REPUBLIC')], ['sv', _('EL SALVADOR')], + ['gd', _('GRENADA')], ['gl', _('GREENLAND')], ['gt', _('GUATEMALA')], + ['ht', _('HAITI')], ['hn', _('HONDURAS')], ['jm', _('JAMAICA')], + ['ky', _('CAYMAN ISLANDS')], ['mx', _('MEXICO')], ['ms', _('MONTSERRAT')], + ['ni', _('NICARAGUA')], ['pa', _('PANAMA')], ['pm', _('SAINT PIERRE AND MIQUELON')], + ['pr', _('PUERTO RICO')], ['tt', _('TRINIDAD AND TOBAGO')], ['us', _('UNITED STATES')], + ['vg', _('VIRGIN ISLANDS (BRITISH)')], ['vi', _('VIRGIN ISLANDS (US)')] + ]], + ['southamerica', [ + ['ar', _('ARGENTINA')], ['bo', _('BOLIVIA')], ['br', _('BRAZIL')], + ['cl', _('CHILE')], ['co', _('COLOMBIA')], ['ec', _('ECUADOR')], + ['fk', _('FALKLAND ISLANDS')], ['gy', _('GUYANA')], ['py', _('PARAGUAY')], + ['pe', _('PERU')], ['sr', _('SURINAME')], ['uy', _('URUGUAY')], + ['ve', _('VENEZUELA')] + ]], + ['oceania', [ + ['as', _('AMERICAN SAMOA')], ['au', _('AUSTRALIA')], ['ck', _('COOK ISLANDS')], + ['fj', _('FIJI')], ['fm', _('MICRONESIA')], ['gu', _('GUAM')], + ['ki', _('KIRIBATI')], ['mh', _('MARSHALL ISLANDS')], ['nc', _('NEW CALEDONIA')], + ['nr', _('NAURU')], ['nu', _('NIUE')], ['nz', _('NEW ZEALAND')], + ['pf', _('FRENCH POLYNESIA')], ['pg', _('PAPUA NEW GUINEA')], ['pw', _('PALAU')], + ['sb', _('SOLOMON ISLANDS')], ['tk', _('TOKELAU')], ['to', _('TONGA')], + ['tv', _('TUVALU')], ['vu', _('VANUATU')], ['wf', _('WALLIS AND FUTUNA')], + ['ws', _('SAMOA')] + ]] +]; + +var countryState = {}; +var wlState = []; +/* dirty guards: render rebuilds state from uci (clean); only real UI + interaction marks dirty. pushCountries never deletes selected/whitelist + unless the user actually touched them (unrendered-save data-loss class). */ +var countryDirty = false; +var wlDirty = false; +var banDirty = false; +var schedDirty = false; +var nameDirty = false; +var countsDiv = null; +var schedState = { freq: 'weekly', hour: '6', min: '0', auto: '1' }; +var nameState = { set: 'allowed-IPList', white: 'CustomAllow' }; +var OLD_GROUPS = ['asia', 'europe', 'africa', 'northamerica', 'southamerica', 'oceania']; + +function wlCheck(v) { + v = (v || '').trim(); + var oct = '(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])'; + var ip = '(' + oct + '\\.){3}' + oct; + var re = new RegExp('^' + ip + '(/([1-9]|[12][0-9]|3[0-2]))?$|^' + ip + '-' + ip + '$'); + if (!re.test(v)) + return _('Invalid format: enter a single IP (e.g. 203.0.113.10), CIDR (e.g. 203.0.113.0/24) or range (e.g. 203.0.113.10-203.0.113.50)'); + return true; +} + +function pushCountries() { + var sel = Object.keys(countryState); + if (countryDirty) { + if (sel.length > 0) + uci.set('geoguard', 'main', 'selected', sel); + else + uci.unset('geoguard', 'main', 'selected'); + } + var i; + for (i = 0; i < OLD_GROUPS.length; i++) + uci.unset('geoguard', 'main', 'sel_' + OLD_GROUPS[i]); + if (schedDirty) { + uci.set('geoguard', 'main', 'update_freq', schedState.freq); + uci.set('geoguard', 'main', 'update_hour', schedState.hour); + uci.set('geoguard', 'main', 'update_min', schedState.min); + uci.set('geoguard', 'main', 'auto_update', schedState.auto); + } + if (nameDirty) { + uci.set('geoguard', 'main', 'setname', nameState.set || 'allowed-IPList'); + uci.set('geoguard', 'main', 'white_name', nameState.white || 'CustomAllow'); + } + if (wlDirty) { + if (wlState.length > 0) + uci.set('geoguard', 'main', 'whitelist', wlState.slice()); + else + uci.unset('geoguard', 'main', 'whitelist'); + } + /* first-save marker: 40-seeding runs only after the user pressed save + once (shipped default 'tw' must not trigger silent downloads). */ + if (countryDirty || schedDirty || nameDirty || wlDirty) + uci.set('geoguard', 'main', 'ui_saved', '1'); + return uci.save(); +} + +function checkSetname() { + var sn = uci.get('geoguard', 'main', 'setname') || ''; + var wn = uci.get('geoguard', 'main', 'white_name') || 'CustomAllow'; + var ok = function(v) { return /^[A-Za-z][A-Za-z0-9_-]*$/.test(v); }; + return ok(sn) && ok(wn); +} + + +return view.extend({ + handleSaveApply: null, + handleReset: null, + + load: function() { + return Promise.all([ + uci.load('geoguard'), + fs.exec('/usr/bin/geoguard-status').then(function(res) { + return (res.code === 0 && res.stdout) ? res.stdout : _('Status script failed'); + }).catch(function(e) { + return fmt(_('Status script failed: %s'), e.message); + }), + fs.exec('/usr/bin/geoguard-counts').then(function(res) { + return (res.code === 0 && res.stdout) ? res.stdout.trim() : ''; + }).catch(function() { + return ''; + }), + fs.exec('/usr/bin/geoguard-ban-status').then(function(res) { + return (res.code === 0 && res.stdout) ? res.stdout : ''; + }).catch(function() { + return ''; + }) + ]); + }, + + render: function(data) { + var m, s, o; + var logText = data[1] || ''; + var countsText = data[2] || ''; + var banText = data[3] || ''; + var banPre = null; + + var countsLine = function() { + var p = (countsText || '').split(/\s+/); + if (p.length < 4 || !p[0]) + return _('Latest merged IP set file: no data yet'); + var fdate = (p[3] || '').replace('_', ' '); + var cdate = (p[4] || '').replace('_', ' '); + if (cdate && cdate !== fdate) + return fmt(_('Latest merged IP set file: /etc/geoguard/%s.cidr (%s lines / live %s entries / updated %s, checked %s — no changes)'), p[0], p[1], p[2], fdate, cdate); + return fmt(_('Latest merged IP set file: /etc/geoguard/%s.cidr (%s lines / live %s entries / updated %s)'), p[0], p[1], p[2], fdate); + }; + + var refreshCounts = function() { + return fs.exec('/usr/bin/geoguard-counts').then(function(res) { + if (res.code !== 0 || !res.stdout) + return; + countsText = res.stdout.trim(); + if (countsDiv) { + while (countsDiv.firstChild) + countsDiv.removeChild(countsDiv.firstChild); + countsDiv.appendChild(E('span', {}, [countsLine()])); + } + }).catch(function() {}); + }; + + m = new form.Map('geoguard', _('GeoGuard Ver:') + VERSION, + _('Select countries + whitelist IPs into merged IP set files. This page only builds IP sets; apply them yourself under Firewall - Port Forwards by picking the set in a rule.')); + + /* counts line lives in its own top section so it renders + between the subtitle and the tab menu (mockup layout) */ + var sTop = m.section(form.NamedSection, 'main'); + sTop.option(form.DummyValue, '_topcounts').render = function(section_id) { + countsDiv = E('div', { 'class': 'country-counts', 'style': 'margin:0.5em 0;font-weight:bold;color:#0a7b1e' }, [countsLine()]); + return countsDiv; + }; + + s = m.section(form.TypedSection, 'geoguard', _('Settings')); + s.anonymous = true; + s.tab('ban', _('Login Guard')); + s.tab('settings', _('IP Sets')); + s.tab('log', _('Log')); + + o = s.taboption('settings', form.DummyValue, '_countries'); + o.render = function(section_id) { + var cur = uci.get('geoguard', 'main', 'selected') || []; + var i, j; + countryState = {}; + countryDirty = false; + (function() { + var old = ['sel_asia', 'sel_europe', 'sel_africa', 'sel_northamerica', 'sel_southamerica', 'sel_oceania']; + for (var k = 0; k < old.length; k++) { + var v = uci.get('geoguard', 'main', old[k]) || []; + for (var n = 0; n < v.length; n++) + if (cur.indexOf(v[n]) < 0) + cur.push(v[n]); + } + })(); + cur.forEach(function(v) { countryState[v] = true; }); + + var rows = []; + var tbody = E('tbody', {}); + var selLine = E('div', { 'class': 'country-selected', 'style': 'flex:1;word-break:break-all' }, []); + var refreshSel = function() { + var arr = Object.keys(countryState).sort(); + while (selLine.firstChild) + selLine.removeChild(selLine.firstChild); + selLine.appendChild(E('strong', {}, [_('Selected:')])); + selLine.appendChild(E('span', {}, [' ' + (arr.length > 0 ? arr.join(',').toUpperCase() : _('(none selected)'))])); + }; + for (i = 0; i < CONTINENTS.length; i++) { + for (j = 0; j < CONTINENTS[i][1].length; j++) { + (function(cc) { + var cb = E('input', { 'type': 'checkbox', 'value': cc[0] }); + if (countryState[cc[0]]) + cb.checked = true; + cb.addEventListener('change', function() { + countryDirty = true; + if (cb.checked) + countryState[cc[0]] = true; + else + delete countryState[cc[0]]; + refreshSel(); + }); + var tr = E('tr', {}, [ + E('td', {}, [cb]), + E('td', {}, [cc[0].toUpperCase()]), + E('td', {}, [cc[1]]) + ]); + rows.push({ cc: cc[0], text: (cc[0] + ' ' + cc[1]).toUpperCase(), el: tr, cb: cb }); + tbody.appendChild(tr); + })(CONTINENTS[i][1][j]); + } + } + rows.sort(function(a, b) { return a.cc < b.cc ? -1 : (a.cc > b.cc ? 1 : 0); }); + rows.forEach(function(r) { tbody.appendChild(r.el); }); + + var allCb = E('input', { 'type': 'checkbox' }); + allCb.addEventListener('change', function() { + countryDirty = true; + rows.forEach(function(r) { + if (r.el.style.display === 'none') + return; + r.cb.checked = allCb.checked ? true : false; + if (allCb.checked) + countryState[r.cc] = true; + else + delete countryState[r.cc]; + }); + refreshSel(); + }); + + var input = E('input', { 'type': 'text', 'placeholder': _('Search, e.g. TW or TAIWAN'), 'class': 'country-search' }); + input.addEventListener('input', function() { + var q = (input.value || '').toUpperCase(); + rows.forEach(function(r) { + r.el.style.display = (q === '' || r.text.indexOf(q) >= 0) ? '' : 'none'; + }); + }); + var clearBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral' }, [_('Clear')]); + clearBtn.addEventListener('click', function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + input.value = ''; + rows.forEach(function(r) { r.el.style.display = ''; }); + }); + + var selAllBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'style': 'margin-right:0.5em' }, [_('Select all')]); + selAllBtn.addEventListener('click', function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + rows.forEach(function(r) { + if (r.el.style.display === 'none') + return; + r.cb.checked = true; + countryState[r.cc] = true; + }); + refreshSel(); + }); + var selNoneBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral' }, [_('Clear selected')]); + selNoneBtn.addEventListener('click', function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + rows.forEach(function(r) { + r.cb.checked = false; + delete countryState[r.cc]; + }); + refreshSel(); + }); + + var table = E('table', { 'class': 'table cbi-section-table', 'style': 'width:100%' }, [ + E('thead', { 'style': 'position:sticky;top:0;background-color:#f0f0f0' }, [ + E('tr', {}, [ + E('th', { 'style': 'width:40px' }, [allCb]), + E('th', { 'style': 'width:90px' }, [_('Code')]), + E('th', {}, [_('Location')]) + ]) + ]), + tbody + ]); + var wrap = E('div', { 'style': 'max-height:420px;overflow:auto;border:1px solid #ccc' }, [table]); + var headRow = E('div', { 'style': 'display:flex;align-items:center;gap:0.5em;margin-bottom:0.5em;flex-wrap:wrap' }, [ + E('strong', {}, [_('Select countries')]), + input, clearBtn, + E('span', { 'style': 'color:#999' }, [':']), + selAllBtn, selNoneBtn, selLine + ]); + input.style.flex = '1'; + refreshSel(); + return E('div', {}, [ + headRow, + wrap + ]); + }; + + o = s.taboption('settings', form.DummyValue, '_whitelist'); + o.render = function(section_id) { + var cur = uci.get('geoguard', 'main', 'whitelist') || []; + if (!Array.isArray(cur)) + cur = [cur]; + wlState = cur.slice(); + wlDirty = false; + var listBox = E('div', { 'class': 'wl-list' }); + var errLine = E('div', { 'class': 'wl-error', 'style': 'color:#c00;margin-top:0.3em' }, []); + var drawList = function() { + while (listBox.firstChild) + listBox.removeChild(listBox.firstChild); + wlState.forEach(function(v, idx) { + var del = E('button', { 'class': 'btn cbi-button cbi-button-neutral' }, [_('Delete')]); + del.addEventListener('click', function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + wlDirty = true; + wlState.splice(idx, 1); + drawList(); + }); + listBox.appendChild(E('div', { 'style': 'margin-bottom:0.3em' }, [ + E('span', {}, [v]), E('span', {}, [' ']), del + ])); + }); + }; + var setErr = function(msg) { + while (errLine.firstChild) + errLine.removeChild(errLine.firstChild); + if (msg) + errLine.appendChild(E('span', {}, [msg])); + }; + var inp = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'placeholder': _('e.g. 203.0.113.10, 203.0.113.0/24, 203.0.113.10-203.0.113.50'), 'style': 'flex:1;margin-right:0.5em' }); + var addBtn = E('button', { 'class': 'btn cbi-button cbi-button-action' }, [_('Add')]); + addBtn.addEventListener('click', function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + var v = (inp.value || '').trim(); + var msg = wlCheck(v); + if (msg !== true) { + setErr(msg); + return; + } + if (wlState.indexOf(v) < 0) + wlState.push(v); + wlDirty = true; + inp.value = ''; + setErr(null); + drawList(); + }); + drawList(); + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, [_('Whitelist beyond countries: IP / CIDR / Range')]), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', { 'style': 'display:flex;margin-bottom:0.5em' }, [inp, addBtn]), + listBox, errLine, + E('div', { 'class': 'cbi-value-description' }, [_('Formats: single IP, CIDR, A-B range. No country + IPs = pure whitelist.')]) + ]) + ]); + }; + + o = s.taboption('settings', form.Value, 'src_primary', _('IP Geolocation - Primary Feed')); + o.default = 'https://www.ipdeny.com/ipblocks/data/aggregated/{cc}-aggregated.zone'; + o.rmempty = false; + o.description = _('{cc} becomes the lowercase code, {CC} uppercase.'); + o = s.taboption('settings', form.Value, 'src_backup', _('IP Geolocation - Backup Feed')); + o.default = 'https://raw.githubusercontent.com/ipverse/country-ip-blocks/master/country/{cc}/ipv4-aggregated.txt'; + o.rmempty = false; + o.description = _('Falls back on failure; keeps the old file if both fail.'); + + /* set names as custom rows so the file path prefix/suffix can flank + the inputs (framework Value cannot); saved via pushCountries, + gated by checkSetname (same message as the retired validate) */ + o = s.taboption('settings', form.DummyValue, '_setnames'); + o.render = function(section_id) { + nameState.set = uci.get('geoguard', 'main', 'setname') || 'allowed-IPList'; + nameState.white = uci.get('geoguard', 'main', 'white_name') || 'CustomAllow'; + nameDirty = false; + var mkName = function(dataName, val, cb) { + var inp = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'data-name': dataName, 'style': 'width:15em;max-width:100%', 'value': val }); + inp.addEventListener('change', function() { cb((inp.value || '').trim()); }); + return inp; + }; + var setInp = mkName('setname', nameState.set, function(v) { nameDirty = true; nameState.set = v; }); + var whiteInp = mkName('white_name', nameState.white, function(v) { nameDirty = true; nameState.white = v; }); + var nameRow = function(title, desc, inp) { + return E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, [title]), + E('div', { 'class': 'cbi-value-field' }, [ + E('div', {}, [ + E('span', {}, ['/etc/geoguard/']), E('span', {}, [' ']), + inp, + E('span', {}, [' ']), E('span', {}, ['.cidr']) + ]), + E('div', { 'class': 'cbi-value-description' }, [desc]) + ]) + ]); + }; + return E('div', {}, [ + nameRow(_('Merged set name (countries + custom whitelist)'), _('Rule: start with a letter; letters/digits/_/- only. File shares the set name; old file kept after rename.'), setInp), + nameRow(_('Whitelist set name'), _('Whitelist lives in its own set, visible under Firewall IP Sets. Same rules as above.'), whiteInp) + ]); + }; + + o = s.taboption('settings', form.DummyValue, '_sched'); + o.render = function(section_id) { + var freq = uci.get('geoguard', 'main', 'update_freq') || 'weekly'; + var hour = uci.get('geoguard', 'main', 'update_hour') || '6'; + var min = uci.get('geoguard', 'main', 'update_min') || '0'; + schedState.freq = freq; + schedState.hour = hour; + schedState.min = min; + schedDirty = false; + var freqSel = E('select', {}, [ + E('option', { 'value': 'daily' }, [_('Daily')]), + E('option', { 'value': 'weekly' }, [_('Weekly (Sun)')]), + E('option', { 'value': 'monthly' }, [_('Monthly (1st)')]) + ]); + freqSel.value = freq; + freqSel.addEventListener('change', function() { schedDirty = true; schedState.freq = freqSel.value; }); + var hourSel = E('select', {}, []); + var minSel = E('select', {}, []); + var i; + for (i = 0; i < 24; i++) { + var hv = String(i); + var ho = E('option', { 'value': hv }, [hv]); + if (hv === String(Number(hour))) + ho.selected = true; + hourSel.appendChild(ho); + } + for (i = 0; i < 60; i++) { + var mv = String(i); + var mo = E('option', { 'value': mv }, [mv]); + if (mv === String(Number(min))) + mo.selected = true; + minSel.appendChild(mo); + } + hourSel.value = String(Number(hour)); + minSel.value = String(Number(min)); + hourSel.addEventListener('change', function() { schedDirty = true; schedState.hour = hourSel.value; }); + minSel.addEventListener('change', function() { schedDirty = true; schedState.min = minSel.value; }); + var autoCb = E('input', { 'type': 'checkbox' }); + if ((uci.get('geoguard', 'main', 'auto_update') || '0') === '1') + autoCb.checked = true; + schedState.auto = autoCb.checked ? '1' : '0'; + autoCb.addEventListener('change', function() { schedDirty = true; schedState.auto = autoCb.checked ? '1' : '0'; }); + return E('div', {}, [ + E('div', { 'class': 'cbi-value' }, [ + E('div', { 'class': 'cbi-value-field' }, [ + autoCb, E('span', {}, [' ']), E('span', {}, [_('Enable auto-update of feed sets')]) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, [_('Auto-update schedule')]), + E('div', { 'class': 'cbi-value-field' }, [ + freqSel, E('span', {}, [' ']), + E('span', {}, [_('Hour')]), E('span', {}, [' ']), hourSel, + E('span', {}, [' ']), E('span', {}, [_('Minute')]), E('span', {}, [' ']), minSel + ]) + ]) + ]); + }; + o = s.taboption('settings', form.DummyValue, '_note'); + o.render = function(section_id) { + return E('div', { 'class': 'cbi-section' }, [ + E('style', {}, ['#cbi-geoguard input.cbi-input-text{width:15em;max-width:100%}#cbi-geoguard .cbi-value-title{white-space:nowrap;text-align:left!important}#cbi-geoguard .cbi-value-field .btn{width:auto}#cbi-geoguard table.cbi-section-table td,#cbi-geoguard table.cbi-section-table th{padding:3px 6px;text-align:left!important}#cbi-geoguard p{margin:0.3em 0;text-align:left}#cbi-geoguard .cbi-dynlist{width:100%;max-width:none}#cbi-geoguard .cbi-dynlist .add-item{display:flex}#cbi-geoguard .cbi-dynlist .add-item input{flex:1;margin-right:0.5em}#cbi-geoguard .cbi-value label.cbi-value-title{width:auto!important;flex:none!important;margin-right:.6em;min-width:12em}#cbi-geoguard div.cbi-value{text-align:left}#cbi-geoguard-main-src_primary input.cbi-input-text,#cbi-geoguard-main-src_backup input.cbi-input-text{width:100%;max-width:640px}#cbi-geoguard pre{overflow-x:auto;max-width:100%;overflow-wrap:anywhere;word-break:break-all}#cbi-geoguard .country-counts{word-break:break-all}']), + E('p', {}, [_('This page only builds IP set files and never changes firewall rules.')]), + E('p', {}, [_('Apply: Network → Firewall → Port Forwards → Add → Advanced → pick the set in IPSet, then Save & Apply.')]), + E('p', {}, [_('Fallback (SSH): uci set firewall.@redirect[N].ipset=set name, commit, then fw4 reload.')]) + ]); + }; + + o = s.taboption('settings', form.DummyValue, '_actions'); + o.render = function(section_id) { + var opt = this; + var runone = function(cmd, okmsg, noexec) { + var map = opt.map; + return pushCountries().then(function() { + return map.save(null, true); + }).then(function() { + return robustApply(); + }).then(function() { + if (!checkSetname()) { + ui.addNotification(null, E('p', _('Bad set name (must start with a letter: letters/digits/_/- only). Settings saved, update skipped.')), 'error'); + throw { handled: true }; + } + if (noexec) { + return fs.exec('/usr/bin/geoguard-cron').then(function() { + return { code: 0, skip: true }; + }); + } + return fs.exec(cmd); + }).then(function(res) { + if (res.skip) + return res; + return fs.exec('/usr/bin/geoguard-cron').then(function() { + return res; + }); + }).then(function(res) { + if (res.skip) { + ui.addNotification(null, E('p', okmsg), 'info'); + } else if (res.code === 0) { + ui.addNotification(null, E('p', okmsg), 'info'); + return refreshCounts(); + } else { + ui.addNotification(null, E('p', fmt(_('Update failed: %s'), res.stderr || res.stdout || _('unknown error'))), 'error'); + } + }).catch(function(e) { + if (e && e.handled) + return; + ui.addNotification(null, E('p', fmt(_('Failed: %s'), e.message)), 'error'); + }); + }; + var mkbtn = function(cmd, title, okmsg, noexec) { + var b = E('button', { 'class': 'btn cbi-button cbi-button-action', 'style': 'margin-right:0.5em' }, [title]); + b.addEventListener('click', guardedClick(function() { return runone(cmd, okmsg, noexec); })); + return trackBtn(b); + }; + return E('div', { 'style': 'display:flex;align-items:center;gap:0.5em;flex-wrap:wrap' }, [ + mkbtn('/usr/bin/geoguard-fetch', _('Update IP Sets Now'), _('IP sets updated (fetch only, not merged)'), false), + mkbtn('/usr/bin/geoguard-update', _('Update and Merge Now'), _('Updated and merged successfully (incl. whitelist)'), false), + mkbtn(null, _('Save Settings'), _('Settings saved (schedule synced)'), true) + ]); + }; + + o = s.taboption('log', form.DummyValue, '_log'); + o.render = function(section_id) { + var pre = E('pre', { 'style': 'white-space:pre-wrap' }, [logText]); + var swapPre = function(text) { + while (pre.firstChild) + pre.removeChild(pre.firstChild); + pre.appendChild(document.createTextNode(text || '')); + }; + var clr = E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'style': 'margin-bottom:0.5em;margin-right:0.5em' }, [_('Clear Update History')]); + clr.addEventListener('click', function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + return fs.exec('/usr/bin/geoguard-clear-history').then(function() { + return fs.exec('/usr/bin/geoguard-status'); + }).then(function(res) { + swapPre(res.stdout); + ui.addNotification(null, E('p', _('Update history cleared')), 'info'); + }).catch(function(e) { + ui.addNotification(null, E('p', fmt(_('Failed: %s'), e.message)), 'error'); + }); + }); + /* read-only refresh: exempt from the action-button busy lock + so progress can be watched while an update runs */ + var rld = E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'style': 'margin-bottom:0.5em' }, [_('Reload Log')]); + rld.addEventListener('click', function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + return fs.exec('/usr/bin/geoguard-status').then(function(res) { + swapPre(res.stdout); + }).catch(function(e) { + ui.addNotification(null, E('p', fmt(_('Failed: %s'), e.message)), 'error'); + }); + }); + return E('div', {}, [clr, rld, pre]); + }; + + /* ---- Login guard tab (compact 7-row layout) ---- */ + o = s.taboption('ban', form.Flag, 'ban_enabled', _('Enable this option to block IP addresses with too many failed logins')); + o.default = '0'; + o.rmempty = false; + o.description = _('Disabling stops the guard service; active bans stay until expiry.'); + + var banState = { maxretry: '8', findtime: '5', bantime: '2', web: '1', ssh: '1', ddnsint: '3', banint: '60' }; + var banClamp = function(v, lo, hi, def) { + v = parseInt(v, 10); + if (isNaN(v)) + return def; + if (v < lo) + return String(lo); + if (v > hi) + return String(hi); + return String(v); + }; + /* Serialize backend actions: rapid clicks otherwise overlap uci + transactions (ubus code 6), leave staged changes behind, and run + heavy updates in parallel. While busy, action buttons lock. */ + var actionBusy = false; + var actionBtns = []; + var trackBtn = function(b) { actionBtns.push(b); return b; }; + var setActionBusy = function(on) { + actionBusy = on; + actionBtns.forEach(function(b) { try { b.disabled = on; } catch (ignore) {} }); + }; + var guardedClick = function(fn) { + return function(ev) { + if (ev && ev.preventDefault) + ev.preventDefault(); + if (actionBusy) + return false; + setActionBusy(true); + return Promise.resolve().then(fn).then( + function() { setActionBusy(false); }, + function(e) { setActionBusy(false); throw e; }); + }; + }; + /* Empty changeset -> rpcd NO_DATA (code 5): not an error. + Contended apply (code 6): retry once after 1s. */ + var robustApply = function(retried) { + return uci.apply().catch(function(e) { + var msg = (e && e.message) || ''; + if (/code 5|NO_DATA|No data/i.test(msg)) + return null; + if (!retried && /code 6|permission denied/i.test(msg)) + return new Promise(function(resolve) { setTimeout(resolve, 1000); }).then(function() { return robustApply(true); }); + throw e; + }); + }; + var pushBan = function() { + if (!banDirty) + return; + uci.set('geoguard', 'main', 'ban_maxretry', banClamp(banState.maxretry, 1, 100, '8')); + uci.set('geoguard', 'main', 'ban_findtime', banClamp(banState.findtime, 1, 60, '5')); + uci.set('geoguard', 'main', 'ban_bantime', banClamp(banState.bantime, 1, 72, '2')); + uci.set('geoguard', 'main', 'ban_web', banState.web === '1' ? '1' : '0'); + uci.set('geoguard', 'main', 'ban_ssh', banState.ssh === '1' ? '1' : '0'); + uci.set('geoguard', 'main', 'ddns_interval', banClamp(banState.ddnsint, 1, 60, '30')); + uci.set('geoguard', 'main', 'ban_interval', banClamp(banState.banint, 5, 300, '60')); + }; + var numIn = function(val, min, max, cb) { + var inp = E('input', { 'type': 'number', 'min': String(min), 'max': String(max), 'value': val, 'style': 'width:5em;margin-right:0.3em' }); + inp.addEventListener('change', function() { banDirty = true; cb(inp.value); }); + return inp; + }; + var flagIn = function(checked, cb) { + var cbx = E('input', { 'type': 'checkbox', 'style': 'margin-right:0.3em' }); + if (checked === '1') + cbx.checked = true; + cbx.addEventListener('change', function() { banDirty = true; cb(cbx.checked ? '1' : '0'); }); + return cbx; + }; + var banRow = function(cells) { + var div = E('div', { 'class': 'cbi-value' }, []); + cells.forEach(function(c, i) { + if (i > 0) + div.appendChild(E('span', { 'style': 'margin:0 1em' }, [' '])); + div.appendChild(E('label', { 'class': 'cbi-value-title', 'style': 'width:auto;flex:none;margin-right:0.4em' }, [c[0]])); + div.appendChild(E('div', { 'class': 'cbi-value-field', 'style': 'display:inline-block' }, [c[1]])); + }); + return div; + }; + + o = s.taboption('ban', form.DummyValue, '_banthresh'); + o.render = function(section_id) { + banState.maxretry = uci.get('geoguard', 'main', 'ban_maxretry') || '8'; + banState.findtime = uci.get('geoguard', 'main', 'ban_findtime') || '5'; + banState.bantime = uci.get('geoguard', 'main', 'ban_bantime') || '2'; + banDirty = false; + return banRow([ + [_('Within (minutes)'), numIn(banState.findtime, 1, 60, function(v) { banState.findtime = v; })], + [_('Fails to ban'), numIn(banState.maxretry, 1, 100, function(v) { banState.maxretry = v; })], + [_('Ban time (hours)'), numIn(banState.bantime, 1, 72, function(v) { banState.bantime = v; })] + ]); + }; + o = s.taboption('ban', form.DummyValue, '_banscope'); + o.render = function(section_id) { + banState.web = uci.get('geoguard', 'main', 'ban_web') || '1'; + banState.ssh = uci.get('geoguard', 'main', 'ban_ssh') || '1'; + banDirty = false; + return banRow([ + [_('Guard LuCI web login'), flagIn(banState.web, function(v) { banState.web = v; })], + [_('Guard SSH login'), flagIn(banState.ssh, function(v) { banState.ssh = v; })] + ]); + }; + o = s.taboption('ban', form.DynamicList, 'ban_exempt', _('Never-block whitelist')); + o.validate = function(section_id, value) { + if (!value || !value.trim()) + return true; + var oct = '(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])'; + var ip = '(' + oct + '\\.){3}' + oct; + if (!new RegExp('^' + ip + '(/([1-9]|[12][0-9]|3[0-2]))?$').test(value.trim())) + return _('Enter an IP or CIDR (e.g. 192.168.0.0/16)'); + return true; + }; + o.rmempty = true; + o.description = _('IPs/subnets here are never banned. Defaults already cover reserved and private ranges. Two more auto-exempt sources need no entry here: IP whitelist (IP Sets tab) and DDNS whitelist (below), whose IPs follow automatically.'); + o = s.taboption('ban', form.DynamicList, 'ddns_allowlist', _('DDNS allowlist')); + o.validate = function(section_id, value) { + if (!value || !value.trim()) + return true; + if (!/^(?=.{1,253}$)[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$/.test(value.trim())) + return _('Enter a valid domain (e.g. home.example.org)'); + return true; + }; + o.rmempty = true; + o.description = _('Add multiple entries; each tracked separately. For guard exemption only; never touches IP sets.'); + o = s.taboption('ban', form.DummyValue, '_banperiod'); + o.render = function(section_id) { + banState.ddnsint = uci.get('geoguard', 'main', 'ddns_interval') || '30'; + banState.banint = uci.get('geoguard', 'main', 'ban_interval') || '60'; + banDirty = false; + return banRow([ + [_('DDNS check interval (min)'), numIn(banState.ddnsint, 1, 60, function(v) { banState.ddnsint = v; })], + [_('Log review and ban interval (sec)'), numIn(banState.banint, 5, 300, function(v) { banState.banint = v; })] + ]); + }; + o = s.taboption('ban', form.Value, 'ban_wan_if', _('WAN interface (auto-detect)')); + o.validate = function(section_id, value) { + if (!value || !value.trim()) + return true; + if (!/^[A-Za-z0-9._-]+$/.test(value.trim())) + return _('Bad interface name'); + return true; + }; + o.rmempty = true; + o.description = _('Leave empty to auto-detect (firewall wan zone → system → default route). Fill in manually only if detection fails (e.g. pppoe-wan).'); + o = s.taboption('ban', form.DummyValue, '_bannote'); + o.render = function(section_id) { + var bh = uci.get('geoguard', 'main', 'ban_bantime') || '2'; + return E('div', { 'class': 'cbi-section' }, [ + E('p', {}, [fmt(_('Banned = invisible: every packet from the WAN (all ports, TCP/UDP/ICMP) is dropped at ingress, auto-released after %s hours.'), bh)]) + ]); + }; + + o = s.taboption('ban', form.DummyValue, '_banstatus'); + o.render = function(section_id) { + banPre = E('pre', { 'style': 'white-space:pre-wrap' }, [banText || _('Loading status…')]); + return E('div', {}, [banPre]); + }; + + o = s.taboption('ban', form.DummyValue, '_banactions'); + o.render = function(section_id) { + var map = this.map; + var refreshBan = function() { + return fs.exec('/usr/bin/geoguard-ban-status').then(function(res) { + banText = res.stdout || ''; + if (banPre) { + while (banPre.firstChild) + banPre.removeChild(banPre.firstChild); + banPre.appendChild(document.createTextNode(banText)); + } + }).catch(function() {}); + }; + var saveBan = function() { + pushBan(); + return map.save(null, true).then(function() { + return robustApply(); + }).then(function() { + /* keep cron in sync: ddns_interval may have changed above */ + return fs.exec('/usr/bin/geoguard-cron'); + }).then(function() { + return fs.exec('/usr/bin/geoguard-ban-guard'); + }).then(function() { + /* boot persistence follows the flag: enable on / disable off + (reload alone starts/stops now but does not survive reboot) */ + var ben = uci.get('geoguard', 'main', 'ban_enabled') || '0'; + return fs.exec('/etc/init.d/geoguard-ban', [ben === '1' ? 'enable' : 'disable']); + }).then(function() { + return fs.exec('/etc/init.d/geoguard-ban', ['reload']); + }).then(function(res) { + if (res.code === 0) + ui.addNotification(null, E('p', _('Guard settings saved and restarted')), 'info'); + else + ui.addNotification(null, E('p', fmt(_('Guard restart failed: %s'), res.stderr || res.stdout || _('unknown error'))), 'error'); + return refreshBan(); + }).catch(function(e) { + ui.addNotification(null, E('p', fmt(_('Failed: %s'), e.message)), 'error'); + }); + }; + var unbanAll = function() { + return fs.exec('/usr/bin/geoguard-ban-unban', ['all']).then(function() { + ui.addNotification(null, E('p', _('All unbanned')), 'info'); + return refreshBan(); + }).catch(function(e) { + ui.addNotification(null, E('p', fmt(_('Failed: %s'), e.message)), 'error'); + }); + }; + var mkb = function(title, fn, cls) { + var b = E('button', { 'class': 'btn cbi-button ' + cls, 'style': 'margin-right:0.5em' }, [title]); + b.addEventListener('click', guardedClick(fn)); + return trackBtn(b); + }; + return E('div', { 'style': 'display:flex;align-items:center;gap:0.5em;flex-wrap:wrap' }, [ + mkb(_('Save & Restart Guard'), saveBan, 'cbi-button-action'), + mkb(_('Unban all IPs'), unbanAll, 'cbi-button-neutral') + ]); + }; + + return m.render(); + } +}); diff --git a/applications/luci-app-geoguard/po/templates/geoguard.pot b/applications/luci-app-geoguard/po/templates/geoguard.pot new file mode 100644 index 000000000000..8f4e5cdf8005 --- /dev/null +++ b/applications/luci-app-geoguard/po/templates/geoguard.pot @@ -0,0 +1,1208 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:16 +msgid "AFGHANISTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:16 +msgid "ARMENIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:16 +msgid "AZERBAIJAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:17 +msgid "BANGLADESH" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:17 +msgid "BAHRAIN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:17 +msgid "BRUNEI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:18 +msgid "BHUTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:18 +msgid "CHINA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:18 +msgid "CYPRUS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:19 +msgid "GEORGIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:19 +msgid "HONG KONG" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:19 +msgid "INDONESIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:20 +msgid "ISRAEL" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:20 +msgid "INDIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:20 +msgid "IRAQ" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:21 +msgid "IRAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:21 +msgid "JORDAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:21 +msgid "JAPAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:22 +msgid "KYRGYZSTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:22 +msgid "CAMBODIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:22 +msgid "NORTH KOREA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:23 +msgid "SOUTH KOREA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:23 +msgid "KUWAIT" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:23 +msgid "KAZAKHSTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:24 +msgid "LAOS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:24 +msgid "LEBANON" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:24 +msgid "SRI LANKA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:25 +msgid "MYANMAR" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:25 +msgid "MONGOLIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:25 +msgid "MACAO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:26 +msgid "MALAYSIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:26 +msgid "NEPAL" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:26 +msgid "OMAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:27 +msgid "PHILIPPINES" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:27 +msgid "PAKISTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:27 +msgid "PALESTINE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:28 +msgid "QATAR" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:28 +msgid "SAUDI ARABIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:28 +msgid "SINGAPORE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:29 +msgid "SYRIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:29 +msgid "THAILAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:29 +msgid "TAJIKISTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:30 +msgid "TIMOR-LESTE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:30 +msgid "TURKMENISTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:30 +msgid "TURKEY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:31 +msgid "TAIWAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:31 +msgid "UZBEKISTAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:31 +msgid "VIETNAM" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:32 +msgid "YEMEN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:35 +msgid "ANDORRA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:35 +msgid "ALBANIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:35 +msgid "AUSTRIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:36 +msgid "ALAND ISLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:36 +msgid "BOSNIA AND HERZEGOVINA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:36 +msgid "BELGIUM" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:37 +msgid "BULGARIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:37 +msgid "BELARUS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:37 +msgid "SWITZERLAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:38 +msgid "CZECHIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:38 +msgid "GERMANY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:38 +msgid "DENMARK" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:39 +msgid "ESTONIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:39 +msgid "SPAIN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:39 +msgid "FINLAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:40 +msgid "FAROE ISLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:40 +msgid "FRANCE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:40 +msgid "UNITED KINGDOM" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:41 +msgid "GUERNSEY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:41 +msgid "GIBRALTAR" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:41 +msgid "GREECE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:42 +msgid "CROATIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:42 +msgid "HUNGARY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:42 +msgid "IRELAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:43 +msgid "ISLE OF MAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:43 +msgid "ICELAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:43 +msgid "ITALY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:44 +msgid "JERSEY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:44 +msgid "LIECHTENSTEIN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:44 +msgid "LITHUANIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:45 +msgid "LUXEMBOURG" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:45 +msgid "LATVIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:45 +msgid "MONACO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:46 +msgid "MOLDOVA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:46 +msgid "MONTENEGRO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:46 +msgid "NORTH MACEDONIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:47 +msgid "MALTA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:47 +msgid "NETHERLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:47 +msgid "NORWAY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:48 +msgid "POLAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:48 +msgid "PORTUGAL" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:48 +msgid "ROMANIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:49 +msgid "SERBIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:49 +msgid "RUSSIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:49 +msgid "SWEDEN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:50 +msgid "SLOVENIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:50 +msgid "SLOVAKIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:50 +msgid "SAN MARINO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:51 +msgid "UKRAINE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:51 +msgid "VATICAN CITY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:54 +msgid "ALGERIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:54 +msgid "ANGOLA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:54 +msgid "BENIN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:55 +msgid "BOTSWANA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:55 +msgid "BURKINA FASO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:55 +msgid "BURUNDI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:56 +msgid "CAMEROON" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:56 +msgid "CABO VERDE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:56 +msgid "CENTRAL AFRICAN REPUBLIC" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:57 +msgid "CHAD" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:57 +msgid "COMOROS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:57 +msgid "CONGO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:58 +msgid "DEMOCRATIC REPUBLIC OF THE CONGO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:58 +msgid "DJIBOUTI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:58 +msgid "EGYPT" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:59 +msgid "EQUATORIAL GUINEA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:59 +msgid "ERITREA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:59 +msgid "ETHIOPIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:60 +msgid "GABON" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:60 +msgid "GAMBIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:60 +msgid "GHANA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:61 +msgid "GUINEA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:61 +msgid "GUINEA-BISSAU" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:61 +msgid "KENYA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:62 +msgid "LIBERIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:62 +msgid "LESOTHO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:62 +msgid "LIBYA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:63 +msgid "MOROCCO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:63 +msgid "MADAGASCAR" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:63 +msgid "MALI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:64 +msgid "MAURITANIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:64 +msgid "MAURITIUS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:64 +msgid "MALAWI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:65 +msgid "MOZAMBIQUE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:65 +msgid "NAMIBIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:65 +msgid "NIGER" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:66 +msgid "NIGERIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:66 +msgid "RWANDA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:66 +msgid "SEYCHELLES" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:67 +msgid "SUDAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:67 +msgid "SAINT HELENA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:67 +msgid "SIERRA LEONE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:68 +msgid "SENEGAL" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:68 +msgid "SOMALIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:68 +msgid "SOUTH SUDAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:69 +msgid "SAO TOME AND PRINCIPE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:69 +msgid "ESWATINI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:69 +msgid "TOGO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:70 +msgid "TUNISIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:70 +msgid "TANZANIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:70 +msgid "UGANDA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:71 +msgid "SOUTH AFRICA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:71 +msgid "ZAMBIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:71 +msgid "ZIMBABWE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:74 +msgid "ANTIGUA AND BARBUDA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:74 +msgid "ANGUILLA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:74 +msgid "BAHAMAS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:75 +msgid "BARBADOS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:75 +msgid "BELIZE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:75 +msgid "BERMUDA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:76 +msgid "CANADA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:76 +msgid "COSTA RICA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:76 +msgid "CUBA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:77 +msgid "DOMINICA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:77 +msgid "DOMINICAN REPUBLIC" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:77 +msgid "EL SALVADOR" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:78 +msgid "GRENADA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:78 +msgid "GREENLAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:78 +msgid "GUATEMALA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:79 +msgid "HAITI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:79 +msgid "HONDURAS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:79 +msgid "JAMAICA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:80 +msgid "CAYMAN ISLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:80 +msgid "MEXICO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:80 +msgid "MONTSERRAT" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:81 +msgid "NICARAGUA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:81 +msgid "PANAMA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:81 +msgid "SAINT PIERRE AND MIQUELON" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:82 +msgid "PUERTO RICO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:82 +msgid "TRINIDAD AND TOBAGO" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:82 +msgid "UNITED STATES" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:83 +msgid "VIRGIN ISLANDS (BRITISH)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:83 +msgid "VIRGIN ISLANDS (US)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:86 +msgid "ARGENTINA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:86 +msgid "BOLIVIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:86 +msgid "BRAZIL" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:87 +msgid "CHILE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:87 +msgid "COLOMBIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:87 +msgid "ECUADOR" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:88 +msgid "FALKLAND ISLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:88 +msgid "GUYANA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:88 +msgid "PARAGUAY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:89 +msgid "PERU" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:89 +msgid "SURINAME" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:89 +msgid "URUGUAY" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:90 +msgid "VENEZUELA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:93 +msgid "AMERICAN SAMOA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:93 +msgid "AUSTRALIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:93 +msgid "COOK ISLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:94 +msgid "FIJI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:94 +msgid "MICRONESIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:94 +msgid "GUAM" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:95 +msgid "KIRIBATI" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:95 +msgid "MARSHALL ISLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:95 +msgid "NEW CALEDONIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:96 +msgid "NAURU" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:96 +msgid "NIUE" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:96 +msgid "NEW ZEALAND" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:97 +msgid "FRENCH POLYNESIA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:97 +msgid "PAPUA NEW GUINEA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:97 +msgid "PALAU" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:98 +msgid "SOLOMON ISLANDS" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:98 +msgid "TOKELAU" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:98 +msgid "TONGA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:99 +msgid "TUVALU" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:99 +msgid "VANUATU" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:99 +msgid "WALLIS AND FUTUNA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:100 +msgid "SAMOA" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:125 +msgid "Invalid format: enter a single IP (e.g. 203.0.113.10), CIDR (e.g. 203.0.113.0/24) or range (e.g. 203.0.113.10-203.0.113.50)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:175 +msgid "Status script failed" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:177 +msgid "Status script failed: %s" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:202 +msgid "Latest merged IP set file: no data yet" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:206 +msgid "Latest merged IP set file: /etc/geoguard/%s.cidr (%s lines / live %s entries / updated %s, checked %s — no changes)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:207 +msgid "Latest merged IP set file: /etc/geoguard/%s.cidr (%s lines / live %s entries / updated %s)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:223 +msgid "GeoGuard Ver:" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:224 +msgid "Select countries + whitelist IPs into merged IP set files. This page only builds IP sets; apply them yourself under Firewall - Port Forwards by picking the set in a rule." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:234 +msgid "Settings" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:236 +msgid "Login Guard" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:237 +msgid "IP Sets" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:238 +msgid "Log" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:264 +msgid "Selected:" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:265 +msgid "(none selected)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:309 +msgid "Search, e.g. TW or TAIWAN" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:316 +msgid "Clear" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:324 +msgid "Select all" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:336 +msgid "Clear selected" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:351 +msgid "Code" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:352 +msgid "Location" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:359 +msgid "Select countries" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:385 +msgid "Delete" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:404 +msgid "e.g. 203.0.113.10, 203.0.113.0/24, 203.0.113.10-203.0.113.50" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:405 +msgid "Add" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:424 +msgid "Whitelist beyond countries: IP / CIDR / Range" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:428 +msgid "Formats: single IP, CIDR, A-B range. No country + IPs = pure whitelist." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:433 +msgid "IP Geolocation - Primary Feed" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:436 +msgid "{cc} becomes the lowercase code, {CC} uppercase." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:437 +msgid "IP Geolocation - Backup Feed" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:440 +msgid "Falls back on failure; keeps the old file if both fail." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:471 +msgid "Merged set name (countries + custom whitelist)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:471 +msgid "Rule: start with a letter; letters/digits/_/- only. File shares the set name; old file kept after rename." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:472 +msgid "Whitelist set name" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:472 +msgid "Whitelist lives in its own set, visible under Firewall IP Sets. Same rules as above." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:486 +msgid "Daily" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:487 +msgid "Weekly (Sun)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:488 +msgid "Monthly (1st)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:521 +msgid "Enable auto-update of feed sets" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:525 +msgid "Auto-update schedule" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:528 +msgid "Hour" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:529 +msgid "Minute" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:538 +msgid "This page only builds IP set files and never changes firewall rules." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:539 +msgid "Apply: Network → Firewall → Port Forwards → Add → Advanced → pick the set in IPSet, then Save & Apply." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:540 +msgid "Fallback (SSH): uci set firewall.@redirect[N].ipset=set name, commit, then fw4 reload." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:555 +msgid "Bad set name (must start with a letter: letters/digits/_/- only). Settings saved, update skipped." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:577 +msgid "Update failed: %s" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:577 +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:824 +msgid "unknown error" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:582 +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:615 +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:627 +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:827 +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:835 +msgid "Failed: %s" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:591 +msgid "Update IP Sets Now" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:591 +msgid "IP sets updated (fetch only, not merged)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:592 +msgid "Update and Merge Now" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:592 +msgid "Updated and merged successfully (incl. whitelist)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:593 +msgid "Save Settings" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:593 +msgid "Settings saved (schedule synced)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:605 +msgid "Clear Update History" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:613 +msgid "Update history cleared" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:620 +msgid "Reload Log" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:634 +msgid "Enable this option to block IP addresses with too many failed logins" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:637 +msgid "Disabling stops the guard service; active bans stay until expiry." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:725 +msgid "Within (minutes)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:726 +msgid "Fails to ban" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:727 +msgid "Ban time (hours)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:736 +msgid "Guard LuCI web login" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:737 +msgid "Guard SSH login" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:740 +msgid "Never-block whitelist" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:747 +msgid "Enter an IP or CIDR (e.g. 192.168.0.0/16)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:751 +msgid "IPs/subnets here are never banned. Defaults already cover reserved and private ranges. Two more auto-exempt sources need no entry here: IP whitelist (IP Sets tab) and DDNS whitelist (below), whose IPs follow automatically." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:752 +msgid "DDNS allowlist" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:757 +msgid "Enter a valid domain (e.g. home.example.org)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:761 +msgid "Add multiple entries; each tracked separately. For guard exemption only; never touches IP sets." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:768 +msgid "DDNS check interval (min)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:769 +msgid "Log review and ban interval (sec)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:772 +msgid "WAN interface (auto-detect)" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:777 +msgid "Bad interface name" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:781 +msgid "Leave empty to auto-detect (firewall wan zone → system → default route). Fill in manually only if detection fails (e.g. pppoe-wan)." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:786 +msgid "Banned = invisible: every packet from the WAN (all ports, TCP/UDP/ICMP) is dropped at ingress, auto-released after %s hours." +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:792 +msgid "Loading status…" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:822 +msgid "Guard settings saved and restarted" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:824 +msgid "Guard restart failed: %s" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:832 +msgid "All unbanned" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:844 +msgid "Save & Restart Guard" +msgstr "" + +#: applications/luci-app-geoguard/htdocs/luci-static/resources/view/geoguard.js:845 +msgid "Unban all IPs" +msgstr "" + +#: applications/luci-app-geoguard/root/usr/share/luci/menu.d/luci-app-geoguard.json +msgid "GeoGuard" +msgstr "" diff --git a/applications/luci-app-geoguard/root/etc/config/geoguard b/applications/luci-app-geoguard/root/etc/config/geoguard new file mode 100644 index 000000000000..2bdbe43ecf31 --- /dev/null +++ b/applications/luci-app-geoguard/root/etc/config/geoguard @@ -0,0 +1,28 @@ + +config geoguard 'main' + # Shipped quiet: no selection, no schedule, no guard until the user + # saves once in the UI (fresh install must not change the system). + option setname 'allowed-IPList' + option src_primary 'https://www.ipdeny.com/ipblocks/data/aggregated/{cc}-aggregated.zone' + option src_backup 'https://raw.githubusercontent.com/ipverse/country-ip-blocks/master/country/{cc}/ipv4-aggregated.txt' + option update_freq 'weekly' + option auto_reload '1' + option auto_update '0' + option update_hour '6' + option update_min '0' + option white_name 'CustomAllow' + # list ddns_allowlist: DDNS whitelist (added via UI, empty = disabled; shipped empty) + option ban_interval '60' + option ddns_interval '30' + list ban_exempt '192.168.0.0/16' + list ban_exempt '127.0.0.0/8' + list ban_exempt '10.0.0.0/8' + list ban_exempt '172.16.0.0/12' + list ban_exempt '0.0.0.0/32' + option ban_enabled '0' + option ban_maxretry '8' + option ban_findtime '5' + option ban_bantime '2' + option ban_web '1' + option ban_ssh '1' + diff --git a/applications/luci-app-geoguard/root/etc/geoguard/cc-en.txt b/applications/luci-app-geoguard/root/etc/geoguard/cc-en.txt new file mode 100644 index 000000000000..e446652eafe4 --- /dev/null +++ b/applications/luci-app-geoguard/root/etc/geoguard/cc-en.txt @@ -0,0 +1,217 @@ +af AFGHANISTAN +am ARMENIA +az AZERBAIJAN +bd BANGLADESH +bh BAHRAIN +bn BRUNEI +bt BHUTAN +cn CHINA +cy CYPRUS +ge GEORGIA +hk HONG KONG +id INDONESIA +il ISRAEL +in INDIA +iq IRAQ +ir IRAN +jo JORDAN +jp JAPAN +kg KYRGYZSTAN +kh CAMBODIA +kp NORTH KOREA +kr SOUTH KOREA +kw KUWAIT +kz KAZAKHSTAN +la LAOS +lb LEBANON +lk SRI LANKA +mm MYANMAR +mn MONGOLIA +mo MACAO +my MALAYSIA +np NEPAL +om OMAN +ph PHILIPPINES +pk PAKISTAN +ps PALESTINE +qa QATAR +sa SAUDI ARABIA +sg SINGAPORE +sy SYRIA +th THAILAND +tj TAJIKISTAN +tl TIMOR-LESTE +tm TURKMENISTAN +tr TURKEY +tw TAIWAN +uz UZBEKISTAN +vn VIETNAM +ye YEMEN +ad ANDORRA +al ALBANIA +at AUSTRIA +ax ALAND ISLANDS +ba BOSNIA AND HERZEGOVINA +be BELGIUM +bg BULGARIA +by BELARUS +ch SWITZERLAND +cz CZECHIA +de GERMANY +dk DENMARK +ee ESTONIA +es SPAIN +fi FINLAND +fo FAROE ISLANDS +fr FRANCE +gb UNITED KINGDOM +gg GUERNSEY +gi GIBRALTAR +gr GREECE +hr CROATIA +hu HUNGARY +ie IRELAND +im ISLE OF MAN +is ICELAND +it ITALY +je JERSEY +li LIECHTENSTEIN +lt LITHUANIA +lu LUXEMBOURG +lv LATVIA +mc MONACO +md MOLDOVA +me MONTENEGRO +mk NORTH MACEDONIA +mt MALTA +nl NETHERLANDS +no NORWAY +pl POLAND +pt PORTUGAL +ro ROMANIA +rs SERBIA +ru RUSSIA +se SWEDEN +si SLOVENIA +sk SLOVAKIA +sm SAN MARINO +ua UKRAINE +va VATICAN CITY +dz ALGERIA +ao ANGOLA +bj BENIN +bw BOTSWANA +bf BURKINA FASO +bi BURUNDI +cm CAMEROON +cv CABO VERDE +cf CENTRAL AFRICAN REPUBLIC +td CHAD +km COMOROS +cg CONGO +cd DEMOCRATIC REPUBLIC OF THE CONGO +dj DJIBOUTI +eg EGYPT +gq EQUATORIAL GUINEA +er ERITREA +et ETHIOPIA +ga GABON +gm GAMBIA +gh GHANA +gn GUINEA +gw GUINEA-BISSAU +ke KENYA +lr LIBERIA +ls LESOTHO +ly LIBYA +ma MOROCCO +mg MADAGASCAR +ml MALI +mr MAURITANIA +mu MAURITIUS +mw MALAWI +mz MOZAMBIQUE +na NAMIBIA +ne NIGER +ng NIGERIA +rw RWANDA +sc SEYCHELLES +sd SUDAN +sh SAINT HELENA +sl SIERRA LEONE +sn SENEGAL +so SOMALIA +ss SOUTH SUDAN +st SAO TOME AND PRINCIPE +sz ESWATINI +tg TOGO +tn TUNISIA +tz TANZANIA +ug UGANDA +za SOUTH AFRICA +zm ZAMBIA +zw ZIMBABWE +ag ANTIGUA AND BARBUDA +ai ANGUILLA +bs BAHAMAS +bb BARBADOS +bz BELIZE +bm BERMUDA +ca CANADA +cr COSTA RICA +cu CUBA +dm DOMINICA +do DOMINICAN REPUBLIC +sv EL SALVADOR +gd GRENADA +gl GREENLAND +gt GUATEMALA +ht HAITI +hn HONDURAS +jm JAMAICA +ky CAYMAN ISLANDS +mx MEXICO +ms MONTSERRAT +ni NICARAGUA +pa PANAMA +pm SAINT PIERRE AND MIQUELON +pr PUERTO RICO +tt TRINIDAD AND TOBAGO +us UNITED STATES +vg VIRGIN ISLANDS (BRITISH) +vi VIRGIN ISLANDS (US) +ar ARGENTINA +bo BOLIVIA +br BRAZIL +cl CHILE +co COLOMBIA +ec ECUADOR +fk FALKLAND ISLANDS +gy GUYANA +py PARAGUAY +pe PERU +sr SURINAME +uy URUGUAY +ve VENEZUELA +as AMERICAN SAMOA +au AUSTRALIA +ck COOK ISLANDS +fj FIJI +fm MICRONESIA +gu GUAM +ki KIRIBATI +mh MARSHALL ISLANDS +nc NEW CALEDONIA +nr NAURU +nu NIUE +nz NEW ZEALAND +pf FRENCH POLYNESIA +pg PAPUA NEW GUINEA +pw PALAU +sb SOLOMON ISLANDS +tk TOKELAU +to TONGA +tv TUVALU +vu VANUATU +wf WALLIS AND FUTUNA +ws SAMOA diff --git a/applications/luci-app-geoguard/root/etc/init.d/geoguard-ban b/applications/luci-app-geoguard/root/etc/init.d/geoguard-ban new file mode 100755 index 000000000000..8d1231ec6452 --- /dev/null +++ b/applications/luci-app-geoguard/root/etc/init.d/geoguard-ban @@ -0,0 +1,34 @@ +#!/bin/sh /etc/rc.common +# geoguard-ban procd service: run geoguard-ban every N seconds (N from UCI ban_interval) +USE_PROCD=1 +START=99 + +start_service() { + [ "$(uci get geoguard.main.ban_enabled 2>/dev/null)" = "1" ] || return 0 + # M3: restore persisted bans surviving reboot (remaining time only) + if [ -f /etc/geoguard/ban-persist.list ]; then + now=$(date +%s) + BH=$(uci -q get geoguard.main.ban_bantime 2>/dev/null) + case "$BH" in ''|*[!0-9]*) BH=2 ;; esac + while read -r pip pstart; do + [ -z "$pip" ] && continue + rem=$(( BH*3600 - (now - pstart) )) + if [ "$rem" -gt 60 ]; then + nft add element inet fw4 geoguard_ban "{ $pip timeout ${rem}s }" 2>/dev/null \ + && logger -t geoguard-ban "restored $pip for ${rem}s (persisted across reboot)" \ + || logger -t geoguard-ban "restore skipped (set not ready)" + fi + done < /etc/geoguard/ban-persist.list + fi + procd_open_instance geoguard-ban + procd_set_param command /bin/sh -c 'while true; do /usr/bin/geoguard-ban; I=$(uci -q get geoguard.main.ban_interval 2>/dev/null); case "$I" in ""|*[!0-9]*) I=60;; esac; [ "$I" -lt 5 ] && I=5; [ "$I" -gt 300 ] && I=300; sleep $I; done' + procd_set_param respawn 5 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +reload_service() { + stop + start +} diff --git a/applications/luci-app-geoguard/root/etc/nftables.d/10-geoguard-guard.nft b/applications/luci-app-geoguard/root/etc/nftables.d/10-geoguard-guard.nft new file mode 100644 index 000000000000..4ea8bda19392 --- /dev/null +++ b/applications/luci-app-geoguard/root/etc/nftables.d/10-geoguard-guard.nft @@ -0,0 +1,3 @@ +# GeoGuard login guard (inert until enabled in the UI). +# geoguard-ban-guard writes the live chain + set on first enable +# and reloads fw4 only when the content changes. diff --git a/applications/luci-app-geoguard/root/etc/uci-defaults/40-luci-app-geoguard b/applications/luci-app-geoguard/root/etc/uci-defaults/40-luci-app-geoguard new file mode 100755 index 000000000000..dffa59179a9f --- /dev/null +++ b/applications/luci-app-geoguard/root/etc/uci-defaults/40-luci-app-geoguard @@ -0,0 +1,23 @@ +#!/bin/sh +# 40-luci-app-geoguard: fresh install stays inert (no cron, no downloads); +# upgrade with a user-saved config restores schedule + missing state. +# Config values come from package default /etc/config/geoguard. +mkdir -p /etc/geoguard 2>/dev/null +if [ "$(uci -q get geoguard.main.ui_saved 2>/dev/null)" != "1" ]; then + logger -t geoguard "installed, open LuCI GeoGuard, configure and press update-merge once" + exit 0 +fi +/usr/bin/geoguard-cron 2>/dev/null +# Restore missing state after upgrade wipe: selection exists but no data +# files yet -> fetch+merge once in background. Non-blocking and never +# fails install: offline boxes just log, the scheduled run (or manual +# update) picks it up later. +SETNAME=$(uci -q get geoguard.main.setname 2>/dev/null) +[ -z "$SETNAME" ] && SETNAME="allowed-IPList" +if [ ! -s "/etc/geoguard/$SETNAME.cidr" ]; then + if [ -n "$(uci -q get geoguard.main.selected 2>/dev/null)$(uci -q get geoguard.main.whitelist 2>/dev/null)" ]; then + logger -t geoguard "empty state with selection, seeding in background" + ( /usr/bin/geoguard-update >/dev/null 2>&1 & ) + fi +fi +exit 0 diff --git a/applications/luci-app-geoguard/root/etc/uci-defaults/41-luci-app-geoguard-ban b/applications/luci-app-geoguard/root/etc/uci-defaults/41-luci-app-geoguard-ban new file mode 100755 index 000000000000..858c1507697f --- /dev/null +++ b/applications/luci-app-geoguard/root/etc/uci-defaults/41-luci-app-geoguard-ban @@ -0,0 +1,17 @@ +#!/bin/sh +# 41-luci-app-geoguard-ban: fresh install stays inert (guard off until the +# user enables it in the UI); upgrade with guard previously enabled +# regenerates the guard and restores the procd service (runs once). +# (No chmod: helpers and init script already ship executable in the tree.) +if [ "$(uci -q get geoguard.main.ban_enabled 2>/dev/null)" != "1" ]; then + logger -t geoguard "ban installed, guard stays off until enabled in the UI" + exit 0 +fi +# generate the guard from the live WAN interface, +# then enable/start the procd service (runs once at install). +/usr/bin/geoguard-ban-guard >/dev/null 2>&1 +/etc/init.d/geoguard-ban enable +/etc/init.d/geoguard-ban start +logger -t geoguard "ban installed, guard generated" +/etc/init.d/firewall reload >/dev/null 2>&1 +exit 0 diff --git a/applications/luci-app-geoguard/root/lib/upgrade/keep.d/luci-app-geoguard b/applications/luci-app-geoguard/root/lib/upgrade/keep.d/luci-app-geoguard new file mode 100644 index 000000000000..32a76d2c26c6 --- /dev/null +++ b/applications/luci-app-geoguard/root/lib/upgrade/keep.d/luci-app-geoguard @@ -0,0 +1 @@ +/etc/geoguard diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-ban b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban new file mode 100755 index 000000000000..e2c96926b9ac --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban @@ -0,0 +1,133 @@ +#!/bin/sh +# geoguard-ban: LuCI + SSH login brute-force guard (all params from UCI, nothing hardcoded) +# Exemption layers: (1) ban_exempt list (UI-editable, reserved ranges by default) +# (2) whitelist file (3) ddns state +# Called by /etc/init.d/geoguard-ban (procd, interval read from ban_interval) +SET=geoguard_ban +CFG=geoguard +PERSIST="/etc/geoguard/ban-persist.list" +nft list set inet fw4 $SET >/dev/null 2>&1 || exit 0 + +g() { uci get $CFG.main."$1" 2>/dev/null; } +num() { case "$1" in ''|*[!0-9]*) echo "$2" ;; *) echo "$1" ;; esac; } + +EN=$(g ban_enabled); [ -z "$EN" ] && EN=0 +[ "$EN" = "0" ] && exit 0 + +# Guard self-heal: WAN interface drift (any save path that misses regen is +# repaired automatically, checked every 60 seconds) +GWANT=$(g ban_wan_if) +if [ -z "$GWANT" ]; then + for zi in $(uci show firewall 2>/dev/null | grep -oE '@zone\[[0-9]+\]=zone' | grep -oE '[0-9]+'); do + if [ "$(uci -q get firewall.@zone[$zi].name 2>/dev/null)" = "wan" ]; then + ZNET=$(uci -q get firewall.@zone[$zi].network 2>/dev/null | awk '{print $1}') + break + fi + done + [ -n "$ZNET" ] && GWANT=$(ubus call "network.interface.$ZNET" status 2>/dev/null | jsonfilter -e '@.l3_device' 2>/dev/null) + [ -z "$GWANT" ] && GWANT=$(ip route show default 2>/dev/null | sed -n 's/.*dev \([^ ]*\).*/\1/p' | head -n 1) +fi +if [ -n "$GWANT" ]; then + GHAVE=$(grep -oE 'iifname "[^"]*"' /etc/nftables.d/10-geoguard-guard.nft 2>/dev/null | head -n 1 | cut -d'"' -f2) + [ "$GWANT" != "$GHAVE" ] && /usr/bin/geoguard-ban-guard >/dev/null 2>&1 +fi +MAX=$(num "$(g ban_maxretry)" 8) +FIND=$(num "$(g ban_findtime)" 5) +BANH=$(num "$(g ban_bantime)" 2) +WEB=$(g ban_web); [ -z "$WEB" ] && WEB=1 +SSHO=$(g ban_ssh); [ -z "$SSHO" ] && SSHO=1 +WHITE=$(g white_name); [ -z "$WHITE" ] && WHITE=CustomAllow +WLFILE="/etc/geoguard/$WHITE.cidr" +STATE="/etc/geoguard/ddns-allowlist.list" + +# CIDR membership test (awk, stdin line by line: CIDR or bare IP) +in_list() { + # $1=ip, stdin=list + awk -v ip="$1" ' + function i2n(a, p, n) { n = split(a, p, "."); return p[1]*16777216+p[2]*65536+p[3]*256+p[4]; } + BEGIN { needle = i2n(ip); } + { line = $1; + if (line ~ /\//) { split(line, a, "/"); b = i2n(a[1]); m = a[2]+0; + if (m == 0) next; + if (int(needle / 2^(32-m)) == int(b / 2^(32-m))) { found = 1; exit; } } + else if (line == ip) { found = 1; exit; } } + END { exit (found ? 0 : 1); }' +} + +in_exempt() { + EX=$(g ban_exempt) + [ -z "$EX" ] && return 1 + # shellcheck disable=SC2086 + printf '%s\n' $EX | in_list "$1" +} + +in_ddns() { + [ -f "$STATE" ] || return 1 + awk -v ip="$1" '$2 == ip { found = 1; exit; } END { exit (found ? 0 : 1); }' "$STATE" +} + +# N-minute sliding window +WIN=/tmp/geoguard-ban-window +: > "$WIN" +i=0 +while [ $i -lt "$FIND" ]; do + T=$(date -d "@$(( $(date +%s) - i * 60 ))" '+%b %e %H:%M' 2>/dev/null | tr -s ' ') + [ -n "$T" ] && echo "$T" >> "$WIN" + i=$((i + 1)) +done +[ -s "$WIN" ] || exit 0 + +persist_ban() { + # record ban start epoch for reboot restore (dedupe by ip) + grep -v "^$1 " "$PERSIST" 2>/dev/null > "$PERSIST.tmp" + echo "$1 $(date +%s)" >> "$PERSIST.tmp" + mv "$PERSIST.tmp" "$PERSIST" 2>/dev/null +} +prune_persist() { + # drop expired entries by time only (NOT by live absence: a fw4 reload + # wipes the live set, and pruning then would defeat reboot restore; + # unban() purges the file explicitly on purpose-unbans). + # tmpfs scratch + rewrite only on change: no overlay wear when idle. + now=$(date +%s) + [ -s "$PERSIST" ] || return 0 + ptmp=/tmp/geoguard-ban-persist.$$ + : > "$ptmp" + while read -r pip pstart; do + [ -z "$pip" ] && continue + if [ $((now - pstart)) -lt $((BANH * 3600)) ]; then + echo "$pip $pstart" >> "$ptmp" + fi + done < "$PERSIST" + cmp -s "$ptmp" "$PERSIST" || mv "$ptmp" "$PERSIST" 2>/dev/null + rm -f "$ptmp" +} +ban_ip() { + ip="$1"; tag="$2" + case "$ip" in ""|0.0.0.0) return 0 ;; esac + in_exempt "$ip" && return 0 + [ -f "$WLFILE" ] && in_list "$ip" < "$WLFILE" && return 0 + in_ddns "$ip" && return 0 + if ! nft get element inet fw4 $SET "{ $ip }" >/dev/null 2>&1; then + if nft add element inet fw4 $SET "{ $ip timeout ${BANH}h }"; then + logger -t geoguard-ban "banned $ip for ${BANH}h ($tag brute force)" + persist_ban "$ip" + fi + fi +} + +LOG=$(logread 2>/dev/null | tr -s ' ') + +if [ "$WEB" = "1" ]; then + echo "$LOG" | grep 'failed login' | grep -F -f "$WIN" \ + | sed -n 's/.* from \([0-9][0-9.]*\)[^0-9.]*$/\1/p' \ + | sort | uniq -c | awk -v max="$MAX" '$1>=max{print $2}' \ + | while read -r ip; do ban_ip "$ip" "LuCI"; done +fi + +if [ "$SSHO" = "1" ]; then + echo "$LOG" | grep -E 'bad password attempt|Login attempt for nonexistent|Exit before auth' | grep -F -f "$WIN" \ + | tr -d '<>' | sed -n 's/.* from \([0-9][0-9.]*\)[: ].*$/\1/p' \ + | sort | uniq -c | awk -v max="$MAX" '$1>=max{print $2}' \ + | while read -r ip; do ban_ip "$ip" "SSH"; done +fi +prune_persist diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-guard b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-guard new file mode 100755 index 000000000000..8b9effad7372 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-guard @@ -0,0 +1,60 @@ +#!/bin/sh +# geoguard-ban-guard: generate the full-block guard from UCI +# (banned = the box disappears for them) +# WAN interface: use ban_wan_if when set, auto-detect when empty +# (firewall wan zone -> ubus -> default route) +# Fixed output /etc/nftables.d/10-geoguard-guard.nft; fw4 reload only on change +OUT=/etc/nftables.d/10-geoguard-guard.nft +TMP=/tmp/luci-guard.new + +# Disabled: emit the inert placeholder (chain + set fully removed). +# Enabled: detect WAN and emit the live guard below. +EN=$(uci -q get geoguard.main.ban_enabled 2>/dev/null) +[ -z "$EN" ] && EN=0 +if [ "$EN" != "1" ]; then + cat > "$TMP" </dev/null) +if [ -z "$WANIF" ]; then + # Auto-detect: walk firewall zones one by one (no grep context, it bleeds into neighbor zones) + for zi in $(uci show firewall 2>/dev/null | grep -oE '@zone\[[0-9]+\]=zone' | grep -oE '[0-9]+'); do + if [ "$(uci -q get firewall.@zone[$zi].name 2>/dev/null)" = "wan" ]; then + ZNET=$(uci -q get firewall.@zone[$zi].network 2>/dev/null | awk '{print $1}') + break + fi + done + [ -n "$ZNET" ] && WANIF=$(ubus call "network.interface.$ZNET" status 2>/dev/null | jsonfilter -e '@.l3_device' 2>/dev/null) + [ -z "$WANIF" ] && WANIF=$(ip route show default 2>/dev/null | sed -n 's/.*dev \([^ ]*\).*/\1/p' | head -n 1) + [ -z "$WANIF" ] && WANIF="pppoe-wan" +fi + +# Interface names go verbatim into nft; reject anything outside [A-Za-z0-9._-] +case "$WANIF" in ''|*[!A-Za-z0-9._-]*) WANIF="pppoe-wan" ;; esac +cat > "$TMP" </dev/null 2>&1 + logger -t geoguard-ban-guard "guard regenerated${WANIF:+ full-block ($WANIF)}" + echo "guard regenerated${WANIF:+ ($WANIF)}" +fi diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-status b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-status new file mode 100755 index 000000000000..038102125902 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-status @@ -0,0 +1,26 @@ +#!/bin/sh +# geoguard-ban-status: guard tab status output (ban list + service state) +SET=geoguard_ban +if nft list set inet fw4 $SET 2>/dev/null | grep -q 'timeout'; then + echo "== Currently banned ==" + nft list set inet fw4 $SET 2>/dev/null | tr ',' '\n' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ timeout [0-9a-z]+ expires [0-9a-zms.]+' || echo "(none)" +else + echo "== Currently banned ==" + echo "(set missing: guard chain not loaded)" +fi +echo "== Service ==" +if ps 2>/dev/null | grep -q '[g]eoguard-ban' || /etc/init.d/geoguard-ban running 2>/dev/null | grep -q running; then + echo "geoguard-ban: running" +else + echo "geoguard-ban: not running" +fi +echo "== Parameters ==" +for k in ban_enabled ban_maxretry ban_findtime ban_bantime ban_web ban_ssh ban_interval ddns_interval; do + echo "$k=$(uci get geoguard.main.$k 2>/dev/null)" +done +echo "== DDNS tracking ==" +if [ -s /etc/geoguard/ddns-allowlist.list ]; then + cat /etc/geoguard/ddns-allowlist.list +else + echo "(none)" +fi diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-unban b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-unban new file mode 100755 index 000000000000..56a9302028de --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-ban-unban @@ -0,0 +1,23 @@ +#!/bin/sh +# geoguard-ban-unban [ip|all]: unban (guard page button); +# also clears the reboot-restore persist, or reboot would re-ban just-unbanned IPs +SET=geoguard_ban +PERSIST="/etc/geoguard/ban-persist.list" +case "$1" in + all|"") + for ip in $(nft list set inet fw4 $SET 2>/dev/null | tr ',' '\n' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ timeout' | awk '{print $1}'); do + nft delete element inet fw4 $SET "{ $ip }" 2>/dev/null + done + : > "$PERSIST" 2>/dev/null + logger -t geoguard-ban "unbanned all" + ;; + *) + case "$1" in *[!0-9.]*) echo "bad IP format"; exit 1 ;; esac + nft delete element inet fw4 $SET "{ $1 }" 2>/dev/null \ + && logger -t geoguard-ban "unbanned $1" + if [ -f "$PERSIST" ]; then + grep -v "^$1 " "$PERSIST" > "$PERSIST.tmp" 2>/dev/null || true + mv "$PERSIST.tmp" "$PERSIST" 2>/dev/null || true + fi + ;; +esac diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-clear-history b/applications/luci-app-geoguard/root/usr/bin/geoguard-clear-history new file mode 100755 index 000000000000..7347c2a9dd70 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-clear-history @@ -0,0 +1,6 @@ +#!/bin/sh +# geoguard-clear-history: clear update history.log only (sets and config untouched) +BASE_DIR="/etc/geoguard" +: > "$BASE_DIR/history.log" +logger -t geoguard "update history cleared manually" +echo "cleared" diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-counts b/applications/luci-app-geoguard/root/usr/bin/geoguard-counts new file mode 100755 index 000000000000..dae86b983110 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-counts @@ -0,0 +1,23 @@ +#!/bin/sh +# geoguard-counts: active set file lines + live elements + file date + last check date (settings page) +# Output: name filelines livecount FILEDATE CHECKDATE (underscore-separated for JS; +# CHECKDATE from history.log last line time, FILEDATE if none) +BASE_DIR="/etc/geoguard" +SETNAME=$(uci -q get geoguard.main.setname 2>/dev/null) +echo "$SETNAME" | grep -qE '^[A-Za-z][A-Za-z0-9_-]*$' || SETNAME="allowed-IPList" +FILE="$BASE_DIR/${SETNAME}.cidr" +FL=0 +LIVE=0 +FDATE="-" +if [ -f "$FILE" ]; then + FL=$(wc -l < "$FILE") + FDATE=$(date -r "$FILE" '+%F_%T' 2>/dev/null || echo "-") +fi +if nft list set inet fw4 "$SETNAME" >/dev/null 2>&1; then + # count elements, not slashes (A-B ranges have no '/') + LIVE=$(nft list set inet fw4 "$SETNAME" 2>/dev/null | sed -n '/elements = {/,/}/p' | tr -d '{}' | sed 's/elements = //' | tr ',' '\n' | grep -c '[^[:space:]]') + [ -z "$LIVE" ] && LIVE=0 +fi +CDATE=$(tail -n 1 "$BASE_DIR/history.log" 2>/dev/null | cut -d'|' -f1 | sed 's/ /_/;s/[^0-9_:\-]//g') +[ -z "$CDATE" ] && CDATE="$FDATE" +echo "$SETNAME $FL $LIVE $FDATE $CDATE" diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-cron b/applications/luci-app-geoguard/root/usr/bin/geoguard-cron new file mode 100755 index 000000000000..fa023314661d --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-cron @@ -0,0 +1,52 @@ +#!/bin/sh +# geoguard schedule manager: daily / weekly (Sun) / monthly (1st) + hour:min +# (takes effect after one run on UCI change) +CRON_FILE="/etc/crontabs/root" +JOB="/usr/bin/geoguard-update" + +sed -i '\#geoguard-update#d' "$CRON_FILE" 2>/dev/null +sed -i '\#geoguard-ddns#d' "$CRON_FILE" 2>/dev/null + +BASE_DIR="/etc/geoguard" +# Cron trailing comments are English-only (no locale data in shell code). +# WARNING: the two NOTE_* lines have identical copies in geoguard-ddns; +# both sides must match byte-for-byte, otherwise the ddns self-heal line +# comparison never matches and rewrites+reloads cron every round. +NOTE_DDNS='# GeoGuard DDNS whitelist tracker (auto-managed, do not edit)' +NOTE_UPDATE='# GeoGuard country IP sets updater (auto-managed, do not edit)' +# DDNS whitelist: static line always present (no save path removes it; +# script exits fast when the list is empty). +# Interval read from UCI (default 30 min); list empty but state non-empty +# means just cleared -> run one purge. +DDNS_I=$(uci -q get geoguard.main.ddns_interval 2>/dev/null) +case "$DDNS_I" in ''|*[!0-9]*) DDNS_I=30 ;; esac +[ "$DDNS_I" -lt 1 ] && DDNS_I=1 +[ "$DDNS_I" -gt 60 ] && DDNS_I=60 +DNOTE="$NOTE_DDNS"; UNOTE="$NOTE_UPDATE" +# crontabs often lack a trailing newline (LuCI editor saves verbatim); +# appending then glues our entry onto the user's last line, breaking both. +[ -s "$CRON_FILE" ] && [ -n "$(tail -c 1 "$CRON_FILE")" ] && echo "" >> "$CRON_FILE" +echo "*/$DDNS_I * * * * /usr/bin/geoguard-ddns $DNOTE" >> "$CRON_FILE" +/usr/bin/geoguard-ddns + +if [ "$(uci -q get geoguard.main.auto_update)" = "1" ]; then + FREQ=$(uci -q get geoguard.main.update_freq 2>/dev/null) + HOUR=$(uci -q get geoguard.main.update_hour) + MIN=$(uci -q get geoguard.main.update_min) + case "$HOUR" in ''|*[!0-9]*) HOUR=6 ;; esac + case "$MIN" in ''|*[!0-9]*) MIN=0 ;; esac + [ "$HOUR" -gt 23 ] && HOUR=6 + [ "$MIN" -gt 59 ] && MIN=0 + case "$FREQ" in + weekly) SPEC="$MIN $HOUR * * 0" ;; + monthly) SPEC="$MIN $HOUR 1 * *" ;; + *) SPEC="$MIN $HOUR * * *" ;; + esac + [ -s "$CRON_FILE" ] && [ -n "$(tail -c 1 "$CRON_FILE")" ] && echo "" >> "$CRON_FILE" + echo "$SPEC $JOB $UNOTE" >> "$CRON_FILE" + logger -t geoguard "schedule set: $SPEC" +else + logger -t geoguard "auto update disabled" +fi + +/etc/init.d/cron restart >/dev/null 2>&1 diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-ddns b/applications/luci-app-geoguard/root/usr/bin/geoguard-ddns new file mode 100755 index 000000000000..5fd0116f5325 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-ddns @@ -0,0 +1,84 @@ +#!/bin/sh +# geoguard-ddns: DDNS whitelist (guard exemption only, IP sets untouched) +# Source: ddns_allowlist (human-entered hostnames); state: ddns-allowlist.list (host ip) +# Removing a host = purge (state + records cut clean); IP change = state swap; success only logs +# Ban exemption reads the state file directly; allow/block via IP sets is +# unrelated to this script (only link is the manual whitelist) +BASE_DIR="/etc/geoguard" +STATE="$BASE_DIR/ddns-allowlist.list" +HIST="$BASE_DIR/history.log" +[ -f "$STATE" ] || : > "$STATE" + +# Cron trailing comments are English-only (no locale data in shell code). +# WARNING: the NOTE_DDNS line has an identical copy in geoguard-cron; +# both sides must match byte-for-byte, otherwise this self-heal line +# comparison never matches and rewrites+reloads cron every round. +NOTE_DDNS='# GeoGuard DDNS whitelist tracker (auto-managed, do not edit)' +# Cron self-heal: a static line dropped by any path, or a changed interval, is repaired automatically +DI=$(uci -q get geoguard.main.ddns_interval 2>/dev/null) +case "$DI" in ''|*[!0-9]*) DI=30 ;; esac +[ "$DI" -lt 1 ] && DI=1 +[ "$DI" -gt 60 ] && DI=60 +DLINE="*/$DI * * * * /usr/bin/geoguard-ddns $NOTE_DDNS" +grep -qxF "$DLINE" /etc/crontabs/root 2>/dev/null || { + sed -i '\#geoguard-ddns#d' /etc/crontabs/root 2>/dev/null + # crontabs often lack a trailing newline; appending then glues our entry + # onto the user's last line, breaking both (same guard as geoguard-cron). + [ -s /etc/crontabs/root ] && [ -n "$(tail -c 1 /etc/crontabs/root)" ] && echo "" >> /etc/crontabs/root + echo "$DLINE" >> /etc/crontabs/root + /etc/init.d/cron restart >/dev/null 2>&1 +} + +resolve() { + nslookup "$1" 2>/dev/null \ + | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' \ + | grep -vE '^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' \ + | head -n 1 +} +valid_ip() { echo "$1" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; } + +hist() { + echo "$(date '+%F %T')|ddns|$1" >> "$HIST" + tail -n 200 "$HIST" > "$HIST.tmp" 2>/dev/null && mv "$HIST.tmp" "$HIST" +} + +CHANGED=0 +NEWSTATE="$STATE.new" +: > "$NEWSTATE" + +# purge: in state but gone from the list -> cut clean everywhere +HOSTS=$(uci -q get geoguard.main.ddns_allowlist 2>/dev/null) +while read -r h ip; do + [ -z "$h" ] && continue + keep=0 + for want in $HOSTS; do [ "$want" = "$h" ] && keep=1; done + if [ "$keep" = "0" ]; then + logger -t geoguard-ddns "purge $h $ip" + hist "purge|$h $ip" + CHANGED=1 + else + echo "$h $ip" >> "$NEWSTATE" + fi +done < "$STATE" + +# Follow: resolve each host and compare +for h in $HOSTS; do + case "$h" in ""|*[!A-Za-z0-9.-]*) logger -t geoguard-ddns "bad host skip: $h"; continue ;; esac + ip=$(resolve "$h") + valid_ip "$ip" || { logger -t geoguard-ddns "resolve fail: $h"; continue; } + old=$(awk -v h="$h" '$1 == h { print $2; exit; }' "$NEWSTATE") + if [ "$ip" != "$old" ]; then + awk -v h="$h" '$1 != h' "$NEWSTATE" > "$NEWSTATE.tmp" 2>/dev/null && mv "$NEWSTATE.tmp" "$NEWSTATE" + echo "$h $ip" >> "$NEWSTATE" + logger -t geoguard-ddns "$h: ${old:-none} -> $ip" + hist "$h ${old:-none} -> $ip" + CHANGED=1 + fi +done +mv -f "$NEWSTATE" "$STATE" + +# State changed -> log only; geoguard-ban picks it up on its next cycle +# (no active kick: never block the caller on a firewall reload) +if [ "$CHANGED" = "1" ]; then + logger -t geoguard-ddns "changed, geoguard-ban will pick it up on its next cycle" +fi diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-fetch b/applications/luci-app-geoguard/root/usr/bin/geoguard-fetch new file mode 100755 index 000000000000..f808e4e73e23 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-fetch @@ -0,0 +1,82 @@ +#!/bin/sh +# geoguard-fetch: download per-country CIDR files only (no merge, no reload) +# Purpose: backfill on first install, re-fetch sources standalone. Run geoguard-update to merge. +BASE_DIR="/etc/geoguard" +TEMP_DIR="/tmp/cafetch-$$" + +SRC1=$(uci -q get geoguard.main.src_primary 2>/dev/null) +SRC2=$(uci -q get geoguard.main.src_backup 2>/dev/null) +[ -z "$SRC1" ] && SRC1="https://www.ipdeny.com/ipblocks/data/aggregated/{cc}-aggregated.zone" +[ -z "$SRC2" ] && SRC2="https://raw.githubusercontent.com/ipverse/country-ip-blocks/master/country/{cc}/ipv4-aggregated.txt" + +SELECTED="" +for l in asia europe africa northamerica southamerica oceania; do + SELECTED="$SELECTED $(uci -q get geoguard.main.sel_$l 2>/dev/null)" +done +SELECTED="$SELECTED $(uci -q get geoguard.main.selected 2>/dev/null)" + +mkdir -p "$BASE_DIR" "$TEMP_DIR" +trap 'rm -rf "$TEMP_DIR"' EXIT +# Single-flight with geoguard-update: shared lockdir on tmpfs (NOT under +# $BASE_DIR: keep.d carries /etc/geoguard across sysupgrade, and a restored +# pid would make kill -0 misfire exit 0 forever). +# Skipped when invoked by geoguard-update itself (GEOGUARD_LOCKED=1 exported +# by the caller, which already holds the lock). +LOCKDIR="/var/lock/geoguard-update.lockdir" +mkdir -p /var/lock 2>/dev/null +if [ -z "$GEOGUARD_LOCKED" ]; then + if ! mkdir "$LOCKDIR" 2>/dev/null; then + LPID=$(cat "$LOCKDIR/pid" 2>/dev/null) + if [ -n "$LPID" ] && kill -0 "$LPID" 2>/dev/null; then + logger -t geoguard "fetch skipped: another run in progress (pid $LPID)" + exit 0 + fi + rm -rf "$LOCKDIR" + mkdir "$LOCKDIR" 2>/dev/null || { logger -t geoguard "fetch skipped: lock contention"; exit 0; } + fi + echo "$$" > "$LOCKDIR/pid" + trap 'rm -rf "$TEMP_DIR" "$LOCKDIR"' EXIT +fi + +upper() { echo "$1" | tr 'a-z' 'A-Z'; } + +fetch_one() { + local cc="$1" tmp="$TEMP_DIR/${cc}.raw" clean="$TEMP_DIR/${cc}.cidr" url srcname + for template in "$SRC1" "$SRC2"; do + [ -z "$template" ] && continue + url=$(echo "$template" | sed -e "s/{cc}/$cc/g" -e "s/{CC}/$(upper $cc)/g") + case "$template" in + *ipdeny*) srcname="primary" ;; + *) srcname="backup" ;; + esac + wget -T 30 -t 2 -q -O "$tmp" "$url" 2>/dev/null || continue + [ -s "$tmp" ] || continue + grep -v '^#' "$tmp" > "$clean" + # Strict per-line filter: only fw4-loadable IPv4 CIDRs are installed, so a + # corrupt source can never poison the on-disk file (N8: raw country files + # feed firewall ipset loadfile directly; one bad line breaks fw4 reload). + grep -E '^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])/([1-9]|[12][0-9]|3[0-2])$' "$clean" > "$clean.strict" 2>/dev/null + mv -f "$clean.strict" "$clean" + [ "$(wc -l < "$clean")" -ge 15 ] || continue + cmp -s "$clean" "$BASE_DIR/${cc}.cidr" || mv -f "$clean" "$BASE_DIR/${cc}.cidr" + logger -t geoguard "$cc updated OK (source:$srcname)" + echo "$cc:OK:$srcname:$(wc -l < "$BASE_DIR/${cc}.cidr") lines, $(wc -c < "$BASE_DIR/${cc}.cidr") B" >> "$BASE_DIR/.fetch-results" + return 0 + done + logger -t geoguard "$cc update FAILED (both sources down, keeping old file)" + echo "$cc:FAIL:keeping old file" >> "$BASE_DIR/.fetch-results" + return 1 +} + +OK=0; FAIL="" +: > "$BASE_DIR/.fetch-results" +for cc in $SELECTED; do + [ -z "$cc" ] && continue + case "$cc" in [a-z][a-z]) ;; *) logger -t geoguard "bad country code, skip: $cc"; continue ;; esac + if fetch_one "$cc"; then OK=$((OK+1)); else FAIL="$FAIL $cc"; fi +done +logger -t geoguard "IP set fetch done: OK $OK countries, FAILED:$FAIL" +echo "$(date '+%F %T')|fetch-only|OK=$OK FAILED:$FAIL" >> "$BASE_DIR/history.log" +tail -n 200 "$BASE_DIR/history.log" > "$TEMP_DIR/hist" 2>/dev/null \ + && mv "$TEMP_DIR/hist" "$BASE_DIR/history.log" +[ -z "$FAIL" ] diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-status b/applications/luci-app-geoguard/root/usr/bin/geoguard-status new file mode 100755 index 000000000000..43b3c84db34a --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-status @@ -0,0 +1,56 @@ +#!/bin/sh +# geoguard-status: set status + update history + related system log (for LuCI log tab) +# Output is display-only English (detailed); machine parsing uses geoguard-counts. +BASE_DIR="/etc/geoguard" +SETNAME=$(uci -q get geoguard.main.setname 2>/dev/null) +echo "$SETNAME" | grep -qE '^[A-Za-z][A-Za-z0-9_-]*$' || SETNAME="allowed-IPList" +WHITENAME=$(uci -q get geoguard.main.white_name 2>/dev/null) +echo "$WHITENAME" | grep -qE '^[A-Za-z][A-Za-z0-9_-]*$' || WHITENAME="CustomAllow" +FILE="$BASE_DIR/${SETNAME}.cidr" +WFILE="$BASE_DIR/${WHITENAME}.cidr" + +# fileline : " lines | B ( KiB) | updated " or "(missing)" +fileline() { + if [ -f "$1" ]; then + _l=$(wc -l < "$1" 2>/dev/null) + _b=$(wc -c < "$1" 2>/dev/null) + _k=$(awk -v b="$_b" 'BEGIN { printf "%.1f", b/1024 }') + _d=$(date -r "$1" '+%F %T' 2>/dev/null || echo "-") + echo "$_l lines | $_b B ($_k KiB) | updated $_d" + else + echo "(missing)" + fi +} + +# livecount: total elements + CIDR/range split (nft merges adjacents for display) +livecount() { + _out=$(nft list set inet fw4 "$1" 2>/dev/null | sed -n '/elements = {/,/}/p' | tr -d '{}' | sed 's/elements = //' | tr ',' '\n' | grep '[^[:space:]]') + [ -z "$_out" ] && { echo "0 (set missing)"; return; } + _t=$(echo "$_out" | wc -l) + _c=$(echo "$_out" | grep -c '/') + _r=$((_t - _c)) + echo "$_t entries ($_c CIDR, $_r merged ranges)" +} + +echo "===== Set status =====" +if [ -f "$FILE" ]; then + echo "Set file: $FILE" + echo " $(fileline "$FILE")" +else + echo "Set file: $FILE (missing, press Update IP sets now)" +fi +echo "live set \`$SETNAME\`: $(livecount "$SETNAME")" +echo "Whitelist file: $WFILE" +echo " $(fileline "$WFILE")" +echo "Per-country files:" +for cc in $(uci -q get geoguard.main.selected 2>/dev/null); do + [ -z "$cc" ] && continue + echo " $cc.cidr: $(fileline "$BASE_DIR/${cc}.cidr")" +done +echo "Schedule: $(grep 'geoguard-update' /etc/crontabs/root 2>/dev/null | head -1)" +echo "" +echo "===== Update history (last 20) =====" +tail -n 20 "$BASE_DIR/history.log" 2>/dev/null || echo "(no records yet)" +echo "" +echo "===== System log (last 15) =====" +logread 2>/dev/null | grep 'geoguard' | tail -n 15 || echo "(no records yet)" diff --git a/applications/luci-app-geoguard/root/usr/bin/geoguard-update b/applications/luci-app-geoguard/root/usr/bin/geoguard-update new file mode 100755 index 000000000000..234a80ec7a30 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/bin/geoguard-update @@ -0,0 +1,284 @@ +#!/bin/sh +# geoguard-update: fetch -> merge -> sync uci ipset sections -> refresh live sets +# Sets: per-country (built only when the file exists) + merged + whitelist +# (skipped when empty, no file kept); default whitelist name CustomAllow +BASE_DIR="/etc/geoguard" +TEMP_DIR="/tmp/geoguard-$$" +CCEN="$BASE_DIR/cc-en.txt" + +SETNAME=$(uci -q get geoguard.main.setname 2>/dev/null) +echo "$SETNAME" | grep -qE '^[A-Za-z][A-Za-z0-9_-]*$' || SETNAME="allowed-IPList" +OUTPUT="$BASE_DIR/${SETNAME}.cidr" +WHITENAME=$(uci -q get geoguard.main.white_name 2>/dev/null) +echo "$WHITENAME" | grep -qE '^[A-Za-z][A-Za-z0-9_-]*$' || WHITENAME="CustomAllow" +WOUTPUT="$BASE_DIR/${WHITENAME}.cidr" + +SELECTED="" +for l in asia europe africa northamerica southamerica oceania; do + SELECTED="$SELECTED $(uci -q get geoguard.main.sel_$l 2>/dev/null)" +done +SELECTED="$SELECTED $(uci -q get geoguard.main.selected 2>/dev/null)" +WHITELIST=$(uci -q get geoguard.main.whitelist 2>/dev/null) + +mkdir -p "$BASE_DIR" "$TEMP_DIR" +trap 'rm -rf "$TEMP_DIR"' EXIT +# H2: single-flight (cron vs manual clicks). mkdir is atomic; stale lock +# (kill -9) is reclaimed via pid check. Lock lives on tmpfs (NOT under +# $BASE_DIR: keep.d carries /etc/geoguard across sysupgrade, and a restored +# pid would make kill -0 misfire exit 0 forever). +LOCKDIR="/var/lock/geoguard-update.lockdir" +mkdir -p /var/lock 2>/dev/null +if ! mkdir "$LOCKDIR" 2>/dev/null; then + LPID=$(cat "$LOCKDIR/pid" 2>/dev/null) + if [ -n "$LPID" ] && kill -0 "$LPID" 2>/dev/null; then + logger -t geoguard "update skipped: another run in progress (pid $LPID)" + exit 0 + fi + rm -rf "$LOCKDIR" + mkdir "$LOCKDIR" 2>/dev/null || { logger -t geoguard "update skipped: lock contention"; exit 0; } +fi +echo "$$" > "$LOCKDIR/pid" +trap 'rm -rf "$TEMP_DIR" "$LOCKDIR"' EXIT + +hist() { + echo "$(date '+%F %T')|update|$SETNAME|countries=$1 $2" >> "$BASE_DIR/history.log" + tail -n 200 "$BASE_DIR/history.log" > "$TEMP_DIR/hist" 2>/dev/null \ + && mv "$TEMP_DIR/hist" "$BASE_DIR/history.log" +} + +cc_en() { + local en="" + [ -f "$CCEN" ] && en=$(grep -i "^$1 " "$CCEN" 2>/dev/null | head -1 | cut -d' ' -f2-) + [ -z "$en" ] && en=$(echo "$1" | tr 'a-z' 'A-Z') + echo "$en" +} + +# fetch runs under this lock (env flag tells it to skip its own acquisition). +GEOGUARD_LOCKED=1 /usr/bin/geoguard-fetch + +# Append per-country fetch results to history +if [ -f "$BASE_DIR/.fetch-results" ]; then + while IFS= read -r line; do + [ -z "$line" ] && continue + echo "$(date '+%F %T')|fetch|$line" >> "$BASE_DIR/history.log" + done < "$BASE_DIR/.fetch-results" + rm -f "$BASE_DIR/.fetch-results" +fi + +# Expand A-B ranges into minimal CIDR groups (awk, no external deps) +range2cidr() { + awk ' + function ip2int(ip, a) { split(ip, a, "[.]"); return (a[1]*16777216)+(a[2]*65536)+(a[3]*256)+a[4]; } + function int2ip(i) { return int(i/16777216) "." (int(i/65536)%256) "." (int(i/256)%256) "." (i%256); } + function valid(ip, n,a,i) { n = split(ip, a, "[.]"); if (n != 4) return 0; + for (i = 1; i <= 4; i++) if (a[i] !~ /^[0-9]+$/ || (a[i]+0) < 0 || (a[i]+0) > 255) return 0; return 1; } + { + if ($0 ~ /^[0-9][0-9.]*-[0-9][0-9.]*$/) { + split($0, r, "-"); + if (!valid(r[1]) || !valid(r[2])) next; + s = ip2int(r[1]); e = ip2int(r[2]); + if (s > e) next; + while (s <= e) { + bits = 0; + while ((bits < 32) && ((s % (2^(bits+1))) == 0) && ((s + (2^(bits+1)) - 1) <= e)) bits++; + print int2ip(s) "/" (32 - bits); + s += 2^bits; + } + } else print $0; + }' +} + +# Normalize whitelist (bare IP gets /32, CIDR kept, ranges expanded) into $1 +build_white() { + > "$1" + for ip in $WHITELIST; do + [ -z "$ip" ] && continue + if echo "$ip" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}$'; then + echo "$ip/32" >> "$1" + elif echo "$ip" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$'; then + echo "$ip" >> "$1" + elif echo "$ip" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}-[0-9]{1,3}(\.[0-9]{1,3}){3}$'; then + echo "$ip" | range2cidr >> "$1" + else + logger -t geoguard "white format skip: $ip" + fi + done + sort -u "$1" -o "$1" + grep -E '^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])/([1-9]|[12][0-9]|3[0-2])$' "$1" > "$1.clean" 2>/dev/null + mv "$1.clean" "$1" +} + +TEMP_ALLOW="$TEMP_DIR/merged.tmp" +> "$TEMP_ALLOW" +for cc in $SELECTED; do + [ -z "$cc" ] && continue + case "$cc" in [a-z][a-z]) ;; *) logger -t geoguard "bad country code, skip: $cc"; continue ;; esac + [ -f "$BASE_DIR/${cc}.cidr" ] && cat "$BASE_DIR/${cc}.cidr" >> "$TEMP_ALLOW" +done +build_white "$TEMP_DIR/white.tmp" +cat "$TEMP_DIR/white.tmp" >> "$TEMP_ALLOW" + +[ -s "$TEMP_ALLOW" ] || { logger -t geoguard "no data, skip"; hist "merged" "fail:nodata"; exit 1; } +sort -u "$TEMP_ALLOW" > "$TEMP_DIR/merged.sorted" + grep -E '^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])/([1-9]|[12][0-9]|3[0-2])$' "$TEMP_DIR/merged.sorted" > "$TEMP_DIR/merged.clean" +[ -s "$TEMP_DIR/merged.clean" ] || { logger -t geoguard "merged empty, skip"; exit 1; } + +# Desired sets: name|file|comment format +DESIRED="" +for cc in $SELECTED; do + [ -z "$cc" ] && continue + case "$cc" in [a-z][a-z]) ;; *) logger -t geoguard "bad country code, skip: $cc"; continue ;; esac + if [ -s "$BASE_DIR/${cc}.cidr" ]; then + DESIRED="$DESIRED ${cc}:$BASE_DIR/${cc}.cidr" + else + logger -t geoguard "country file missing, skip set: $cc" + fi +done +DESIRED="$DESIRED ${SETNAME}:$OUTPUT" +if [ -s "$TEMP_DIR/white.tmp" ]; then + DESIRED="$DESIRED ${WHITENAME}:$WOUTPUT" +else + rm -f "$WOUTPUT" +fi + +changed_files=0 +if [ ! -f "$OUTPUT" ] || ! cmp -s "$TEMP_DIR/merged.clean" "$OUTPUT"; then + mv -f "$TEMP_DIR/merged.clean" "$OUTPUT" + changed_files=1 +fi +if [ -s "$TEMP_DIR/white.tmp" ]; then + if [ ! -f "$WOUTPUT" ] || ! cmp -s "$TEMP_DIR/white.tmp" "$WOUTPUT"; then + mv -f "$TEMP_DIR/white.tmp" "$WOUTPUT" + changed_files=1 + fi +fi + +comment_for() { + case "$1" in + "$SETNAME") echo "Merged allow IPs" ;; + "$WHITENAME") echo "Custom allow IPs" ;; + *) echo "$(cc_en "$1") IPs" ;; + esac +} + +sync_one() { + local name="$1" file="$2" comment idx sec nm found + comment="$(comment_for "$name")" + for idx in $(uci show firewall 2>/dev/null | grep '=ipset$' | cut -d. -f2 | cut -d[ -f2 | cut -d] -f1 | sort -rn | uniq); do + case "$idx" in ''|*[!0-9]*) continue ;; esac + sec="firewall.@ipset[$idx]" + nm=$(uci -q get "$sec.name" 2>/dev/null) + if [ "$nm" = "$name" ]; then + if [ -z "$found" ]; then + found="$sec" + else + uci delete "$sec" + NEED_RELOAD=1 + logger -t geoguard "dup section removed: $name" + fi + fi + done + if [ -z "$found" ]; then + found="firewall.$(uci add firewall ipset)" + NEED_RELOAD=1 + fi + if [ "$(uci -q get $found.name)" != "$name" ] || \ + [ "$(uci -q get $found.loadfile)" != "$file" ] || \ + [ "$(uci -q get $found.family)" != "ipv4" ] || \ + [ "$(uci -q get $found.match)" != "src_net" ] || \ + [ "$(uci -q get $found.comment)" != "$comment" ]; then + uci set $found.name="$name" + uci set $found.family='ipv4' + uci set $found.match='src_net' + uci set $found.loadfile="$file" + uci set $found.comment="$comment" + uci commit firewall + NEED_RELOAD=1 + fi +} + +NEED_RELOAD=0 +MANAGED=$(uci -q get geoguard.main.managed 2>/dev/null) +for item in $DESIRED; do + sync_one "${item%%:*}" "${item#*:}" +done +# rename repoint runs BEFORE stale cleanup so a renamed set is not +# mistaken for a still-referenced stale set (H1 guard below). +APPLIED_S=$(uci -q get geoguard.main.applied_name 2>/dev/null) +APPLIED_W=$(uci -q get geoguard.main.applied_white 2>/dev/null) +if [ -n "$APPLIED_S" ] && [ "$APPLIED_S" != "$SETNAME" ]; then + for rp in $(uci show firewall 2>/dev/null | grep "\.ipset='$APPLIED_S'$" | cut -d= -f1); do + uci set "$rp"="$SETNAME" + NEED_RELOAD=1 + logger -t geoguard "rule repoint: $rp -> $SETNAME" + done +fi +if [ -n "$APPLIED_W" ] && [ "$APPLIED_W" != "$WHITENAME" ]; then + for rp in $(uci show firewall 2>/dev/null | grep "\.ipset='$APPLIED_W'$" | cut -d= -f1); do + uci set "$rp"="$WHITENAME" + NEED_RELOAD=1 + logger -t geoguard "rule repoint: $rp -> $WHITENAME" + done +fi +STALE_GONE="" +# Sweep every ipset section whose loadfile lives in our uploads dir. +# The old MANAGED-list loop could never see names that were never recorded +# (legacy/hand-made orphans); managed names are a subset of this sweep. +for idx in $(uci show firewall 2>/dev/null | grep '=ipset$' | cut -d. -f2 | cut -d[ -f2 | cut -d] -f1 | sort -rn | uniq); do + case "$idx" in ''|*[!0-9]*) continue ;; esac + sec="firewall.@ipset[$idx]" + nm=$(uci -q get "$sec.name" 2>/dev/null) + lf=$(uci -q get "$sec.loadfile" 2>/dev/null) + [ -z "$nm" ] && continue + case "$lf" in "$BASE_DIR/"*) ;; *) continue ;; esac + echo " $DESIRED " | grep -q " $nm:" && continue + if uci show firewall 2>/dev/null | grep -q "\.ipset='$nm'$"; then + # H1: still referenced by rules -> keep section, refuse silently-dangerous delete + logger -t geoguard "ERROR stale set $nm still referenced by rules, kept (re-bind rules first)" + hist "stale-kept|$nm referenced by rules" + else + nft flush set inet fw4 "$nm" 2>/dev/null + uci delete "$sec" + NEED_RELOAD=1 + STALE_GONE="$STALE_GONE $nm" + logger -t geoguard "stale section removed (live flushed): $nm" + fi +done +uci set geoguard.main.applied_name="$SETNAME" +uci set geoguard.main.applied_white="$WHITENAME" +uci delete geoguard.main.managed 2>/dev/null +for item in $DESIRED; do + uci add_list geoguard.main.managed="${item%%:*}" +done +uci commit geoguard +uci commit firewall + +if [ "$NEED_RELOAD" = "1" ]; then + logger -t geoguard "sections changed, firewall reload" + /etc/init.d/firewall reload >/dev/null 2>&1 +fi +# L5: drop orphan live-set shells left behind by stale cleanup +for gone in $STALE_GONE; do + [ -z "$gone" ] && continue + nft delete set inet fw4 "$gone" 2>/dev/null \ + && logger -t geoguard "orphan live set removed: $gone" +done +for item in $DESIRED; do + nm="${item%%:*}" + fl="${item#*:}" + if nft list set inet fw4 "$nm" >/dev/null 2>&1; then + # Atomic refresh: flush + refill in a single nft transaction, so + # readers never observe an empty set (refill takes minutes on mips). + # On any error the whole batch aborts and the old content stays. + { printf 'flush set inet fw4 %s;\n' "$nm"; + printf 'add element inet fw4 %s { ' "$nm"; + tr '\n' ',' < "$fl" | sed 's/,$//'; printf ' }\n'; } \ + | nft -f - 2>/dev/null || logger -t geoguard "live refresh failed: $nm" + fi +done +logger -t geoguard "done $SETNAME +$WHITENAME" +if [ "$changed_files" = "1" ]; then + hist "merged+white" "ok $(wc -l < "$OUTPUT" 2>/dev/null) lines, $(wc -c < "$OUTPUT" 2>/dev/null) B" +else + hist "merged+white" "no new updates at the moment" +fi diff --git a/applications/luci-app-geoguard/root/usr/share/luci/menu.d/luci-app-geoguard.json b/applications/luci-app-geoguard/root/usr/share/luci/menu.d/luci-app-geoguard.json new file mode 100644 index 000000000000..29585effdf14 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/share/luci/menu.d/luci-app-geoguard.json @@ -0,0 +1,13 @@ +{ + "admin/network/geoguard": { + "title": "GeoGuard", + "order": 55, + "action": { + "type": "view", + "path": "geoguard" + }, + "depends": { + "acl": [ "luci-app-geoguard" ] + } + } +} diff --git a/applications/luci-app-geoguard/root/usr/share/rpcd/acl.d/luci-app-geoguard.json b/applications/luci-app-geoguard/root/usr/share/rpcd/acl.d/luci-app-geoguard.json new file mode 100644 index 000000000000..b457ab1318e5 --- /dev/null +++ b/applications/luci-app-geoguard/root/usr/share/rpcd/acl.d/luci-app-geoguard.json @@ -0,0 +1,33 @@ +{ + "luci-app-geoguard": { + "description": "Grant access to GeoGuard", + "read": { + "uci": [ "geoguard" ], + "file": { + "/usr/bin/geoguard-status": [ "exec" ], + "/usr/bin/geoguard-counts": [ "exec" ], + "/usr/bin/geoguard-ban-status": [ "exec" ] + }, + "ubus": { + "file": [ "exec" ] + } + }, + "write": { + "uci": [ "geoguard" ], + "file": { + "/usr/bin/geoguard-fetch": [ "exec" ], + "/usr/bin/geoguard-update": [ "exec" ], + "/usr/bin/geoguard-cron": [ "exec" ], + "/usr/bin/geoguard-clear-history": [ "exec" ], + "/usr/bin/geoguard-ban-guard": [ "exec" ], + "/usr/bin/geoguard-ban-unban all": [ "exec" ], + "/etc/init.d/geoguard-ban reload": [ "exec" ], + "/etc/init.d/geoguard-ban enable": [ "exec" ], + "/etc/init.d/geoguard-ban disable": [ "exec" ] + }, + "ubus": { + "file": [ "exec" ] + } + } + } +}