mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 13:03:56 +08:00
🏅 Sync 2026-10-29 12:54:11
This commit is contained in:
1 parent
96c15c82a4
commit
5536ec1272
34 files changed
+579
-158
No files matched your search
+6
-11
@@ -8,13 +8,13 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=apfree-wifidog
|
||||
PKG_VERSION:=9.10.2968
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/liudf0716/apfree-wifidog.git
|
||||
PKG_SOURCE_DATE:=2026-10-25
|
||||
PKG_SOURCE_VERSION:=4075182ffedaf19b36926dc2f068fe413901aba3
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
@@ -23,11 +23,6 @@ PKG_LICENSE_FILES:=COPYING
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/cmake.mk
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) /media/windows/github/wifidogx/apfree-wifidog/* $(PKG_BUILD_DIR)/
|
||||
endef
|
||||
|
||||
define Package/apfree-wifidog
|
||||
SUBMENU:=Captive Portals
|
||||
SECTION:=net
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-honk
|
||||
PKG_VERSION:=1.1.4
|
||||
PKG_RELEASE:=27
|
||||
PKG_RELEASE:=28
|
||||
|
||||
LUCI_TITLE:=LuCI support for honk
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
@@ -285,6 +285,9 @@ msgstr "选择当前生效的 Web 管理面板。"
|
||||
msgid "Doona"
|
||||
msgstr "Doona"
|
||||
|
||||
msgid "Doona Lite"
|
||||
msgstr "Doona Lite"
|
||||
|
||||
msgid "Native API is configured, but dashboard files are missing in the UI directory. You can download and deploy it directly."
|
||||
msgstr "Native API 已配置,但 UI 目录中缺少面板文件。您可以直接下载并部署。"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
TARGET_DIR="${1:-}"
|
||||
DOWNLOAD_URL="${2:-}"
|
||||
TARGET_TYPE="${3:-}"
|
||||
|
||||
LOG_FILE="/tmp/honk_dashboard_download.log"
|
||||
STATUS_FILE="/tmp/honk_dashboard_download.status"
|
||||
@@ -21,7 +22,7 @@ set_status() {
|
||||
: > "$LOG_FILE"
|
||||
|
||||
if [ -z "$TARGET_DIR" ] || [ -z "$DOWNLOAD_URL" ]; then
|
||||
log "Error: usage: $0 <target-dir> <archive-url>"
|
||||
log "Error: usage: $0 <target-dir> <archive-url> [target-type]"
|
||||
set_status "FAILED"
|
||||
exit 1
|
||||
fi
|
||||
@@ -61,6 +62,14 @@ case "$DOWNLOAD_URL" in
|
||||
PREFIX="${DOWNLOAD_URL%%https://github.com/*}"
|
||||
log "Checking latest release..."
|
||||
|
||||
IS_LITE=0
|
||||
case "$TARGET_TYPE" in
|
||||
*lite*) IS_LITE=1 ;;
|
||||
esac
|
||||
case "$TARGET_DIR" in
|
||||
*lite*) IS_LITE=1 ;;
|
||||
esac
|
||||
|
||||
RESOLVED_URL=""
|
||||
MAX_RETRIES=3
|
||||
attempt=1
|
||||
@@ -75,15 +84,26 @@ case "$DOWNLOAD_URL" in
|
||||
|
||||
if [ -n "$LATEST_TAG" ]; then
|
||||
log "Latest tag: $LATEST_TAG"
|
||||
TAG_NO_V="${LATEST_TAG#[vV]}"
|
||||
ASSETS_HTML=$(fetch_text "https://github.com/Zakkaus/doona/releases/expanded_assets/$LATEST_TAG")
|
||||
ASSET_PATH=$(printf "%s" "$ASSETS_HTML" | grep -o '/Zakkaus/doona/releases/download/[^"]*/doona-[^"]*\.tar\.gz' | grep -v 'fonts' | head -n 1)
|
||||
if [ -n "$ASSET_PATH" ]; then
|
||||
RESOLVED_URL="https://github.com${ASSET_PATH}"
|
||||
log "Found package: $ASSET_PATH"
|
||||
if [ "$IS_LITE" -eq 1 ]; then
|
||||
ASSET_PATH=$(printf "%s" "$ASSETS_HTML" | grep -o '/Zakkaus/doona/releases/download/[^"]*/doona-lite-[^"]*\.tar\.gz' | head -n 1)
|
||||
if [ -n "$ASSET_PATH" ]; then
|
||||
RESOLVED_URL="https://github.com${ASSET_PATH}"
|
||||
log "Found package: $ASSET_PATH"
|
||||
else
|
||||
RESOLVED_URL="https://github.com/Zakkaus/doona/releases/download/${LATEST_TAG}/doona-lite-${TAG_NO_V}.tar.gz"
|
||||
log "Inferred package URL: $RESOLVED_URL"
|
||||
fi
|
||||
else
|
||||
TAG_NO_V="${LATEST_TAG#[vV]}"
|
||||
RESOLVED_URL="https://github.com/Zakkaus/doona/releases/download/${LATEST_TAG}/doona-${TAG_NO_V}.tar.gz"
|
||||
log "Inferred package URL: $RESOLVED_URL"
|
||||
ASSET_PATH=$(printf "%s" "$ASSETS_HTML" | grep -o '/Zakkaus/doona/releases/download/[^"]*/doona-[^"]*\.tar\.gz' | grep -v 'fonts' | grep -v 'lite' | grep -v 'precompressed' | head -n 1)
|
||||
if [ -n "$ASSET_PATH" ]; then
|
||||
RESOLVED_URL="https://github.com${ASSET_PATH}"
|
||||
log "Found package: $ASSET_PATH"
|
||||
else
|
||||
RESOLVED_URL="https://github.com/Zakkaus/doona/releases/download/${LATEST_TAG}/doona-${TAG_NO_V}.tar.gz"
|
||||
log "Inferred package URL: $RESOLVED_URL"
|
||||
fi
|
||||
fi
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"/etc/honk/config.dae",
|
||||
"/etc/honk/config.d/*",
|
||||
"/etc/honk/doona/*",
|
||||
"/etc/honk/doona-lite/*",
|
||||
"/etc/honk/zashboard/*"
|
||||
]
|
||||
},
|
||||
@@ -22,6 +23,7 @@
|
||||
"/etc/honk/config.dae",
|
||||
"/etc/honk/config.d/*",
|
||||
"/etc/honk/doona/*",
|
||||
"/etc/honk/doona-lite/*",
|
||||
"/etc/honk/zashboard/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,17 +11,35 @@ const DEFAULT_SECRET = "honk114514";
|
||||
|
||||
const DASHBOARD_DIRS = {
|
||||
zashboard: "/etc/honk/zashboard",
|
||||
doona: "/etc/honk/doona"
|
||||
doona: "/etc/honk/doona",
|
||||
doona_lite: "/etc/honk/doona-lite"
|
||||
};
|
||||
|
||||
function normalize_dashboard_type(type) {
|
||||
if (type == "doona-lite")
|
||||
return "doona_lite";
|
||||
return type;
|
||||
}
|
||||
|
||||
function get_dashboard_dir(type) {
|
||||
type = normalize_dashboard_type(type);
|
||||
return DASHBOARD_DIRS[type] || DASHBOARD_DIRS.doona;
|
||||
}
|
||||
|
||||
function is_other_default_ui(ui_path, current_type) {
|
||||
current_type = normalize_dashboard_type(current_type);
|
||||
for (let t, dir in DASHBOARD_DIRS) {
|
||||
if (t != current_type && dir == ui_path)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function is_valid_dashboard_type(type) {
|
||||
if (type == "none")
|
||||
return true;
|
||||
|
||||
type = normalize_dashboard_type(type);
|
||||
for (let known in keys(DASHBOARD_DIRS))
|
||||
if (known == type)
|
||||
return true;
|
||||
@@ -328,8 +346,6 @@ function get_dashboard_info(req) {
|
||||
}
|
||||
|
||||
let target_default_ui = get_dashboard_dir(requested_type);
|
||||
let other_type = (requested_type == "zashboard") ? "doona" : "zashboard";
|
||||
let other_default_ui = get_dashboard_dir(other_type);
|
||||
|
||||
if (parsed_native && parsed_native.enabled && parsed_native.listen) {
|
||||
res.external_controller = parsed_native.listen;
|
||||
@@ -337,7 +353,7 @@ function get_dashboard_info(req) {
|
||||
res.port = parsed_native.port || DEFAULT_PORT;
|
||||
res.secret = parsed_native.secret;
|
||||
|
||||
res.external_ui = (parsed_native.ui && parsed_native.ui != other_default_ui) ? parsed_native.ui : target_default_ui;
|
||||
res.external_ui = (parsed_native.ui && !is_other_default_ui(parsed_native.ui, requested_type)) ? parsed_native.ui : target_default_ui;
|
||||
|
||||
let index_path = res.external_ui + "/index.html";
|
||||
let s = stat(index_path);
|
||||
@@ -345,7 +361,7 @@ function get_dashboard_info(req) {
|
||||
|
||||
if (!res.has_ui) {
|
||||
res.configured = true;
|
||||
} else if (parsed_native.ui && parsed_native.ui != other_default_ui) {
|
||||
} else if (parsed_native.ui && !is_other_default_ui(parsed_native.ui, requested_type)) {
|
||||
res.configured = true;
|
||||
} else {
|
||||
res.configured = false;
|
||||
@@ -363,6 +379,7 @@ function download_dashboard(req) {
|
||||
if (!is_valid_dashboard_type(requested_type) || requested_type == "none") {
|
||||
requested_type = "doona";
|
||||
}
|
||||
requested_type = normalize_dashboard_type(requested_type);
|
||||
|
||||
let url = (req && req.args && req.args.url) ? req.args.url : "";
|
||||
if (!match(url, /^https?:\/\//) || match(url, /[ \t\r\n'"`]/)) {
|
||||
@@ -371,10 +388,8 @@ function download_dashboard(req) {
|
||||
|
||||
let api_cfg = get_api_config();
|
||||
let parsed_native = api_cfg.parsed_native;
|
||||
let other_type = (requested_type == "zashboard") ? "doona" : "zashboard";
|
||||
let other_default_ui = get_dashboard_dir(other_type);
|
||||
let target_dir = get_dashboard_dir(requested_type);
|
||||
if (parsed_native && parsed_native.ui && parsed_native.ui != other_default_ui &&
|
||||
if (parsed_native && parsed_native.ui && !is_other_default_ui(parsed_native.ui, requested_type) &&
|
||||
is_safe_ui_dir(parsed_native.ui)) {
|
||||
target_dir = parsed_native.ui;
|
||||
}
|
||||
@@ -391,8 +406,9 @@ function download_dashboard(req) {
|
||||
let safe_script = replace(script, "'", "'\\''");
|
||||
let safe_target = replace(target_dir, "'", "'\\''");
|
||||
let safe_url = replace(url, "'", "'\\''");
|
||||
let safe_type = replace(requested_type, "'", "'\\''");
|
||||
|
||||
let cmd = sprintf("/bin/sh '%s' '%s' '%s' >/dev/null 2>&1 &", safe_script, safe_target, safe_url);
|
||||
let cmd = sprintf("/bin/sh '%s' '%s' '%s' '%s' >/dev/null 2>&1 &", safe_script, safe_target, safe_url, safe_type);
|
||||
let rc = system(cmd);
|
||||
if (rc != 0)
|
||||
return { success: false, message: "failed to dispatch the download task" };
|
||||
@@ -403,6 +419,7 @@ function download_dashboard(req) {
|
||||
function switch_dashboard_api(target_type) {
|
||||
cached_pid = null;
|
||||
|
||||
target_type = normalize_dashboard_type(target_type);
|
||||
if (!is_valid_dashboard_type(target_type))
|
||||
return { success: false, message: "Invalid dashboard type" };
|
||||
|
||||
|
||||
@@ -50,6 +50,25 @@ var DASHBOARD_PROFILES = {
|
||||
ghfastMirror: 'https://ghfast.top/https://github.com/Zakkaus/doona',
|
||||
ghproxyMirror: 'https://ghproxy.net/https://github.com/Zakkaus/doona',
|
||||
pkgName: 'doona-*.tar.gz'
|
||||
},
|
||||
doona_lite: {
|
||||
name: 'Doona Lite',
|
||||
label: function() { return _('Doona Lite'); },
|
||||
apiName: 'Native API',
|
||||
defaultPort: DEFAULT_PORT,
|
||||
defaultDir: '/etc/honk/doona-lite',
|
||||
buildUrl: function(info, targetHost, port, secret, protocol, forceFresh) {
|
||||
var hostPart = (targetHost.indexOf(':') !== -1 && targetHost.charAt(0) !== '[') ? '[' + targetHost + ']' : targetHost;
|
||||
var url = protocol + '://' + hostPart + ':' + port + '/ui/';
|
||||
if (forceFresh) {
|
||||
url += '?_t=' + Date.now();
|
||||
}
|
||||
return url;
|
||||
},
|
||||
githubRelease: 'https://github.com/Zakkaus/doona',
|
||||
ghfastMirror: 'https://ghfast.top/https://github.com/Zakkaus/doona',
|
||||
ghproxyMirror: 'https://ghproxy.net/https://github.com/Zakkaus/doona',
|
||||
pkgName: 'doona-lite-*.tar.gz'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,6 +80,8 @@ function exampleConfig(profile) {
|
||||
}
|
||||
|
||||
function getProfile(type) {
|
||||
if (type === 'doona-lite')
|
||||
type = 'doona_lite';
|
||||
return DASHBOARD_PROFILES[type] || DASHBOARD_PROFILES.zashboard;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.10.29
|
||||
PKG_RELEASE:=354
|
||||
PKG_RELEASE:=355
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -113,7 +113,7 @@ uci -q commit passwall
|
||||
|
||||
sed -i "s#add_from#group#g" /etc/config/passwall 2>/dev/null
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -f /tmp/etc/passwall_tmp/binver_* /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
killall -HUP rpcd 2>/dev/null
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.10.29
|
||||
PKG_RELEASE:=155
|
||||
PKG_RELEASE:=156
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -131,6 +131,16 @@ if api.is_finded("geoview") then
|
||||
.. "<li>" .. translate("Analyzes and preloads GeoIP data to enhance the shunt performance.") .. "</li>"
|
||||
.. "<li>" .. translate("Note: Increases resource usage.") .. " " .. translate("Maybe conflict with some special circumstances.") .. "</li>"
|
||||
.. "</ul>"
|
||||
-- The preloaded NFTSet/IPSet are kept across restarts, so they must be
|
||||
-- flushed when this option changes, otherwise stale GeoIP entries stay.
|
||||
local write = o.write
|
||||
o.write = function(self, section, value)
|
||||
local old = m:get(current_node_id, self.option) or "0"
|
||||
write(self, section, value)
|
||||
if (m:get(current_node_id, self.option) or "0") ~= old then
|
||||
m:set("@global[0]", "flush_set", "1")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shunt_group = add_option(ListValue, "shunt_group", translate("Shunt Rule Group"))
|
||||
|
||||
@@ -42,7 +42,7 @@ uci commit firewall
|
||||
|
||||
chmod +x /usr/share/passwall2/*.sh
|
||||
|
||||
rm -f /tmp/etc/passwall2_tmp/passwall2_version /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -f /tmp/etc/passwall2_tmp/passwall2_version /tmp/etc/passwall2_tmp/binver_* /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
killall -HUP rpcd 2>/dev/null
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wwand
|
||||
PKG_VERSION:=1.8.0_pre1
|
||||
PKG_RELEASE:=46
|
||||
PKG_VERSION:=1.8.0_pre3
|
||||
PKG_RELEASE:=47
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
||||
PKG_SOURCE_VERSION:=9101de373fe07ae2c52f171639cc6e05d689cd59
|
||||
PKG_SOURCE_VERSION:=9bf66bcf76fa083eb3fff7ed1a6d266a26f871a4
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +USE_APK:wget-any +!USE_APK:wget +jsonfilter +ucode-mod
|
||||
# Keep existing translation package names and the argon-config message domain.
|
||||
LUCI_BASENAME:=argon-config
|
||||
PKG_VERSION:=3.1.1
|
||||
PKG_RELEASE:=38
|
||||
PKG_RELEASE:=39
|
||||
PKG_LICENSE:=Apache-2.0 MIT
|
||||
|
||||
# LuCI derives translations independently of the theme version. Keep release
|
||||
|
||||
@@ -7389,6 +7389,39 @@ pre.command-output {
|
||||
#cbi-dockerd .cbi-section.cbi-tblsection {
|
||||
padding: 0;
|
||||
}
|
||||
[data-page="admin-docker"] #docker_apply_overlay,
|
||||
[data-page^="admin-docker-"] #docker_apply_overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
padding: var(--page-gutter);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
[data-page="admin-docker"] #docker_apply_overlay > .alert-message,
|
||||
[data-page^="admin-docker-"] #docker_apply_overlay > .alert-message {
|
||||
position: static;
|
||||
inset: auto;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
max-width: 60rem;
|
||||
max-height: calc(100vh - 2 * var(--page-gutter));
|
||||
max-height: calc(100dvh - 2 * var(--page-gutter));
|
||||
min-width: 0;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
[data-page="admin-docker"] #docker_apply_overlay > .alert-message > span,
|
||||
[data-page^="admin-docker-"] #docker_apply_overlay > .alert-message > span {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
[data-page="admin-docker"].apply-overlay-active #docker_apply_overlay,
|
||||
[data-page^="admin-docker-"].apply-overlay-active #docker_apply_overlay {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#cbi-smartdns #service_status {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -8306,20 +8339,20 @@ pre.command-output {
|
||||
flex-direction: column;
|
||||
gap: var(--field-gap);
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] #app_version > .cbi-value > .cbi-value-field,
|
||||
[data-page^="admin-vpn-passwall-"] #app_version > .cbi-value > .cbi-value-field,
|
||||
[data-page="admin-services-passwall"] #app_version > .cbi-value > .cbi-value-field,
|
||||
[data-page^="admin-services-passwall-"] #app_version > .cbi-value > .cbi-value-field {
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]),
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]),
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]),
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 10rem) auto;
|
||||
grid-template-columns: minmax(0, 10rem) max-content max-content minmax(0, 1fr);
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: var(--control-gap) var(--action-gap);
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] #app_version > .cbi-value > .cbi-value-field > span:first-child,
|
||||
[data-page^="admin-vpn-passwall-"] #app_version > .cbi-value > .cbi-value-field > span:first-child,
|
||||
[data-page="admin-services-passwall"] #app_version > .cbi-value > .cbi-value-field > span:first-child,
|
||||
[data-page^="admin-services-passwall-"] #app_version > .cbi-value > .cbi-value-field > span:first-child {
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > span:first-child,
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span:first-child,
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > span:first-child,
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span:first-child {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
@@ -8327,30 +8360,77 @@ pre.command-output {
|
||||
overflow-wrap: anywhere;
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] #app_version > .cbi-value > .cbi-value-field > input[data-update-app],
|
||||
[data-page^="admin-vpn-passwall-"] #app_version > .cbi-value > .cbi-value-field > input[data-update-app],
|
||||
[data-page="admin-services-passwall"] #app_version > .cbi-value > .cbi-value-field > input[data-update-app],
|
||||
[data-page^="admin-services-passwall-"] #app_version > .cbi-value > .cbi-value-field > input[data-update-app] {
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app],
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app],
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app],
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app] {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: start;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"],
|
||||
[data-page^="admin-vpn-passwall-"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"],
|
||||
[data-page="admin-services-passwall"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"],
|
||||
[data-page^="admin-services-passwall-"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"] {
|
||||
grid-column: 1 / -1;
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"],
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"],
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"],
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"] {
|
||||
grid-column: 3;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"],
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"],
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"],
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"] {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"]:empty,
|
||||
[data-page^="admin-vpn-passwall-"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"]:empty,
|
||||
[data-page="admin-services-passwall"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"]:empty,
|
||||
[data-page^="admin-services-passwall-"] #app_version > .cbi-value > .cbi-value-field > span[id$="-detail"]:empty {
|
||||
display: none;
|
||||
}
|
||||
@container (max-width: 40rem) {
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]),
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]),
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]),
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--control-gap) var(--action-gap);
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > span:first-child,
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span:first-child,
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > span:first-child,
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span:first-child {
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app],
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app],
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app],
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app] {
|
||||
flex: 0 0 auto;
|
||||
min-width: max-content;
|
||||
max-width: 100%;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"],
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"],
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"],
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > input[data-update-app][id$="-force_btn"] {
|
||||
grid-column: auto;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"],
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"],
|
||||
[data-page="admin-services-passwall"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"],
|
||||
[data-page^="admin-services-passwall-"] .cbi-value-field:has(> input[data-update-app]) > span[id$="-detail"] {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
}
|
||||
[data-page="admin-vpn-passwall"] .cbi-value:has(> .cbi-value-field > .share-toolbar) > .cbi-value-title,
|
||||
[data-page^="admin-vpn-passwall-"] .cbi-value:has(> .cbi-value-field > .share-toolbar) > .cbi-value-title,
|
||||
[data-page="admin-services-passwall"] .cbi-value:has(> .cbi-value-field > .share-toolbar) > .cbi-value-title,
|
||||
|
||||
@@ -30,3 +30,41 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Dockerman appends its own overlay styles after the theme. Scope these rules
|
||||
// to its pages so positioning wins without changing the native show/hide state.
|
||||
[data-page="admin-docker"],
|
||||
[data-page^="admin-docker-"] {
|
||||
#docker_apply_overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
padding: var(--page-gutter);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
> .alert-message {
|
||||
position: static;
|
||||
inset: auto;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
max-width: 60rem;
|
||||
max-height: calc(100vh - 2 * var(--page-gutter));
|
||||
max-height: calc(100dvh - 2 * var(--page-gutter));
|
||||
min-width: 0;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
|
||||
> span {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.apply-overlay-active #docker_apply_overlay {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -193,41 +193,47 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--field-gap);
|
||||
}
|
||||
|
||||
> .cbi-value > .cbi-value-field {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 10rem) auto;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: var(--control-gap) var(--action-gap);
|
||||
// Match update fields even when a plugin version adds a section wrapper.
|
||||
.cbi-value-field:has(> input[data-update-app]) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 10rem) max-content max-content minmax(0, 1fr);
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: var(--control-gap) var(--action-gap);
|
||||
|
||||
> span:first-child {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
> span:first-child {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
> input[data-update-app] {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
max-width: 100%;
|
||||
}
|
||||
> input[data-update-app] {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: start;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
|
||||
> span[id$="-detail"] {
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
&[id$="-force_btn"] {
|
||||
grid-column: 3;
|
||||
}
|
||||
}
|
||||
|
||||
> span[id$="-detail"] {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@container (max-width: 40rem) {
|
||||
@@ -235,8 +241,39 @@
|
||||
[data-page^="admin-vpn-passwall-"],
|
||||
[data-page="admin-services-passwall"],
|
||||
[data-page^="admin-services-passwall-"] {
|
||||
// Its label is an ideographic space in the native template; when
|
||||
// fields stack, it otherwise leaves a blank line above the actions.
|
||||
.cbi-value-field:has(> input[data-update-app]) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--control-gap) var(--action-gap);
|
||||
|
||||
> span:first-child {
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
> input[data-update-app] {
|
||||
flex: 0 0 auto;
|
||||
min-width: max-content;
|
||||
max-width: 100%;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
|
||||
&[id$="-force_btn"] {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> span[id$="-detail"] {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.cbi-value:has(> .cbi-value-field > .share-toolbar) > .cbi-value-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=minigate
|
||||
PKG_VERSION:=2026.10.28-2
|
||||
PKG_RELEASE:=9
|
||||
PKG_VERSION:=2026.10.29
|
||||
PKG_RELEASE:=10
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_MAINTAINER:=MiniGate
|
||||
|
||||
|
||||
+20
-15
@@ -2,9 +2,9 @@
|
||||
|
||||
一个类似 Lucky 的轻量级 OpenWrt 应用,提供四大核心功能:DDNS、SSL 证书、反向代理、登录防护。
|
||||
|
||||
当前版本:**v2026.10.28-2**。界面名称为 **MiniGate**,仓库名及 OpenWrt 内部包名均为 **`minigate`**。
|
||||
当前版本:**v2026.10.29**。界面名称为 **MiniGate**,仓库名及 OpenWrt 内部包名均为 **`minigate`**。
|
||||
|
||||
> OpenWrt 用户请从 [Releases](https://github.com/tpxcer/minigate/releases/latest) 下载 `minigate_2026.10.28-2_all.ipk`。不要把 GitHub 自动生成的 `Source code (zip)` / `Source code (tar.gz)` 当安装包上传到 LuCI。
|
||||
> OpenWrt 用户请从 [Releases](https://github.com/tpxcer/minigate/releases/latest) 下载 `minigate_2026.10.29_all.ipk`。不要把 GitHub 自动生成的 `Source code (zip)` / `Source code (tar.gz)` 当安装包上传到 LuCI。
|
||||
|
||||
## 项目定位
|
||||
|
||||
@@ -78,6 +78,7 @@ MiniGate 面向需要自托管轻量网关能力的 OpenWrt / ImmortalWrt 用户
|
||||
- 持久化存储 `/etc/minigate/login-guard/bans.txt`,重启/固件升级后已生效封禁自动恢复
|
||||
- LuCI 页面实时显示封禁列表、剩余时间、归属地(结合归属地查询 API)
|
||||
- 失败计数列表默认显示 5 条,支持切换 5 / 20 / 30 条,并复用访客追踪归属地查询;显示最近访问时间并按最新时间排序
|
||||
- 封禁列表和失败计数列表显示**访问类型**:SSH、LuCI(含经反代登录)或手动封禁;升级前的旧记录显示「未记录」
|
||||
- 支持网页**一键解封**、**手动封禁**、**清空全部**(清空按钮在列表底部,避免误点)
|
||||
- 封禁集合与拦截规则随包放在 `/usr/share/nftables.d`,防火墙每次重载自动加载;重载清空的封禁 30 秒内从持久化文件恢复
|
||||
- 由 procd 管理,异常退出后自动拉起;停止时结束所有子进程
|
||||
@@ -102,20 +103,20 @@ MiniGate 面向需要自托管轻量网关能力的 OpenWrt / ImmortalWrt 用户
|
||||
|
||||
从 [Releases](https://github.com/tpxcer/minigate/releases/latest) 下载:
|
||||
|
||||
`minigate_2026.10.28-2_all.ipk`
|
||||
`minigate_2026.10.29_all.ipk`
|
||||
|
||||
**通过 LuCI 界面安装:**
|
||||
1. 打开 LuCI → **系统** → **软件包**
|
||||
2. 点击 **上传软件包**
|
||||
3. 选择 `minigate_2026.10.28-2_all.ipk`,点击安装
|
||||
3. 选择 `minigate_2026.10.29_all.ipk`,点击安装
|
||||
|
||||
**通过命令行安装:**
|
||||
|
||||
```bash
|
||||
cd /tmp
|
||||
wget -O minigate_2026.10.28-2_all.ipk https://github.com/tpxcer/minigate/releases/download/v2026.10.28-2/minigate_2026.10.28-2_all.ipk
|
||||
wget -O minigate_2026.10.29_all.ipk https://github.com/tpxcer/minigate/releases/download/v2026.10.29/minigate_2026.10.29_all.ipk
|
||||
opkg update
|
||||
opkg install /tmp/minigate_2026.10.28-2_all.ipk
|
||||
opkg install /tmp/minigate_2026.10.29_all.ipk
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache 2>/dev/null
|
||||
/etc/init.d/uhttpd restart
|
||||
/etc/init.d/minigate restart
|
||||
@@ -134,7 +135,7 @@ Release 用的 `.ipk` 请用仓库脚本生成,避免生成 Debian 风格 ar
|
||||
脚本会输出:
|
||||
|
||||
```text
|
||||
dist/minigate_2026.10.28-2_all.ipk
|
||||
dist/minigate_2026.10.29_all.ipk
|
||||
```
|
||||
|
||||
该文件外层是 OpenWrt/ImmortalWrt 24.10 兼容的 `tar.gz`,内部成员顺序为 `debian-binary`、`control.tar.gz`、`data.tar.gz`。
|
||||
@@ -162,7 +163,7 @@ wget -qO- https://raw.githubusercontent.com/tpxcer/minigate/main/scripts/install
|
||||
需要指定并核对具体版本时,执行:
|
||||
|
||||
```bash
|
||||
d="$(mktemp -d /tmp/minigate.XXXXXX)" && cd "$d" && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.28-2/minigate-v2026.10.28-2-src.tar.gz && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.28-2/SHA256SUMS && grep 'minigate-v2026.10.28-2-src.tar.gz$' SHA256SUMS | sha256sum -c - && tar xzf minigate-v2026.10.28-2-src.tar.gz && sh install.sh && /etc/init.d/uhttpd restart && /etc/init.d/minigate restart
|
||||
d="$(mktemp -d /tmp/minigate.XXXXXX)" && cd "$d" && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.29/minigate-v2026.10.29-src.tar.gz && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.29/SHA256SUMS && grep 'minigate-v2026.10.29-src.tar.gz$' SHA256SUMS | sha256sum -c - && tar xzf minigate-v2026.10.29-src.tar.gz && sh install.sh && /etc/init.d/uhttpd restart && /etc/init.d/minigate restart
|
||||
```
|
||||
|
||||
其中 `minigate.XXXXXX` 是临时目录模板,系统会自动把六个 `X` 替换为随机字符,避免与已有目录重名。该命令会校验源码包的 SHA-256、保留现有 minigate 配置并重启 LuCI 与 minigate 服务。
|
||||
@@ -177,14 +178,14 @@ ssh root@192.168.1.1
|
||||
|
||||
# 2. 在路由器上直接下载 Release 源码包
|
||||
cd /tmp
|
||||
wget -O minigate-src.tar.gz https://github.com/tpxcer/minigate/releases/download/v2026.10.28-2/minigate-v2026.10.28-2-src.tar.gz
|
||||
wget -O minigate-SHA256SUMS https://github.com/tpxcer/minigate/releases/download/v2026.10.28-2/SHA256SUMS
|
||||
awk '$2 == "minigate-v2026.10.28-2-src.tar.gz" {print $1 " minigate-src.tar.gz"}' minigate-SHA256SUMS | sha256sum -c - || exit 1
|
||||
wget -O minigate-src.tar.gz https://github.com/tpxcer/minigate/releases/download/v2026.10.29/minigate-v2026.10.29-src.tar.gz
|
||||
wget -O minigate-SHA256SUMS https://github.com/tpxcer/minigate/releases/download/v2026.10.29/SHA256SUMS
|
||||
awk '$2 == "minigate-v2026.10.29-src.tar.gz" {print $1 " minigate-src.tar.gz"}' minigate-SHA256SUMS | sha256sum -c - || exit 1
|
||||
|
||||
# 3. 解压到独立目录并安装
|
||||
mkdir -p /tmp/minigate-v2026.10.28-2
|
||||
tar xzf minigate-src.tar.gz -C /tmp/minigate-v2026.10.28-2
|
||||
cd /tmp/minigate-v2026.10.28-2
|
||||
mkdir -p /tmp/minigate-v2026.10.29
|
||||
tar xzf minigate-src.tar.gz -C /tmp/minigate-v2026.10.29
|
||||
cd /tmp/minigate-v2026.10.29
|
||||
sh install.sh
|
||||
|
||||
# 4. 启动服务
|
||||
@@ -234,7 +235,7 @@ SSH 登录后执行上方「方法 2」的一条命令,保留原有配置并
|
||||
### IPK 升级
|
||||
|
||||
```bash
|
||||
opkg install --force-reinstall /tmp/minigate_2026.10.28-2_all.ipk
|
||||
opkg install --force-reinstall /tmp/minigate_2026.10.29_all.ipk
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache 2>/dev/null
|
||||
/etc/init.d/minigate restart
|
||||
```
|
||||
@@ -425,6 +426,10 @@ minigate/
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v2026.10.29
|
||||
- 登录防护的「已封禁 IP 列表」和「失败计数中」新增「访问类型」列,区分 SSH、LuCI(直接登录或经反代登录)和手动封禁;升级前已有的封禁没有类型,显示「未记录」
|
||||
- 封禁文件和失败计数文件以可选的第三列保存类型,旧格式照常可读;防火墙重载后恢复封禁时类型保持不变;登录防护日志的失败和封禁记录也带上类型
|
||||
|
||||
### v2026.10.28-2
|
||||
- 修复 v2026.10.28 引入的登录防护回归:达到失败阈值时,封禁地址被修复防火墙的读取过程清空,日志显示「封禁 失败」,新的攻击来源不会被封禁(已有封禁不受影响)。升级后会立即恢复正常封禁
|
||||
|
||||
|
||||
@@ -740,6 +740,19 @@ local function valid_ban_ip(ip)
|
||||
return #ip <= 39 and ip:match("^[%x:]+$") ~= nil and select(2, ip:gsub(":", "")) >= 2
|
||||
end
|
||||
|
||||
-- bans.txt 每行「IP 到期时间 [访问类型]」;旧版记录没有类型
|
||||
local function ban_kinds()
|
||||
local kinds = {}
|
||||
local file = io.open("/etc/minigate/login-guard/bans.txt", "r")
|
||||
if not file then return kinds end
|
||||
for line in file:lines() do
|
||||
local ip, kind = line:match("^(%S+)%s+%d+%s+(%a+)")
|
||||
if ip then kinds[ip] = kind end
|
||||
end
|
||||
file:close()
|
||||
return kinds
|
||||
end
|
||||
|
||||
local function parse_ban_set(raw, banned)
|
||||
-- nft -j 在 key 和 value 之间有空格,要用 %s* 匹配;按 elem 块配对 val 与 expires
|
||||
for block in raw:gmatch('{[^{}]-"val"%s*:%s*"[^"]+"[^{}]-}') do
|
||||
@@ -775,6 +788,8 @@ function action_lg_status()
|
||||
local banned = {}
|
||||
parse_ban_set(sys.exec("nft -j list set inet fw4 login_banned_v4 2>/dev/null") or "", banned)
|
||||
parse_ban_set(sys.exec("nft -j list set inet fw4 login_banned_v6 2>/dev/null") or "", banned)
|
||||
local kinds = ban_kinds()
|
||||
for _, item in ipairs(banned) do item.kind = kinds[item.ip] or "" end
|
||||
-- 按剩余时间倒序
|
||||
table.sort(banned, function(a,b) return a.remaining > b.remaining end)
|
||||
local banned_total = #banned
|
||||
@@ -794,7 +809,7 @@ function action_lg_status()
|
||||
if fh then
|
||||
local line = fh:read("*l") or ""
|
||||
fh:close()
|
||||
local first, count = line:match("(%d+)%s+(%d+)")
|
||||
local first, count, kind = line:match("^(%d+)%s+(%d+)%s*(%a*)")
|
||||
if first and count then
|
||||
local first_time = tonumber(first)
|
||||
local stat = fs.stat(path)
|
||||
@@ -803,6 +818,7 @@ function action_lg_status()
|
||||
if age <= window_s then
|
||||
watching[#watching+1] = {
|
||||
ip = ip,
|
||||
kind = kind,
|
||||
count = tonumber(count),
|
||||
age = age,
|
||||
first_time = first_time,
|
||||
|
||||
@@ -27,6 +27,12 @@ o.cfgvalue = function()
|
||||
return v
|
||||
end
|
||||
|
||||
local function kind_html(kind)
|
||||
local labels = {ssh = {"SSH", "ssh"}, luci = {"LuCI", "luci"}, manual = {"手动", "manual"}}
|
||||
local k = labels[kind or ""] or {"未记录", "none"}
|
||||
return '<span class="lg-kind lg-kind-' .. k[2] .. '">' .. k[1] .. '</span>'
|
||||
end
|
||||
|
||||
local function fmt_duration(s)
|
||||
s = tonumber(s) or 0
|
||||
if s >= 3600 then
|
||||
@@ -49,12 +55,13 @@ o.cfgvalue = function()
|
||||
if fh then
|
||||
local line = fh:read("*l") or ""
|
||||
fh:close()
|
||||
local first, count = line:match("^(%d+)%s+(%d+)")
|
||||
local first, count, kind = line:match("^(%d+)%s+(%d+)%s*(%a*)")
|
||||
if first and count then
|
||||
local stat = fs.stat(path)
|
||||
local last_time = (stat and stat.mtime) or tonumber(first)
|
||||
watching_items[#watching_items + 1] = {
|
||||
ip = ip,
|
||||
kind = kind,
|
||||
count = tonumber(count) or 0,
|
||||
age = now - tonumber(first),
|
||||
last_time = last_time,
|
||||
@@ -71,6 +78,7 @@ o.cfgvalue = function()
|
||||
local style = (i > 5) and ' style="display:none"' or ''
|
||||
watching_rows[#watching_rows + 1] =
|
||||
'<tr class="lg-watch-row" data-ip="' .. esc(item.ip) .. '" data-index="' .. tostring(i) .. '"' .. style .. '><td><code class="lg-ip">' .. esc(item.ip) .. '</code></td>' ..
|
||||
'<td>' .. kind_html(item.kind) .. '</td>' ..
|
||||
'<td id="lg-watch-geo-initial-' .. tostring(i) .. '"><span style="color:var(--lg-muted)">查询中...</span></td>' ..
|
||||
'<td><span style="font-weight:bold">' .. esc(item.count) .. ' / ' .. esc(threshold) .. '</span></td>' ..
|
||||
'<td>' .. esc(item.last_seen) .. '</td>' ..
|
||||
@@ -80,7 +88,7 @@ o.cfgvalue = function()
|
||||
local initial_watching = '<div class="lg-empty">无</div>'
|
||||
if #watching_rows > 0 then
|
||||
initial_watching =
|
||||
'<table class="lg-table lg-table-watch"><thead><tr><th>IP 地址</th><th>归属地</th><th>失败次数</th><th>最近访问时间</th><th>距首次失败</th></tr></thead><tbody>' ..
|
||||
'<table class="lg-table lg-table-watch"><thead><tr><th>IP 地址</th><th>访问类型</th><th>归属地</th><th>失败次数</th><th>最近访问时间</th><th>距首次失败</th></tr></thead><tbody>' ..
|
||||
table.concat(watching_rows, "") ..
|
||||
'</tbody></table>'
|
||||
end
|
||||
@@ -115,6 +123,9 @@ o.cfgvalue = function()
|
||||
.lg-ip{display:inline-block;border-radius:6px;background:var(--lg-code-bg);color:var(--lg-text);padding:2px 7px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px}
|
||||
.lg-ip-danger{background:var(--lg-danger-bg);color:var(--lg-danger-text)}
|
||||
.lg-empty{padding:18px 16px;color:var(--lg-text-muted);font-size:12px}
|
||||
.lg-kind{display:inline-block;min-width:44px;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;text-align:center;white-space:nowrap;background:var(--lg-code-bg);color:var(--lg-text-muted)}
|
||||
.lg-kind-ssh{background:rgba(240,140,0,.16);color:var(--lg-warning)}
|
||||
.lg-kind-luci{background:rgba(91,85,200,.16);color:var(--lg-primary)}
|
||||
.lg-unban{height:26px;border:1px solid var(--lg-border);border-radius:6px;background:var(--lg-control);color:var(--lg-text);padding:0 10px;font-size:11px;cursor:pointer}
|
||||
:root[data-darkmode="true"] .lg-wrap{--lg-success:#78c98d;--lg-warning:#d7a64a;--lg-danger:#e58a84;--lg-muted:#9aa7b3;--lg-primary:#8e8add;--lg-surface:var(--background-color-medium,#171e26);--lg-surface-muted:var(--background-color-high,#131a22);--lg-surface-alt:var(--background-color-low,#19212a);--lg-surface-hover:#202a34;--lg-control:var(--background-color-high,#111820);--lg-text:var(--text-color-high,#d5dbe3);--lg-text-muted:var(--text-color-medium,#9aa7b3);--lg-border:var(--border-color-medium,rgba(148,163,184,.18));--lg-border-soft:var(--border-color-low,rgba(148,163,184,.12));--lg-code-bg:var(--background-color-low,#253142);--lg-danger-bg:#42303a;--lg-danger-text:#efb2b0;--lg-shadow:none}
|
||||
@media(prefers-color-scheme:dark){
|
||||
@@ -130,11 +141,12 @@ o.cfgvalue = function()
|
||||
.lg-table td:nth-child(n+3){grid-column:1/-1}
|
||||
.lg-table td:before{flex:0 0 auto;color:var(--lg-text-muted)}
|
||||
.lg-table-ban td:nth-child(2),.lg-table-watch td:nth-child(2){justify-content:flex-end}
|
||||
.lg-table-ban td:nth-child(3):before{content:"剩余时间"}
|
||||
.lg-table-ban td:nth-child(4){justify-content:flex-end}
|
||||
.lg-table-watch td:nth-child(3):before{content:"失败次数"}
|
||||
.lg-table-watch td:nth-child(4):before{content:"最近访问"}
|
||||
.lg-table-watch td:nth-child(5):before{content:"距首次失败"}
|
||||
.lg-table-ban td:nth-child(3):before,.lg-table-watch td:nth-child(3):before{content:"归属地"}
|
||||
.lg-table-ban td:nth-child(4):before{content:"剩余时间"}
|
||||
.lg-table-ban td:nth-child(5){justify-content:flex-end}
|
||||
.lg-table-watch td:nth-child(4):before{content:"失败次数"}
|
||||
.lg-table-watch td:nth-child(5):before{content:"最近访问"}
|
||||
.lg-table-watch td:nth-child(6):before{content:"距首次失败"}
|
||||
.lg-tools .lg-btn{flex:1}
|
||||
}
|
||||
</style>
|
||||
@@ -223,6 +235,13 @@ function lgGeoHtml(loc){
|
||||
return loc?'<span style="font-size:11px">'+lgEscHtml(loc)+'</span>':'<span style="color:'+lgColor('muted')+'">查询中...</span>';
|
||||
}
|
||||
|
||||
// 访问类型:ssh / luci / manual;旧版封禁记录没有类型
|
||||
function lgKindHtml(kind){
|
||||
var map={ssh:['SSH','ssh'],luci:['LuCI','luci'],manual:['手动','manual']};
|
||||
var k=map[kind]||['未记录','none'];
|
||||
return '<span class="lg-kind lg-kind-'+k[1]+'">'+k[0]+'</span>';
|
||||
}
|
||||
|
||||
function lgEscHtml(s){
|
||||
return String(s||'').replace(/[&<>"']/g,function(c){
|
||||
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
||||
@@ -324,7 +343,7 @@ function lgRenderWatching(watching,threshold){
|
||||
return;
|
||||
}
|
||||
var h2='<table class="lg-table lg-table-watch">';
|
||||
h2+='<thead><tr><th>IP 地址</th><th>归属地</th><th>失败次数</th><th>最近访问时间</th><th>距首次失败</th></tr></thead><tbody>';
|
||||
h2+='<thead><tr><th>IP 地址</th><th>访问类型</th><th>归属地</th><th>失败次数</th><th>最近访问时间</th><th>距首次失败</th></tr></thead><tbody>';
|
||||
for(var j=0;j<watching.length;j++){
|
||||
var w=watching[j]||{};
|
||||
var ip=w.ip||'--';
|
||||
@@ -335,6 +354,7 @@ function lgRenderWatching(watching,threshold){
|
||||
var color=pct>=66?lgColor('danger'):(pct>=33?lgColor('warning'):lgColor('muted'));
|
||||
h2+='<tr>';
|
||||
h2+='<td><code class="lg-ip">'+lgEscHtml(ip)+'</code></td>';
|
||||
h2+='<td>'+lgKindHtml(w.kind)+'</td>';
|
||||
h2+='<td id="lg-watch-geo-'+j+'">'+lgGeoHtml(_lgGeoCache[ip])+'</td>';
|
||||
h2+='<td><span style="color:'+color+';font-weight:bold">'+count+' / '+threshold+'</span></td>';
|
||||
h2+='<td>'+lgEscHtml(lastSeen)+'</td>';
|
||||
@@ -385,11 +405,12 @@ function lgApplyStatus(d){
|
||||
listEl.innerHTML='<div class="lg-empty">暂无封禁</div>';
|
||||
}else{
|
||||
var h='<table class="lg-table lg-table-ban">';
|
||||
h+='<thead><tr><th>IP 地址</th><th>归属地</th><th>剩余时间</th><th>操作</th></tr></thead><tbody>';
|
||||
h+='<thead><tr><th>IP 地址</th><th>访问类型</th><th>归属地</th><th>剩余时间</th><th>操作</th></tr></thead><tbody>';
|
||||
for(var i=0;i<banned.length;i++){
|
||||
var b=banned[i];
|
||||
h+='<tr>';
|
||||
h+='<td><code class="lg-ip lg-ip-danger">'+lgEscHtml(b.ip)+'</code></td>';
|
||||
h+='<td>'+lgKindHtml(b.kind)+'</td>';
|
||||
h+='<td id="lg-geo-'+i+'">'+lgGeoHtml(_lgGeoCache[b.ip])+'</td>';
|
||||
h+='<td>'+fmtDuration(b.remaining)+'</td>';
|
||||
h+='<td><button class="lg-unban" onclick="lgUnban(\''+lgEscHtml(b.ip)+'\',this)">解封</button></td>';
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
## 登录防护显示访问类型
|
||||
|
||||
「登录防护」页的「已封禁 IP 列表」和「失败计数中(未达阈值)」新增「访问类型」列,可以直接看出每个地址是从哪里触发的:
|
||||
|
||||
- **SSH**:dropbear 登录失败。
|
||||
- **LuCI**:网页登录失败,包括直接登录和经 MiniGate 反代登录。
|
||||
- **手动**:在页面上手动封禁。
|
||||
- **未记录**:升级前已有的封禁,当时没有记录类型。
|
||||
|
||||
手机上,类型标签显示在 IP 同一行的右侧。登录防护日志的失败和封禁记录也带上类型,例如 `BAN 139.162.161.162 30天(SSH,达到 3 次失败)`。
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 封禁文件 `/etc/minigate/login-guard/bans.txt` 和失败计数文件,以可选的第三列保存类型。旧格式照常可读,已有封禁的到期时间不变。
|
||||
- 防火墙重载后恢复封禁时,类型保持不变。
|
||||
|
||||
## 说明
|
||||
|
||||
「公网访问范围」只限制 MiniGate 反代的网站。四川以外的公网 IP 经反代访问 LuCI 时会被直接断开,打不开登录页,也就不会出现 LuCI 登录失败。所以四川以外的封禁通常来自 SSH。
|
||||
|
||||
## 验证
|
||||
|
||||
- 本地完整测试全部通过,无跳过。新增测试覆盖:
|
||||
- 记录 SSH 和 LuCI 失败、手动封禁;
|
||||
- 兼容旧记录;
|
||||
- 防火墙重载后按类型恢复封禁;
|
||||
- 状态接口返回类型;
|
||||
- 表格渲染类型列。
|
||||
- 在浏览器中检查了深色主题下桌面和 375px 手机宽度,显示正常,页面没有横向溢出;页面外框为模拟。
|
||||
- 本次不自动更新路由器。
|
||||
|
||||
## 安装包
|
||||
|
||||
- opkg 安装包:`minigate_2026.10.29_all.ipk`。
|
||||
- 源码安装包:`minigate-v2026.10.29-src.tar.gz`。
|
||||
- 兼容旧更新器:`luci-app-minigate-v2026.10.29-src.tar.gz`。
|
||||
- 使用随附的 `SHA256SUMS` 校验文件完整性。
|
||||
@@ -209,11 +209,11 @@ ensure_nft() {
|
||||
# 防火墙重载后,随包的集合会重新出现但内容为空;持久化文件里仍有未过期封禁时恢复
|
||||
sets_lost_bans() {
|
||||
# ip/exp 必须是局部变量:shell 的 local 是动态作用域,读到文件末尾会清空调用方的 ip
|
||||
local now set ip exp
|
||||
local now set ip exp kind
|
||||
now=$(date +%s)
|
||||
for set in "$SET4" "$SET6"; do
|
||||
nft list set $TABLE $set 2>/dev/null | grep -q 'elements' && continue
|
||||
while read -r ip exp; do
|
||||
while read -r ip exp kind; do
|
||||
[ -n "$ip" ] || continue
|
||||
case "$exp" in ''|*[!0-9]*) continue ;; esac
|
||||
[ "$exp" -gt "$now" ] || continue
|
||||
@@ -230,23 +230,32 @@ add_ban_element() {
|
||||
nft add element $TABLE $set "{ $ip timeout ${seconds}s }" 2>/dev/null
|
||||
}
|
||||
|
||||
# 访问类型:ssh / luci / manual。bans.txt 每行「IP 到期时间 类型」,旧版没有第三列
|
||||
kind_label() {
|
||||
case "$1" in
|
||||
ssh) echo "SSH" ;;
|
||||
luci) echo "LuCI" ;;
|
||||
manual) echo "手动" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
persist_ban() {
|
||||
local ip="$1" exp="$2"
|
||||
local ip="$1" exp="$2" kind="$3"
|
||||
grep -v "^$ip " "$BANS_FILE" > "${BANS_FILE}.tmp" 2>/dev/null || true
|
||||
echo "$ip $exp" >> "${BANS_FILE}.tmp"
|
||||
echo "$ip $exp${kind:+ $kind}" >> "${BANS_FILE}.tmp"
|
||||
mv "${BANS_FILE}.tmp" "$BANS_FILE"
|
||||
}
|
||||
|
||||
# === 把 IP 加入封禁集合 + 持久化 ===
|
||||
ban_ip() {
|
||||
local ip="$1"
|
||||
local ip="$1" kind="$2"
|
||||
local exp=$(($(date +%s) + BANTIME))
|
||||
if ! add_ban_element "$ip" "$BANTIME"; then
|
||||
log "封禁 $ip 失败:防火墙未接受该地址"
|
||||
return 1
|
||||
fi
|
||||
persist_ban "$ip" "$exp"
|
||||
log "BAN $ip $(fmt_duration "$BANTIME")(达到 $THRESHOLD 次失败)"
|
||||
persist_ban "$ip" "$exp" "$kind"
|
||||
log "BAN $ip $(fmt_duration "$BANTIME")(${kind:+$(kind_label "$kind"),}达到 $THRESHOLD 次失败)"
|
||||
rm -f "$COUNTER_DIR/$ip"
|
||||
}
|
||||
|
||||
@@ -256,13 +265,13 @@ restore_bans() {
|
||||
local now=$(date +%s)
|
||||
local tmp="${BANS_FILE}.tmp"
|
||||
> "$tmp"
|
||||
local count=0 ip exp
|
||||
while read -r ip exp; do
|
||||
local count=0 ip exp kind
|
||||
while read -r ip exp kind; do
|
||||
[ -z "$ip" ] && continue
|
||||
case "$exp" in ''|*[!0-9]*) continue ;; esac
|
||||
if [ "$exp" -gt "$now" ] && [ -n "$(set_for_ip "$ip")" ]; then
|
||||
add_ban_element "$ip" $((exp - now))
|
||||
echo "$ip $exp" >> "$tmp"
|
||||
echo "$ip $exp${kind:+ $kind}" >> "$tmp"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
done < "$BANS_FILE"
|
||||
@@ -272,7 +281,7 @@ restore_bans() {
|
||||
|
||||
# === 处理一条登录失败 ===
|
||||
handle_failure() {
|
||||
local ip="$1"
|
||||
local ip="$1" kind="$2"
|
||||
[ -z "$ip" ] && return
|
||||
[ -n "$(ip_family "$ip")" ] || return
|
||||
is_whitelisted "$ip" && return
|
||||
@@ -282,8 +291,9 @@ handle_failure() {
|
||||
local now=$(date +%s)
|
||||
local first=0
|
||||
local count=0
|
||||
local last_kind
|
||||
if [ -f "$f" ]; then
|
||||
read -r first count < "$f"
|
||||
read -r first count last_kind < "$f"
|
||||
if [ -n "$first" ] && [ $((now - first)) -gt "$WINDOW" ]; then
|
||||
first=$now
|
||||
count=0
|
||||
@@ -292,13 +302,14 @@ handle_failure() {
|
||||
first=$now
|
||||
fi
|
||||
count=$((count + 1))
|
||||
echo "$first $count" > "$f"
|
||||
log "FAIL $ip ($count/$THRESHOLD)"
|
||||
# 计数文件第三列记录最近一次失败的访问类型
|
||||
echo "$first $count${kind:+ $kind}" > "$f"
|
||||
log "FAIL $ip ${kind:+$(kind_label "$kind") }($count/$THRESHOLD)"
|
||||
|
||||
if [ "$count" -ge "$THRESHOLD" ]; then
|
||||
# 加 ban 前再确认 nft 还在(fw4 可能重载)
|
||||
repair_nft
|
||||
ban_ip "$ip"
|
||||
ban_ip "$ip" "$kind"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -335,7 +346,7 @@ access_log_loop() {
|
||||
tail -n 0 -f "$ACCESS_LOG" 2>/dev/null | while read -r line; do
|
||||
is_luci_failed_post "$line" || continue
|
||||
ip=$(echo "$line" | extract_json_field client)
|
||||
handle_failure "$ip"
|
||||
handle_failure "$ip" luci
|
||||
done
|
||||
}
|
||||
|
||||
@@ -344,10 +355,10 @@ syslog_loop() {
|
||||
case "$line" in
|
||||
*"Bad password attempt"*"from"*|\
|
||||
*"Login attempt for nonexistent user"*"from"*)
|
||||
handle_failure "$(extract_ip "$line" 1)"
|
||||
handle_failure "$(extract_ip "$line" 1)" ssh
|
||||
;;
|
||||
*"luci: failed login"*"from"*)
|
||||
handle_failure "$(extract_ip "$line" 0)"
|
||||
handle_failure "$(extract_ip "$line" 0)" luci
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@@ -509,7 +520,7 @@ cmd_ban() {
|
||||
echo "✗ 封禁 $ip 失败"
|
||||
exit 1
|
||||
fi
|
||||
persist_ban "$ip" $(($(date +%s) + BANTIME))
|
||||
persist_ban "$ip" $(($(date +%s) + BANTIME)) manual
|
||||
echo "✓ 已封禁 $ip"
|
||||
log "MANUAL ban $ip"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export PATH
|
||||
|
||||
CURRENT_VERSION="2026.10.28-2"
|
||||
CURRENT_VERSION="2026.10.29"
|
||||
REPOSITORY="tpxcer/minigate"
|
||||
API_URL="https://api.github.com/repos/${REPOSITORY}/releases/latest"
|
||||
DOWNLOAD_ROOT="https://github.com/${REPOSITORY}/releases/download"
|
||||
|
||||
@@ -5,7 +5,7 @@ ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
OUT_DIR="${1:-$ROOT_DIR/dist}"
|
||||
|
||||
PKG_NAME="minigate"
|
||||
PKG_VERSION="2026.10.28-2"
|
||||
PKG_VERSION="2026.10.29"
|
||||
PKG_ARCH="all"
|
||||
PKG_FILE="${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.ipk"
|
||||
DEPS="libc, luci-base, luci-compat, nginx-ssl, nginx-mod-stream, openssl-util, wget, curl, jsonfilter, coreutils-stat, nftables"
|
||||
|
||||
@@ -213,3 +213,63 @@ test('reaching the threshold bans the attacking address even when other bans are
|
||||
assert.equal(fs.existsSync(path.join(dir, 'run/counters/139.162.161.162')), false, 'counter is cleared after a ban');
|
||||
}
|
||||
});
|
||||
|
||||
function guardLibrary(dir, body) {
|
||||
return spawnSync('/bin/sh', ['-c', `
|
||||
logger() { :; }
|
||||
uci() { case "$3" in minigate.login_guard.threshold) echo 3 ;; *) return 1 ;; esac; }
|
||||
nft() {
|
||||
printf '%s\\n' "$*" >> ${JSON.stringify(path.join(dir, 'nft.log'))}
|
||||
case "$*" in
|
||||
'list set inet fw4 login_banned_v4'|'list set inet fw4 login_banned_v6') [ -f ${JSON.stringify(path.join(dir, 'sets-empty'))} ] || echo 'elements = { 1.1.1.1 }' ;;
|
||||
'list chain inet fw4 input') echo '@login_banned_v4 @login_banned_v6' ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
export MINIGATE_LG_LIBRARY_ONLY=1 MINIGATE_LG_DATA_DIR=${JSON.stringify(path.join(dir, 'data'))}
|
||||
export MINIGATE_LG_RUN_DIR=${JSON.stringify(path.join(dir, 'run'))} MINIGATE_LG_LOG=${JSON.stringify(path.join(dir, 'lg.log'))}
|
||||
. ${JSON.stringify(script)}
|
||||
${body}
|
||||
`], { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
test('bans and failure counters record whether the attempt came over SSH or LuCI', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lg-kind-'));
|
||||
fs.mkdirSync(path.join(dir, 'data'));
|
||||
fs.writeFileSync(path.join(dir, 'data/bans.txt'), '111.170.34.182 9999999999\n');
|
||||
const result = guardLibrary(dir, `
|
||||
handle_failure 198.51.100.30 luci
|
||||
cat "$COUNTER_DIR/198.51.100.30"
|
||||
for i in 1 2 3; do handle_failure 139.162.161.162 ssh; done
|
||||
cmd_ban 198.51.100.40 >/dev/null
|
||||
`);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /^\d+ 1 luci$/m);
|
||||
const bans = fs.readFileSync(path.join(dir, 'data/bans.txt'), 'utf8');
|
||||
assert.match(bans, /^111\.170\.34\.182 9999999999$/m, 'old records without a type are kept as they are');
|
||||
assert.match(bans, /^139\.162\.161\.162 \d+ ssh$/m);
|
||||
assert.match(bans, /^198\.51\.100\.40 \d+ manual$/m);
|
||||
const log = fs.readFileSync(path.join(dir, 'lg.log'), 'utf8');
|
||||
assert.match(log, /FAIL 198\.51\.100\.30 LuCI \(1\/3\)/);
|
||||
assert.match(log, /BAN 139\.162\.161\.162 12小时(SSH,达到 3 次失败)/);
|
||||
});
|
||||
|
||||
test('typed ban records survive a firewall reload and are restored with their type', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lg-kind-restore-'));
|
||||
fs.mkdirSync(path.join(dir, 'data'));
|
||||
fs.writeFileSync(path.join(dir, 'sets-empty'), '');
|
||||
fs.writeFileSync(path.join(dir, 'data/bans.txt'), '139.162.161.162 9999999999 ssh\n2001:db8::9 9999999999 luci\n5.231.242.181 9999999999\n');
|
||||
const result = guardLibrary(dir, 'repair_nft');
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const calls = fs.readFileSync(path.join(dir, 'nft.log'), 'utf8');
|
||||
assert.match(calls, /add element inet fw4 login_banned_v4 \{ 139\.162\.161\.162 timeout \d+s \}/);
|
||||
assert.match(calls, /add element inet fw4 login_banned_v6 \{ 2001:db8::9 timeout \d+s \}/);
|
||||
assert.equal(fs.readFileSync(path.join(dir, 'data/bans.txt'), 'utf8'), '139.162.161.162 9999999999 ssh\n2001:db8::9 9999999999 luci\n5.231.242.181 9999999999\n');
|
||||
});
|
||||
|
||||
test('each log source reports its own access type', () => {
|
||||
const source = fs.readFileSync(script, 'utf8');
|
||||
assert.match(source, /handle_failure "\$\(extract_ip "\$line" 1\)" ssh/);
|
||||
assert.match(source, /handle_failure "\$\(extract_ip "\$line" 0\)" luci/);
|
||||
assert.match(source, /handle_failure "\$ip" luci/);
|
||||
});
|
||||
@@ -208,3 +208,27 @@ test('LuCI forms validate domains, Cloudflare IDs, intervals, thresholds and whi
|
||||
'range(1,100)\ttrue\tfalse\tfalse'
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('login guard status adds the access type from the persisted ban list and counters', { skip: !lua }, () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lg-status-'));
|
||||
fs.writeFileSync(path.join(dir, 'bans.txt'), '103.160.205.37 1795830696 ssh\n5.231.242.181 1795794101\n');
|
||||
const prelude = controllerPrelude.replace("get=function(_, _, sec) if sec == 'cfg01' then return 'ddns' end end",
|
||||
"get=function(_, _, sec) local v; if sec == 'cfg01' then v = 'ddns' end; return v end");
|
||||
assert.notEqual(prelude, controllerPrelude);
|
||||
const out = runLua(prelude.replace("exec=function() return '' end", `exec=function(cmd)
|
||||
if cmd:find('login_banned_v4', 1, true) then
|
||||
return '{"nftables": [{"set": {"elem": [{"elem": {"val": "103.160.205.37", "timeout": 2592000, "expires": 3600}}, {"elem": {"val": "5.231.242.181", "timeout": 2592000, "expires": 60}}]}}]}'
|
||||
end
|
||||
return ''
|
||||
end`) + `
|
||||
local real_open = io.open
|
||||
io.open = function(file, mode)
|
||||
if file == '/etc/minigate/login-guard/bans.txt' then file = ${JSON.stringify(path.join(dir, 'bans.txt'))} end
|
||||
return real_open(file, mode)
|
||||
end
|
||||
package.loaded['nixio.fs'] = {dir=function() return nil end, stat=function() return nil end}
|
||||
c.action_lg_status()
|
||||
for _, b in ipairs(response.banned) do print(b.ip, b.remaining, b.kind == '' and '(none)' or b.kind) end
|
||||
`);
|
||||
assert.equal(out, '103.160.205.37\t3600\tssh\n5.231.242.181\t60\t(none)');
|
||||
});
|
||||
@@ -248,3 +248,26 @@ test('state-changing LuCI endpoints require POST with the CSRF token', () => {
|
||||
assert.match(controller, /"acme_job"\}, call\("action_acme_job"\)\)/);
|
||||
assert.match(controller, /sec:match\("\^\[%w_\]\+\$"\)/);
|
||||
});
|
||||
|
||||
test('login guard lists show whether each address failed over SSH, LuCI or was banned by hand', () => {
|
||||
const page = fakePage(loginGuardScript);
|
||||
take(page, 'lu').callback(null, {
|
||||
enabled: '1', running: true, threshold: 3,
|
||||
banned: [
|
||||
{ ip: '103.160.205.37', remaining: 3600, kind: 'ssh' },
|
||||
{ ip: '198.51.100.9', remaining: 600, kind: 'manual' },
|
||||
{ ip: '5.231.242.181', remaining: 60, kind: '' }
|
||||
],
|
||||
banned_total: 3,
|
||||
watching: [{ ip: '198.51.100.20', kind: 'luci', count: 1, age: 5, last_seen: '2026-10-29 12:00:00' }],
|
||||
watching_total: 1
|
||||
});
|
||||
const banned = page.element('lg-banned-list').innerHTML;
|
||||
assert.match(banned, /<th>IP 地址<\/th><th>访问类型<\/th><th>归属地<\/th>/);
|
||||
assert.match(banned, /103\.160\.205\.37<\/code><\/td><td><span class="lg-kind lg-kind-ssh">SSH<\/span>/);
|
||||
assert.match(banned, /lg-kind-manual">手动</);
|
||||
assert.match(banned, /5\.231\.242\.181<\/code><\/td><td><span class="lg-kind lg-kind-none">未记录</);
|
||||
const watching = page.element('lg-watching-list').innerHTML;
|
||||
assert.match(watching, /<th>访问类型<\/th>/);
|
||||
assert.match(watching, /198\.51\.100\.20<\/code><\/td><td><span class="lg-kind lg-kind-luci">LuCI</);
|
||||
});
|
||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||
|
||||
PKG_NAME:=mise
|
||||
PKG_VERSION:=2026.10.21
|
||||
PKG_RELEASE:=44
|
||||
PKG_VERSION:=2026.10.22
|
||||
PKG_RELEASE:=45
|
||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=qBittorrent
|
||||
PKG_VERSION:=5.3.0
|
||||
PKG_RELEASE:=9
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/qbittorrent/qBittorrent/tar.gz/release-$(PKG_VERSION)?
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
--- a/cmake/Modules/CommonConfig.cmake
|
||||
+++ b/cmake/Modules/CommonConfig.cmake
|
||||
@@ -60,7 +60,6 @@ if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU
|
||||
-Wold-style-cast
|
||||
-Wnon-virtual-dtor
|
||||
@@ -57,7 +57,6 @@ if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU
|
||||
-Wcast-align
|
||||
-Winvalid-pch
|
||||
-pedantic
|
||||
- -pedantic-errors
|
||||
)
|
||||
|
||||
# Clang 11 still doesn't support -Wstrict-null-sentinel
|
||||
$<$<COMPILE_LANGUAGE:CXX>:
|
||||
-Woverloaded-virtual
|
||||
-Wold-style-cast
|
||||
+2
-2
@@ -2,11 +2,11 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=tun2socks
|
||||
PKG_VERSION:=2.7.0
|
||||
PKG_RELEASE:=15
|
||||
PKG_RELEASE:=16
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/xjasonlyu/tun2socks.git
|
||||
PKG_SOURCE_VERSION:=b986c2434d62da41c26d17b7050df37f293a3328
|
||||
PKG_SOURCE_VERSION:=9c66760ef5887e7c2613983885cb41a07bc2e85a
|
||||
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlako@132lan.ru>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
+3
-3
@@ -50,12 +50,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.8.0_pre29
|
||||
PKG_RELEASE:=83
|
||||
PKG_VERSION:=1.8.0_pre31
|
||||
PKG_RELEASE:=84
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=0944a14ea95e90b6596f81bc49f07e4f73d905e3
|
||||
PKG_SOURCE_VERSION:=ae002a7e9a7a9d45b267b961e45ef1ec88fddebe
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
+6
-5
@@ -8,12 +8,13 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=xkcptun
|
||||
PKG_VERSION:=1.10.567
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/xkcptun/tar.gz/$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/liudf0716/xkcptun.git
|
||||
PKG_SOURCE_DATE:=2026-10-25
|
||||
PKG_SOURCE_VERSION:=e5ec5ed74a20a75e9c3b22221d7f07346ffa0d1e
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
Reference in new issue
Block a user