diff --git a/aw-bpf/Makefile b/aw-bpf/Makefile new file mode 100644 index 00000000..2e1eff34 --- /dev/null +++ b/aw-bpf/Makefile @@ -0,0 +1,79 @@ +# +# Copyright (C) 2025 Dengfeng Liu +# +# This is free software, licensed under the GNU General Public License v3. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=aw-bpf +PKG_VERSION:=1.08.23 +PKG_RELEASE:=1 + +PKG_SOURCE:=aw-bpf-$(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_MAINTAINER:=Dengfeng Liu +PKG_LICENSE:=GPL-2.0-or-later +PKG_LICENSE_FILES:=LICENSE + +PKG_BUILD_DEPENDS:=bpf-headers + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk +include $(INCLUDE_DIR)/bpf.mk +include $(INCLUDE_DIR)/kernel.mk + +define Package/aw-bpf + SECTION:=net + CATEGORY:=Network + TITLE:=ApFree eBPF programs and tools + DEPENDS:=+libbpf +libelf +libjson-c +libmnl +libnftnl +tc-full +bpftool-minimal +ip +kmod-xdpi +kmod-sched-bpf $(BPF_DEPENDS) + URL:=https://github.com/liudf0716/aw-bpf +endef + +define Package/aw-bpf/description + ApFree eBPF provides eBPF programs for session tracking, xDPI, DNS monitoring + and user-space tools: aw-bpfctl (control CLI) and aw-eventd (session / DNS + consumer with independent nftables table inet awbpf). +endef + + +define KernelPackage/xdpi + SECTION:=kernel + SUBMENU:=Network Support + TITLE:=xdpi ebpf module + FILES:=$(PKG_BUILD_DIR)/xdpi_bpf.ko + AUTOLOAD:=$(call AutoProbe,xdpi_bpf) +endef + +# Build cmake userspace first (aw-bpfctl, aw-eventd), then BPF objects and kmod. +define Build/Compile + $(call Build/Compile/Default) + $(call CompileBPF,$(PKG_BUILD_DIR)/aw_bpf.c,$(if $(filter arm aarch64 x86_64 i386,$(ARCH)),-DENABLE_XDPI_FEATURE)) + $(call CompileBPF,$(PKG_BUILD_DIR)/dns_bpf.c,$(if $(filter arm aarch64 x86_64 i386,$(ARCH)),-DENABLE_XDPI_FEATURE)) + $(KERNEL_MAKE) M=$(PKG_BUILD_DIR) modules +endef + +define Package/aw-bpf/conffiles +/etc/config/aw-bpf +endef + +define Package/aw-bpf/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/aw-bpfctl $(1)/usr/bin/aw-bpfctl + $(INSTALL_BIN) $(PKG_BUILD_DIR)/aw-eventd $(1)/usr/bin/aw-eventd + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/aw-bpf.init $(1)/etc/init.d/aw-bpf + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/aw-bpf.conf $(1)/etc/config/aw-bpf + $(INSTALL_DIR) $(1)/lib/bpf + $(INSTALL_DATA) $(PKG_BUILD_DIR)/aw_bpf.o $(1)/lib/bpf/aw-bpf.o + $(INSTALL_DATA) $(PKG_BUILD_DIR)/dns_bpf.o $(1)/lib/bpf/dns-bpf.o +endef + +$(eval $(call BuildPackage,aw-bpf)) +$(eval $(call KernelPackage,xdpi)) diff --git a/aw-bpf/files/aw-bpf.conf b/aw-bpf/files/aw-bpf.conf new file mode 100644 index 00000000..bb6d5091 --- /dev/null +++ b/aw-bpf/files/aw-bpf.conf @@ -0,0 +1,2 @@ +config aw-bpf 'common' + option enable_event_log '0' diff --git a/aw-bpf/files/aw-bpf.init b/aw-bpf/files/aw-bpf.init new file mode 100644 index 00000000..640d272c --- /dev/null +++ b/aw-bpf/files/aw-bpf.init @@ -0,0 +1,325 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2025-2026 Dengfeng Liu +# aw-bpf service - manages aw-bpf lifecycle: aw-eventd, xdpi module, BPF maps + +START=90 +STOP=10 +USE_PROCD=1 +NAME=aw-bpf +PROG="/usr/bin/aw-eventd" +CONF_FILE="/var/etc/eventd.conf" +BPF_FS_ROOT="/sys/fs/bpf" +AW_BPF_PIN_DIR="$BPF_FS_ROOT/tc/globals" +AW_BPF_LEGACY_PIN_DIR="$BPF_FS_ROOT/xdp/globals" +AW_BPF_MAPS="ipv4_map ipv6_map mac_map tcp_conn_map udp_conn_map xdpi_l7_map session_events_map prog_array_map dns_ringbuf dns_ringbuf_portal dns_stats_map" +AW_BPF_PROGS="tc_ingress tc_egress" +AW_BPF_LAN_IFACES="" +AW_BPF_WAN_IFACES="" + +resolve_lan_ifaces() { + local lan_device lan_ifname + + AW_BPF_LAN_IFACES="" + + config_load network + config_get lan_device lan device + config_get lan_ifname lan ifname + + if [ -n "$lan_device" ] && [ -e "/sys/class/net/$lan_device" ]; then + AW_BPF_LAN_IFACES="$lan_device" + return 0 + fi + + for iface in $lan_ifname; do + if [ -n "$iface" ] && [ -e "/sys/class/net/$iface" ]; then + AW_BPF_LAN_IFACES="$iface" + return 0 + fi + done +} + +resolve_wan_ifaces() { + local wan_device wan_ifname + + AW_BPF_WAN_IFACES="" + + config_load network + config_get wan_device wan device + config_get wan_ifname wan ifname + + if [ -n "$wan_device" ] && [ -e "/sys/class/net/$wan_device" ]; then + AW_BPF_WAN_IFACES="$wan_device" + return 0 + fi + + for iface in $wan_ifname; do + if [ -n "$iface" ] && [ -e "/sys/class/net/$iface" ]; then + AW_BPF_WAN_IFACES="$iface" + return 0 + fi + done +} + +detach_tc_programs() { + local iface="$1" + + [ -z "$iface" ] && return 0 + [ ! -e "/sys/class/net/$iface" ] && return 0 + + tc filter del dev "$iface" ingress 2>/dev/null || true + tc filter del dev "$iface" egress 2>/dev/null || true + tc qdisc del dev "$iface" clsact 2>/dev/null || true +} + +attach_tc_programs() { + local iface="$1" + local aw_bpf_file="/lib/bpf/aw-bpf.o" + + [ -z "$iface" ] && return 0 + [ ! -e "/sys/class/net/$iface" ] && return 0 + [ ! -f "$aw_bpf_file" ] && { + echo "aw-bpf.o not found at $aw_bpf_file" >&2 + return 1 + } + + tc qdisc del dev "$iface" clsact 2>/dev/null || true + tc qdisc add dev "$iface" clsact 2>/dev/null || true + + if ! tc filter add dev "$iface" ingress prio 1 bpf da obj "$aw_bpf_file" sec tc/ingress 2>/tmp/aw-bpf.ingress.err; then + echo "Failed to attach aw-bpf ingress to $iface: $(cat /tmp/aw-bpf.ingress.err 2>/dev/null)" >&2 + rm -f /tmp/aw-bpf.ingress.err + return 1 + fi + rm -f /tmp/aw-bpf.ingress.err + + if ! tc filter add dev "$iface" egress prio 1 bpf da obj "$aw_bpf_file" sec tc/egress 2>/tmp/aw-bpf.egress.err; then + echo "Failed to attach aw-bpf egress to $iface: $(cat /tmp/aw-bpf.egress.err 2>/dev/null)" >&2 + rm -f /tmp/aw-bpf.egress.err + tc filter del dev "$iface" ingress 2>/dev/null || true + tc qdisc del dev "$iface" clsact 2>/dev/null || true + return 1 + fi + rm -f /tmp/aw-bpf.egress.err + + echo "aw-bpf attached on $iface" >&2 + return 0 +} + +cleanup_aw_bpf_pins() { + local entry + + for entry in $AW_BPF_PROGS $AW_BPF_MAPS; do + [ -e "$BPF_FS_ROOT/$entry" ] && rm -f "$BPF_FS_ROOT/$entry" + [ -e "$AW_BPF_PIN_DIR/$entry" ] && rm -f "$AW_BPF_PIN_DIR/$entry" + [ -e "$AW_BPF_LEGACY_PIN_DIR/$entry" ] && rm -f "$AW_BPF_LEGACY_PIN_DIR/$entry" + done +} + +load_bpf_resources() { + echo "Loading BPF resources..." >&2 + mkdir -p "$AW_BPF_PIN_DIR" 2>/dev/null || true + + cleanup_aw_bpf_pins + resolve_lan_ifaces + + if [ -z "$AW_BPF_LAN_IFACES" ]; then + echo "No valid interfaces found from network.lan" >&2 + return 1 + fi + + for iface in $AW_BPF_LAN_IFACES; do + attach_tc_programs "$iface" || return 1 + done + + load_dns_bpf_program + + return 0 +} + +load_xdpi() { + if lsmod | grep -q xdpi; then + echo "xdpi module already loaded" >&2 + return 0 + fi + + if command -v modprobe >/dev/null 2>&1; then + if modprobe xdpi_bpf 2>/dev/null || modprobe xdpi-bpf 2>/dev/null; then + echo "xdpi module loaded via modprobe" >&2 + return 0 + fi + fi + + for modfile in /lib/modules/$(uname -r)/xdpi_bpf.ko /lib/modules/$(uname -r)/xdpi-bpf.ko /lib/modules/$(uname -r)/extra/xdpi_bpf.ko /lib/modules/$(uname -r)/extra/xdpi-bpf.ko /lib/xdpi_bpf.ko /lib/xdpi-bpf.ko; do + if [ -f "$modfile" ]; then + insmod "$modfile" 2>/dev/null && echo "xdpi module loaded via insmod $modfile" >&2 && return 0 || true + fi + done + + echo "Warning: xdpi module not found or failed to load" >&2 + return 1 +} + +load_dns_bpf_program() { + local dns_bpf_file="/lib/bpf/dns-bpf.o" + local xdp_pin_base="/sys/fs/bpf/aw" + + if [ ! -f "$dns_bpf_file" ]; then + echo "dns-bpf.o not found at $dns_bpf_file" >&2 + return 0 + fi + + mkdir -p "$xdp_pin_base" 2>/dev/null || true + + resolve_wan_ifaces + if [ -z "$AW_BPF_WAN_IFACES" ]; then + echo "No valid WAN interfaces found. Skip loading DNS XDP." >&2 + return 1 + fi + + for iface in $AW_BPF_WAN_IFACES; do + echo "Attaching DNS XDP program to $iface..." >&2 + ip link set dev "$iface" xdp off 2>/dev/null || true + if ! ip link set dev "$iface" xdp obj "$dns_bpf_file" sec xdp 2>/tmp/aw-bpf.xdp.err; then + echo "Failed to attach dns xdp to $iface: $(cat /tmp/aw-bpf.xdp.err 2>/dev/null)" >&2 + rm -f /tmp/aw-bpf.xdp.err + return 1 + fi + rm -f /tmp/aw-bpf.xdp.err + done + + sleep 1 + + local xdp_prog_id=$(bpftool prog show name xdp_dns_monitor 2>/dev/null | head -n1 | cut -d':' -f1) + [ ! -e "$xdp_pin_base/dns_ringbuf" ] && bpftool map pin name dns_ringbuf "$xdp_pin_base/dns_ringbuf" 2>/dev/null + if [ ! -e "$xdp_pin_base/dns_ringbuf_portal" ]; then + bpftool map pin name dns_ringbuf_por "$xdp_pin_base/dns_ringbuf_portal" 2>/dev/null || \ + bpftool map pin name dns_ringbuf_portal "$xdp_pin_base/dns_ringbuf_portal" 2>/dev/null + fi + [ ! -e "$xdp_pin_base/dns_stats_map" ] && bpftool map pin name dns_stats_map "$xdp_pin_base/dns_stats_map" 2>/dev/null + + return 0 +} + +cleanup_bpf_resources() { + echo "Cleaning up global BPF resources..." >&2 + + resolve_lan_ifaces + for iface in $AW_BPF_LAN_IFACES; do + detach_tc_programs "$iface" + done + + resolve_wan_ifaces + for iface in $AW_BPF_WAN_IFACES; do + ip link set dev "$iface" xdp off 2>/dev/null || true + done + + rm -rf /sys/fs/bpf/aw/dns_* 2>/dev/null + cleanup_aw_bpf_pins + + return 0 +} + +unload_xdpi() { + if ! lsmod | grep -q xdpi; then + echo "xdpi module not loaded" >&2 + return 0 + fi + + local rmmod_output + rmmod_output=$(rmmod xdpi_bpf 2>&1 || rmmod xdpi-bpf 2>&1) + local rmmod_status=$? + + if [ $rmmod_status -ne 0 ]; then + echo "Note: Failed to unload xdpi module (may still be in use). Error: $rmmod_output" >&2 + return 0 + fi + + if lsmod | grep -q xdpi; then + echo "Note: Module still loaded after rmmod attempt" >&2 + return 0 + fi + + echo "xdpi module unloaded successfully" >&2 + return 0 +} + +generate_conf() { + local enable_event_log + local location_id ap_device_id ap_mac_address ap_longitude ap_latitude + + config_load "aw-bpf" + config_get enable_event_log "common" enable_event_log "0" + config_get location_id "common" location_id "" + config_get ap_device_id "common" ap_device_id "" + config_get ap_mac_address "common" ap_mac_address "" + config_get ap_longitude "common" ap_longitude "" + config_get ap_latitude "common" ap_latitude "" + + # Inherit location/device audit info from wifidogx if not explicitly set in aw-bpf + if [ -f "/etc/config/wifidogx" ]; then + local w_loc w_dev w_mac w_long w_lat + config_load "wifidogx" + config_get w_loc "common" location_id "" + config_get w_dev "common" ap_device_id "" + config_get w_mac "common" ap_mac_address "" + config_get w_long "common" ap_longitude "" + config_get w_lat "common" ap_latitude "" + + [ -z "$location_id" ] && location_id="$w_loc" + [ -z "$ap_device_id" ] && ap_device_id="$w_dev" + [ -z "$ap_mac_address" ] && ap_mac_address="$w_mac" + [ -z "$ap_longitude" ] && ap_longitude="$w_long" + [ -z "$ap_latitude" ] && ap_latitude="$w_lat" + fi + + [ -z "$location_id" ] && location_id="UNKNOWN" + [ -z "$ap_device_id" ] && ap_device_id="UNKNOWN" + [ -z "$ap_mac_address" ] && ap_mac_address="UNKNOWN" + [ -z "$ap_longitude" ] && ap_longitude="0.000000" + [ -z "$ap_latitude" ] && ap_latitude="0.000000" + + mkdir -p /var/etc + cat > "$CONF_FILE" </dev/null 2>&1; then + echo "aw-eventd binary not found" >&2 + return 1 + fi + + generate_conf + + echo "Starting aw-bpf service (aw-eventd)..." >&2 + procd_open_instance + procd_set_param command "$PROG" -c "$CONF_FILE" + procd_set_param respawn 3600 5 0 + procd_set_param file "$CONF_FILE" + procd_close_instance +} + +stop_service() { + echo "Stopping aw-bpf service..." >&2 + cleanup_bpf_resources + unload_xdpi + echo "aw-bpf cleanup completed" >&2 +} + +service_triggers() { + procd_add_reload_trigger "aw-bpf" +} + +reload_service() { + restart +} diff --git a/luci-app-aw-bpf/LICENSE b/luci-app-aw-bpf/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/luci-app-aw-bpf/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/luci-app-aw-bpf/Makefile b/luci-app-aw-bpf/Makefile new file mode 100644 index 00000000..46dd8062 --- /dev/null +++ b/luci-app-aw-bpf/Makefile @@ -0,0 +1,13 @@ +# This is free software, licensed under the Apache License, Version 2.0 + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Support for aw-bpf traffic control +LUCI_DEPENDS:=+aw-bpf +luci-lib-echarts + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=Dengfeng Liu + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf.css b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf.css new file mode 100644 index 00000000..d08a665c --- /dev/null +++ b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf.css @@ -0,0 +1,570 @@ +/* Theme tokens: follow LuCI bootstrap vars, Argon, or JS-set data-aw-theme */ +.l7-view-container, +.display-view-container { + --aw-card-bg: var(--background-color-medium, var(--background-color, #f9f9f9)); + --aw-card-border: var(--border-color-medium, #e0e0e0); + --aw-chart-bg: var(--background-color-high, #ffffff); + --aw-text: var(--text-color-highest, var(--text-color-high, #333333)); + --aw-text-muted: var(--text-color-medium, #666666); + --aw-accent: var(--primary-color-high, var(--primary, #3771c8)); + --aw-hover: var(--background-color-low, #f0f0f0); + --aw-error-bg: #ffefef; + --aw-error-fg: #c62828; + --aw-dl: #28a745; + --aw-ul: #007bff; + --aw-vol-dl: #17a2b8; + --aw-vol-ul: #6610f2; + --aw-pkt: #6c757d; +} + +@media (prefers-color-scheme: dark) { + .l7-view-container:not([data-aw-theme="light"]), + .display-view-container:not([data-aw-theme="light"]) { + --aw-card-bg: #2a2a2a; + --aw-card-border: #3c3c3c; + --aw-chart-bg: #252526; + --aw-text: #cccccc; + --aw-text-muted: #adb5bd; + --aw-accent: #a5b2ff; + --aw-hover: #333333; + --aw-error-bg: #3a1f1f; + --aw-error-fg: #ff8a80; + --aw-dl: #5dd879; + --aw-ul: #6eb6ff; + --aw-vol-dl: #4ecbd8; + --aw-vol-ul: #b794f6; + --aw-pkt: #adb5bd; + } +} + +[data-darkmode="true"] .l7-view-container, +[data-darkmode="true"] .display-view-container, +.l7-view-container[data-aw-theme="dark"], +.display-view-container[data-aw-theme="dark"] { + --aw-card-bg: #2a2a2a; + --aw-card-border: #3c3c3c; + --aw-chart-bg: #252526; + --aw-text: #cccccc; + --aw-text-muted: #adb5bd; + --aw-accent: #a5b2ff; + --aw-hover: #333333; + --aw-error-bg: #3a1f1f; + --aw-error-fg: #ff8a80; + --aw-dl: #5dd879; + --aw-ul: #6eb6ff; + --aw-vol-dl: #4ecbd8; + --aw-vol-ul: #b794f6; + --aw-pkt: #adb5bd; +} + +[data-darkmode="false"] .l7-view-container, +[data-darkmode="false"] .display-view-container, +.l7-view-container[data-aw-theme="light"], +.display-view-container[data-aw-theme="light"] { + --aw-card-bg: #f9f9f9; + --aw-card-border: #e0e0e0; + --aw-chart-bg: #ffffff; + --aw-text: #333333; + --aw-text-muted: #666666; + --aw-accent: #3771c8; + --aw-hover: #f0f0f0; + --aw-error-bg: #ffefef; + --aw-error-fg: #c62828; + --aw-dl: #28a745; + --aw-ul: #007bff; + --aw-vol-dl: #17a2b8; + --aw-vol-ul: #6610f2; + --aw-pkt: #6c757d; +} + +.aw-inner-tabs { + margin-top: 4px; +} + +.aw-inner-tabs > .cbi-tabmenu { + margin-bottom: 12px; +} + +.pie label { + font-weight: bold; + font-size: 14px; + display: block; + margin-bottom: 10px; + text-align: center; +} + +.kpi ul { + list-style: none; +} + +.kpi li { + margin: 10px; + display: none; +} + +.kpi big { + font-weight: bold; +} + +.head { + text-align: center; + position: relative; + display: flex; + flex-wrap: wrap; + white-space: normal; +} + +.head .pie { + /* min-width: 200px; */ + padding: 5px; + flex: 1 1 30%; +} + +.cbi-tooltip .head .pie { + min-width: 100px; +} + +.head .kpi { + padding: 5px; + font-size: smaller; + text-align: left; + align-self: center; + flex: 1 0 33%; + min-width: 150px; + display: flex; + justify-content: center; +} + +.head .kpi ul { + margin: 0; +} + +.td.double > span { + display: block; +} + +.cbi-tooltip { + box-shadow: 0 0 5px #000; +} + +@media screen and (max-width: 992px) { + .td.hide-xs { + display: none; + } + + .td.double:not(.hide-xs) > span { + white-space: nowrap; + text-align: left; + } + + .td.double:not(.hide-xs) > span:first-child::before { + content: "IPv4: "; + font-weight: bold; + } + + .td.double:not(.hide-xs) > span:last-child::before { + content: "IPv6: "; + font-weight: bold; + } +} + +.form-group { + display: flex; + align-items: center; /* 垂直居中 */ + margin-bottom: 15px; /* 项间距 */ + gap: 10px; /* Label 和 Input 间距 */ +} + +/* 标签固定最小宽度 + 右对齐 */ +.form-label { + flex: 0 0 120px; /* 不伸缩、不收缩、基础宽度120px */ + text-align: right; + font-weight: bold; + color: var(--aw-text, #333); +} + +.th-sort-asc::after { + content: " ▲"; +} + +.th-sort-desc::after { + content: " ▼"; +} + +.table .th { + cursor: pointer; +} + +.table .th:hover { + background-color: var(--aw-hover, #f0f0f0); +} + +/* L7 View Specific Styles */ +.l7-view-container #l7-error-message { color: var(--aw-error-fg, red); background-color: var(--aw-error-bg, #ffefef); border: 1px solid var(--aw-error-fg, red); padding: 10px; margin-bottom: 10px; display: none; } +.l7-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; } +.l7-view-container .line-chart-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; } +.l7-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; } +.l7-view-container .kpi-card { background-color: var(--aw-card-bg, #f9f9f9); border-radius: 8px; padding: 15px; text-align: center; border: 1px solid var(--aw-card-border, #e0e0e0); } +.l7-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: var(--aw-accent, #3771c8); } +.l7-view-container .kpi-card-label { font-size: 0.9em; color: var(--aw-text-muted, #666); } +.l7-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } +.l7-view-container .chart-card { background-color: var(--aw-chart-bg, #ffffff); border-radius: 8px; padding: 20px; border: 1px solid var(--aw-card-border, #e0e0e0); } +.l7-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; color: var(--aw-text, inherit); } +.l7-view-container .l7-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding: 15px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + color: white; +} +.l7-view-container .l7-controls-left, .l7-view-container .l7-controls-right { + display: flex; + align-items: center; + gap: 15px; +} + +/* Display View Specific Styles */ +.display-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; } +.display-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; } +.display-view-container .kpi-card { background-color: var(--aw-card-bg, #f9f9f9); border-radius: 8px; padding: 15px; text-align: center; border: 1px solid var(--aw-card-border, #e0e0e0); } +.display-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: var(--aw-accent, #3771c8); } +.display-view-container .kpi-card-label { font-size: 0.9em; color: var(--aw-text-muted, #666); } +.display-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } +.display-view-container .chart-card { background-color: var(--aw-chart-bg, #ffffff); border-radius: 8px; padding: 20px; border: 1px solid var(--aw-card-border, #e0e0e0); } +.display-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; color: var(--aw-text, inherit); } +.display-view-container .display-controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + margin-top: 20px; +} + +/* Control Groups */ +.control-group { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background-color: rgba(255, 255, 255, 0.15); + border-radius: 6px; + backdrop-filter: blur(10px); +} + +.control-icon { + font-size: 1.2em; + display: inline-block; +} + +.control-label { + font-weight: 500; + color: white; + margin: 0; + white-space: nowrap; +} + +.control-input { + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.9); + padding: 6px 12px; + transition: all 0.3s ease; +} + +.control-input:focus { + background-color: white; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); + outline: none; +} + +.control-buttons { + display: flex; + align-items: center; + gap: 10px; +} + +.status-group { + background-color: rgba(255, 255, 255, 0.2); +} + +.last-updated-text { + color: rgba(255, 255, 255, 0.95); + font-size: 0.9em; + font-weight: 500; +} + +/* Enhanced Select Styling */ +.cbi-input-select { + padding: 6px 30px 6px 12px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.9); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23667eea' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 12px; + appearance: none; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 500; +} + +.cbi-input-select:hover { + background-color: white; + border-color: #667eea; +} + +.cbi-input-select:focus { + background-color: white; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); + outline: none; +} + +/* Button Enhancements for Controls */ +.l7-controls .cbi-button, +.display-controls .cbi-button { + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.cbi-button-action { + background: rgba(255, 193, 7, 0.9); + color: #333; + border-color: rgba(255, 193, 7, 0.3); +} + +.cbi-button-action:hover { + background: #ffc107; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(255, 193, 7, 0.4); +} + +.cbi-button-positive { + background: rgba(40, 167, 69, 0.9); + color: white; + border-color: rgba(40, 167, 69, 0.3); +} + +.cbi-button-positive:hover { + background: #28a745; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(40, 167, 69, 0.4); +} + +/* Responsive Controls */ +@media screen and (max-width: 768px) { + .l7-controls, + .display-controls { + flex-direction: column; + gap: 15px; + } + + .l7-controls-left, + .l7-controls-right, + .control-buttons { + width: 100%; + justify-content: center; + } + + .control-group { + justify-content: center; + } +} + +/* Table Cell Icon Styles */ +.activity-indicator { + font-size: 12px; + display: inline-block; + margin-right: 2px; +} + +.icon, .btn-icon, .th-icon { + display: inline-block; + font-style: normal; + margin-right: 4px; +} + +.th-icon { + opacity: 0.7; +} + +/* Cell Specific Styles */ +.sid-cell, .id-cell, .host-cell, .hostname-cell, .protocol-cell { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.speed-cell { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + font-weight: 500; + min-width: 100px; +} + +.speed-cell.download { + color: var(--aw-dl, #28a745); +} + +.speed-cell.upload { + color: var(--aw-ul, #007bff); +} + +.volume-cell { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + min-width: 90px; +} + +.volume-cell.download { + color: var(--aw-vol-dl, #17a2b8); +} + +.volume-cell.upload { + color: var(--aw-vol-ul, #6610f2); +} + +.packet-cell { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + color: var(--aw-pkt, #6c757d); + min-width: 80px; +} + +/* Data value with monospace font for better alignment */ +.data-value { + font-family: 'Courier New', Consolas, monospace; + font-size: 0.95em; + letter-spacing: 0.5px; +} + +/* Table header enhancements */ +.table .th { + font-weight: 600; + padding: 10px 8px; + white-space: nowrap; +} + +.table .td { + padding: 8px; + vertical-align: middle; +} + +.protocol-icon.l7::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #5470c6; + margin-right: 4px; +} + +.protocol-icon.domain::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #91cc75; + margin-right: 4px; +} + +/* Button Enhancements */ +.cbi-button { + display: inline-flex; + align-items: center; + gap: 4px; + transition: all 0.2s ease; +} + +.cbi-button:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.cbi-button-edit { + background-color: #ffc107; + border-color: #ffc107; +} + +.cbi-button-edit:hover { + background-color: #e0a800; + border-color: #e0a800; +} + +.cbi-button-remove { + background-color: #dc3545; + border-color: #dc3545; + color: white; +} + +.cbi-button-remove:hover { + background-color: #c82333; + border-color: #bd2130; +} + +.cbi-button-add { + background-color: #28a745; + border-color: #28a745; + color: white; +} + +.cbi-button-add:hover:not(:disabled) { + background-color: #218838; + border-color: #1e7e34; +} + +.cbi-button-add:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Table Row Hover Effects */ +.table .tr:not(.table-titles):not(.placeholder):hover { + background-color: var(--aw-hover, #f8f9fa); + transition: background-color 0.2s ease; +} + +/* Active/Inactive Status Styles */ +.activity-indicator[title*="Active"] { + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Responsive Icon Handling */ +@media screen and (max-width: 768px) { + .icon, .btn-icon { + margin-right: 2px; + } + + .btn-icon + span { + display: none; + } + + .th-icon { + margin-right: 0; + } +} diff --git a/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/display.js b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/display.js new file mode 100644 index 00000000..82adaf4e --- /dev/null +++ b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/display.js @@ -0,0 +1,787 @@ +'use strict'; +'require view'; +'require fs'; +'require ui'; +'require poll'; +'require rpc'; +'require dom'; +'require uci'; + +// Global variables from original display.js +var chartRegistry = {}; +var hostNames = {}; // mac => hostname +var hostInfo = {}; // ip => mac +var hostNameMacSectionId = ""; +var isPaused = false; +var lastUpdated = null; + +// Line chart variables (from l7.js) +var downloadLineChart = {}, uploadLineChart = {}; +var lineCategories = { ipv4: [], ipv6: [], mac: [] }; +var downloadSeriesData = { ipv4: {}, ipv6: {}, mac: {} }; +var uploadSeriesData = { ipv4: {}, ipv6: {}, mac: {} }; + +// Color palette for chart series +var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']; + +var resizeListenerAdded = false; + +// Pre-fill with 60 empty points for a smooth start +['ipv4', 'ipv6', 'mac'].forEach(function(type) { + for (var i = 0; i < 60; i++) { + lineCategories[type].push(''); + } +}); + +// Helper to convert hex to rgba (from l7.js) +function hexToRgba(hex, opacity) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? + 'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' : + null; +}; + +function isDarkMode() { + var attr = document.documentElement.getAttribute('data-darkmode'); + if (attr === 'true') + return true; + if (attr === 'false') + return false; + + var bg = getComputedStyle(document.body).backgroundColor; + var m = bg && bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (m) { + var lum = (0.299 * m[1] + 0.587 * m[2] + 0.114 * m[3]) / 255; + return lum < 0.5; + } + + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +function getChartColors() { + var dark = isDarkMode(); + return { + background: 'transparent', + text: dark ? '#cccccc' : '#333333', + muted: dark ? '#adb5bd' : '#666666', + axis: dark ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.25)', + split: dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)', + pieBorder: dark ? '#252526' : '#ffffff', + tooltipBg: dark ? 'rgba(32,32,32,0.94)' : 'rgba(255,255,255,0.95)', + tooltipBorder: dark ? '#555555' : '#cccccc', + tooltipText: dark ? '#eeeeee' : '#333333' + }; +} + +function applyViewTheme() { + var theme = isDarkMode() ? 'dark' : 'light'; + document.querySelectorAll('.l7-view-container, .display-view-container').forEach(function(el) { + el.setAttribute('data-aw-theme', theme); + }); + return theme; +} + +function observeChartEl(chart, el) { + if (!chart || !el || !window.ResizeObserver || el._awRo) + return; + el._awRo = new ResizeObserver(function() { + chart.resize(); + }); + el._awRo.observe(el); +} + +function chartAxisTheme(colors) { + return { + backgroundColor: colors.background, + textStyle: { color: colors.text }, + legend: { textStyle: { color: colors.text } }, + tooltip: { + backgroundColor: colors.tooltipBg, + borderColor: colors.tooltipBorder, + textStyle: { color: colors.tooltipText } + }, + xAxis: { + axisLine: { lineStyle: { color: colors.axis } }, + axisLabel: { color: colors.muted }, + splitLine: { show: false } + }, + yAxis: { + axisLine: { lineStyle: { color: colors.axis } }, + axisLabel: { color: colors.muted }, + splitLine: { lineStyle: { color: colors.split } } + } + }; +} + +return view.extend({ + // --- Core Data Logic from display.js --- + + loadHostNames: async function() { + try { + await uci.sections('hostnames', "hostname", function (params) { + hostNameMacSectionId = params['.name']; + for (var key in params) { + if (key.startsWith('.')) continue; + var macAddr = key.split('_').join(':'); + hostNames[macAddr] = params[key]; + } + }); + + const dhcpLeases = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $2, $3, $4}', '/tmp/dhcp.leases'], 'text'); + dhcpLeases.split('\n').forEach(function(line) { + if (line === '') return; + const [mac, ip, hostname] = line.split(' '); + if (!hostNames.hasOwnProperty(mac)) { + hostNames[mac] = hostname; + } + }); + + const arp = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $1, $4}', '/proc/net/arp'], 'text'); + arp.split('\n').forEach(function(line, i) { + if (i === 0 || line === '') return; + const [ip, mac] = line.split(' '); + hostInfo[ip] = mac; + }); + + } catch (e) { + console.error('Error getting host names:', e); + } + }, + + loadHostSpeedData: async function() { + var self = this; + try { + const results = await Promise.all([ + fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv4', 'json'], 'json'), + fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv6', 'json'], 'json'), + fs.exec_direct('/usr/bin/aw-bpfctl', ['mac', 'json'], 'json') + ]); + + const defaultData = {status: "success", data: []}; + const ipv4Data = results[0] || defaultData; + const ipv6Data = results[1] || defaultData; + const macData = results[2] || defaultData; + + ipv4Data.data.forEach(function(item) { + const mac = hostInfo[item.ip]; + if (mac) { + item.mac = mac; + item.hostname = hostNames[mac]; + } + }); + macData.data.forEach(function(item) { + const mac = item.mac; + if (mac) { + item.hostname = hostNames[mac]; + } + }); + + self.renderHostSpeed(ipv4Data, "ipv4"); + self.renderHostSpeed(ipv6Data, "ipv6"); + self.renderHostSpeed(macData, "mac"); + + lastUpdated = new Date(); + var timestampEl = document.getElementById('display-last-updated'); + if (timestampEl) { + timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString()); + } + + } catch (e) { + console.error('Error polling data:', e); + } + }, + + pollData: function() { + poll.add(L.bind(async function() { + if (isPaused) return; + await this.loadHostNames(); + await this.loadHostSpeedData(); + }, this), 5); + }, + + resizeAllCharts: function() { + ['ipv4', 'ipv6', 'mac'].forEach(function(type) { + if (downloadLineChart[type]) + downloadLineChart[type].resize(); + if (uploadLineChart[type]) + uploadLineChart[type].resize(); + }); + Object.keys(chartRegistry).forEach(function(chartId) { + if (chartRegistry[chartId]) + chartRegistry[chartId].resize(); + }); + }, + + bindTabChartResize: function(root) { + var self = this; + if (!root) + return; + var host = root.parentNode || root; + if (host._awTabResizeBound) + return; + host._awTabResizeBound = true; + + var schedule = function() { + setTimeout(function() { self.resizeAllCharts(); }, 80); + }; + + host.addEventListener('click', function(ev) { + if (ev.target.closest && ev.target.closest('ul.cbi-tabmenu')) + schedule(); + }); + root.querySelectorAll('[data-tab]').forEach(function(pane) { + pane.addEventListener('cbi-tab-active', schedule); + }); + }, + + // --- UI Rendering and Interaction (New structure based on l7.js) --- + + pie: function(id, data, valueFormatter) { + var total = data.reduce(function(n, d) { return n + d.value; }, 0); + data.sort(function(a, b) { return b.value - a.value; }); + + if (total === 0) { + data = [{ value: 1, color: '#cccccc', name: _('no traffic') }]; + } + + data.forEach(function(d, i) { + if (!d.color) { + var hue = (i * 137.508) % 360; + d.color = 'hsl(' + hue + ', 75%, 55%)'; + } + }); + + var colors = getChartColors(); + var option = { + backgroundColor: colors.background, + textStyle: { color: colors.text }, + tooltip: { + trigger: 'item', + backgroundColor: colors.tooltipBg, + borderColor: colors.tooltipBorder, + textStyle: { color: colors.tooltipText }, + formatter: function(params) { + if (valueFormatter) { + // 将 ECharts params 对象转换为自定义格式 + return valueFormatter({ + name: params.name, + value: params.value, + percent: params.percent.toFixed(2) + }); + } + return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)'; + } + }, + series: [{ + type: 'pie', + radius: ['25%', '80%'], + avoidLabelOverlap: false, + padAngle: 10, + itemStyle: { borderRadius: 10, borderColor: colors.pieBorder, borderWidth: 2 }, + label: { show: false, position: 'center', color: colors.text }, + emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold', color: colors.text } }, + labelLine: { show: false }, + data: data.map(function(d) { + return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } }; + }) + }] + }; + + var dom = typeof id === 'string' ? document.getElementById(id) : id; + if (!chartRegistry[id]) { + chartRegistry[id] = echarts.init(dom); + observeChartEl(chartRegistry[id], dom); + } + chartRegistry[id].setOption(option, true); + return chartRegistry[id]; + }, + + updateStackedLineCharts: function(type, perHostDownload, perHostUpload) { + var now = new Date().toLocaleTimeString(); + lineCategories[type].push(now); + lineCategories[type].shift(); + + var processChartData = function(seriesData, perHostData) { + var allHosts = Object.keys(seriesData); + Object.keys(perHostData).forEach(function(host) { + if (allHosts.indexOf(host) === -1) { + allHosts.push(host); + } + }); + + allHosts.forEach(function(host) { + if (!seriesData[host]) { + seriesData[host] = Array(59).fill(0); + } + var rate = perHostData[host] || 0; + seriesData[host].push(rate); + seriesData[host].shift(); + }); + + return Object.keys(seriesData).map(function(host, index) { + var color = colorPalette[index % colorPalette.length]; + return { + name: host, + type: 'line', + stack: 'Total', + smooth: true, + lineStyle: { width: 1, color: color }, + showSymbol: false, + itemStyle: { color: color }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: hexToRgba(color, 0.5) }, + { offset: 1, color: hexToRgba(color, 0) } + ]) + }, + data: seriesData[host] + }; + }); + }; + + var downloadChartSeries = processChartData(downloadSeriesData[type], perHostDownload); + var uploadChartSeries = processChartData(uploadSeriesData[type], perHostUpload); + + var legendData = downloadChartSeries.map(function(s) { return s.name; }); + + var colors = getChartColors(); + if (downloadLineChart[type]) { + downloadLineChart[type].setOption({ + legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } }, + series: downloadChartSeries, + xAxis: { data: lineCategories[type] } + }); + } + + if (uploadLineChart[type]) { + uploadLineChart[type].setOption({ + legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } }, + series: uploadChartSeries, + xAxis: { data: lineCategories[type] } + }); + } + }, + + renderHostSpeed: function(data, type) { + if (!data || data.status !== "success" || !Array.isArray(data.data)) return; + + var rows = []; + var txRateData = [], rxRateData = []; + var txVolumeData = [], rxVolumeData = []; + var tx_rate_total = 0, rx_rate_total = 0; + var tx_bytes_total = 0, rx_bytes_total = 0; + var perHostTxRate = {}; + var perHostRxRate = {}; + + data.data.forEach(item => { + if (!item || !item.incoming || !item.outgoing) return; + + var host = item.ip || item.mac || ''; + var hostname = item.hostname || hostNames[item.mac] || ''; + var displayName = hostname || host; + + // 判断连接是否活跃 + var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0; + var activityIcon = isActive ? '🟢' : '⚪'; + + rows.push([ + E('span', { 'class': 'host-cell' }, [ + E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon), + E('span', {}, ' ' + host) + ]), + E('span', { 'class': 'hostname-cell' }, [ + E('span', { 'class': 'icon' }, hostname ? '👤' : '❓'), + E('span', {}, ' ' + (hostname || _('Unknown'))) + ]), + E('span', { 'class': 'speed-cell download' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.incoming.rate)) + ]), + E('span', { 'class': 'volume-cell download' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes)) + ]), + E('span', { 'class': 'packet-cell download' }, [ + E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets)) + ]), + E('span', { 'class': 'speed-cell upload' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.outgoing.rate)) + ]), + E('span', { 'class': 'volume-cell upload' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes)) + ]), + E('span', { 'class': 'packet-cell upload' }, [ + E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets)) + ]), + E('div', { 'class': 'button-container' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-edit', + 'style': 'margin-right: 5px;', + 'click': ui.createHandlerFn(this, () => this.handleEditSpeed(host, item.mac, hostname, type)) + }, [ + E('span', { 'class': 'btn-icon' }, '✏️'), + E('span', {}, ' ' + _('Edit')) + ]), + E('button', { + 'class': 'btn cbi-button cbi-button-remove', + 'click': ui.createHandlerFn(this, () => this.handleDeleteHost(host, type)) + }, [ + E('span', { 'class': 'btn-icon' }, '🗑️'), + E('span', {}, ' ' + _('Delete')) + ]) + ]) + ]); + rx_rate_total += item.outgoing.rate; + tx_rate_total += item.incoming.rate; + rx_bytes_total += item.outgoing.total_bytes; + tx_bytes_total += item.incoming.total_bytes; + + rxRateData.push({ value: item.outgoing.rate, label: displayName }); + txRateData.push({ value: item.incoming.rate, label: displayName }); + rxVolumeData.push({ value: item.outgoing.total_bytes, label: displayName }); + txVolumeData.push({ value: item.incoming.total_bytes, label: displayName }); + + perHostTxRate[displayName] = (perHostTxRate[displayName] || 0) + item.incoming.rate; + perHostRxRate[displayName] = (perHostRxRate[displayName] || 0) + item.outgoing.rate; + }); + + this.updateStackedLineCharts(type, perHostTxRate, perHostRxRate); + + var table = document.getElementById(type + '-speed-data'); + cbi_update_table(table, rows, E('em', _('No data recorded yet.'))); + + this.pie(type + '-tx-rate-pie', txRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`); + this.pie(type + '-rx-rate-pie', rxRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`); + this.pie(type + '-tx-volume-pie', txVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`); + this.pie(type + '-rx-volume-pie', rxVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`); + + var hostEl = document.getElementById(type + '-host-val'); + if (hostEl) hostEl.textContent = data.data.length; + + var txRateEl = document.getElementById(type + '-tx-rate-val'); + if (txRateEl) txRateEl.textContent = '%1024.2mBps'.format(tx_rate_total); + + var rxRateEl = document.getElementById(type + '-rx-rate-val'); + if (rxRateEl) rxRateEl.textContent = '%1024.2mBps'.format(rx_rate_total); + + var txVolEl = document.getElementById(type + '-tx-volume-val'); + if (txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total); + + var rxVolEl = document.getElementById(type + '-rx-volume-val'); + if (rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total); + }, + + // --- Interaction Handlers from display.js --- + + handleDeleteHost: function(host, type) { + ui.showModal(_('Delete Host'), [ + E('p', _('Are you sure you want to delete this host?')), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + E('button', { 'class': 'btn cbi-button-negative', 'click': ui.createHandlerFn(this, async () => { + try { + await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'del', host], 'text'); + this.loadHostSpeedData(); + ui.hideModal(); + } catch (e) { + ui.addNotification(null, E('p', _('Error: ') + e.message)); + ui.hideModal(); + } + })}, _('Delete')) + ]) + ]); + }, + + handleEditSpeed: function(host, mac, hostname, type) { + fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'json'], 'json').then(L.bind(res => { + let rate_limit_dl = 0, rate_limit_ul = 0; + if (res && res.status === 'success' && Array.isArray(res.data)) { + const item = res.data.find(d => (d.ip === host || d.mac === host)); + if (item) { + rate_limit_dl = (item.incoming.incoming_rate_limit || 0) / 1024 / 1024; + rate_limit_ul = (item.outgoing.outgoing_rate_limit || 0) / 1024 / 1024; + } + } + this.displaySpeedLimitDialog(host, mac, hostname, type, rate_limit_dl, rate_limit_ul); + }, this)).catch(e => { + console.error('Error getting speed limit:', e); + this.displaySpeedLimitDialog(host, mac, hostname, type, 0, 0); + }); + }, + + displaySpeedLimitDialog: function(host, mac, hostname, type, dl, ul) { + const inputDom = E('input', { type: 'text', id: 'host-name', class: 'cbi-input-text', value: hostname, disabled: !mac }); + + ui.showModal(_('Edit Speed Limit'), [ + E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Host')), E('span',{}, host) ]), + E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Hostname')), inputDom ]), + E('div', { 'class': 'form-group' }, [ + E('label', { 'class': 'form-label' }, _('Download Limit')), + E('input', { type: 'number', id: 'dl-rate', class: 'cbi-input-number', min: '0', value: dl }), + E('span',{}, " Mbps") + ]), + E('div', { 'class': 'form-group' }, [ + E('label', { 'class': 'form-label' }, _('Upload Limit')), + E('input', { type: 'number', id: 'ul-rate', class: 'cbi-input-number', min: '0', value: ul }), + E('span',{}, " Mbps") + ]), + E('div', { 'class': 'cbi-page-actions right' }, [ + E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'click': ui.hideModal }, _('Cancel')), + E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, async ev => { + const dl_val = document.getElementById('dl-rate').value; + const ul_val = document.getElementById('ul-rate').value; + const newName = document.getElementById('host-name').value; + try { + if (mac && newName !== hostname) { + hostNames[mac] = newName; + await uci.set('hostnames', hostNameMacSectionId, mac.split(':').join('_'), newName); + await uci.save('hostnames'); + await uci.apply('hostnames'); + } + await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'update', host, "downrate", dl_val*1024*1024 || '0', "uprate", ul_val*1024*1024 || '0']); + this.loadHostSpeedData(); + ui.addNotification(null, E('p',_('Speed limit updated'))); + ui.hideModal(); + } catch (e) { + ui.addNotification(null, E('p', _('Error: ') + e.message)); + } + })}, _('Save')) + ]) + ]); + }, + + validateData: function(value, type) { + if (typeof value !== 'string') return false; + const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/; + const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$/i; + const macRegex = /^([0-9A-Fa-f]{2}([-:]))([0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}$|^([0-9A-Fa-f]{12})$/i; + return (type === 'ipv4') ? ipv4Regex.test(value) : (type === 'ipv6') ? ipv6Regex.test(value) : macRegex.test(value); + }, + + createAddControls: function(type, placeholder) { + const input = E('input', { + type: 'text', + class: 'cbi-input-text control-input', + style: (type === 'ipv6') ? 'width:320px' : 'width:180px', + placeholder: _(placeholder) + }); + const addBtn = E('button', { + class: 'btn cbi-button cbi-button-add', + disabled: true + }, [ + E('span', { 'class': 'btn-icon' }, '➕'), + E('span', {}, ' ' + _('Add')) + ]); + const refreshBtn = E('button', { + class: 'btn cbi-button cbi-button-action', + click: () => this.loadHostSpeedData() + }, [ + E('span', { 'class': 'btn-icon' }, '🔄'), + E('span', {}, ' ' + _('Refresh')) + ]); + + input.addEventListener('input', () => { addBtn.disabled = (input.value.trim() === ''); }); + addBtn.addEventListener('click', ui.createHandlerFn(this, async () => { + const value = input.value.trim(); + if (!this.validateData(value, type)) { + return ui.addNotification(null, E('p', _('Data format error'))); + } + try { + await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'add', value]); + this.loadHostSpeedData(); + ui.addNotification(null, E('p',_('Updated successfully!'))); + input.value = ''; + addBtn.disabled = true; + } catch (e) { + ui.addNotification(null, E('p', _('Error: ') + e.message)); + } + })); + + return E('div', { 'class': 'display-controls' }, [ + E('div', { 'class': 'control-group' }, [ + E('span', { 'class': 'control-icon' }, '🖥️'), + E('label', { 'class': 'control-label' }, _('Add Host:')), + input + ]), + E('div', { 'class': 'control-buttons' }, [ + addBtn, + refreshBtn, + E('div', { 'class': 'control-group status-group' }, [ + E('span', { 'class': 'control-icon' }, '🕐'), + E('span', { 'id': 'display-last-updated', 'class': 'last-updated-text' }, _('Ready')) + ]) + ]) + ]); + }, + + initializeUI: function() { + applyViewTheme(); + if (window.echarts) { + var self = this; + var colors = getChartColors(); + var axisTheme = chartAxisTheme(colors); + ['ipv4', 'ipv6', 'mac'].forEach(function(type) { + var dlChartEl = document.getElementById(type + '-download-speed-line-chart'); + var ulChartEl = document.getElementById(type + '-upload-speed-line-chart'); + if (!dlChartEl || !ulChartEl) return; + + var baseChartOption = { + backgroundColor: axisTheme.backgroundColor, + textStyle: axisTheme.textStyle, + legend: axisTheme.legend, + tooltip: Object.assign({ + trigger: 'axis', + formatter: function (params) { + if (!params || params.length === 0) { + return null; + } + var tooltipContent = params[0].axisValueLabel + '
'; + params.sort(function(a, b) { return b.value - a.value; }); + params.forEach(function(item) { + if (item.value > 0) { + tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mBps'.format(item.value) + '
'; + } + }); + return tooltipContent; + } + }, axisTheme.tooltip), + grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true }, + xAxis: { + type: 'category', + boundaryGap: false, + data: lineCategories[type], + axisLine: axisTheme.xAxis.axisLine, + axisLabel: axisTheme.xAxis.axisLabel, + splitLine: axisTheme.xAxis.splitLine + }, + yAxis: { + type: 'value', + axisLine: axisTheme.yAxis.axisLine, + splitLine: axisTheme.yAxis.splitLine, + axisLabel: { formatter: function(val) { return '%1024.2mBps'.format(val); }, color: colors.muted } + }, + series: [] + }; + + downloadLineChart[type] = echarts.init(dlChartEl); + downloadLineChart[type].setOption(baseChartOption); + observeChartEl(downloadLineChart[type], dlChartEl); + + uploadLineChart[type] = echarts.init(ulChartEl); + uploadLineChart[type].setOption(baseChartOption); + observeChartEl(uploadLineChart[type], ulChartEl); + }); + + // 添加窗口大小变化监听器,使图表能够响应式调整 + if (!resizeListenerAdded) { + var resizeTimer = null; + var resizeHandler = function() { + // 使用防抖,避免频繁触发 resize + if (resizeTimer) { + clearTimeout(resizeTimer); + } + resizeTimer = setTimeout(function() { + self.resizeAllCharts(); + }, 200); + }; + + window.addEventListener('resize', resizeHandler); + resizeListenerAdded = true; + } + + this.pollData(); + } else { + setTimeout(this.initializeUI.bind(this), 50); + } + }, + + // --- Main Render Function (New) --- + + render: function() { + var self = this; + + const createTab = (type, title, placeholder) => { + var innerTabs = E('div', { 'class': 'aw-inner-tabs' }, [ + E('div', { 'class': 'cbi-section', 'data-tab': type + '-trend', 'data-tab-title': _('Speed Trend') }, [ + E('div', { 'class': 'dashboard-container' }, [ + E('div', { 'class': 'kpi-row' }, [ + E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-host-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Hosts')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ]) + ]), + E('div', { 'class': 'line-chart-row' }, [ + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Real-time Download Speed')]), + E('div', { id: type + '-download-speed-line-chart', style: 'width: 100%; height: 350px;' }) + ]), + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Real-time Upload Speed')]), + E('div', { id: type + '-upload-speed-line-chart', style: 'width: 100%; height: 350px;' }) + ]) + ]) + ]) + ]), + E('div', { 'class': 'cbi-section', 'data-tab': type + '-share', 'data-tab-title': _('Traffic Share') }, [ + E('div', { 'class': 'dashboard-container' }, [ + E('div', { 'class': 'chart-grid' }, [ + E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Speed / Host')]), E('div', { id: type + '-tx-rate-pie', style: 'width:100%; height:300px;' }) ]), + E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Speed / Host')]), E('div', { id: type + '-rx-rate-pie', style: 'width:100%; height:300px;' }) ]), + E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Total')]), E('div', { id: type + '-tx-volume-pie', style: 'width:100%; height:300px;' }) ]), + E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Total')]), E('div', { id: type + '-rx-volume-pie', style: 'width:100%; height:300px;' }) ]) + ]) + ]) + ]), + E('div', { 'class': 'cbi-section', 'data-tab': type + '-hosts', 'data-tab-title': _('Host List') }, [ + E('table', { 'class': 'table', 'id': type + '-speed-data' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🖥️'), ' ', _('Host') ]), + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '👤'), ' ', _('Hostname') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download Total') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download Packets') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload Total') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload Packets') ]), + E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚙️'), ' ', _('Actions') ]) + ]), + E('tr', { 'class': 'tr placeholder' }, [ E('td', { 'class': 'td', 'colspan': '9' }, [ E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) ]) ]) + ]), + self.createAddControls(type, placeholder) + ]) + ]); + + return E('div', { 'class': 'cbi-section', 'data-tab': type, 'data-tab-title': _(title) }, [ + innerTabs + ]); + }; + + var tabContainer = E('div', {}, [ + createTab('ipv4', 'IPv4', 'Please enter a valid IPv4 address'), + createTab('ipv6', 'IPv6', 'Please enter a valid IPv6 address'), + createTab('mac', 'MAC', 'Please enter a valid MAC address') + ]); + + var node = E([], [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('view/aw-bpf.css') }), + E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }), + E('div', { 'class': 'l7-view-container', 'data-aw-theme': isDarkMode() ? 'dark' : 'light' }, [ + E('h2', [ _('Host Speed Monitor') ]), + tabContainer + ]) + ]); + + tabContainer.querySelectorAll('.aw-inner-tabs').forEach(function(inner) { + ui.tabs.initTabGroup(inner.childNodes); + }); + ui.tabs.initTabGroup(tabContainer.childNodes); + this.bindTabChartResize(tabContainer); + + setTimeout(() => this.initializeUI(), 0); + + return node; + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); diff --git a/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/l7.js b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/l7.js new file mode 100644 index 00000000..7bc7e09d --- /dev/null +++ b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/l7.js @@ -0,0 +1,996 @@ +'use strict'; +'require view'; +'require fs'; +'require ui'; +'require poll'; +'require rpc'; +'require dom'; + +var chartRegistry = {}; +var downloadLineChart, uploadLineChart; + +// Data structures for stacked line charts +var lineCategories = []; +var downloadSeriesData = {}; +var uploadSeriesData = {}; + +// Color palette for chart series +var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']; + +var currentSortInfo = { + table: null, + column: null, + reverse: false +}; +var sidLookupTable = {}; +var isPaused = false; +var lastUpdated = null; +var pollActive = false; +var lastSIDData = null; +var lastL7ProtoData = null; +var resizeListenerAdded = false; +var resizeTimer = null; + +// Pre-fill with 60 empty points for a smooth start +for (var i = 0; i < 60; i++) { + lineCategories.push(''); +} + +// Helper to convert hex to rgba +function hexToRgba(hex, opacity) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? + 'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' : + null; +}; + +function isDarkMode() { + var attr = document.documentElement.getAttribute('data-darkmode'); + if (attr === 'true') + return true; + if (attr === 'false') + return false; + + var bg = getComputedStyle(document.body).backgroundColor; + var m = bg && bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (m) { + var lum = (0.299 * m[1] + 0.587 * m[2] + 0.114 * m[3]) / 255; + return lum < 0.5; + } + + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +function getChartColors() { + var dark = isDarkMode(); + return { + background: 'transparent', + text: dark ? '#cccccc' : '#333333', + muted: dark ? '#adb5bd' : '#666666', + axis: dark ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.25)', + split: dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)', + pieBorder: dark ? '#252526' : '#ffffff', + tooltipBg: dark ? 'rgba(32,32,32,0.94)' : 'rgba(255,255,255,0.95)', + tooltipBorder: dark ? '#555555' : '#cccccc', + tooltipText: dark ? '#eeeeee' : '#333333' + }; +} + +function applyViewTheme() { + var theme = isDarkMode() ? 'dark' : 'light'; + document.querySelectorAll('.l7-view-container, .display-view-container').forEach(function(el) { + el.setAttribute('data-aw-theme', theme); + }); + return theme; +} + +function observeChartEl(chart, el) { + if (!chart || !el || !window.ResizeObserver || el._awRo) + return; + el._awRo = new ResizeObserver(function() { + chart.resize(); + }); + el._awRo.observe(el); +} + +function chartAxisTheme(colors) { + return { + backgroundColor: colors.background, + textStyle: { color: colors.text }, + legend: { textStyle: { color: colors.text } }, + tooltip: { + backgroundColor: colors.tooltipBg, + borderColor: colors.tooltipBorder, + textStyle: { color: colors.tooltipText } + }, + xAxis: { + axisLine: { lineStyle: { color: colors.axis } }, + axisLabel: { color: colors.muted }, + splitLine: { show: false } + }, + yAxis: { + axisLine: { lineStyle: { color: colors.axis } }, + axisLabel: { color: colors.muted }, + splitLine: { lineStyle: { color: colors.split } } + } + }; +} + +return view.extend({ + hasXdns: false, + xdnsDomains: {}, + + load: function() { + return Promise.all([ + this.loadSIDData(), + this.loadL7ProtoData(), + this.checkXdnsStatus() + ]); + }, + + checkXdnsStatus: function() { + var self = this; + return fs.stat('/usr/bin/xdns-ctl').then(function(stat) { + if (stat && stat.type === 'file') { + self.hasXdns = true; + return fs.read_direct('/etc/xdns/whitelist.txt').then(function(content) { + var domains = {}; + if (content) { + content.split('\n').forEach(function(line) { + line = line.trim(); + if (!line || line.charAt(0) === '#') return; + if (line.indexOf('*.') === 0) line = line.substring(2); + if (line.charAt(0) === '.') line = line.substring(1); + domains[line.toLowerCase()] = true; + }); + } + self.xdnsDomains = domains; + return true; + }).catch(function() { + self.xdnsDomains = {}; + return true; + }); + } else { + self.hasXdns = false; + return false; + } + }).catch(function() { + self.hasXdns = false; + return false; + }); + }, + + isDomainProxied: function(dName) { + if (!dName || !this.xdnsDomains) return false; + dName = dName.toLowerCase(); + if (this.xdnsDomains[dName]) return true; + var parts = dName.split('.'); + for (var i = 1; i < parts.length - 1; i++) { + var parent = parts.slice(i).join('.'); + if (this.xdnsDomains[parent]) return true; + } + return false; + }, + + handleAddXdnsDomain: function(domain, btn) { + var self = this; + if (!domain) return; + btn.disabled = true; + var origText = btn.textContent; + btn.textContent = _('添加中...'); + + fs.exec_direct('/usr/bin/xdns-ctl', ['add-domain', domain]).then(function() { + self.xdnsDomains[domain.toLowerCase()] = true; + ui.addNotification(null, E('p', _('域名「%s」已成功加入 xdns-bpf 代理名单并即刻生效!').format(domain)), 'info'); + if (lastL7ProtoData) { + self.renderL7ProtoData(lastL7ProtoData); + } + }).catch(function(err) { + btn.disabled = false; + btn.textContent = origText; + ui.addNotification(null, E('p', _('加入代理名单失败: %s').format(err.message || err)), 'error'); + }); + }, + + showError: function(message) { + var errorEl = document.getElementById('l7-error-message'); + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = 'block'; + } + }, + + hideError: function() { + var errorEl = document.getElementById('l7-error-message'); + if (errorEl) { + errorEl.style.display = 'none'; + } + }, + + loadSIDData: function() { + var self = this; + return fs.exec_direct('/usr/bin/aw-bpfctl', ['sid', 'json'], 'json').then(function(result) { + self.hideError(); + lastSIDData = result; + return result; + }).catch(function(error) { + console.error('Error loading SID data:', error); + self.showError(_('Error loading SID data: %s').format(error.message)); + return { status: 'error', data: [] }; + }); + }, + + loadL7ProtoData: function() { + var self = this; + return fs.exec_direct('/usr/bin/aw-bpfctl', ['l7', 'json'], 'json').then(function(result) { + self.hideError(); + return result; + }).catch(function(error) { + console.error('Error loading L7 protocol data:', error); + self.showError(_('Error loading L7 protocol data: %s').format(error.message)); + return { status: 'error', data: [] }; + }); + }, + + updateStackedLineCharts: function(perServiceDownload, perServiceUpload) { + var now = new Date().toLocaleTimeString(); + lineCategories.push(now); + lineCategories.shift(); + + var processChartData = function(seriesData, perServiceData) { + var allServices = Object.keys(seriesData); + Object.keys(perServiceData).forEach(function(service) { + if (allServices.indexOf(service) === -1) { + allServices.push(service); + } + }); + + allServices.forEach(function(service) { + if (!seriesData[service]) { + seriesData[service] = Array(59).fill(0); + } + var rate = perServiceData[service] || 0; + seriesData[service].push(rate); + seriesData[service].shift(); + }); + + return Object.keys(seriesData).map(function(service, index) { + var color = colorPalette[index % colorPalette.length]; + return { + name: service, + type: 'line', + stack: 'Total', + smooth: true, + lineStyle: { width: 1, color: color }, + showSymbol: false, + itemStyle: { color: color }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: hexToRgba(color, 0.5) }, + { offset: 1, color: hexToRgba(color, 0) } + ]) + }, + data: seriesData[service] + }; + }); + }; + + var downloadChartSeries = processChartData(downloadSeriesData, perServiceDownload); + var uploadChartSeries = processChartData(uploadSeriesData, perServiceUpload); + + var legendData = downloadChartSeries.map(function(s) { return s.name; }); + var colors = getChartColors(); + + if (downloadLineChart) { + downloadLineChart.setOption({ + legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } }, + series: downloadChartSeries, + xAxis: { data: lineCategories } + }); + } + + if (uploadLineChart) { + uploadLineChart.setOption({ + legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } }, + series: uploadChartSeries, + xAxis: { data: lineCategories } + }); + } + }, + + pie: function(id, data, valueFormatter) { + var total = data.reduce(function(n, d) { return n + d.value; }, 0); + + data.sort(function(a, b) { return b.value - a.value; }); + + if (total === 0) { + data = [{ value: 1, color: '#cccccc', name: _('no traffic') }]; + } + + data.forEach(function(d, i) { + if (!d.color) { + var hue = (i * 137.508) % 360; + d.color = 'hsl(' + hue + ', 75%, 55%)'; + } + }); + + var colors = getChartColors(); + var option = { + backgroundColor: colors.background, + textStyle: { color: colors.text }, + tooltip: { + trigger: 'item', + backgroundColor: colors.tooltipBg, + borderColor: colors.tooltipBorder, + textStyle: { color: colors.tooltipText }, + formatter: function(params) { + if (valueFormatter) { + // 将 ECharts params 对象转换为自定义格式 + return valueFormatter({ + name: params.name, + value: params.value, + percent: params.percent.toFixed(2) + }); + } + return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)'; + } + }, + series: [{ + type: 'pie', + radius: ['25%', '80%'], + avoidLabelOverlap: false, + padAngle: 10, + itemStyle: { borderRadius: 10, borderColor: colors.pieBorder, borderWidth: 2 }, + label: { show: false, position: 'center', color: colors.text }, + emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold', color: colors.text } }, + labelLine: { show: false }, + data: data.map(function(d) { + return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } }; + }) + }] + }; + + var dom = typeof id === 'string' ? document.getElementById(id) : id; + + if (!chartRegistry[id]) { + chartRegistry[id] = echarts.init(dom); + observeChartEl(chartRegistry[id], dom); + } + + chartRegistry[id].setOption(option, true); + + return chartRegistry[id]; + }, + + sortTable: function(table, column) { + var tbody = table.querySelector('tbody'); + if (!tbody) return; + var rows = Array.from(tbody.querySelectorAll('tr:not(.table-titles):not(.placeholder)')); + var reverse = (currentSortInfo.table === table && currentSortInfo.column === column) ? !currentSortInfo.reverse : false; + + table.querySelectorAll('th').forEach(function(th) { + th.classList.remove('th-sort-asc', 'th-sort-desc'); + }); + + var th = table.querySelector('th:nth-child(' + (column + 1) + ')'); + th.classList.add(reverse ? 'th-sort-desc' : 'th-sort-asc'); + + rows.sort(function(row1, row2) { + var a = row1.cells[column].getAttribute('data-value') || row1.cells[column].textContent; + var b = row2.cells[column].getAttribute('data-value') || row2.cells[column].textContent; + + if (!isNaN(a) && !isNaN(b)) { a = Number(a); b = Number(b); } + + if (a < b) return reverse ? 1 : -1; + if (a > b) return reverse ? -1 : 1; + return 0; + }); + + currentSortInfo.table = table; + currentSortInfo.column = column; + currentSortInfo.reverse = reverse; + + rows.forEach(function(row) { tbody.removeChild(row); }); + rows.forEach(function(row) { tbody.appendChild(row); }); + }, + + formatMbps: function(bits) { + if (typeof bits !== 'number') return '0.00 Mbps'; + return (bits / 1024 / 1024).toFixed(2) + ' Mbps'; + }, + + formatMB: function(bytes) { + if (typeof bytes !== 'number') return '0.00 MB'; + return (bytes / 1024 / 1024).toFixed(2) + ' MB'; + }, + + renderSIDData: function(data) { + var rows = []; + var txRateData = [], rxRateData = []; + var txVolumeData = [], rxVolumeData = []; + var tx_rate_total = 0, rx_rate_total = 0; + var tx_bytes_total = 0, rx_bytes_total = 0; + var perServiceTxRate = {}; + var perServiceRxRate = {}; + var self = this; + var allItems = []; + + if (data && data.status === 'success' && Array.isArray(data.data)) { + allItems = data.data; + var listSizeEl = document.getElementById('sid-size-select'); + var listSize = listSizeEl ? parseInt(listSizeEl.value, 10) : 10; + + var activeConnections = allItems.filter(function(item) { return item.incoming.rate > 0 || item.outgoing.rate > 0; }); + var inactiveConnections = allItems.filter(function(item) { return item.incoming.rate === 0 && item.outgoing.rate === 0; }); + + activeConnections.sort(function(a, b) { return (b.incoming.rate + b.outgoing.rate) - (a.incoming.rate + a.outgoing.rate); }); + inactiveConnections.sort(function(a, b) { return b.incoming.total_bytes - a.incoming.total_bytes; }); + + var displayData = activeConnections; + if (displayData.length < listSize) { + displayData = displayData.concat(inactiveConnections.slice(0, listSize - displayData.length)); + } + + if (displayData.length > listSize) { + displayData = displayData.slice(0, listSize); + } + + displayData.forEach(function(item) { + var domainOrL7Proto = 'unknown'; + var lookupInfo = sidLookupTable[item.sid]; + + if (lookupInfo) { + domainOrL7Proto = lookupInfo.name; + } else if (item.sid_type === 'Domain' && item.domain && item.domain !== 'unknown') { + domainOrL7Proto = item.domain; + } else if (item.sid_type === 'L7' && item.l7_proto_desc && item.l7_proto_desc !== 'unknown') { + domainOrL7Proto = item.l7_proto_desc; + } + + // 判断连接是否活跃 + var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0; + var activityIcon = isActive ? '🟢' : '⚪'; + + rows.push([ + E('span', { 'class': 'sid-cell' }, [ + E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon), + E('span', {}, ' ' + item.sid) + ]), + E('span', { 'class': 'protocol-cell' }, [ + E('span', { 'class': 'protocol-icon' }, '🌐'), + E('span', {}, ' ' + domainOrL7Proto) + ]), + [ item.incoming.rate, E('span', { 'class': 'speed-cell download' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.incoming.rate)) + ])], + [ item.incoming.total_bytes, E('span', { 'class': 'volume-cell download' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes)) + ])], + [ item.incoming.total_packets, E('span', { 'class': 'packet-cell download' }, [ + E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets)) + ])], + [ item.outgoing.rate, E('span', { 'class': 'speed-cell upload' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.outgoing.rate)) + ])], + [ item.outgoing.total_bytes, E('span', { 'class': 'volume-cell upload' }, [ + E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes)) + ])], + [ item.outgoing.total_packets, E('span', { 'class': 'packet-cell upload' }, [ + E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets)) + ])] + ]); + + txRateData.push({ value: item.incoming.rate, label: domainOrL7Proto }); + rxRateData.push({ value: item.outgoing.rate, label: domainOrL7Proto }); + txVolumeData.push({ value: item.incoming.total_bytes, label: domainOrL7Proto }); + rxVolumeData.push({ value: item.outgoing.total_bytes, label: domainOrL7Proto }); + + perServiceTxRate[domainOrL7Proto] = (perServiceTxRate[domainOrL7Proto] || 0) + item.incoming.rate; + perServiceRxRate[domainOrL7Proto] = (perServiceRxRate[domainOrL7Proto] || 0) + item.outgoing.rate; + }); + + allItems.forEach(function(item) { + tx_rate_total += item.incoming.rate; + rx_rate_total += item.outgoing.rate; + tx_bytes_total += item.incoming.total_bytes; + rx_bytes_total += item.outgoing.total_bytes; + }); + } + + this.updateStackedLineCharts(perServiceTxRate, perServiceRxRate); + + var table = document.getElementById('sid-data'); + cbi_update_table(table, rows, E('em', _('No data recorded yet.'))); + + var headers = table.querySelectorAll('th'); + + if (!table.hasAttribute('data-sort-initialized')) { + headers.forEach(function(header, index) { + header.style.cursor = 'pointer'; + header.addEventListener('click', function() { self.sortTable(table, index); }); + }); + table.setAttribute('data-sort-initialized', 'true'); + } + + table.querySelectorAll('tr:not(.table-titles):not(.placeholder)').forEach(function(row, rowIndex) { + if (!rows[rowIndex]) return; + Array.from(row.cells).forEach(function(cell, cellIndex) { + if (Array.isArray(rows[rowIndex][cellIndex])) { + cell.setAttribute('data-value', rows[rowIndex][cellIndex][0]); + } + }); + }); + + this.pie('sid-tx-rate-pie', txRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; }); + this.pie('sid-rx-rate-pie', rxRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; }); + this.pie('sid-tx-volume-pie', txVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; }); + this.pie('sid-rx-volume-pie', rxVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; }); + + var sidTotalEl = document.getElementById('sid-total-val'); + if(sidTotalEl) sidTotalEl.textContent = allItems.length; + + var txRateEl = document.getElementById('sid-tx-rate-val'); + if(txRateEl) txRateEl.textContent = '%1024.2mbps'.format(tx_rate_total); + + var rxRateEl = document.getElementById('sid-rx-rate-val'); + if(rxRateEl) rxRateEl.textContent = '%1024.2mbps'.format(rx_rate_total); + + var txVolEl = document.getElementById('sid-tx-volume-val'); + if(txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total); + + var rxVolEl = document.getElementById('sid-rx-volume-val'); + if(rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total); + + lastUpdated = new Date(); + var timestampEl = document.getElementById('last-updated'); + if (timestampEl) { + timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString()); + } + }, + + fillSortableTable: function(tableId, rows) { + var table = document.getElementById(tableId); + var self = this; + if (!table) + return; + + if (!table.hasAttribute('data-sort-initialized')) { + table.querySelectorAll('th').forEach(function(header, index) { + header.style.cursor = 'pointer'; + header.addEventListener('click', function() { self.sortTable(table, index); }); + }); + table.setAttribute('data-sort-initialized', 'true'); + } + + cbi_update_table(table, rows, E('em', _('No data recorded yet.'))); + + table.querySelectorAll('tr:not(.table-titles):not(.placeholder)').forEach(function(row, rowIndex) { + if (!rows[rowIndex]) + return; + Array.from(row.cells).forEach(function(cell, cellIndex) { + if (Array.isArray(rows[rowIndex][cellIndex])) + cell.setAttribute('data-value', rows[rowIndex][cellIndex][0]); + }); + }); + }, + + renderL7ProtoData: function(data) { + var self = this; + var protoRows = []; + var domainRows = []; + + lastL7ProtoData = data; + sidLookupTable = {}; + + if (data && data.status === 'success' && data.data) { + if (Array.isArray(data.data.protocols)) { + data.data.protocols.forEach(function(item) { + sidLookupTable[item.sid] = { type: 'protocol', name: item.protocol }; + protoRows.push([ + [ item.id, E('span', { 'class': 'id-cell' }, item.id) ], + E('span', { 'class': 'protocol-cell' }, [ + E('span', { 'class': 'protocol-icon l7' }, '🔌'), + E('span', {}, ' ' + item.protocol) + ]), + [ item.sid, E('span', { 'class': 'sid-cell' }, item.sid) ] + ]); + }); + } + + if (Array.isArray(data.data.domains)) { + var domains = data.data.domains.slice().sort(function(a, b) { + var ac = (b.access_count || 0) - (a.access_count || 0); + if (ac !== 0) + return ac; + return (b.last_access || 0) - (a.last_access || 0); + }); + + domains.forEach(function(item) { + sidLookupTable[item.sid] = { type: 'domain', name: item.domain }; + var row = [ + [ item.id, E('span', { 'class': 'id-cell' }, item.id) ], + E('span', { 'class': 'protocol-cell' }, [ + E('span', { 'class': 'protocol-icon domain' }, '🌍'), + E('span', {}, ' ' + item.domain) + ]), + [ item.sid, E('span', { 'class': 'sid-cell' }, item.sid) ], + [ item.access_count || 0, E('span', { 'class': 'data-value' }, item.access_count || 0) ], + item.first_seen_str || '-', + item.last_access_str || '-' + ]; + + if (self.hasXdns) { + var isProxied = self.isDomainProxied(item.domain); + if (isProxied) { + row.push(E('span', { + 'class': 'badge success', + 'style': 'color: #2ecc71; background: rgba(46,204,113,0.12); border: 1px solid rgba(46,204,113,0.3); padding: 2px 8px; border-radius: 4px; font-size: 85%; white-space: nowrap;' + }, [ '✔ ', _('已代理') ])); + } else { + row.push(E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'style': 'padding: 2px 8px; font-size: 85%; white-space: nowrap;', + 'click': function(ev) { + var b = ev.target.closest('button'); + self.handleAddXdnsDomain(item.domain, b); + } + }, [ '➕ ', _('加入代理') ])); + } + } + + domainRows.push(row); + }); + } + } + + this.fillSortableTable('l7-protocol-data', protoRows); + this.fillSortableTable('l7-domain-data', domainRows); + + var protoCountEl = document.getElementById('l7-protocol-count'); + if (protoCountEl) + protoCountEl.textContent = protoRows.length; + var domainCountEl = document.getElementById('l7-domain-count'); + if (domainCountEl) + domainCountEl.textContent = domainRows.length; + }, + + pollL7Data: function() { + if (pollActive) return; + + var self = this; + pollActive = true; + + self.loadL7ProtoData().then(function(l7data) { + self.renderL7ProtoData(l7data); + return self.loadSIDData(); + }).then(function(sidData){ + self.renderSIDData(sidData); + }); + + poll.add(function() { + if (isPaused) return Promise.resolve(); + + return self.loadL7ProtoData().then(function(data) { + self.renderL7ProtoData(data); + }).then(function() { + return self.loadSIDData().then(function(data) { + self.renderSIDData(data); + }); + }); + }, 5); + }, + + initializeUI: function() { + applyViewTheme(); + if (window.echarts) { + var self = this; + var dlChartEl = document.getElementById('download-speed-line-chart'); + var ulChartEl = document.getElementById('upload-speed-line-chart'); + if (!dlChartEl || !ulChartEl) return; + + var colors = getChartColors(); + var axisTheme = chartAxisTheme(colors); + var baseChartOption = { + backgroundColor: axisTheme.backgroundColor, + textStyle: axisTheme.textStyle, + legend: axisTheme.legend, + tooltip: Object.assign({ + trigger: 'axis', + formatter: function (params) { + if (!params || params.length === 0) { + return null; + } + var tooltipContent = params[0].axisValueLabel + '
'; + params.sort(function(a, b) { return b.value - a.value; }); + params.forEach(function(item) { + if (item.value > 0) { + tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mbps'.format(item.value) + '
'; + } + }); + return tooltipContent; + } + }, axisTheme.tooltip), + grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true }, + xAxis: { + type: 'category', + boundaryGap: false, + data: lineCategories, + axisLine: axisTheme.xAxis.axisLine, + axisLabel: axisTheme.xAxis.axisLabel, + splitLine: axisTheme.xAxis.splitLine + }, + yAxis: { + type: 'value', + axisLine: axisTheme.yAxis.axisLine, + splitLine: axisTheme.yAxis.splitLine, + axisLabel: { formatter: function(val) { return '%1024.2mbps'.format(val); }, color: colors.muted } + }, + series: [] + }; + + + downloadLineChart = echarts.init(dlChartEl); + downloadLineChart.setOption(baseChartOption); + observeChartEl(downloadLineChart, dlChartEl); + + uploadLineChart = echarts.init(ulChartEl); + uploadLineChart.setOption(baseChartOption); + observeChartEl(uploadLineChart, ulChartEl); + + // 添加窗口大小变化监听器,使图表能够响应式调整 + if (!resizeListenerAdded) { + var resizeTimer = null; + var resizeHandler = function() { + // 使用防抖,避免频繁触发 resize + if (resizeTimer) { + clearTimeout(resizeTimer); + } + resizeTimer = setTimeout(function() { + self.resizeAllCharts(); + }, 200); + }; + + window.addEventListener('resize', resizeHandler); + resizeListenerAdded = true; + } + + this.pollL7Data(); + } else { + setTimeout(this.initializeUI.bind(this), 50); + } + }, + + resizeAllCharts: function() { + if (downloadLineChart) + downloadLineChart.resize(); + if (uploadLineChart) + uploadLineChart.resize(); + Object.keys(chartRegistry).forEach(function(chartId) { + if (chartRegistry[chartId]) + chartRegistry[chartId].resize(); + }); + }, + + bindTabChartResize: function(root) { + var self = this; + if (!root) + return; + var host = root.parentNode || root; + if (host._awTabResizeBound) + return; + host._awTabResizeBound = true; + + var schedule = function() { + setTimeout(function() { self.resizeAllCharts(); }, 80); + }; + + host.addEventListener('click', function(ev) { + if (ev.target.closest && ev.target.closest('ul.cbi-tabmenu')) + schedule(); + }); + root.querySelectorAll('[data-tab]').forEach(function(pane) { + pane.addEventListener('cbi-tab-active', schedule); + }); + }, + + render: function() { + var self = this; + + var controls = E('div', { 'class': 'l7-controls' }, [ + E('div', { 'class': 'l7-controls-left' }, [ + E('div', { 'class': 'control-group' }, [ + E('span', { 'class': 'control-icon' }, '📊'), + E('label', { 'for': 'sid-size-select', 'class': 'control-label' }, _('Show entries:')), + E('select', { + 'id': 'sid-size-select', + 'class': 'cbi-input-select', + 'change': ui.createHandlerFn(this, function() { + if (lastSIDData) { + self.renderSIDData(lastSIDData); + } + }) + }, [ + E('option', { 'value': '10' }, '10'), + E('option', { 'value': '15' }, '15'), + E('option', { 'value': '20' }, '20'), + E('option', { 'value': '25' }, '25'), + E('option', { 'value': '50' }, '50') + ]) + ]) + ]), + E('div', { 'class': 'l7-controls-right' }, [ + E('div', { 'class': 'control-group' }, [ + E('span', { 'class': 'control-icon' }, '🕐'), + E('span', { 'id': 'last-updated', 'class': 'last-updated-text' }, _('Last updated: never')) + ]), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'id': 'pause-resume-btn', + 'click': function(ev) { + isPaused = !isPaused; + var btn = ev.target; + if (isPaused) { + btn.innerHTML = '▶️ ' + _('Resume'); + btn.classList.remove('cbi-button-action'); + btn.classList.add('cbi-button-positive'); + } else { + btn.innerHTML = '⏸️ ' + _('Pause'); + btn.classList.remove('cbi-button-positive'); + btn.classList.add('cbi-button-action'); + } + } + }, [ + E('span', { 'class': 'btn-icon' }, '⏸️'), + E('span', {}, ' ' + _('Pause')) + ]) + ]) + ]); + + var sidInnerTabs = E('div', { 'class': 'aw-inner-tabs' }, [ + E('div', { 'class': 'cbi-section', 'data-tab': 'sid-trend', 'data-tab-title': _('Speed Trend') }, [ + E('div', { 'class': 'dashboard-container' }, [ + E('div', { 'class': 'kpi-row' }, [ + E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-total-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('L7 Protocol Data')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]), + E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ]) + ]), + E('div', { 'class': 'line-chart-row' }, [ + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Real-time Download Speed')]), + E('div', { id: 'download-speed-line-chart', style: 'width: 100%; height: 350px;' }) + ]), + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Real-time Upload Speed')]), + E('div', { id: 'upload-speed-line-chart', style: 'width: 100%; height: 350px;' }) + ]) + ]) + ]) + ]), + E('div', { 'class': 'cbi-section', 'data-tab': 'sid-share', 'data-tab-title': _('Traffic Share') }, [ + E('div', { 'class': 'dashboard-container' }, [ + E('div', { 'class': 'chart-grid' }, [ + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Download Speed / SID')]), + E('div', { id: 'sid-tx-rate-pie', style: 'width: 100%; height: 300px;' }) + ]), + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Upload Speed / SID')]), + E('div', { id: 'sid-rx-rate-pie', style: 'width: 100%; height: 300px;' }) + ]), + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Download Total')]), + E('div', { id: 'sid-tx-volume-pie', style: 'width: 100%; height: 300px;' }) + ]), + E('div', { 'class': 'chart-card' }, [ + E('h4', [_('Upload Total')]), + E('div', { id: 'sid-rx-volume-pie', style: 'width: 100%; height: 300px;' }) + ]) + ]) + ]) + ]), + E('div', { 'class': 'cbi-section', 'data-tab': 'sid-list', 'data-tab-title': _('SID List') }, [ + E('table', { 'class': 'table', 'id': 'sid-data' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🆔'), ' ', _('SID') ]), + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed (Bit/s)') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download (Bytes)') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download (Packets)') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed (Bit/s)') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload (Bytes)') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload (Packets)') ]) + ]), + E('tr', { 'class': 'tr placeholder' }, [ + E('td', { 'class': 'td', 'colspan': '8' }, [ + E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) + ]) + ]) + ]), + controls + ]) + ]); + + var tabContainer = E('div', {}, [ + E('div', { 'class': 'cbi-section', 'data-tab': 'sid', 'data-tab-title': _('L7 SID Data') }, [ + sidInnerTabs + ]), + E('div', { 'class': 'cbi-section', 'data-tab': 'l7proto', 'data-tab-title': _('L7 Protocol Data') }, [ + E('div', { 'class': 'aw-inner-tabs' }, [ + E('div', { 'class': 'cbi-section', 'data-tab': 'l7-protocols', 'data-tab-title': _('Protocol Library') }, [ + E('p', { 'class': 'cbi-section-descr' }, [ + _('Built-in L7 protocol signatures from aw-bpf.'), + ' ', + _('Entries:'), + ' ', + E('strong', { 'id': 'l7-protocol-count' }, '0') + ]), + E('table', { 'class': 'table', 'id': 'l7-protocol-data' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '#️⃣'), ' ', _('ID') ]), + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🔌'), ' ', _('Protocol') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '🔑'), ' ', _('SID') ]) + ]), + E('tr', { 'class': 'tr placeholder' }, [ + E('td', { 'class': 'td', 'colspan': '3' }, [ + E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) + ]) + ]) + ]) + ]), + E('div', { 'class': 'cbi-section', 'data-tab': 'l7-domains', 'data-tab-title': _('常用域名') }, [ + E('p', { 'class': 'cbi-section-descr' }, [ + _('Frequently accessed domains discovered by xDPI, sorted by access count.'), + ' ', + _('Entries:'), + ' ', + E('strong', { 'id': 'l7-domain-count' }, '0') + ]), + E('table', { 'class': 'table', 'id': 'l7-domain-data' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '#️⃣'), ' ', _('ID') ]), + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌍'), ' ', _('Domain') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '🔑'), ' ', _('SID') ]), + E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📊'), ' ', _('Access Count') ]), + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🕒'), ' ', _('First Seen') ]), + E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🕒'), ' ', _('Last Access') ]), + this.hasXdns ? E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚡'), ' ', _('xdns代理') ]) : null + ].filter(Boolean)), + E('tr', { 'class': 'tr placeholder' }, [ + E('td', { 'class': 'td', 'colspan': this.hasXdns ? '7' : '6' }, [ + E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) + ]) + ]) + ]) + ]) + ]) + ]) + ]); + + var node = E([], [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('view/aw-bpf.css') }), + E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }), + + E('div', { 'class': 'l7-view-container', 'data-aw-theme': isDarkMode() ? 'dark' : 'light' }, [ + E('h2', [ _('L7 Data Monitor') ]), + E('div', { 'id': 'l7-error-message' }), + tabContainer + ]) + ]); + + tabContainer.querySelectorAll('.aw-inner-tabs').forEach(function(inner) { + ui.tabs.initTabGroup(inner.childNodes); + }); + ui.tabs.initTabGroup(tabContainer.childNodes); + this.bindTabChartResize(tabContainer); + + setTimeout(this.initializeUI.bind(this), 0); + + return node; + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); \ No newline at end of file diff --git a/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/settings.js b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/settings.js new file mode 100644 index 00000000..3aa37de3 --- /dev/null +++ b/luci-app-aw-bpf/htdocs/luci-static/resources/view/aw-bpf/settings.js @@ -0,0 +1,28 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; + +return view.extend({ + load: function() { + return uci.load('aw-bpf'); + }, + + render: function() { + var m, s, o; + + m = new form.Map('aw-bpf', _('eBPF Traffic Control & DPI'), + _('eBPF kernel-level bandwidth control, session audit logging and xDPI L7 application/domain recognition.')); + + s = m.section(form.TypedSection, 'aw-bpf', _('General Settings')); + s.anonymous = true; + s.addremove = false; + + o = s.option(form.Flag, 'enable_event_log', _('Enable Session Event Logging'), + _('Record TCP/UDP session connection events as structured JSON to system log / syslog. (DNS learning and xDPI protocol recognition are always active in background).')); + o.rmempty = false; + o.default = '0'; + + return m.render(); + } +}); diff --git a/luci-app-aw-bpf/po/zh-cn b/luci-app-aw-bpf/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-aw-bpf/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-aw-bpf/po/zh_Hans/aw-bpf.po b/luci-app-aw-bpf/po/zh_Hans/aw-bpf.po new file mode 100644 index 00000000..2ed0f2de --- /dev/null +++ b/luci-app-aw-bpf/po/zh_Hans/aw-bpf.po @@ -0,0 +1,1207 @@ +# Translations for LuCI +# Copyright (C) 2022 Jo-Philipp Wich +# This file is distributed under the Apache License, Version 2.0. +# +msgid "" +msgstr "" +"Project-Id-Version: luci-app-aw-bpf 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-03-16 00:00+0000\n" +"PO-Revision-Date: 2025-03-16 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: Chinese (Simplified)\n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "apfree-wifidog" +msgstr "门户认证" + +msgid "ApFree-WiFiDog" +msgstr "门户认证" + +msgid "apfree-wifidog offers a stable and secure captive portal solution." +msgstr "apfree-wifidog 提供了一个稳定安全的门户认证解决方案" + +msgid "Enable apfree-wifidog service." +msgstr "启用apfree-wifidog门户认证服务" + +msgid "Log Level" +msgstr "日志级别" + +msgid "The log level of the apfree-wifidog." +msgstr "apfree-wifidog的日志级别" + +msgid "Debug" +msgstr "调试" + +msgid "Info" +msgstr "信息" + +msgid "Notice" +msgstr "通知" + +msgid "Warning" +msgstr "警告" + +msgid "Error" +msgstr "错误" + +msgid "Critical" +msgstr "严重" + +msgid "Alert" +msgstr "警报" + +msgid "Emergency" +msgstr "紧急" + +msgid "None" +msgstr "无" + +msgid "Enable" +msgstr "启用" + +msgid "Auth Enabled" +msgstr "启用认证" + +msgid "Enable the authentication of the gateway." +msgstr "启用网关的认证功能" + +msgid "Gateway Name" +msgstr "网关名称" + +msgid "The name of the gateway." +msgstr "开启认证的网关所属网络区域" + +msgid "Gateway Channel" +msgstr "网关通道" + +msgid "The channel of the gateway." +msgstr "网关的通道" + +msgid "Gateway Subnetv4" +msgstr "网关IPv4子网" + +msgid "The ipv4 subnet of the gateway." +msgstr "网关的IPv4子网" + +msgid "Gateway ID" +msgstr "网关 ID" + +msgid "The ID of the gateway." +msgstr "网关的ID(一般为设备的MAC地址)" + +msgid "Channel Path" +msgstr "用户渠道" + +msgid "The channel path of the gateway." +msgstr "网关的用户渠道" + +msgid "Auth Server Hostname" +msgstr "认证服务器主机名" + +msgid "The domain or IP address of the authentication server." +msgstr "认证服务器的域名或 IP 地址" + +msgid "Auth Server Port" +msgstr "认证服务器端口" + +msgid "The port of the authentication server." +msgstr "认证服务器的端口" + +msgid "Auth Server URI path" +msgstr "认证服务器URI路径" + +msgid "The URI path of the authentication server." +msgstr "认证服务器的URI路径" + +msgid "Trusted Domains" +msgstr "信任的域名" + +msgid "The trusted domains of the gateway" +msgstr "网关的信任域名" + +msgid "The trusted domains of the gateway, for example: \"www.baidu.com,www.qq.com,...\"." +msgstr "域名白名单(多域名用逗号分隔),例如:\"www.baidu.com,www.qq.com,...\"" + +msgid "Trusted MACs" +msgstr "信任的MAC地址" + +msgid "The trusted MAC addresses of the gateway" +msgstr "网关的 MAC 地址白名单" + +msgid "The trusted MAC addresses of the gateway, for example: \"00:11:22:33:44:55,66:77:88:99:00:11,...\"." +msgstr "MAC 地址白名单(多 MAC 地址用逗号分隔),例如:\"00:11:22:33:44:55,66:77:88:99:00:11,...\"" + +msgid "running..." +msgstr "运行中" + +msgid "not running..." +msgstr "未运行" + +msgid "Value" +msgstr "值" + +msgid "Internet Connectivity" +msgstr "互联网连接" + +msgid "Auth server reachable" +msgstr "认证服务器可达" + +msgid "Authentication servers" +msgstr "认证服务器" + +msgid "Enable Wildcard Domain" +msgstr "启用通配符域名" + +msgid "Enable wildcard domain support." +msgstr "启用通配符域名支持" + +msgid "Trusted Wildcard Domains" +msgstr "信任的通配符域名" + +msgid "The trusted wildcard domains of the gateway." +msgstr "网关的信任通配符域名" + +msgid "App White List" +msgstr "应用白名单" + +msgid "The app white list of the gateway." +msgstr "网关的应用白名单" + +msgid "MAC White List" +msgstr "MAC白名单" + +msgid "The MAC white list of the gateway." +msgstr "网关的MAC白名单" + +msgid "Wildcard White List" +msgstr "通配符白名单" + +msgid "The wildcard domain white list of the gateway." +msgstr "网关的通配符域名白名单" + +msgid "Check Interval" +msgstr "检查间隔" + +msgid "The interval of the check(s)." +msgstr "检查的时间间隔(秒)" + +msgid "Check Timeout" +msgstr "检查超时" + +msgid "Client Timeout" +msgstr "客户端超时" + +msgid "The timeout of the client." +msgstr "客户端的超时时间" + +msgid "Wired Passed" +msgstr "有线免认证" + +msgid "Wired users do not need to authenticate to access the internet." +msgstr "有线用户无需认证即可访问互联网" + +msgid "Apple CNA" +msgstr "苹果CNA" + +msgid "Enable Apple Captive Network Assistant." +msgstr "启用苹果强制网络门户助手" + +msgid "JS Filter" +msgstr "JS过滤" + +msgid "Enable JS redirect." +msgstr "启用JS重定向" + +msgid "Enable WebSocket" +msgstr "启用 Websocket" + +msgid "Enable websocket support." +msgstr "启用 Websocket 支持, 用于实现认证服务器端放行功能" + +msgid "Enable Anti NAT" +msgstr "启用防NAT" + +msgid "Enable Anti NAT devices." +msgstr "启用防NAT设备" + +msgid "TTL Value" +msgstr "TTL值" + +msgid "The TTL value of the gateway support." +msgstr "网关支持的TTL值" + +msgid "Bypass Auth" +msgstr "旁认证" + +msgid "Anti NAT Permit MAC" +msgstr "防NAT允许的MAC" + +msgid "The MAC address of the Anti NAT permit." +msgstr "防NAT允许的MAC地址" + +msgid "Enable Global QoS" +msgstr "启用全局QoS" + +msgid "Enable Global QoS." +msgstr "启用全局QoS" + +msgid "Global QoS Up" +msgstr "全局QoS上行" + +msgid "The global QoS up value(Mbps)." +msgstr "全局QoS上行速度(Mbps)" + +msgid "Global QoS Down" +msgstr "全局QoS下行" + +msgid "The global QoS down value(Mbps)." +msgstr "全局QoS下行速度(Mbps)" + +msgid "WebSocket Hostname" +msgstr "WebSocket主机名" + +msgid "The hostname of the websocket, if the field is left empty, automatically use the same hostname as the auth server." +msgstr "WebSocket的主机名,如果此字段为空,自动使用与认证服务器相同的主机名" + +msgid "WebSocket Port" +msgstr "WebSocket端口" + +msgid "The port of the websocket, if the field is left empty, automatically use the same port as the auth server." +msgstr "WebSocket的端口,如果此字段为空,自动使用与认证服务器相同的端口" + +msgid "WebSocket URI path" +msgstr "WebSocket URI路径" + +msgid "The URI path of the websocket." +msgstr "WebSocket的URI路径" + +msgid "MQTT Port" +msgstr "MQTT端口" + +msgid "The port of the mqtt." +msgstr "MQTT的端口" + +msgid "MQTT Username" +msgstr "MQTT用户名" + +msgid "The username of the mqtt." +msgstr "MQTT的用户名" + +msgid "MQTT Password" +msgstr "MQTT密码" + +msgid "The password of the mqtt." +msgstr "MQTT的密码" + +msgid "Device ID" +msgstr "设备ID" + +msgid "The ID of the device." +msgstr "设备的ID" + +msgid "Local Portal" +msgstr "本地门户" + +msgid "The local portal url." +msgstr "本地门户网址" + +msgid "External Interface" +msgstr "外部接口" + +msgid "The external interface of the device, if bypass mode, do not choose." +msgstr "设备的外部接口,如果是旁路模式,请不要选择" + +msgid "Disable Portal Authentication" +msgstr "禁用Portal认证" + +msgid "When enabled, users can access the internet without portal authentication. Firewall redirect rules will not be created. Use this mode for pure traffic statistics without captive portal." +msgstr "启用后,用户无需Portal认证即可上网。不会创建防火墙重定向规则。适用于纯流量统计场景,不需要强制Portal认证页面。" + +msgid "Configuration" +msgstr "配置" + +msgid "Basic Settings" +msgstr "基本设置" + +msgid "Gateway Settings" +msgstr "网关设置" + +msgid "Advanced Settings" +msgstr "高级设置" + +msgid "Long Connection Settings" +msgstr "长连接设置" + +msgid "Auth Server Settings" +msgstr "认证服务器设置" + +msgid "Rule Settings" +msgstr "规则设置" + +msgid "QoS Settings" +msgstr "QoS设置" + +msgid "Authentication Location Settings" +msgstr "认证位置设置" + +msgid "Group Define" +msgstr "组定义" + +msgid "Group Type" +msgstr "组类型" + +msgid "The type of the group." +msgstr "组的类型" + +msgid "Domain Group" +msgstr "域名组" + +msgid "MAC Group" +msgstr "MAC地址组" + +msgid "Wildcard Domain Group" +msgstr "通配符域名组" + +msgid "Domain Name" +msgstr "域名" + +msgid "The domain name of the group." +msgstr "组的域名" + +msgid "MAC Address" +msgstr "MAC地址" + +msgid "The MAC address of the group." +msgstr "组的MAC地址" + +msgid "Wildcard Domain" +msgstr "通配符域名" + +msgid "The wildcard domain of the group." +msgstr "组的通配符域名" + +msgid "Group Description" +msgstr "组描述" + +msgid "The description of the group." +msgstr "组的描述" + +msgid "The group is used by " +msgstr "该组正被 " + +msgid " please remove it from " +msgstr " 使用,请先从 " + +msgid " first." +msgstr " 中移除。" + +# Client status UI additions +msgid "Download Rate" +msgstr "下载速率" + +msgid "Upload Rate" +msgstr "上传速率" + +#~ msgid "Down. (Bytes / Pkts.)" +#~ msgstr "下载(字节 / 数据包)" + +#~ msgid "Download (Bytes / Packets)" +#~ msgstr "下载(字节 / 数据包)" + +#~ msgid "Up. (Bytes / Pkts.)" +#~ msgstr "上传(字节 / 数据包)" + +#~ msgid "Upload (Bytes / Packets)" +#~ msgstr "上传(字节 / 数据包)" + +msgid "Speed Distribution" +msgstr "速率分布" + +msgid "Download Speed (Bit/s)" +msgstr "下载速率 (比特/秒)" + +msgid "Upload Speed (Bit/s)" +msgstr "上传速率 (比特/秒)" + +msgid "Upload Speed / Host" +msgstr "上传速率 / 主机" + +msgid "Download Speed / Host" +msgstr "下载速率 / 主机" + +msgid "Traffic History" +msgstr "流量统计" + +msgid "Network Speed Monitor" +msgstr "网络速度监控" + +msgid "Auth User Speed Monitor" +msgstr "认证用户速率监控" + +msgid "Host Speed Monitor" +msgstr "主机速率监控" + +msgid "Speed Trend" +msgstr "实时趋势" + +msgid "Traffic Share" +msgstr "流量分布" + +msgid "Host List" +msgstr "主机列表" + +msgid "SID List" +msgstr "SID 列表" + +msgid "Protocol Library" +msgstr "协议库" + +msgid "常用域名" +msgstr "常用域名" + +msgid "Access Count" +msgstr "访问次数" + +msgid "First Seen" +msgstr "首次发现" + +msgid "Last Access" +msgstr "最近访问" + +msgid "Entries:" +msgstr "条目:" + +msgid "Built-in L7 protocol signatures from aw-bpf." +msgstr "aw-bpf 内置的 L7 协议特征库。" + +msgid "Domains discovered by xDPI DNS inspection." +msgstr "xDPI 通过 DNS 发现的域名。" + +msgid "Frequently accessed domains discovered by xDPI, sorted by access count." +msgstr "xDPI 发现的常用域名,按访问次数排序。" + +msgid "aw-bpf QoS" +msgstr "aw-bpf 流控" + +msgid "Hosts" +msgstr "主机" + +msgid "0 hosts" +msgstr "主机:0" + +msgid "0 download speed" +msgstr "总下载速度:0" + +msgid "0 upload speed" +msgstr "总上传速度:0" + +msgid "0 hosts" +msgstr "主机:0" + +msgid "0 download speed" +msgstr "总下载速度:0" + +msgid "0 upload speed" +msgstr "总上传速度:0" + +msgid "0 hosts" +msgstr "主机:0" + +msgid "0 download speed" +msgstr "总下载速度:0" + +msgid "0 upload speed" +msgstr "总上传速度:0" + +msgid "0 cause the most download" +msgstr "下载量最大的协议:0" + +msgid "0 cause the most upload" +msgstr "上传量最大的协议:0" + +msgid "0 different application protocols" +msgstr "应用层协议计数:0" + +msgid "L7 Protocol Support" +msgstr "应用层协议支持" + +msgid "Initial traffic" +msgstr "初始流量" + +msgid "apfree-wifidog Status" +msgstr "apfree-wifidog 状态" + +msgid "Upload (Bytes)" +msgstr "上传 (字节)" + +msgid "Upload (Packets)" +msgstr "上传 (数据包)" + +msgid "Download (Bytes)" +msgstr "下载 (字节)" + +msgid "Download (Packets)" +msgstr "下载 (数据包)" + +msgid "download speed" +msgstr "下载速度" + +msgid "upload speed" +msgstr "上传速度" + +msgid "Display" +msgstr "主机流速显示" + +msgid "Auth User" +msgstr "认证用户" + +msgid "No data recorded yet." +msgstr "暂无数据记录" + +msgid "Edit Speed Limit" +msgstr "带宽控制" + +msgid "Download Limit" +msgstr "限制下载速度" + +msgid "Upload Limit" +msgstr "限制上传速度" + +msgid "Speed limit updated" +msgstr "限速配置已更新" + +msgid "Data format error" +msgstr "数据格式错误" + +msgid "Please enter a valid IPv4 address" +msgstr "请输入有效的 IPv4 地址" + +msgid "Please enter a valid IPv6 address" +msgstr "请输入有效的 IPv6 地址" + +msgid "Please enter a valid MAC address" +msgstr "请输入有效的 MAC 地址" + +msgid "Updated successfully!" +msgstr "更新成功!" + +msgid "L7 SID Data" +msgstr "L7 应用数据" + +msgid "L7 Protocol Data" +msgstr "L7 协议数据" + +msgid "Download / SID" +msgstr "下载 / SID" + +msgid "Upload / SID" +msgstr "上传 / SID" + +msgid "Download / Protocol" +msgstr "下载 / 协议" + +msgid "Upload / Protocol" +msgstr "上传 / 协议" + +msgid "SID" +msgstr "应用" + +msgid "Name" +msgstr "名称" + +msgid "Incoming" +msgstr "接收" + +msgid "Outgoing" +msgstr "发送" + +msgid "ID" +msgstr "ID" + +msgid "Description" +msgstr "描述" + +msgid "Bytes" +msgstr "字节" + +msgid "Collecting data..." +msgstr "正在收集数据..." + +msgid "different SIDs" +msgstr "个不同的应用" + +msgid "total download" +msgstr "总下载" + +msgid "total upload" +msgstr "总上传" + +msgid "different protocols" +msgstr "个不同的协议" + +msgid "no traffic" +msgstr "无流量" + +msgid "QoS" +msgstr "流控" + +msgid "L7 application" +msgstr "L7应用" + +msgid "L7 Data Monitor" +msgstr "L7 数据监控" + +msgid "Upload Speed / SID" +msgstr "上传速率 / 应用" + +msgid "Download Speed / SID" +msgstr "下载速率 / 应用" + +msgid "0 different SIDs" +msgstr "应用计数:0" + +msgid "0 download speed" +msgstr "总下载速度:0" + +msgid "0 upload speed" +msgstr "总上传速度:0" + +msgid "Actions" +msgstr "操作" + +msgid "Edit" +msgstr "编辑" + +msgid "Add" +msgstr "添加" + +msgid "Refreshing" +msgstr "刷新" + +msgid "Error: " +msgstr "错误:" + +msgid "Delete Host" +msgstr "删除主机" + +msgid "Are you sure you want to delete this host?" +msgstr "您确定要删除这个主机吗?" + +msgid "Enable Internet Access Log" +msgstr "开启上网记录功能" + +msgid "Enable logging of internet access events and user activities." +msgstr "启用互联网访问事件和用户活动的日志记录功能。" + +msgid "Client Status" +msgstr "客户端状态" + +msgid "Authentication Server Status" +msgstr "认证服务器状态" + +msgid "Internet Connected" +msgstr "互联网连接" + +msgid "Auth Server Connected" +msgstr "认证服务器连接" + +msgid "MQTT Connected" +msgstr "MQTT 连接" + +msgid "WebSocket Connected" +msgstr "WebSocket 连接" + +msgid "Auth Server Mode" +msgstr "认证服务器模式" + +msgid "Online Clients" +msgstr "在线客户端" + +msgid "Active Clients" +msgstr "活跃客户端" + +msgid "Connected Clients" +msgstr "已连接客户端" + +msgid "IP Address" +msgstr "IP地址" + +msgid "Status" +msgstr "状态" + +msgid "Online Time" +msgstr "在线时间" + +msgid "Connection Type" +msgstr "连接类型" + +msgid "Online" +msgstr "在线" + +msgid "Offline" +msgstr "离线" + +msgid "Wired" +msgstr "有线" + +msgid "Wireless" +msgstr "无线" + +msgid "Auth Server Online" +msgstr "认证服务器在线" + +msgid "Failed to get wifidogx status" +msgstr "获取wifidogx状态失败" + +msgid "Failed to get client status" +msgstr "获取客户端状态失败" + +msgid "Failed to get auth server status" +msgstr "获取认证服务器状态失败" + +msgid "Error getting status information: " +msgstr "获取状态信息错误:" + +msgid "Cloud Auth" +msgstr "云认证" + +msgid "Local Auth" +msgstr "本地认证" + +msgid "Unknown" +msgstr "未知" + +msgid "d" +msgstr "天" + +msgid "h" +msgstr "小时" + +msgid "m" +msgstr "分钟" + +msgid "s" +msgstr "秒" + +msgid "Authentication Server" +msgstr "认证服务器" + +msgid "Select the authentication server to use for cloud authentication." +msgstr "选择用于云认证的认证服务器" + +msgid "Please create an authentication server first" +msgstr "请先创建认证服务器" + +msgid "Domain&L7Protocol" +msgstr "域名&L7协议" + +msgid "Protocol" +msgstr "协议" + +msgid "Domain" +msgstr "域名" + +msgid "Download Total" +msgstr "下载总量" + +msgid "Upload Total" +msgstr "上传总量" + +msgid "Download Speed" +msgstr "下载速度" + +msgid "Upload Speed" +msgstr "上传速度" + +msgid "Showing top %d by %s" +msgstr "按%s显示前%d项" + +msgid "Download Traffic / SID" +msgstr "下载流量 / 应用" + +msgid "Upload Traffic / SID" +msgstr "上传流量 / 应用" + +msgid "Show entries: " +msgstr "显示条目: " + +msgid "Pause" +msgstr "暂停" + +msgid "Resume" +msgstr "恢复" + +msgid "Last updated: %s" +msgstr "上次更新:%s" + +msgid "Error loading SID data: %s" +msgstr "加载 SID 数据错误:%s" + +msgid "Error loading L7 protocol data: %s" +msgstr "加载 L7 协议数据错误:%s" + +msgid "Last updated: never" +msgstr "上次更新:从未" + +msgid "Real-time Download Speed" +msgstr "实时下载速度" + +msgid "Real-time Upload Speed" +msgstr "实时上传速度" + +msgid "AP Device ID" +msgstr "AP设备ID" + +msgid "AP Device ID must be exactly 21 characters long" +msgstr "AP设备ID必须是21个字符长" + +msgid "AP Device ID must contain only uppercase letters and digits" +msgstr "AP设备ID只能包含大写字母和数字" + +msgid "AP MAC Address" +msgstr "AP MAC地址" + +msgid "Auto Fill" +msgstr "自动填充" + +msgid "Auto Fill All Fields" +msgstr "自动填充所有字段" + +msgid "Auto fill completed successfully! All fields have been filled." +msgstr "自动填充成功!所有字段已填写。" + +msgid "Auto fill failed. No fields could be filled. Please check your network connection and try again." +msgstr "自动填充失败。无法填写任何字段。请检查您的网络连接并重试。" + +msgid "Auto fill partially completed." +msgstr "自动填充部分完成。" + +msgid "Automatically fill AP MAC address, Device ID, Location ID, Longitude, and Latitude" +msgstr "自动填充 AP MAC地址、设备ID、位置ID、经度和纬度" + +msgid "Device ID: " +msgstr "设备ID: " + +msgid "First 6 digits must be valid administrative division code (GB/T 2260)" +msgstr "前6位必须是有效的行政区划代码 (GB/T 2260)" + +msgid "Generating Device ID..." +msgstr "生成设备ID..." + +msgid "Generating Location ID..." +msgstr "生成位置ID..." + +msgid "Getting WAN MAC address..." +msgstr "正在获取 WAN MAC 地址..." + +msgid "Getting device information and location data, please wait..." +msgstr "正在获取设备信息和位置数据,请稍候..." + +msgid "Getting location coordinates..." +msgstr "正在获取位置坐标..." + +msgid "Last 12 characters must be a valid MAC address in uppercase hexadecimal format (e.g., 00E04C3B7D2F)" +msgstr "最后12个字符必须是大写十六进制格式的有效MAC地址(例如:00E04C3B7D2F)" + +msgid "Last 5 digits must be sequence number (00000-99999)" +msgstr "最后5位必须是序列号 (00000-99999)" + +msgid "Latitude must be a valid number" +msgstr "纬度必须是一个有效的数字" + +msgid "Latitude must be between -90.000000 and 90.000000" +msgstr "纬度必须在 -90.000000 到 90.000000 之间" + +msgid "Latitude must be in format ±XXX.XXXXXX (3 integer digits + 6 decimal digits, e.g., 39.900000 or -33.000000). Suggested format: " +msgstr "纬度格式必须为 ±XXX.XXXXXX(3位整数 + 6位小数,例如:39.900000 或 -33.000000)。建议格式:" + +msgid "Latitude: " +msgstr "纬度: " + +msgid "Location ID" +msgstr "位置ID" + +msgid "Location ID is required" +msgstr "必须填写位置ID" + +msgid "Location ID must be exactly 14 digits long" +msgstr "位置ID必须是14位数字" + +msgid "Location ID must contain only digits (0-9)" +msgstr "位置ID只能包含数字 (0-9)" + +msgid "Location ID: " +msgstr "位置ID: " + +msgid "Longitude must be a valid number" +msgstr "经度必须是一个有效的数字" + +msgid "Longitude must be between -180.000000 and 180.000000" +msgstr "经度必须在 -180.000000 到 180.000000 之间" + +msgid "Longitude must be in format ±XXX.XXXXXX (3 integer digits + 6 decimal digits, e.g., 123.230000 or -133.000000). Suggested format: " +msgstr "经度格式必须为 ±XXX.XXXXXX(3位整数 + 6位小数,例如:123.230000 或 -133.000000)。建议格式:" + +msgid "Longitude: " +msgstr "经度: " + +msgid "MAC Address: " +msgstr "MAC地址: " + +msgid "MAC address must be exactly 17 characters long" +msgstr "MAC地址必须是17个字符长" + +msgid "MAC address must be in format XX-XX-XX-XX-XX-XX (uppercase hexadecimal separated by hyphens)" +msgstr "MAC地址格式必须为 XX-XX-XX-XX-XX-XX(大写十六进制,由连字符分隔)" + +msgid "MAC address should match the MAC address part (last 12 characters) in AP Device ID" +msgstr "MAC地址应与 AP设备ID 中的 MAC地址部分(最后12个字符)匹配" + +msgid "MQTT Connection Mode" +msgstr "MQTT连接模式" + +msgid "MQTT Secure Connection Mode" +msgstr "MQTT安全连接模式" + +msgid "MQTT Hostname" +msgstr "MQTT主机名" + +msgid "Mobile AP Latitude" +msgstr "移动AP纬度" + +msgid "Mobile AP Longitude" +msgstr "移动AP经度" + +msgid "Persistent Connection Mode" +msgstr "持久连接模式" + +msgid "RPC failed, trying direct method..." +msgstr "RPC失败,正在尝试直接方法..." + +msgid "RPC failed, trying fallback..." +msgstr "RPC失败,正在尝试备用方法..." + +msgid "Service type code (7-9 digits) must be \"100\" for commercial internet service locations, \"2XX\" for non-commercial locations, or \"3XX\" for WiFi wireless collection terminals" +msgstr "服务类型代码(第7-9位)必须是:商业上网场所为\"100\",非商业场所为\"2XX\",或WiFi无线采集终端为\"3XX\"" + +msgid "The MAC address of the AP device. Must be 17 characters in format XX-XX-XX-XX-XX-XX (uppercase, separated by hyphens)." +msgstr "AP设备的MAC地址。必须为17个字符,格式为 XX-XX-XX-XX-XX-XX(大写,由连字符分隔)。" + +msgid "The hostname of the mqtt." +msgstr "MQTT的主机名。" + +msgid "The latitude coordinate using format: ±XXX.XXXXXX (3 integer digits + 6 decimal digits). Positive for North, negative for South." +msgstr "纬度坐标,使用格式:±XXX.XXXXXX(3位整数 + 6位小数)。北纬为正,南纬为负。" + +msgid "The longitude coordinate using format: ±XXX.XXXXXX (3 integer digits + 6 decimal digits). Positive for East, negative for West." +msgstr "经度坐标,使用格式:±XXX.XXXXXX(3位整数 + 6位小数)。东经为正,西经为负。" + +msgid "The mode of the authentication server." +msgstr "认证服务器的模式。" + +msgid "The persistent connection mode of the device to auth server." +msgstr "设备到认证服务器的持久连接模式。" + +msgid "The trusted wildcard domains of the gateway" +msgstr "网关信任的通配符域名" + +msgid "The unique identifier of the AP device. Must be 21 characters: 9-character vendor code + 12-character MAC address (uppercase)." +msgstr "AP设备的唯一标识符。必须是21个字符:9位厂商代码 + 12位MAC地址(大写)。" + +msgid "The unique identifier of the internet service location." +msgstr "上网服务场所的唯一标识符。" + +msgid "WebSocket Connection Mode" +msgstr "WebSocket连接模式" + +msgid "WebSocket Secure Connection Mode" +msgstr "WebSocket加密连接模式" + +msgid "fields filled successfully." +msgstr "个字段已填充。" + +msgid "✓ Device ID generated: " +msgstr "✓ 设备ID已生成: " + +msgid "✓ Location ID generated: " +msgstr "✓ 位置ID已生成: " + +msgid "✓ Location obtained (fallback): " +msgstr "✓ 已获取位置(备用方法): " + +msgid "✓ Location obtained: " +msgstr "✓ 已获取位置: " + +msgid "✓ WAN MAC address obtained (fallback): " +msgstr "✓ 已获取 WAN MAC 地址(备用方法): " + +msgid "✓ WAN MAC address obtained: " +msgstr "✓ 已获取 WAN MAC 地址: " + +msgid "✗ Cannot generate Device ID due to MAC error" +msgstr "✗ 由于 MAC 错误,无法生成设备 ID" + +msgid "✗ Cannot generate Device ID without MAC address" +msgstr "✗ 没有 MAC 地址,无法生成设备 ID" + +msgid "✗ Error getting WAN MAC address: " +msgstr "✗ 获取 WAN MAC 地址错误: " + +msgid "✗ Error getting location: " +msgstr "✗ 获取位置错误: " + +msgid "✗ Failed to get WAN MAC address: " +msgstr "✗ 获取 WAN MAC 地址失败: " + +msgid "✗ Failed to get location data" +msgstr "✗ 获取位置数据失败" + +msgid "✗ Failed to get location: " +msgstr "✗ 获取位置失败: " + +msgid "✗ Failed to parse location data" +msgstr "✗ 解析位置数据失败" + +msgid "✗ No valid coordinates found" +msgstr "✗ 未找到有效的坐标" + +msgid "✗ RPC call failed, trying fallback..." +msgstr "✗ RPC 调用失败,正在尝试备用方法..." + +msgid " (interface: " +msgstr " (接口: " + +msgid ")" +msgstr ")" + +msgid "Unknown error" +msgstr "未知错误" + +msgid "Unknown" +msgstr "未知" + +msgid "Long Connection Profile" +msgstr "长连接配置" + +msgid "Select which long connection configuration to use. This option is always available regardless of authentication mode." +msgstr "选择使用的长连接配置。该选项在所有认证模式下都可用。" + +msgid "Please create a long connection profile first" +msgstr "请先创建一个长连接配置" + +msgid "Persistent Connection Profiles" +msgstr "持久连接配置" + +msgid "Create one or more long connection profiles for cloud authentication and OpenClaw intelligent control channels." +msgstr "创建长连接配置用于云认证和OpenClaw(龙虾)智能管控通道" + +msgid "Connection Mode" +msgstr "连接模式" + +msgid "The type of persistent connection to the remote management server." +msgstr "到远程管理服务器的持久连接类型。" + +msgid "WebSocket (ws://)" +msgstr "WebSocket (ws://)" + +msgid "WebSocket Secure (wss://)" +msgstr "WebSocket加密 (wss://)" + +msgid "MQTT Secure" +msgstr "MQTT安全" + +msgid "The hostname or IP address of the WebSocket server." +msgstr "WebSocket服务器的主机名或IP地址。" + +msgid "The port of the WebSocket server." +msgstr "WebSocket服务器的端口。" + +msgid "WebSocket Path" +msgstr "WebSocket路径" + +msgid "The URI path for the WebSocket connection." +msgstr "WebSocket连接的URI路径。" + +msgid "MQTT Hostname" +msgstr "MQTT主机名" + +msgid "The hostname or IP address of the MQTT broker." +msgstr "MQTT代理的主机名或IP地址。" + +msgid "The port of the MQTT broker." +msgstr "MQTT代理的端口。" + +msgid "The username for MQTT authentication." +msgstr "MQTT认证的用户名。" + +msgid "The password for MQTT authentication." +msgstr "MQTT认证的密码。" + +msgid "Add Authentication Server" +msgstr "添加认证服务器" + +msgid "Enter a unique name for this authentication server profile." +msgstr "请输入认证服务器配置文件的唯一名称。" + +msgid "Add Long Connection Profile" +msgstr "添加长连接配置" + +msgid "Enter a unique name for this long connection profile." +msgstr "请输入长连接配置文件的唯一名称。" + +msgid "Rename Authentication Server" +msgstr "重命名认证服务器" + +msgid "Enter a new unique name for this authentication server profile." +msgstr "请输入认证服务器配置文件的新名称。" + +msgid "Rename Long Connection Profile" +msgstr "重命名长连接配置" + +msgid "Enter a new unique name for this long connection profile." +msgstr "请输入长连接配置文件的新名称。" + +msgid "Please enter a name." +msgstr "请输入名称。" + +msgid "The name \"%s\" already exists in Auth Server profiles. Please choose a different name." +msgstr "名称\"%s\"已存在于认证服务器配置中,请选择其他名称。" + +msgid "The name \"%s\" already exists in Long Connection profiles. Please choose a different name." +msgstr "名称\"%s\"已存在于长连接配置中,请选择其他名称。" + +msgid "Created new authentication server profile \"%s\"." +msgstr "已创建新的认证服务器配置\"%s\"。" + +msgid "Created new long connection profile \"%s\"." +msgstr "已创建新的长连接配置\"%s\"。" + +msgid "Renamed authentication server profile from \"%s\" to \"%s\"." +msgstr "已将认证服务器配置从\"%s\"重命名为\"%s\"。" + +msgid "Renamed long connection profile from \"%s\" to \"%s\"." +msgstr "已将长连接配置从\"%s\"重命名为\"%s\"。" + +msgid "eBPF Traffic Control & DPI" +msgstr "eBPF 流控与 xDPI" + +msgid "eBPF kernel-level bandwidth control, session audit logging and xDPI L7 application/domain recognition." +msgstr "基于内核级 eBPF 的网络带宽控制、连接会话审计日志与 xDPI 七层应用/域名识别。" + +msgid "General Settings" +msgstr "常规设置" + +msgid "Enable Session Event Logging" +msgstr "启用会话事件日志" + +msgid "Record TCP/UDP session connection events as structured JSON to system log / syslog. (DNS learning and xDPI protocol recognition are always active in background)." +msgstr "将 TCP/UDP 连接建立与销毁等会话事件以结构化 JSON 格式输出至系统日志(DNS 域名学习与 xDPI 协议识别始终在后台常驻运行)。" + +msgid "Settings" +msgstr "设置" + + diff --git a/luci-app-aw-bpf/root/etc/uci-defaults/luci-app-aw-bpf.sh b/luci-app-aw-bpf/root/etc/uci-defaults/luci-app-aw-bpf.sh new file mode 100644 index 00000000..cf3a7cbd --- /dev/null +++ b/luci-app-aw-bpf/root/etc/uci-defaults/luci-app-aw-bpf.sh @@ -0,0 +1,6 @@ +#!/bin/sh +[ -f "/etc/config/hostnames" ] || { + echo 'config hostname' > /etc/config/hostnames +} + +exit 0 diff --git a/luci-app-aw-bpf/root/usr/share/luci/menu.d/luci-app-aw-bpf.json b/luci-app-aw-bpf/root/usr/share/luci/menu.d/luci-app-aw-bpf.json new file mode 100644 index 00000000..3716b9b2 --- /dev/null +++ b/luci-app-aw-bpf/root/usr/share/luci/menu.d/luci-app-aw-bpf.json @@ -0,0 +1,46 @@ +{ + "admin/qos": { + "title": "QoS", + "order": 60, + "action": { + "type": "firstchild", + "recurse": true + }, + "depends": { + "acl": [ "luci-app-aw-bpf" ] + } + }, + "admin/qos/hosts": { + "title": "Hosts", + "order": 10, + "action": { + "type": "view", + "path": "aw-bpf/display" + }, + "depends": { + "acl": [ "luci-app-aw-bpf" ] + } + }, + "admin/qos/l7": { + "title": "L7 application", + "order": 20, + "action": { + "type": "view", + "path": "aw-bpf/l7" + }, + "depends": { + "acl": [ "luci-app-aw-bpf" ] + } + }, + "admin/qos/settings": { + "title": "Settings", + "order": 30, + "action": { + "type": "view", + "path": "aw-bpf/settings" + }, + "depends": { + "acl": [ "luci-app-aw-bpf" ] + } + } +} diff --git a/luci-app-aw-bpf/root/usr/share/rpcd/acl.d/luci-app-aw-bpf.json b/luci-app-aw-bpf/root/usr/share/rpcd/acl.d/luci-app-aw-bpf.json new file mode 100644 index 00000000..3df9e871 --- /dev/null +++ b/luci-app-aw-bpf/root/usr/share/rpcd/acl.d/luci-app-aw-bpf.json @@ -0,0 +1,19 @@ +{ + "luci-app-aw-bpf": { + "description": "Grant access to LuCI app aw-bpf", + "read": { + "file": { + "/usr/bin/aw-bpfctl": [ "exec" ], + "/usr/bin/xdns-ctl": [ "exec" ], + "/usr/bin/awk": [ "exec" ], + "/tmp/dhcp.leases": [ "read" ], + "/proc/net/arp": [ "read" ], + "/etc/xdns/whitelist.txt": [ "read" ] + }, + "uci": [ "hostnames", "network", "aw-bpf" ] + }, + "write": { + "uci": [ "hostnames", "aw-bpf" ] + } + } +} diff --git a/luci-app-xkcptun/Makefile b/luci-app-xkcptun/Makefile new file mode 100644 index 00000000..46fc6ddc --- /dev/null +++ b/luci-app-xkcptun/Makefile @@ -0,0 +1,13 @@ +# This is free software, licensed under the Apache License, Version 2.0 + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Support for xkcptun +LUCI_DEPENDS:=+xkcptun + +PKG_LICENSE:=GPL-3.0-or-later +PKG_MAINTAINER:=Dengfeng Liu + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-xkcptun/README.md b/luci-app-xkcptun/README.md new file mode 100644 index 00000000..a84b0c7a --- /dev/null +++ b/luci-app-xkcptun/README.md @@ -0,0 +1,10 @@ +# LuCI Support for xkcptun + +LuCI Web user interface for configuring and managing `xkcptun` (C language high-performance kcptun) client and server instances. + +## Features + +- Real-time service status monitoring +- Manage multiple client and server instances +- Configurable KCP performance profiles (`fast3`, `fast2`, `fast`, `normal`, `manual`) +- Advanced tuning: MTU, sndwnd/rcvwnd, Reed-Solomon FEC shards, DSCP, nodelay, interval, fast resend, loss-driven AIMD window adaptation, send pacing, and keepalive/timeout settings. diff --git a/luci-app-xkcptun/htdocs/luci-static/resources/view/xkcptun.js b/luci-app-xkcptun/htdocs/luci-static/resources/view/xkcptun.js new file mode 100644 index 00000000..94c534af --- /dev/null +++ b/luci-app-xkcptun/htdocs/luci-static/resources/view/xkcptun.js @@ -0,0 +1,495 @@ +'use strict'; +'require view'; +'require dom'; +'require ui'; +'require form'; +'require rpc'; + +const callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { '': {} } +}); + +function getServiceStatus() { + return L.resolveDefault(callServiceList('xkcptun'), {}).then(function (res) { + let status = { + client: null, + server: null + }; + try { + let instances = res['xkcptun']['instances']; + if (instances['client'] && instances['client']['running']) { + status.client = instances['client']['pid']; + } + if (instances['server'] && instances['server']['running']) { + status.server = instances['server']['pid']; + } + } catch (e) {} + return status; + }); +} + +function renderStatus(status) { + let clientHTML = ''; + let serverHTML = ''; + let spanGreen = '%s (PID %d)'; + let spanRed = '%s'; + + if (status.client) { + clientHTML = String.format(spanGreen, _('RUNNING'), status.client); + } else { + clientHTML = String.format(spanRed, _('NOT RUNNING')); + } + + if (status.server) { + serverHTML = String.format(spanGreen, _('RUNNING'), status.server); + } else { + serverHTML = String.format(spanRed, _('NOT RUNNING')); + } + + return E('div', { class: 'cbi-map' }, + E('fieldset', { class: 'cbi-section' }, [ + E('p', {}, [ + E('strong', {}, _('Client Daemon') + ': '), + E('span', {}, [clientHTML]), + E('span', { style: 'margin-left: 20px;' }, [ + E('strong', {}, _('Server Daemon') + ': '), + E('span', {}, [serverHTML]) + ]) + ]) + ]) + ); +} + +const modeProfiles = [ + ['fast3', 'fast3 (nodelay=1, interval=10ms, resend=2, nc=1)'], + ['fast2', 'fast2 (nodelay=1, interval=10ms, resend=2, nc=0)'], + ['fast', 'fast (nodelay=0, interval=20ms, resend=2, nc=0)'], + ['normal', 'normal (nodelay=0, interval=30ms, resend=2, nc=0)'], + ['manual', _('manual (custom parameters)')] +]; + +function addCommonAdvancedOptions(s, tabName, isServer) { + let tab = tabName || 'advanced'; + let o; + + o = s.taboption(tab, form.Value, 'mtu', _('MTU'), + _('Maximum Transmission Unit for UDP packets. Default is 1350.')); + o.datatype = 'uinteger'; + o.placeholder = '1350'; + o.optional = true; + + o = s.taboption(tab, form.Value, 'sndwnd', _('Send Window (sndwnd)'), + _('Send window size (number of packets).')); + o.datatype = 'uinteger'; + o.placeholder = isServer ? '4096' : '1024'; + o.optional = true; + + o = s.taboption(tab, form.Value, 'rcvwnd', _('Receive Window (rcvwnd)'), + _('Receive window size (number of packets).')); + o.datatype = 'uinteger'; + o.placeholder = isServer ? '1024' : '4096'; + o.optional = true; + + o = s.taboption(tab, form.Flag, 'fec', _('Enable FEC (fec)'), + _('Frame all UDP datagrams with Reed-Solomon FEC header. Default is enabled.')); + o.default = '1'; + o.rmempty = false; + + o = s.taboption(tab, form.Value, 'datashard', _('Data Shards (FEC)'), + _('Reed-Solomon erasure coding data shards. Default is 10.')); + o.datatype = 'uinteger'; + o.placeholder = '10'; + o.optional = true; + o.depends('fec', '1'); + + o = s.taboption(tab, form.Value, 'parityshard', _('Parity Shards (FEC)'), + _('Reed-Solomon erasure coding parity shards. Default is 3.')); + o.datatype = 'uinteger'; + o.placeholder = '3'; + o.optional = true; + o.depends('fec', '1'); + + o = s.taboption(tab, form.Value, 'dscp', _('DSCP'), + _('DSCP IP TOS value (0-63). Default is 0.')); + o.datatype = 'range(0,63)'; + o.placeholder = '0'; + o.optional = true; + + o = s.taboption(tab, form.Flag, 'lossctrl', _('Loss-Driven AIMD (lossctrl)'), + _('Enable loss-driven AIMD send window adaptation. Default is disabled.')); + o.default = '0'; + o.rmempty = false; + + o = s.taboption(tab, form.Value, 'pacing', _('Send Pacing (pacing)'), + _('Max KCP segments per flush tick to smooth send bursts (0 = off). Default is 0.')); + o.datatype = 'uinteger'; + o.placeholder = '0'; + o.optional = true; + + o = s.taboption(tab, form.ListValue, 'nodelay', _('No Delay (nodelay)'), + _('Enable KCP nodelay mode.')); + o.value('0', '0 (' + _('Disable') + ')'); + o.value('1', '1 (' + _('Enable') + ')'); + o.default = '1'; + o.optional = true; + + o = s.taboption(tab, form.Value, 'interval', _('Internal Interval'), + _('Internal clock interval in milliseconds (10-5000). Default is 10ms.')); + o.datatype = 'range(10,5000)'; + o.placeholder = '10'; + o.optional = true; + + o = s.taboption(tab, form.Value, 'resend', _('Fast Resend'), + _('Fast resend count. 0 = off, 2 = fast resend. Default is 2.')); + o.datatype = 'uinteger'; + o.placeholder = '2'; + o.optional = true; + + o = s.taboption(tab, form.Flag, 'nc', _('No Congestion Control (nc)'), + _('Disable congestion window flow control. Default is 1 (disabled).')); + o.default = '1'; + o.rmempty = false; + + o = s.taboption(tab, form.Value, 'sockbuf', _('Socket Buffer Size'), + _('UDP socket receive/send buffer size in bytes. Default is 16777216 (16MB).')); + o.datatype = 'uinteger'; + o.placeholder = '16777216'; + o.optional = true; + + o = s.taboption(tab, form.Value, 'keepalive', _('Keepalive Interval'), + _('KCP keepalive ping interval in seconds. Default is 10s.')); + o.datatype = 'uinteger'; + o.placeholder = '10'; + o.optional = true; + + o = s.taboption(tab, form.Value, 'conntimeout', _('Connection Timeout'), + _('Idle connection timeout in seconds (0 = disable timeout sweep). Default is 60s.')); + o.datatype = 'uinteger'; + o.placeholder = '60'; + o.optional = true; +} + +return view.extend({ + render: function() { + let m, s, o; + + m = new form.Map('xkcptun', _('xkcptun'), + _('xkcptun is a high-performance C implementation of kcptun with dynamic destination multi-tunnel unified daemon architecture.')); + + // Status Section + s = m.section(form.NamedSection, '_status'); + s.anonymous = true; + s.render = function (section_id) { + let container = E('div', { id: 'service_status' }, _('Collecting status ...')); + L.Poll.add(function () { + return L.resolveDefault(getServiceStatus(), {}).then(function(status) { + let view = document.getElementById('service_status'); + if (view) { + dom.content(view, renderStatus(status)); + } + }); + }); + + return container; + }; + + // ===== Client Global Default Settings ===== + s = m.section(form.NamedSection, 'client', 'global', _('Client Global Settings'), + _('Common connection parameters and KCP profile inherited by all client tunnels.')); + s.anonymous = false; + s.addremove = false; + + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + + o = s.taboption('general', form.Value, 'remote_addr', _('Default Server Address'), + _('Remote xkcptun server IP address or domain name.')); + o.datatype = 'host'; + o.placeholder = '1.2.3.4'; + o.rmempty = false; + + o = s.taboption('general', form.Value, 'remote_port', _('Default Server Port'), + _('Remote xkcptun server UDP listen port.')); + o.datatype = 'port'; + o.placeholder = '9089'; + o.rmempty = false; + + o = s.taboption('general', form.Value, 'key', _('Pre-Shared Key'), + _('Pre-shared key for HMAC-SHA256 dynamic target authentication. Must match the server.')); + o.password = true; + o.placeholder = "it's a secret"; + o.optional = true; + + o = s.taboption('general', form.ListValue, 'mode', _('Mode Profile'), + _('KCP performance tuning profile.')); + for (let i = 0; i < modeProfiles.length; i++) { + o.value(modeProfiles[i][0], modeProfiles[i][1]); + } + o.default = 'fast3'; + + addCommonAdvancedOptions(s, 'advanced', false); + + // ===== Client Tunnels ===== + s = m.section(form.GridSection, 'client', _('Client Tunnels'), + _('Configure local client listening ports and dynamic backend target ports. All tunnels share the single unified client daemon.')); + s.anonymous = false; + s.addremove = true; + s.sortable = true; + s.addbtntitle = _('Add Client Tunnel'); + + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Overrides')); + + // Overview Columns in table + o = s.option(form.Flag, 'disabled', _('Enabled')); + o.enabled = '0'; + o.disabled = '1'; + o.default = '0'; + o.rmempty = false; + o.editable = true; + + o = s.option(form.Value, 'name', _('Tunnel Name')); + o.modalonly = false; + o.placeholder = _('(Auto)'); + + o = s.option(form.ListValue, 'proto', _('Protocol')); + o.value('tcp', 'TCP'); + o.value('udp', 'UDP'); + o.default = 'tcp'; + o.modalonly = false; + + o = s.option(form.Value, 'local_port', _('Local Port')); + o.modalonly = false; + o.placeholder = '9088'; + + o = s.option(form.Value, 'target_port', _('Target Port')); + o.modalonly = false; + o.placeholder = '22'; + + o = s.option(form.Value, 'target_addr', _('Target Host')); + o.modalonly = false; + o.placeholder = '127.0.0.1'; + + // Modal / Tab: General Settings + o = s.taboption('general', form.Flag, 'disabled', _('Enable Tunnel')); + o.enabled = '0'; + o.disabled = '1'; + o.default = '0'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'name', _('Tunnel Name / Alias'), + _('Optional descriptive name for this tunnel (e.g. ssh_tunnel, web_proxy).')); + o.placeholder = 'my_tunnel'; + o.optional = true; + o.modalonly = true; + + o = s.taboption('general', form.ListValue, 'proto', _('Tunnel Protocol'), + _('Protocol to tunnel: TCP (standard KCP stream) or UDP (direct low-latency datagram forwarding, e.g. for DNS).')); + o.value('tcp', 'TCP (TCP-over-KCP)'); + o.value('udp', 'UDP (Direct UDP Datagram)'); + o.default = 'tcp'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'local_interface', _('Local Interface'), + _('Network interface to bind (default: br-lan).')); + o.placeholder = 'br-lan'; + o.default = 'br-lan'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'local_port', _('Local Listen Port'), + _('Local port to listen for incoming application traffic (e.g. 9088).')); + o.datatype = 'port'; + o.placeholder = '9088'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'target_port', _('Dynamic Target Port'), + _('Target port on the server to forward traffic to (e.g. 22, 53, 80, 443).')); + o.datatype = 'port'; + o.placeholder = '22'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'target_addr', _('Dynamic Target Host'), + _('Target host on the server to connect/forward to (default: 127.0.0.1).')); + o.datatype = 'host'; + o.placeholder = '127.0.0.1'; + o.default = '127.0.0.1'; + o.optional = true; + o.modalonly = true; + + // Modal / Tab: Advanced Overrides (Optional) + o = s.taboption('advanced', form.Value, 'remote_addr', _('Override Server Address'), + _('Override remote server address for this specific tunnel (defaults to global).')); + o.datatype = 'host'; + o.placeholder = _('(Inherit from Global)'); + o.optional = true; + + o = s.taboption('advanced', form.Value, 'remote_port', _('Override Server Port'), + _('Override remote server port for this specific tunnel (defaults to global).')); + o.datatype = 'port'; + o.placeholder = _('(Inherit from Global)'); + o.optional = true; + + o = s.taboption('advanced', form.Value, 'key', _('Override Pre-Shared Key'), + _('Override pre-shared authentication key for this specific tunnel (defaults to global).')); + o.password = true; + o.placeholder = _('(Inherit from Global)'); + o.optional = true; + + // ===== Server Global Settings (Dynamic Gateway) ===== + s = m.section(form.NamedSection, 'server', 'global', _('Server Global Settings (Dynamic Gateway)'), + _('Unified xkcptun server dynamic gateway. The server listens on a single UDP port and dynamically routes each client tunnel to its requested backend target.')); + s.anonymous = false; + s.addremove = false; + + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + + o = s.taboption('general', form.Flag, 'enabled', _('Enable Server Daemon'), + _('Enable unified xkcptun server dynamic gateway daemon.')); + o.enabled = '1'; + o.disabled = '0'; + o.default = '0'; + o.rmempty = false; + + o = s.taboption('general', form.Value, 'local_interface', _('Listen Interface'), + _('Network interface to bind (e.g. eth0, br-lan, wan).')); + o.placeholder = 'eth0'; + o.default = 'eth0'; + o.rmempty = false; + + o = s.taboption('general', form.Value, 'local_port', _('Listen Port (UDP)'), + _('UDP port to listen for incoming client KCP connections (default: 9089).')); + o.datatype = 'port'; + o.placeholder = '9089'; + o.default = '9089'; + o.rmempty = false; + + o = s.taboption('general', form.Value, 'key', _('Pre-Shared Key'), + _('Pre-shared key for dynamic gateway authentication. Clients must present a valid HMAC token generated with this key.')); + o.password = true; + o.placeholder = "it's a secret"; + o.optional = true; + + o = s.taboption('general', form.ListValue, 'mode', _('Mode Profile'), + _('KCP performance tuning profile.')); + for (let i = 0; i < modeProfiles.length; i++) { + o.value(modeProfiles[i][0], modeProfiles[i][1]); + } + o.default = 'fast3'; + + addCommonAdvancedOptions(s, 'advanced', true); + + // ===== Server Fallback Tunnels (Optional) ===== + s = m.section(form.GridSection, 'server', _('Server Fallback Tunnels (Optional)'), + _('Optional fallback static target rules for legacy clients that do not send dynamic destination headers. Modern dynamic clients do not require any server tunnels configured here.')); + s.anonymous = false; + s.addremove = true; + s.sortable = true; + s.addbtntitle = _('Add Fallback Tunnel'); + + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + + // Overview Columns in table + o = s.option(form.Flag, 'disabled', _('Enabled')); + o.enabled = '0'; + o.disabled = '1'; + o.default = '0'; + o.rmempty = false; + o.editable = true; + + o = s.option(form.Value, 'name', _('Tunnel Name')); + o.modalonly = false; + o.placeholder = _('(Auto)'); + + o = s.option(form.Value, 'local_interface', _('Interface')); + o.modalonly = false; + o.placeholder = 'eth0'; + + o = s.option(form.Value, 'local_port', _('Listen Port')); + o.modalonly = false; + o.placeholder = '9089'; + + o = s.option(form.Value, 'remote_addr', _('Fallback Target Host')); + o.modalonly = false; + o.placeholder = '127.0.0.1'; + + o = s.option(form.Value, 'remote_port', _('Fallback Target Port')); + o.modalonly = false; + o.placeholder = '443'; + + o = s.option(form.ListValue, 'mode', _('Mode')); + o.modalonly = false; + for (let i = 0; i < modeProfiles.length; i++) { + o.value(modeProfiles[i][0], modeProfiles[i][0]); + } + + o = s.option(form.Flag, 'fec', _('FEC')); + o.modalonly = false; + o.default = '0'; + o.editable = true; + + // Modal / Tab: General Settings + o = s.taboption('general', form.Flag, 'disabled', _('Enable Tunnel')); + o.enabled = '0'; + o.disabled = '1'; + o.default = '0'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'name', _('Tunnel Name / Alias'), + _('Optional descriptive name for this tunnel (e.g. main_srv).')); + o.placeholder = 'srv_tunnel'; + o.optional = true; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'local_interface', _('Local Interface'), + _('Network interface to bind (e.g. eth0, br-lan, wan).')); + o.placeholder = 'eth0'; + o.default = 'eth0'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'local_port', _('Local Listen Port (UDP)'), + _('UDP port to listen for incoming KCP connections from clients.')); + o.datatype = 'port'; + o.placeholder = '9089'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'remote_addr', _('Fallback Target Host'), + _('Default target host if client did not specify a dynamic destination (default: 127.0.0.1).')); + o.datatype = 'host'; + o.default = '127.0.0.1'; + o.placeholder = '127.0.0.1'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.Value, 'remote_port', _('Fallback Target Port'), + _('Default target port if client did not specify a dynamic destination (e.g. 443, 22).')); + o.datatype = 'port'; + o.placeholder = '443'; + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('general', form.ListValue, 'mode', _('Mode Profile'), + _('KCP performance tuning profile.')); + for (let i = 0; i < modeProfiles.length; i++) { + o.value(modeProfiles[i][0], modeProfiles[i][1]); + } + o.default = 'fast3'; + o.modalonly = true; + + addCommonAdvancedOptions(s, 'advanced', true); + + return m.render(); + } +}); diff --git a/luci-app-xkcptun/po/zh-cn b/luci-app-xkcptun/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-xkcptun/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-xkcptun/po/zh_Hans/xkcptun.po b/luci-app-xkcptun/po/zh_Hans/xkcptun.po new file mode 100644 index 00000000..dedc28c9 --- /dev/null +++ b/luci-app-xkcptun/po/zh_Hans/xkcptun.po @@ -0,0 +1,365 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-xkcptun\n" +"PO-Revision-Date: 2026-08-31 10:00+0800\n" +"Last-Translator: Dengfeng Liu \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgid "xkcptun" +msgstr "xkcptun" + +msgid "xkcptun is a high-performance C implementation of kcptun with dynamic destination multi-tunnel unified daemon architecture." +msgstr "xkcptun 是采用动态目标多隧道统一守护进程架构的高性能 C 语言版 kcptun 实现。" + +msgid "xkcptun is a high-performance C implementation of kcptun with multi-tunnel unified daemon architecture." +msgstr "xkcptun 是采用多隧道统一守护进程架构的高性能 C 语言版 kcptun 实现。" + +msgid "RUNNING" +msgstr "运行中" + +msgid "NOT RUNNING" +msgstr "未运行" + +msgid "Collecting status ..." +msgstr "正在收集状态…" + +msgid "Client Daemon" +msgstr "客户端守护进程" + +msgid "Server Daemon" +msgstr "服务端守护进程" + +msgid "Client Global Settings" +msgstr "客户端全局默认设置" + +msgid "Common connection parameters and KCP profile inherited by all client tunnels." +msgstr "所有客户端隧道共享继承的公共连接参数及 KCP 性能配置。" + +msgid "Default Server Address" +msgstr "默认服务器地址" + +msgid "Default Server Port" +msgstr "默认服务器端口" + +msgid "Client Tunnels" +msgstr "客户端隧道" + +msgid "Configure local client listening ports and dynamic backend target ports. All tunnels share the single unified client daemon." +msgstr "配置客户端本地监听端口与远端动态后端目标端口。所有隧道共享单一统一守护进程。" + +msgid "Configure xkcptun client tunnels. All enabled tunnels are managed concurrently by a single unified client daemon." +msgstr "配置 xkcptun 客户端隧道。所有启用的隧道将由单一客户端守护进程统一并发管理。" + +msgid "Add Client Tunnel" +msgstr "添加客户端隧道" + +msgid "Server Tunnels" +msgstr "服务端隧道" + +msgid "Configure xkcptun server listener. A single server tunnel can dynamically serve multiple client tunnels." +msgstr "配置 xkcptun 服务端监听。单一服务端监听端口即可动态服务多个不同目标的客户端隧道。" + +msgid "Configure xkcptun server tunnels. All enabled tunnels are managed concurrently by a single unified server daemon." +msgstr "配置 xkcptun 服务端隧道。所有启用的隧道将由单一服务端守护进程统一并发管理。" + +msgid "Add Server Tunnel" +msgstr "添加服务端隧道" + +msgid "General Settings" +msgstr "常规设置" + +msgid "Advanced Settings" +msgstr "高级设置" + +msgid "Advanced Overrides" +msgstr "高级覆盖选项" + +msgid "Enabled" +msgstr "已启用" + +msgid "Enable Tunnel" +msgstr "启用隧道" + +msgid "Tunnel Name" +msgstr "隧道名称" + +msgid "Tunnel Name / Alias" +msgstr "隧道名称 / 别名" + +msgid "(Auto)" +msgstr "(自动)" + +msgid "Optional descriptive name for this tunnel (e.g. ssh_tunnel, web_proxy)." +msgstr "此隧道的可选描述名称(例如 ssh_tunnel、web_proxy)。" + +msgid "Optional descriptive name for this tunnel (e.g. k_bwg, ssh_tunnel)." +msgstr "此隧道的可选描述名称(例如 k_bwg、ssh_tunnel)。" + +msgid "Optional descriptive name for this tunnel (e.g. main_srv)." +msgstr "此隧道的可选描述名称(例如 main_srv)。" + +msgid "Optional descriptive name for this tunnel (e.g. srv_ssh, srv_https)." +msgstr "此隧道的可选描述名称(例如 srv_ssh、srv_https)。" + +msgid "Interface" +msgstr "接口" + +msgid "Local Interface" +msgstr "本地接口" + +msgid "Network interface to bind (default: br-lan)." +msgstr "要绑定的网络接口(默认:br-lan)。" + +msgid "Network interface to bind (e.g. br-lan, eth0)." +msgstr "要绑定的网络接口(例如 br-lan、eth0)。" + +msgid "Network interface to bind (e.g. eth0, br-lan, wan)." +msgstr "要绑定的网络接口(例如 eth0、br-lan、wan)。" + +msgid "Local Port" +msgstr "本地端口" + +msgid "Local Listen Port" +msgstr "本地监听端口" + +msgid "Local TCP port to listen for incoming application traffic (e.g. 9088)." +msgstr "监听传入应用程序流量的本地 TCP 端口(例如 9088)。" + +msgid "Local TCP port to listen for incoming application traffic." +msgstr "监听传入应用程序流量的本地 TCP 端口。" + +msgid "Local Listen Port (UDP)" +msgstr "本地监听端口 (UDP)" + +msgid "UDP port to listen for incoming KCP connections from clients." +msgstr "监听客户端传入 KCP 连接的 UDP 端口。" + +msgid "Server Address" +msgstr "服务器地址" + +msgid "Remote Server Address" +msgstr "远程服务器地址" + +msgid "Remote xkcptun server IP address or domain name." +msgstr "远程 xkcptun 服务器 IP 地址或域名。" + +msgid "Server Port" +msgstr "服务器端口" + +msgid "Remote Server Port" +msgstr "远程服务器端口" + +msgid "Remote xkcptun server UDP listen port." +msgstr "远程 xkcptun 服务器 UDP 监听端口。" + +msgid "Target Host" +msgstr "目标主机" + +msgid "Target Address" +msgstr "目标地址" + +msgid "Target TCP Host" +msgstr "目标 TCP 主机" + +msgid "Target application host/IP to forward traffic to (e.g. 127.0.0.1)." +msgstr "要转发流量的目标应用程序主机/IP(例如 127.0.0.1)。" + +msgid "Dynamic Target Host" +msgstr "动态目标主机" + +msgid "Target TCP host on the server to connect to (default: 127.0.0.1)." +msgstr "服务端上要连接的目标 TCP 主机(默认:127.0.0.1)。" + +msgid "Target Port" +msgstr "目标端口" + +msgid "Target TCP Port" +msgstr "目标 TCP 端口" + +msgid "Dynamic Target Port" +msgstr "动态目标端口" + +msgid "Target TCP port on the server to forward traffic to (e.g. 22, 80, 443)." +msgstr "服务端上要转发的目标 TCP 端口(例如 22、80、443)。" + +msgid "Target application port to forward traffic to (e.g. 443, 22)." +msgstr "要转发流量的目标应用程序端口(例如 443、22)。" + +msgid "Fallback Target Host" +msgstr "缺省回退目标主机" + +msgid "Default target host if client did not specify a dynamic destination (default: 127.0.0.1)." +msgstr "当客户端未指定动态目标时使用的默认目标主机(默认:127.0.0.1)。" + +msgid "Fallback Target Port" +msgstr "缺省回退目标端口" + +msgid "Default target port if client did not specify a dynamic destination (e.g. 443, 22)." +msgstr "当客户端未指定动态目标时使用的默认目标端口(例如 443、22)。" + +msgid "Override Server Address" +msgstr "覆盖服务器地址" + +msgid "Override remote server address for this specific tunnel (defaults to global)." +msgstr "为该特定隧道覆盖远程服务器地址(默认继承全局配置)。" + +msgid "Override Server Port" +msgstr "覆盖服务器端口" + +msgid "Override remote server port for this specific tunnel (defaults to global)." +msgstr "为该特定隧道覆盖远程服务器端口(默认继承全局配置)。" + +msgid "(Inherit from Global)" +msgstr "(继承全局配置)" + +msgid "Mode" +msgstr "模式" + +msgid "Mode Profile" +msgstr "模式预设" + +msgid "KCP performance tuning profile." +msgstr "KCP 性能调优预设模式。" + +msgid "manual (custom parameters)" +msgstr "手动调优 (自定义参数)" + +msgid "MTU" +msgstr "MTU" + +msgid "Maximum Transmission Unit for UDP packets. Default is 1350." +msgstr "UDP 数据包的最大传输单元 (MTU)。默认为 1350。" + +msgid "Send Window (sndwnd)" +msgstr "发送窗口 (sndwnd)" + +msgid "Send window size (number of packets)." +msgstr "发送窗口大小(数据包数量)。" + +msgid "Receive Window (rcvwnd)" +msgstr "接收窗口 (rcvwnd)" + +msgid "Receive window size (number of packets)." +msgstr "接收窗口大小(数据包数量)。" + +msgid "Enable FEC (fec)" +msgstr "启用 FEC (fec)" + +msgid "FEC" +msgstr "FEC" + +msgid "Frame all UDP datagrams with Reed-Solomon FEC header. Default is disabled." +msgstr "使用 Reed-Solomon FEC 头封装所有 UDP 数据报。默认禁用。" + +msgid "Data Shards (FEC)" +msgstr "数据分片 (FEC)" + +msgid "Reed-Solomon erasure coding data shards. Default is 10." +msgstr "Reed-Solomon 纠错编码数据分片数 (datashard)。默认为 10。" + +msgid "Parity Shards (FEC)" +msgstr "校验分片 (FEC)" + +msgid "Reed-Solomon erasure coding parity shards. Default is 3." +msgstr "Reed-Solomon 纠错编码校验分片数 (parityshard)。默认为 3。" + +msgid "DSCP" +msgstr "DSCP" + +msgid "DSCP IP TOS value (0-63). Default is 0." +msgstr "DSCP IP TOS 差分服务值 (0-63)。默认为 0。" + +msgid "Loss-Driven AIMD (lossctrl)" +msgstr "丢包驱动 AIMD 拥塞窗口 (lossctrl)" + +msgid "Enable loss-driven AIMD send window adaptation. Default is enabled." +msgstr "启用基于丢包反馈的 AIMD 发送窗口自适应调整。默认启用。" + +msgid "Send Pacing (pacing)" +msgstr "发送速率整形 (pacing)" + +msgid "Max KCP segments per flush tick to smooth send bursts (0 = off)." +msgstr "每次 flush 周期允许发送的最大 KCP 分片数,平滑突发流量 (0 = 禁用)。" + +msgid "No Delay (nodelay)" +msgstr "无延迟模式 (nodelay)" + +msgid "Enable KCP nodelay mode." +msgstr "启用 KCP nodelay 快速模式。" + +msgid "Enable" +msgstr "启用" + +msgid "Disable" +msgstr "禁用" + +msgid "Internal Interval" +msgstr "内部时钟周期 (interval)" + +msgid "Internal clock interval in milliseconds (10-5000). Default is 20ms." +msgstr "内部协议循环时钟间隔,单位毫秒 (10-5000)。默认为 20ms。" + +msgid "Fast Resend" +msgstr "快速重传 (resend)" + +msgid "Fast resend count. 0 = off, 2 = fast resend. Default is 2." +msgstr "快速重传跳过确认包次数。0 = 关闭,2 = 快速重传。默认为 2。" + +msgid "No Congestion Control (nc)" +msgstr "关闭常规拥塞控制 (nc)" + +msgid "Disable congestion window flow control. Default is 1 (disabled)." +msgstr "关闭拥塞窗口流量控制。默认为 1(关闭控制)。" + +msgid "Socket Buffer Size" +msgstr "套接字缓冲区大小 (sockbuf)" + +msgid "UDP socket receive/send buffer size in bytes. Default is 4194304 (4MB)." +msgstr "UDP 套接字接收与发送缓冲区大小,单位字节。默认为 4194304 (4MB)。" + +msgid "Keepalive Interval" +msgstr "保活心跳间隔 (keepalive)" + +msgid "KCP keepalive ping interval in seconds. Default is 10s." +msgstr "KCP 保活心跳 ping 间隔,单位秒。默认为 10s。" + +msgid "Connection Timeout" +msgstr "连接超时时间 (conntimeout)" + +msgid "Idle connection timeout in seconds (0 = disable timeout sweep). Default is 60s." +msgstr "空闲会话超时释放时间,单位秒 (0 = 禁用超时回收)。默认为 60s。" + +msgid "Pre-Shared Key" +msgstr "预共享密钥 (key)" + +msgid "Pre-shared key for HMAC-SHA256 dynamic target authentication. Must match the server." +msgstr "用于动态目标 HMAC-SHA256 鉴权的预共享密钥,必须与服务端保持一致。" + +msgid "Override Pre-Shared Key" +msgstr "覆盖预共享密钥" + +msgid "Override pre-shared authentication key for this specific tunnel (defaults to global)." +msgstr "为此特定隧道覆盖预共享鉴权密钥(默认继承全局配置)。" + +msgid "Pre-shared key for dynamic gateway authentication. Clients must present a valid HMAC token generated with this key." +msgstr "用于动态网关鉴权的预共享密钥。客户端必须出示基于该密钥生成的有效 HMAC 令牌。" + +msgid "Protocol" +msgstr "协议" + +msgid "Tunnel Protocol" +msgstr "隧道协议" + +msgid "Protocol to tunnel: TCP (standard KCP stream) or UDP (direct low-latency datagram forwarding, e.g. for DNS)." +msgstr "隧道转发协议:TCP(标准 KCP 可靠流)或 UDP(原生极低延迟报文直传,适用于 DNS 查询等场景)。" + +msgid "TCP (TCP-over-KCP)" +msgstr "TCP (基于 KCP 可靠流)" + +msgid "UDP (Direct UDP Datagram)" +msgstr "UDP (原生报文直传,适用于 DNS)" diff --git a/luci-app-xkcptun/root/usr/share/luci/menu.d/luci-app-xkcptun.json b/luci-app-xkcptun/root/usr/share/luci/menu.d/luci-app-xkcptun.json new file mode 100644 index 00000000..94213d76 --- /dev/null +++ b/luci-app-xkcptun/root/usr/share/luci/menu.d/luci-app-xkcptun.json @@ -0,0 +1,13 @@ +{ + "admin/services/xkcptun": { + "title": "xkcptun", + "order": 60, + "action": { + "type": "view", + "path": "xkcptun" + }, + "depends": { + "acl": [ "luci-app-xkcptun" ] + } + } +} diff --git a/luci-app-xkcptun/root/usr/share/rpcd/acl.d/luci-app-xkcptun.json b/luci-app-xkcptun/root/usr/share/rpcd/acl.d/luci-app-xkcptun.json new file mode 100644 index 00000000..02f8fdd2 --- /dev/null +++ b/luci-app-xkcptun/root/usr/share/rpcd/acl.d/luci-app-xkcptun.json @@ -0,0 +1,14 @@ +{ + "luci-app-xkcptun": { + "description": "Grant access to LuCI app xkcptun", + "read": { + "ubus": { + "service": [ "list" ] + }, + "uci": [ "xkcptun" ] + }, + "write": { + "uci": [ "xkcptun" ] + } + } +} diff --git a/xkcptun/Makefile b/xkcptun/Makefile new file mode 100644 index 00000000..e908cc45 --- /dev/null +++ b/xkcptun/Makefile @@ -0,0 +1,60 @@ +# +# Copyright (C) 2026 Dengfeng Liu +# +# This is free software, licensed under the GNU General Public License v3. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=xkcptun +PKG_VERSION:=1.09.561 +PKG_RELEASE:=1 + +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_MAINTAINER:=Dengfeng Liu +PKG_LICENSE:=GPL-3.0-or-later +PKG_LICENSE_FILES:=LICENSE + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +define Package/xkcptun + SECTION:=net + CATEGORY:=Network + SUBMENU:=Web Servers/Proxies + TITLE:=KCP-based Secure Tunnel in C + URL:=https://github.com/liudf0716/xkcptun + DEPENDS:=+libevent2 +endef + +define Package/xkcptun/description + xkcptun is a lightweight and high-performance C language implementation of kcptun. + This package contains both client and server binaries. +endef + +define Package/xkcptun/conffiles +/etc/config/xkcptun +endef + +define Package/xkcptun/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/xkcp_client $(1)/usr/bin/xkcp_client + $(LN) xkcp_client $(1)/usr/bin/xkcptun-client + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/xkcp_server $(1)/usr/bin/xkcp_server + $(LN) xkcp_server $(1)/usr/bin/xkcptun-server + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/xkcp_spy $(1)/usr/bin/xkcp_spy + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/xkcptun.config $(1)/etc/config/xkcptun + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/xkcptun.init $(1)/etc/init.d/xkcptun +endef + +CMAKE_OPTIONS += \ + -DAUTO_INSTALL_DEPS=OFF + +$(eval $(call BuildPackage,xkcptun)) diff --git a/xkcptun/README.md b/xkcptun/README.md new file mode 100644 index 00000000..68d3966e --- /dev/null +++ b/xkcptun/README.md @@ -0,0 +1,12 @@ +# xkcptun + +xkcptun is a high-performance C language implementation of kcptun based on KCP protocol and libevent. + +## Features + +- Written in pure C with high throughput and low memory footprint +- KCP protocol tuning profiles (`fast3`, `fast2`, `fast`, `normal`) +- Reed-Solomon Forward Error Correction (FEC) support +- Loss-driven AIMD congestion control & send pacing +- Multi-instance client and server daemon management via procd +- Full LuCI web UI configuration support diff --git a/xkcptun/files/xkcptun.config b/xkcptun/files/xkcptun.config new file mode 100644 index 00000000..9c8ffed7 --- /dev/null +++ b/xkcptun/files/xkcptun.config @@ -0,0 +1,53 @@ + +config global 'client' + option remote_addr '1.2.3.4' + option remote_port '9089' + option mode 'fast3' + option mtu '1350' + option sndwnd '1024' + option rcvwnd '4096' + option datashard '10' + option parityshard '3' + option dscp '0' + option nodelay '1' + option interval '10' + option resend '2' + option nc '1' + option lossctrl '0' + option pacing '0' + option fec '1' + option sockbuf '16777216' + option keepalive '10' + option conntimeout '60' + option key '7f7ac683d720e1437019692eadb0b811' + +config client 'client1' + option disabled '1' + option name 'ssh_tunnel' + option local_interface 'br-lan' + option local_port '9088' + option target_port '22' + option target_addr '127.0.0.1' + +config global 'server' + option enabled '0' + option local_interface 'eth0' + option local_port '9089' + option mode 'fast3' + option mtu '1350' + option sndwnd '4096' + option rcvwnd '1024' + option datashard '10' + option parityshard '3' + option dscp '0' + option nodelay '1' + option interval '10' + option resend '2' + option nc '1' + option lossctrl '0' + option pacing '0' + option fec '1' + option sockbuf '16777216' + option keepalive '10' + option conntimeout '60' + option key '7f7ac683d720e1437019692eadb0b811' diff --git a/xkcptun/files/xkcptun.init b/xkcptun/files/xkcptun.init new file mode 100644 index 00000000..535d7d5e --- /dev/null +++ b/xkcptun/files/xkcptun.init @@ -0,0 +1,262 @@ +#!/bin/sh /etc/rc.common +# +# Copyright (C) 2026 Dengfeng Liu +# +# This is free software, licensed under the GNU General Public License v3. +# See /LICENSE for more information. +# + +USE_PROCD=1 +START=99 + +confdir=/var/etc/xkcptun +bindir=/usr/bin + +append_toml_global() { + local cfg="$1" + local cfgtype="$2" + local outfile="$3" + + eval "$(validate_global_options "$cfg" validate_mklocal)" + validate_global_options "$cfg" || return 0 + + [ -z "$remote_addr" ] || echo "remote_addr = \"$remote_addr\"" >> "$outfile" + [ -z "$remote_port" ] || echo "remote_port = $remote_port" >> "$outfile" + [ -z "$key" ] || echo "key = \"$key\"" >> "$outfile" + [ -z "$local_interface" ] || echo "local_interface = \"$local_interface\"" >> "$outfile" + [ -z "$local_port" ] || echo "local_port = $local_port" >> "$outfile" + [ -z "$mode" ] || echo "mode = \"$mode\"" >> "$outfile" + [ -z "$mtu" ] || echo "mtu = $mtu" >> "$outfile" + [ -z "$sndwnd" ] || echo "sndwnd = $sndwnd" >> "$outfile" + [ -z "$rcvwnd" ] || echo "rcvwnd = $rcvwnd" >> "$outfile" + [ -z "$datashard" ] || echo "data_shard = $datashard" >> "$outfile" + [ -z "$parityshard" ] || echo "parity_shard = $parityshard" >> "$outfile" + [ -z "$dscp" ] || echo "dscp = $dscp" >> "$outfile" + [ -z "$nodelay" ] || echo "nodelay = $nodelay" >> "$outfile" + [ -z "$interval" ] || echo "interval = $interval" >> "$outfile" + [ -z "$resend" ] || echo "resend = $resend" >> "$outfile" + [ -z "$nc" ] || echo "nc = $nc" >> "$outfile" + [ -z "$lossctrl" ] || echo "loss_ctrl = $lossctrl" >> "$outfile" + [ -z "$pacing" ] || echo "pacing = $pacing" >> "$outfile" + [ -z "$fec" ] || echo "fec = $fec" >> "$outfile" + [ -z "$sockbuf" ] || echo "sock_buf = $sockbuf" >> "$outfile" + [ -z "$keepalive" ] || echo "keepalive = $keepalive" >> "$outfile" + [ -z "$conntimeout" ] || echo "conn_timeout = $conntimeout" >> "$outfile" + + return 0 +} + +append_toml_tunnel() { + local cfg="$1" + local cfgtype="$2" + local outfile="$3" + + eval "$("validate_${cfgtype}_section" "$cfg" validate_mklocal)" + "validate_${cfgtype}_section" "$cfg" || return 1 + [ "$disabled" = 0 ] || return 1 + [ -n "$local_port" ] || return 1 + + cat << EOF >> "$outfile" + +[[tunnel]] +name = "${name:-$cfg}" +local_interface = "${local_interface:-br-lan}" +local_port = $local_port +EOF + [ -z "$proto" ] || echo "proto = \"$proto\"" >> "$outfile" + [ -z "$proxy_type" ] || echo "proxy_type = \"$proxy_type\"" >> "$outfile" + [ -z "$target_port" ] || echo "target_port = $target_port" >> "$outfile" + [ -z "$target_addr" ] || echo "target_addr = \"$target_addr\"" >> "$outfile" + [ -z "$remote_addr" ] || echo "remote_addr = \"$remote_addr\"" >> "$outfile" + [ -z "$remote_port" ] || echo "remote_port = $remote_port" >> "$outfile" + [ -z "$key" ] || echo "key = \"$key\"" >> "$outfile" + [ -z "$mode" ] || echo "mode = \"$mode\"" >> "$outfile" + [ -z "$mtu" ] || echo "mtu = $mtu" >> "$outfile" + [ -z "$sndwnd" ] || echo "sndwnd = $sndwnd" >> "$outfile" + [ -z "$rcvwnd" ] || echo "rcvwnd = $rcvwnd" >> "$outfile" + [ -z "$datashard" ] || echo "data_shard = $datashard" >> "$outfile" + [ -z "$parityshard" ] || echo "parity_shard = $parityshard" >> "$outfile" + [ -z "$dscp" ] || echo "dscp = $dscp" >> "$outfile" + [ -z "$nodelay" ] || echo "nodelay = $nodelay" >> "$outfile" + [ -z "$interval" ] || echo "interval = $interval" >> "$outfile" + [ -z "$resend" ] || echo "resend = $resend" >> "$outfile" + [ -z "$nc" ] || echo "nc = $nc" >> "$outfile" + [ -z "$lossctrl" ] || echo "loss_ctrl = $lossctrl" >> "$outfile" + [ -z "$pacing" ] || echo "pacing = $pacing" >> "$outfile" + [ -z "$fec" ] || echo "fec = $fec" >> "$outfile" + [ -z "$sockbuf" ] || echo "sock_buf = $sockbuf" >> "$outfile" + [ -z "$keepalive" ] || echo "keepalive = $keepalive" >> "$outfile" + [ -z "$conntimeout" ] || echo "conn_timeout = $conntimeout" >> "$outfile" + + return 0 +} + +start_daemon() { + local cfgtype="$1" + local bin="$bindir/xkcp_$cfgtype" + [ -x "$bin" ] || bin="$bindir/xkcptun-$cfgtype" + [ -x "$bin" ] || return + + local conftoml="$confdir/$cfgtype.toml" + rm -f "$conftoml" + + local mon_port=9086 + [ "$cfgtype" = "server" ] && mon_port=9087 + + cat << EOF > "$conftoml" +[global] +syslog = true +mon_port = $mon_port +EOF + + # Append global parameters if configured + append_global_match() { + local cfg="$1" + local match="$2" + [ "$cfg" = "$match" ] && append_toml_global "$cfg" "$match" "$conftoml" + } + config_foreach append_global_match global "$cfgtype" + + local count=0 + append_section() { + local cfg="$1" + [ "$cfg" = "global" ] && return + if append_toml_tunnel "$cfg" "$cfgtype" "$conftoml"; then + count=$((count + 1)) + fi + } + + config_foreach append_section "$cfgtype" + + if [ "$cfgtype" = "client" ] && [ "$count" -eq 0 ]; then + rm -f "$conftoml" + return + fi + + if [ "$cfgtype" = "server" ]; then + local srv_enabled + config_get_bool srv_enabled server enabled 0 + if [ "$srv_enabled" -ne 1 ] && [ "$count" -eq 0 ]; then + rm -f "$conftoml" + return + fi + fi + + procd_open_instance "$cfgtype" + procd_set_param command "$bin" -f --syslog -c "$conftoml" + procd_set_param file "$conftoml" + procd_set_param respawn + procd_close_instance +} + +start_service() { + mkdir -p "$confdir" + config_load xkcptun + start_daemon server + start_daemon client +} + +stop_service() { + rm -rf "$confdir" +} + +service_triggers() { + procd_add_reload_interface_trigger wan + procd_add_reload_interface_trigger lan + procd_add_reload_trigger xkcptun + procd_open_validate + validate_global_section + validate_server_section + validate_client_section + procd_close_validate +} + +validate_mklocal() { + local tuple opts + + shift 2 + for tuple in "$@"; do + opts="${tuple%%:*} $opts" + done + [ -z "$opts" ] || echo "local $opts" +} + +validate() { + uci_validate_section xkcptun "$@" +} + +validate_common_options() { + local cfgtype="$1"; shift + local cfg="$1"; shift + local func="$1"; shift + local mode_profiles='"fast3", "fast2", "fast", "normal", "manual"' + + "${func:-validate}" "$cfgtype" "$cfg" "$@" \ + 'disabled:bool:0' \ + 'name:string' \ + "mode:or($mode_profiles)" \ + 'mtu:uinteger' \ + 'sndwnd:uinteger' \ + 'rcvwnd:uinteger' \ + 'datashard:uinteger' \ + 'parityshard:uinteger' \ + 'dscp:uinteger' \ + 'nodelay:uinteger' \ + 'interval:uinteger' \ + 'resend:uinteger' \ + 'nc:uinteger' \ + 'lossctrl:uinteger' \ + 'pacing:uinteger' \ + 'fec:uinteger' \ + 'sockbuf:uinteger' \ + 'keepalive:uinteger' \ + 'conntimeout:uinteger' \ + 'key:string' \ + 'syslog:bool:1' \ + 'user:string' +} + +validate_global_options() { + validate_common_options global "$1" "$2" \ + 'enabled:bool:0' \ + 'remote_addr:host' \ + 'remote_port:port' \ + 'local_interface:string' \ + 'local_port:port' +} + +validate_server_options() { + validate_common_options server "$1" "$2" \ + 'local_interface:string:eth0' \ + 'local_port:port:9089' \ + 'remote_addr:host:127.0.0.1' \ + 'remote_port:port:443' +} + +validate_client_options() { + validate_common_options client "$1" "$2" \ + 'local_interface:string:br-lan' \ + 'local_port:port:9088' \ + 'proto:or("tcp", "udp")' \ + 'proxy_type:or("forward", "redir", "socks5", "transparent")' \ + 'target_addr:host' \ + 'target_port:port' \ + 'remote_addr:host' \ + 'remote_port:port' +} + +validate_global_section() { + validate_global_options "$1" "$2" +} + +validate_server_section() { + validate_server_options "$1" "$2" +} + +validate_client_section() { + validate_client_options "$1" "$2" +} + +reload_service() { + restart +}