fix: dynamic firewall config + UFW support + anti-spoof bug fix
Root cause: UFW FORWARD policy DROP overrides lmvpn_nat accept rules. DNS (small UDP) worked but TCP packets were silently dropped. - Add firewall_linux.go: dynamic NAT/forward/UFW config from ApplySettings - Add firewall_darwin.go/firewall_other.go: stubs - Fix anti-spoof bug in switch.go: return dropPacket sentinel instead of nil, so spoofed packets are no longer written to TUN - Simplify install_linux.sh: remove hardcoded subnets and NAT config - Add UFW detection to diag panel
This commit is contained in:
+5
-101
@@ -1,14 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# VPN 子网(与后台 VPN 设置中的子网保持一致)
|
||||
# 若后台修改了子网,需同步修改此处并重新执行脚本,或手动更新 iptables 规则
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
VPN_SUBNET="192.168.77.0/24"
|
||||
# IPv6 子网(留空则不配置 IPv6 NAT;与后台设置保持一致)
|
||||
VPN_SUBNET6="fd00:dead:beef::/112"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "请使用 root 用户执行此脚本: sudo bash install_linux.sh"
|
||||
exit 1
|
||||
@@ -49,106 +41,14 @@ chown -R lmvpn:lmvpn /opt/lmvpn
|
||||
echo ">>> 配置内核 IP 转发..."
|
||||
# 临时生效
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
if [ -n "$VPN_SUBNET6" ]; then
|
||||
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null
|
||||
fi
|
||||
# 持久化
|
||||
cat > /etc/sysctl.d/99-lmvpn.conf << EOF
|
||||
net.ipv4.ip_forward = 1
|
||||
net.ipv6.conf.all.forwarding = 1
|
||||
EOF
|
||||
if [ -n "$VPN_SUBNET6" ]; then
|
||||
echo "net.ipv6.conf.all.forwarding = 1" >> /etc/sysctl.d/99-lmvpn.conf
|
||||
fi
|
||||
sysctl -p /etc/sysctl.d/99-lmvpn.conf >/dev/null
|
||||
|
||||
echo ">>> 配置 NAT 与转发规则..."
|
||||
# 自动检测默认路由出口网卡
|
||||
WAN_IFACE=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
||||
if [ -z "$WAN_IFACE" ]; then
|
||||
echo "警告: 未能自动检测出口网卡,跳过 NAT 配置,请手动设置"
|
||||
else
|
||||
echo "出口网卡: $WAN_IFACE"
|
||||
|
||||
# 选择可用的防火墙前端:优先 nft(原生,Debian 12+ 的 iptables 是 nft 包装器,操作原生 nft nat 表会不兼容)
|
||||
NAT_TOOL=""
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
NAT_TOOL="nft"
|
||||
elif command -v iptables >/dev/null 2>&1; then
|
||||
NAT_TOOL="iptables"
|
||||
fi
|
||||
|
||||
if [ -z "$NAT_TOOL" ]; then
|
||||
echo "警告: 未找到 nft 或 iptables,跳过 NAT 配置"
|
||||
echo " 客户端将无法上外网。请安装: apt install nftables"
|
||||
echo " 安装后重新执行本脚本即可自动配置"
|
||||
elif [ "$NAT_TOOL" = "nft" ]; then
|
||||
echo "使用 nft 配置 NAT..."
|
||||
# 幂等:先删除旧表(忽略错误),再创建
|
||||
nft delete table inet lmvpn_nat 2>/dev/null || true
|
||||
nft add table inet lmvpn_nat
|
||||
nft 'add chain inet lmvpn_nat postrouting { type nat hook postrouting priority 100 ; }'
|
||||
nft add rule inet lmvpn_nat postrouting oifname "$WAN_IFACE" ip saddr "$VPN_SUBNET" masquerade
|
||||
if [ -n "$VPN_SUBNET6" ]; then
|
||||
nft add rule inet lmvpn_nat postrouting oifname "$WAN_IFACE" ip6 saddr "$VPN_SUBNET6" masquerade
|
||||
fi
|
||||
nft 'add chain inet lmvpn_nat forward { type filter hook forward priority 0 ; policy accept ; }'
|
||||
nft add rule inet lmvpn_nat forward ip saddr "$VPN_SUBNET" accept
|
||||
nft add rule inet lmvpn_nat forward ip daddr "$VPN_SUBNET" accept
|
||||
if [ -n "$VPN_SUBNET6" ]; then
|
||||
nft add rule inet lmvpn_nat forward ip6 saddr "$VPN_SUBNET6" accept
|
||||
nft add rule inet lmvpn_nat forward ip6 daddr "$VPN_SUBNET6" accept
|
||||
fi
|
||||
|
||||
# 持久化 nft 规则
|
||||
mkdir -p /etc/nftables.d
|
||||
nft list ruleset > /etc/nftables.d/lmvpn.nft
|
||||
# 确保主配置文件 include 该目录
|
||||
if [ -f /etc/nftables.conf ] && ! grep -q 'include "/etc/nftables.d' /etc/nftables.conf; then
|
||||
echo 'include "/etc/nftables.d/*.nft"' >> /etc/nftables.conf
|
||||
fi
|
||||
systemctl enable nftables 2>/dev/null || true
|
||||
echo "nft 规则已写入 /etc/nftables.d/lmvpn.nft"
|
||||
elif [ "$NAT_TOOL" = "iptables" ]; then
|
||||
echo "使用 iptables 配置 NAT..."
|
||||
# 幂等:先删除旧规则(忽略错误),再添加
|
||||
iptables -t nat -D POSTROUTING -s "$VPN_SUBNET" -o "$WAN_IFACE" -j MASQUERADE 2>/dev/null || true
|
||||
iptables -t nat -A POSTROUTING -s "$VPN_SUBNET" -o "$WAN_IFACE" -j MASQUERADE
|
||||
|
||||
iptables -D FORWARD -s "$VPN_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -d "$VPN_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||
iptables -A FORWARD -s "$VPN_SUBNET" -j ACCEPT
|
||||
iptables -A FORWARD -d "$VPN_SUBNET" -j ACCEPT
|
||||
|
||||
# IPv6 NAT(需 ip6tables)
|
||||
if [ -n "$VPN_SUBNET6" ] && command -v ip6tables >/dev/null 2>&1; then
|
||||
echo "配置 IPv6 NAT (ip6tables)..."
|
||||
ip6tables -t nat -D POSTROUTING -s "$VPN_SUBNET6" -o "$WAN_IFACE" -j MASQUERADE 2>/dev/null || true
|
||||
ip6tables -t nat -A POSTROUTING -s "$VPN_SUBNET6" -o "$WAN_IFACE" -j MASQUERADE
|
||||
ip6tables -D FORWARD -s "$VPN_SUBNET6" -j ACCEPT 2>/dev/null || true
|
||||
ip6tables -D FORWARD -d "$VPN_SUBNET6" -j ACCEPT 2>/dev/null || true
|
||||
ip6tables -A FORWARD -s "$VPN_SUBNET6" -j ACCEPT
|
||||
ip6tables -A FORWARD -d "$VPN_SUBNET6" -j ACCEPT
|
||||
fi
|
||||
|
||||
# 持久化 iptables 规则
|
||||
if command -v netfilter-persistent >/dev/null 2>&1; then
|
||||
netfilter-persistent save
|
||||
echo "iptables 规则已通过 netfilter-persistent 持久化"
|
||||
elif command -v iptables-save >/dev/null 2>&1; then
|
||||
mkdir -p /etc/iptables
|
||||
iptables-save > /etc/iptables/rules.v4
|
||||
if [ -n "$VPN_SUBNET6" ] && command -v ip6tables-save >/dev/null 2>&1; then
|
||||
ip6tables-save > /etc/iptables/rules.v6
|
||||
echo "ip6tables 规则已写入 /etc/iptables/rules.v6"
|
||||
fi
|
||||
echo "iptables 规则已写入 /etc/iptables/rules.v4"
|
||||
echo "注意: 重启后规则持久化需配合 iptables-persistent 包,建议安装: apt install iptables-persistent"
|
||||
else
|
||||
echo "警告: 未找到 iptables-save,规则仅在本次运行有效,请手动持久化"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ">>> 安装 systemd 服务..."
|
||||
cat > /etc/systemd/system/lmvpn.service << 'EOF'
|
||||
[Unit]
|
||||
@@ -176,4 +76,8 @@ systemctl enable lmvpn
|
||||
systemctl restart lmvpn
|
||||
|
||||
echo ">>> 安装完成"
|
||||
echo ""
|
||||
echo "NAT/转发/UFW 规则由服务端程序在启动时根据后台子网配置自动管理,无需手动设置。"
|
||||
echo "若修改了后台 VPN 子网,只需在后台保存即可,程序会自动更新防火墙规则。"
|
||||
echo ""
|
||||
systemctl status lmvpn --no-pager
|
||||
|
||||
@@ -18,6 +18,8 @@ type DiagResult struct {
|
||||
IP6ForwardNote string `json:"ip6_forward_note,omitempty"`
|
||||
Masquerade6 *bool `json:"masquerade6"`
|
||||
Masquerade6Note string `json:"masquerade6_note,omitempty"`
|
||||
UFWActive *bool `json:"ufw_active"`
|
||||
UFWForwardNote string `json:"ufw_forward_note,omitempty"`
|
||||
TUNCreate string `json:"tun_create"`
|
||||
TUNRunning bool `json:"tun_running"`
|
||||
TUNName string `json:"tun_name,omitempty"`
|
||||
|
||||
@@ -34,6 +34,15 @@ func fillPlatformDiag(r *DiagResult) {
|
||||
m6, note6 := checkMasquerade6()
|
||||
r.Masquerade6 = m6
|
||||
r.Masquerade6Note = note6
|
||||
|
||||
ufwActive := checkUFWActive()
|
||||
r.UFWActive = &ufwActive
|
||||
if ufwActive {
|
||||
ufwOk, ufwNote := checkUFWForward()
|
||||
if !ufwOk {
|
||||
r.UFWForwardNote = ufwNote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ptrBool(b bool) *bool { return &b }
|
||||
@@ -163,3 +172,31 @@ func checkMasquerade6() (*bool, string) {
|
||||
|
||||
return nil, "ip6tables 与 nft 均未安装,无法检测 IPv6 NAT 规则"
|
||||
}
|
||||
|
||||
// checkUFWForward checks if VPN forward rules exist in UFW's user-forward chains.
|
||||
func checkUFWForward() (bool, string) {
|
||||
nftPath := findExecutable("nft")
|
||||
if nftPath == "" {
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// Check IPv4
|
||||
out, err := exec.Command(nftPath, "list", "chain", "ip", "filter", "ufw-user-forward").Output()
|
||||
if err == nil {
|
||||
s := string(out)
|
||||
if !strings.Contains(s, "jump lmvpn-fwd") && !strings.Contains(s, "192.168.") {
|
||||
return false, "UFW 已启用但 IPv4 转发规则未配置,客户端可能无法上网"
|
||||
}
|
||||
}
|
||||
|
||||
// Check IPv6
|
||||
out6, err := exec.Command(nftPath, "list", "chain", "ip6", "filter", "ufw6-user-forward").Output()
|
||||
if err == nil {
|
||||
s := string(out6)
|
||||
if !strings.Contains(s, "jump lmvpn6-fwd") && !strings.Contains(s, "fd00:") {
|
||||
return false, "UFW 已启用但 IPv6 转发规则未配置,IPv6 客户端可能无法上网"
|
||||
}
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build darwin
|
||||
|
||||
package vpn
|
||||
|
||||
import "log"
|
||||
|
||||
func configureFirewall(ipNet, ipNet6 string, tunName string) {
|
||||
log.Printf("防火墙配置在当前平台不支持,请手动配置 NAT 和转发规则")
|
||||
}
|
||||
|
||||
func checkUFWActive() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//go:build linux
|
||||
|
||||
package vpn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// configureFirewall dynamically configures NAT masquerade, forward accept rules,
|
||||
// and UFW forward rules based on the current VPN subnets.
|
||||
// This is called from ApplySettings() so subnet changes in the backend
|
||||
// are automatically reflected in firewall rules.
|
||||
func configureFirewall(ipNet, ipNet6 string, tunName string) {
|
||||
wanIface := detectWANInterface()
|
||||
if wanIface == "" {
|
||||
log.Printf("警告: 未能检测出口网卡,跳过防火墙配置")
|
||||
return
|
||||
}
|
||||
log.Printf("出口网卡: %s", wanIface)
|
||||
|
||||
configureNAT(wanIface, ipNet, ipNet6)
|
||||
configureForward(ipNet, ipNet6)
|
||||
configureUFWForward(ipNet, ipNet6)
|
||||
}
|
||||
|
||||
func detectWANInterface() string {
|
||||
out, err := exec.Command("ip", "route", "show", "default").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
fields := strings.Fields(string(out))
|
||||
for i, f := range fields {
|
||||
if f == "dev" && i+1 < len(fields) {
|
||||
return fields[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func nftExists(args ...string) bool {
|
||||
return exec.Command("nft", args...).Run() == nil
|
||||
}
|
||||
|
||||
func nftExec(args ...string) {
|
||||
cmd := exec.Command("nft", args...)
|
||||
cmd.Stderr = nil
|
||||
_ = cmd.Run()
|
||||
}
|
||||
|
||||
func configureNAT(wanIface, ipNet, ipNet6 string) {
|
||||
if !nftExists("list", "table", "inet", "lmvpn_nat") {
|
||||
nftExec("add", "table", "inet", "lmvpn_nat")
|
||||
}
|
||||
|
||||
// postrouting chain
|
||||
if !nftExists("list", "chain", "inet", "lmvpn_nat", "postrouting") {
|
||||
nftExec("add", "chain", "inet", "lmvpn_nat", "postrouting",
|
||||
"{ type nat hook postrouting priority 100 ; }")
|
||||
}
|
||||
nftExec("flush", "chain", "inet", "lmvpn_nat", "postrouting")
|
||||
if ipNet != "" {
|
||||
nftExec("add", "rule", "inet", "lmvpn_nat", "postrouting",
|
||||
"oifname", wanIface, "ip", "saddr", ipNet, "masquerade")
|
||||
}
|
||||
if ipNet6 != "" {
|
||||
nftExec("add", "rule", "inet", "lmvpn_nat", "postrouting",
|
||||
"oifname", wanIface, "ip6", "saddr", ipNet6, "masquerade")
|
||||
}
|
||||
log.Printf("NAT masquerade 已配置: wan=%s v4=%s", wanIface, ipNet)
|
||||
if ipNet6 != "" {
|
||||
log.Printf("NAT masquerade IPv6: %s", ipNet6)
|
||||
}
|
||||
}
|
||||
|
||||
func configureForward(ipNet, ipNet6 string) {
|
||||
if !nftExists("list", "chain", "inet", "lmvpn_nat", "forward") {
|
||||
nftExec("add", "chain", "inet", "lmvpn_nat", "forward",
|
||||
"{ type filter hook forward priority 0 ; policy accept ; }")
|
||||
}
|
||||
nftExec("flush", "chain", "inet", "lmvpn_nat", "forward")
|
||||
if ipNet != "" {
|
||||
nftExec("add", "rule", "inet", "lmvpn_nat", "forward",
|
||||
"ip", "saddr", ipNet, "accept")
|
||||
nftExec("add", "rule", "inet", "lmvpn_nat", "forward",
|
||||
"ip", "daddr", ipNet, "accept")
|
||||
}
|
||||
if ipNet6 != "" {
|
||||
nftExec("add", "rule", "inet", "lmvpn_nat", "forward",
|
||||
"ip6", "saddr", ipNet6, "accept")
|
||||
nftExec("add", "rule", "inet", "lmvpn_nat", "forward",
|
||||
"ip6", "daddr", ipNet6, "accept")
|
||||
}
|
||||
}
|
||||
|
||||
// configureUFWForward adds VPN subnet accept rules to UFW's user-forward chains.
|
||||
// UFW's FORWARD chain has policy DROP by default, which overrides the lmvpn_nat
|
||||
// forward chain's accept rules. We use dedicated chains (lmvpn-fwd/lmvpn6-fwd)
|
||||
// that are jumped to from ufw-user-forward/ufw6-user-forward.
|
||||
func configureUFWForward(ipNet, ipNet6 string) {
|
||||
// IPv4
|
||||
if nftExists("list", "chain", "ip", "filter", "ufw-user-forward") {
|
||||
if !nftExists("list", "chain", "ip", "filter", "lmvpn-fwd") {
|
||||
nftExec("add", "chain", "ip", "filter", "lmvpn-fwd")
|
||||
}
|
||||
nftExec("flush", "chain", "ip", "filter", "lmvpn-fwd")
|
||||
if ipNet != "" {
|
||||
nftExec("add", "rule", "ip", "filter", "lmvpn-fwd",
|
||||
"ip", "saddr", ipNet, "accept")
|
||||
nftExec("add", "rule", "ip", "filter", "lmvpn-fwd",
|
||||
"ip", "daddr", ipNet, "accept")
|
||||
}
|
||||
// Add jump if not already present
|
||||
if !nftJumpExists("ip", "filter", "ufw-user-forward", "lmvpn-fwd") {
|
||||
nftExec("add", "rule", "ip", "filter", "ufw-user-forward", "jump", "lmvpn-fwd")
|
||||
}
|
||||
log.Printf("UFW IPv4 转发规则已配置")
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if ipNet6 != "" && nftExists("list", "chain", "ip6", "filter", "ufw6-user-forward") {
|
||||
if !nftExists("list", "chain", "ip6", "filter", "lmvpn6-fwd") {
|
||||
nftExec("add", "chain", "ip6", "filter", "lmvpn6-fwd")
|
||||
}
|
||||
nftExec("flush", "chain", "ip6", "filter", "lmvpn6-fwd")
|
||||
nftExec("add", "rule", "ip6", "filter", "lmvpn6-fwd",
|
||||
"ip6", "saddr", ipNet6, "accept")
|
||||
nftExec("add", "rule", "ip6", "filter", "lmvpn6-fwd",
|
||||
"ip6", "daddr", ipNet6, "accept")
|
||||
if !nftJumpExists("ip6", "filter", "ufw6-user-forward", "lmvpn6-fwd") {
|
||||
nftExec("add", "rule", "ip6", "filter", "ufw6-user-forward", "jump", "lmvpn6-fwd")
|
||||
}
|
||||
log.Printf("UFW IPv6 转发规则已配置")
|
||||
}
|
||||
}
|
||||
|
||||
// nftJumpExists checks if a jump rule to the target chain already exists
|
||||
// in the source chain.
|
||||
func nftJumpExists(family, table, chain, target string) bool {
|
||||
out, err := exec.Command("nft", "list", "chain", family, table, chain).Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(out), fmt.Sprintf("jump %s", target))
|
||||
}
|
||||
|
||||
// checkUFWActive checks if UFW is active by looking for its forward chain.
|
||||
func checkUFWActive() bool {
|
||||
return nftExists("list", "chain", "ip", "filter", "ufw-user-forward")
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package vpn
|
||||
|
||||
import "log"
|
||||
|
||||
func configureFirewall(ipNet, ipNet6 string, tunName string) {
|
||||
log.Printf("防火墙配置在当前平台不支持,请手动配置 NAT 和转发规则")
|
||||
}
|
||||
|
||||
func checkUFWActive() bool {
|
||||
return false
|
||||
}
|
||||
@@ -155,6 +155,14 @@ func (s *VpnService) ApplySettings(settings model.VpnSetting, reservations4, res
|
||||
s.mu.Unlock()
|
||||
|
||||
go s.serveTUN()
|
||||
|
||||
subnet4 := ipNet.String()
|
||||
var subnet6Str string
|
||||
if ipNet6 != nil {
|
||||
subnet6Str = ipNet6.String()
|
||||
}
|
||||
configureFirewall(subnet4, subnet6Str, tun.Name())
|
||||
|
||||
log.Printf("VPN 服务已启动: tun=%s subnet=%s server=%s", tun.Name(), ipNet.String(), serverIP.String())
|
||||
if ipNet6 != nil {
|
||||
log.Printf(" IPv6: subnet=%s server=%s", ipNet6.String(), serverIP6.String())
|
||||
|
||||
+17
-4
@@ -106,6 +106,16 @@ func parseIPAddrs(packet []byte) (src, dest net.IP, ok bool) {
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// dropPacket is a sentinel non-nil, non-empty slice that signals the caller
|
||||
// to silently drop the packet (do not write to TUN, do not relay).
|
||||
var dropPacket = []SwitchConn{nil}
|
||||
|
||||
// isDrop returns true if the result from RouteFromClient indicates the packet
|
||||
// should be silently dropped rather than forwarded to TUN or relayed.
|
||||
func isDrop(targets []SwitchConn) bool {
|
||||
return len(targets) == 1 && targets[0] == nil
|
||||
}
|
||||
|
||||
func (s *PacketSwitch) allowC2C() bool {
|
||||
s.mu.RLock()
|
||||
v := s.allowClientToClient
|
||||
@@ -113,21 +123,24 @@ func (s *PacketSwitch) allowC2C() bool {
|
||||
return v
|
||||
}
|
||||
|
||||
// RouteFromClient returns the list of target connections to relay the packet to.
|
||||
// Returns nil to indicate the packet should be written to TUN (internet-bound).
|
||||
// Returns dropPacket to indicate the packet should be silently dropped (e.g. anti-spoof).
|
||||
func (s *PacketSwitch) RouteFromClient(src SwitchConn, packet []byte) []SwitchConn {
|
||||
srcIP, dest, ok := parseIPAddrs(packet)
|
||||
if !ok {
|
||||
return nil
|
||||
return dropPacket
|
||||
}
|
||||
// anti-spoof: enforce assigned source IP by version
|
||||
if srcIP != nil {
|
||||
if srcIP.To4() != nil {
|
||||
if !srcIP.Equal(src.AssignedIP()) {
|
||||
return nil
|
||||
return dropPacket
|
||||
}
|
||||
} else {
|
||||
assigned6 := src.AssignedIP6()
|
||||
if assigned6 == nil || !srcIP.Equal(assigned6) {
|
||||
return nil
|
||||
return dropPacket
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +153,7 @@ func (s *PacketSwitch) RouteFromClient(src SwitchConn, packet []byte) []SwitchCo
|
||||
if s.allowC2C() {
|
||||
return s.allExcept(src)
|
||||
}
|
||||
return nil
|
||||
return dropPacket
|
||||
}
|
||||
|
||||
func (s *PacketSwitch) RouteFromTUN(packet []byte) []SwitchConn {
|
||||
|
||||
@@ -217,6 +217,9 @@ func runTunnel(conn *websocket.Conn, user *model.User) {
|
||||
tc.rxBytes.Add(int64(len(data)))
|
||||
|
||||
targets := VPN.RouteFromClient(tc, data)
|
||||
if isDrop(targets) {
|
||||
continue
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
if err := VPN.WriteToTUN(data); err != nil {
|
||||
log.Printf("用户 %s 写入 TUN 失败: %v", user.Username, err)
|
||||
|
||||
Reference in New Issue
Block a user