mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
💐 Sync 2026-09-10 18:21:54
This commit is contained in:
@@ -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 <liudf0716@gmail.com>
|
||||
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))
|
||||
@@ -0,0 +1,2 @@
|
||||
config aw-bpf 'common'
|
||||
option enable_event_log '0'
|
||||
@@ -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" <<EOF
|
||||
# Generated automatically by /etc/init.d/aw-bpf from UCI
|
||||
enable_event_log=${enable_event_log}
|
||||
location_id=${location_id}
|
||||
ap_device_id=${ap_device_id}
|
||||
ap_mac_address=${ap_mac_address}
|
||||
ap_longitude=${ap_longitude}
|
||||
ap_latitude=${ap_latitude}
|
||||
EOF
|
||||
}
|
||||
|
||||
start_service() {
|
||||
load_xdpi
|
||||
load_bpf_resources
|
||||
|
||||
if ! command -v "$PROG" >/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
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -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 <liudf0716@gmail.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + '<br/>';
|
||||
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) + '<br/>';
|
||||
}
|
||||
});
|
||||
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
|
||||
});
|
||||
@@ -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 + '<br/>';
|
||||
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) + '<br/>';
|
||||
}
|
||||
});
|
||||
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 = '<span class="btn-icon">▶️</span> ' + _('Resume');
|
||||
btn.classList.remove('cbi-button-action');
|
||||
btn.classList.add('cbi-button-positive');
|
||||
} else {
|
||||
btn.innerHTML = '<span class="btn-icon">⏸️</span> ' + _('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
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
[ -f "/etc/config/hostnames" ] || {
|
||||
echo 'config hostname' > /etc/config/hostnames
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -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" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <liudf0716@gmail.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -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.
|
||||
@@ -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 = '<em><span style="color:green"><strong>%s (PID %d)</strong></span></em>';
|
||||
let spanRed = '<em><span style="color:grey"><strong>%s</strong></span></em>';
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
@@ -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 <liudf0716@gmail.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <zh_Hans>\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)"
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/services/xkcptun": {
|
||||
"title": "xkcptun",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "xkcptun"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-xkcptun" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"luci-app-xkcptun": {
|
||||
"description": "Grant access to LuCI app xkcptun",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "xkcptun" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "xkcptun" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# Copyright (C) 2026 Dengfeng Liu <liudf0716@gmail.com>
|
||||
#
|
||||
# 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 <liudf0716@gmail.com>
|
||||
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))
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#
|
||||
# Copyright (C) 2026 Dengfeng Liu <liudf0716@gmail.com>
|
||||
#
|
||||
# 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
|
||||
}
|
||||
Reference in New Issue
Block a user