fix: deadlock when wg open port

This commit is contained in:
VaalaCat
2025-11-11 17:17:32 +00:00
parent ecdb1612fc
commit a29a989147
11 changed files with 120 additions and 28 deletions

View File

@@ -39,9 +39,13 @@ func PullWireGuards(appInstance app.Application, clientID, clientSecret string)
wgCfg := &defs.WireGuardConfig{WireGuardConfig: wireGuard}
wgSvc, ok := wgMgr.GetService(wireGuard.GetInterfaceName())
if ok {
log.Debugf("wireguard [%s] already exists, skip create, update peers if need", wireGuard.GetInterfaceName())
wgSvc.PatchPeers(wgCfg.GetParsedPeers())
continue
if wgSvc.NeedRecreate(wgCfg) {
wgMgr.RemoveService(wireGuard.GetInterfaceName())
} else {
log.Debugf("wireguard [%s] already exists, skip create, update peers if need", wireGuard.GetInterfaceName())
wgSvc.PatchPeers(wgCfg.GetParsedPeers())
continue
}
}
wgSvc, err := wgMgr.CreateService(&defs.WireGuardConfig{WireGuardConfig: wireGuard})

View File

@@ -81,7 +81,8 @@ func (w *WireGuardPeerConfig) GetParsedPresharedKey() *wgtypes.Key {
func (w *WireGuardPeerConfig) Equal(other *WireGuardPeerConfig) bool {
endpointEqual := false
if w.Endpoint != nil && other.Endpoint != nil {
endpointEqual = (w.Endpoint.Host == other.Endpoint.Host && w.Endpoint.Port == other.Endpoint.Port)
endpointEqual = (w.Endpoint.Host == other.Endpoint.Host && w.Endpoint.Port == other.Endpoint.Port &&
w.Endpoint.Uri == other.Endpoint.Uri && w.Endpoint.Type == other.Endpoint.Type)
} else if w.Endpoint == nil && other.Endpoint == nil {
endpointEqual = true
}

2
go.mod
View File

@@ -49,6 +49,7 @@ require (
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c
k8s.io/apimachinery v0.28.8
)
@@ -155,7 +156,6 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlserver v1.5.3 // indirect
gorm.io/plugin/dbresolver v1.5.2 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
modernc.org/libc v1.38.0 // indirect
modernc.org/mathutil v1.7.1 // indirect

View File

@@ -6,6 +6,7 @@ ARCH=$(uname -m)
# --- Argument Parsing ---
custom_proxy=""
frp_panel_args=() # Use an array to hold arguments for frp-panel
version="latest"
# Parse arguments
while [[ "$#" -gt 0 ]]; do
@@ -19,6 +20,11 @@ while [[ "$#" -gt 0 ]]; do
echo "使用自定义的 GitHub 镜像: $custom_proxy"
shift 2 # shift past argument name and value
;;
--version)
version="$2"
echo "使用自定义的版本: $version"
shift 2 # shift past argument name and value
;;
*)
# Collect remaining arguments for frp-panel
frp_panel_args+=("$1") # Add the argument to the array
@@ -54,16 +60,16 @@ case "$OS" in
Linux)
case "$ARCH" in
x86_64)
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/latest/download/frp-panel-linux-amd64"
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/download/${version}/frp-panel-linux-amd64"
;;
aarch64)
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/latest/download/frp-panel-linux-arm64"
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/download/${version}/frp-panel-linux-arm64"
;;
armv7l)
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/latest/download/frp-panel-linux-armv7l"
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/download/${version}/frp-panel-linux-armv7l"
;;
armv6l)
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/latest/download/frp-panel-linux-armv6l"
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/download/${version}/frp-panel-linux-armv6l"
;;
*)
echo "Unsupported Linux architecture: $ARCH"
@@ -74,10 +80,10 @@ case "$OS" in
Darwin)
case "$ARCH" in
x86_64)
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/latest/download/frp-panel-darwin-amd64"
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/download/${version}/frp-panel-darwin-amd64"
;;
arm64)
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/latest/download/frp-panel-darwin-arm64"
wget -O frp-panel "${prefix}https://github.com/VaalaCat/frp-panel/releases/download/${version}/frp-panel-darwin-arm64"
;;
*)
echo "Unsupported Darwin architecture: $ARCH"

View File

@@ -37,6 +37,7 @@ type WireGuardEntity struct {
Tags GormArray[string] `json:"tags" gorm:"type:varchar(255)"`
WsListenPort uint32 `json:"ws_listen_port" gorm:"uniqueIndex:idx_client_id_ws_listen_port"`
UseGvisorNet bool `json:"use_gvisor_net"`
}
func (*WireGuard) TableName() string {
@@ -117,7 +118,7 @@ func (w *WireGuard) FromPB(pb *pb.WireGuardConfig) {
w.NetworkID = uint(pb.GetNetworkId())
w.Tags = GormArray[string](pb.GetTags())
w.WsListenPort = pb.GetWsListenPort()
w.UseGvisorNet = pb.GetUseGvisorNet()
w.AdvertisedEndpoints = make([]*Endpoint, 0, len(pb.GetAdvertisedEndpoints()))
for _, e := range pb.GetAdvertisedEndpoints() {
endpointModel := &Endpoint{}
@@ -144,6 +145,7 @@ func (w *WireGuard) ToPB() *pb.WireGuardConfig {
return e.ToPB()
}),
WsListenPort: w.ListenPort,
UseGvisorNet: w.UseGvisorNet,
}
}

View File

@@ -247,6 +247,7 @@ type WireGuard interface {
// Interface相关
GetIfceConfig() (*defs.WireGuardConfig, error)
GetBaseIfceConfig() *defs.WireGuardConfig
NeedRecreate(newCfg *defs.WireGuardConfig) bool
// Config相关
GenWGConfig() (string, error) // unimplemented

View File

@@ -132,11 +132,17 @@ func generateUAPIConfigString(cfg *defs.WireGuardConfig,
wgPrivateKey wgtypes.Key,
peerConfigs []*defs.WireGuardPeerConfig,
firstStart bool,
skipListenPort bool,
) string {
uapiBuilder := NewUAPIBuilder()
uapiBuilder.WithPrivateKey(wgPrivateKey).
WithListenPort(int(cfg.ListenPort)).
ReplacePeers(!firstStart).
uapiBuilder.WithPrivateKey(wgPrivateKey)
// 只在首次启动且未跳过时设置 listen_port,避免在设备运行时更新端口导致死锁
if firstStart && !skipListenPort {
uapiBuilder.WithListenPort(int(cfg.ListenPort))
}
uapiBuilder.ReplacePeers(!firstStart).
AddPeers(peerConfigs)
return uapiBuilder.Build()

View File

@@ -3,6 +3,7 @@ package multibind
import (
"errors"
"fmt"
"runtime/debug"
"sync/atomic"
"github.com/sirupsen/logrus"
@@ -64,7 +65,10 @@ func (m *MultiBind) Close() error {
// Open implements conn.Bind.
func (m *MultiBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
if m.opened.Load() {
return nil, 0, conn.ErrBindAlreadyOpen
m.svcLogger.Debugf("multibind already opened, closing and reopening for port %d", port)
if closeErr := m.Close(); closeErr != nil {
m.svcLogger.WithError(closeErr).Warnf("failed to close multibind before reopening")
}
}
m.opened.Store(true)
@@ -77,7 +81,7 @@ func (m *MultiBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16
return nil, 0, err
}
if p != 0 {
if p != 0 && t.name == "udp" {
actualPort = p
}
@@ -146,6 +150,17 @@ func (m *MultiBind) SetMark(mark uint32) error {
func (m *MultiBind) recvWrapper(trans *Transport, fns conn.ReceiveFunc) conn.ReceiveFunc {
return func(packets [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) {
defer func() {
if panicRecover := recover(); panicRecover != nil {
err = fmt.Errorf("multibind recvWrapper panic: %v, debugStack: %s", panicRecover, debug.Stack())
m.svcLogger.WithError(err).Error("multibind recvWrapper panic")
}
}()
defer func() {
if err != nil {
m.svcLogger.WithError(err).Error("multibind recvWrapper error")
}
}()
log := m.svcLogger.WithField("op", "recvWrapper").WithField("transport", trans.name)
tmpEps := make([]conn.Endpoint, len(eps))

View File

@@ -61,7 +61,10 @@ func (w *WSBind) Close() error {
// Open implements conn.Bind.
func (w *WSBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
if w.opened.Load() {
return nil, 0, conn.ErrBindAlreadyOpen
w.ctx.Logger().Debugf("ws bind already opened, closing and reopening for port %d", port)
if closeErr := w.Close(); closeErr != nil {
w.ctx.Logger().WithError(closeErr).Warnf("failed to close ws bind before reopening")
}
}
w.opened.Store(true)
w.registerChan = make(chan *serverIncoming, defaultRegisterChanSize)

View File

@@ -3,6 +3,7 @@ package wg
import (
"encoding/hex"
"fmt"
"net"
"strings"
"github.com/VaalaCat/frp-panel/defs"
@@ -158,7 +159,12 @@ func normalizeEndpoint(ep *pb.Endpoint) string {
return ep.GetUri()
}
return fmt.Sprintf("%s:%d", ep.GetHost(), ep.GetPort())
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ep.GetHost(), ep.GetPort()))
if err != nil {
return ""
}
return addr.String()
}
func isZeroKey(key wgtypes.Key) bool {

View File

@@ -115,16 +115,17 @@ func (w *wireGuard) Start() error {
return errors.Join(fmt.Errorf("apply peer config failed"), err)
}
// 在应用配置后再启动设备
if err := w.wgDevice.Up(); err != nil {
return errors.Join(fmt.Errorf("wgDevice.Up '%s'", w.ifce.GetInterfaceName()), err)
}
if !w.useGvisorNet {
if err := w.initNetwork(); err != nil {
return errors.Join(errors.New("init network failed"), err)
}
}
if err := w.wgDevice.Up(); err != nil {
return errors.Join(fmt.Errorf("wgDevice.Up '%s'", w.ifce.GetInterfaceName()), err)
}
// 在 WireGuard 设备启动后配置 gvisor
if w.useGvisorNet {
if err := w.initGvisorNetwork(); err != nil {
@@ -325,11 +326,31 @@ func (w *wireGuard) GetWGRuntimeInfo() (*pb.WGDeviceRuntimeInfo, error) {
}
runtimeInfo.PingMap = w.pingMap.Export()
runtimeInfo.InterfaceName = w.ifce.GetInterfaceName()
if w.useGvisorNet {
runtimeInfo.InterfaceName = w.ifce.GetInterfaceName()
} else {
link, err := netlink.LinkByName(w.ifce.GetInterfaceName())
if err != nil {
return nil, errors.Join(fmt.Errorf("get iface '%s' via netlink", w.ifce.GetInterfaceName()), err)
}
runtimeInfo.InterfaceName = link.Attrs().Name
}
return runtimeInfo, nil
}
func (w *wireGuard) NeedRecreate(newCfg *defs.WireGuardConfig) bool {
return w.ifce.GetId() != newCfg.GetId() ||
w.ifce.GetInterfaceName() != newCfg.GetInterfaceName() ||
w.ifce.GetPrivateKey() != newCfg.GetPrivateKey() ||
w.ifce.GetLocalAddress() != newCfg.GetLocalAddress() ||
w.ifce.GetListenPort() != newCfg.GetListenPort() ||
w.ifce.GetWsListenPort() != newCfg.GetWsListenPort() ||
w.ifce.GetInterfaceMtu() != newCfg.GetInterfaceMtu() ||
w.ifce.GetUseGvisorNet() != newCfg.GetUseGvisorNet() ||
w.ifce.GetNetworkId() != newCfg.GetNetworkId()
}
func (w *wireGuard) initTransports() error {
log := w.svcLogger.WithField("op", "initTransports")
@@ -402,6 +423,15 @@ func (w *wireGuard) initWGDevice() error {
}
log.Debugf("TUN device '%s' (MTU %d) created successfully", w.ifce.GetInterfaceName(), w.ifce.GetInterfaceMtu())
// 在创建 WireGuard 设备之前,先打开 bind 并使用正确的端口,避免后续更新端口导致死锁
log.Debugf("opening multibind with port %d before creating WireGuard device", w.ifce.GetListenPort())
_, _, err = w.multiBind.Open(uint16(w.ifce.GetListenPort()))
if err != nil {
return errors.Join(fmt.Errorf("open multibind with port %d failed", w.ifce.GetListenPort()), err)
}
log.Debugf("multibind opened successfully with port %d", w.ifce.GetListenPort())
log.Debugf("start to create WireGuard device '%s'", w.ifce.GetInterfaceName())
w.wgDevice = device.NewDevice(w.tunDevice, w.multiBind, &device.Logger{
@@ -430,13 +460,17 @@ func (w *wireGuard) applyPeerConfig() error {
log.Debugf("wgTypedPeerConfigs: %v", wgTypedPeerConfigs)
uapiConfigString := generateUAPIConfigString(w.ifce, w.ifce.GetParsedPrivKey(), wgTypedPeerConfigs, !w.running)
// 跳过 listen_port 设置,因为我们已经在 initWGDevice 中通过 bind.Open() 设置了端口
// 在运行时通过 UAPI 更新 listen_port 会导致死锁
uapiConfigString := generateUAPIConfigString(w.ifce, w.ifce.GetParsedPrivKey(), wgTypedPeerConfigs, !w.running, true)
log.Debugf("uapiBuilder: %s", uapiConfigString)
log.Debugf("calling IpcSet...")
if err = w.wgDevice.IpcSet(uapiConfigString); err != nil {
return errors.Join(errors.New("IpcSet error"), err)
}
log.Debugf("IpcSet completed successfully")
return nil
}
@@ -444,10 +478,24 @@ func (w *wireGuard) applyPeerConfig() error {
func (w *wireGuard) initNetwork() error {
log := w.svcLogger.WithField("op", "initNetwork")
link, err := netlink.LinkByName(w.ifce.GetInterfaceName())
if err != nil {
return errors.Join(fmt.Errorf("get iface '%s' via netlink", w.ifce.GetInterfaceName()), err)
// 等待 TUN 设备在内核中完全注册,避免竞态条件
var link netlink.Link
var err error
maxRetries := 10
for i := 0; i < maxRetries; i++ {
link, err = netlink.LinkByName(w.ifce.GetInterfaceName())
if err == nil {
break
}
if i < maxRetries-1 {
log.Debugf("attempt %d: waiting for iface '%s' to be ready, will retry...", i+1, w.ifce.GetInterfaceName())
time.Sleep(100 * time.Millisecond)
}
}
if err != nil {
return errors.Join(fmt.Errorf("get iface '%s' via netlink after %d retries", w.ifce.GetInterfaceName(), maxRetries), err)
}
log.Debugf("successfully found interface '%s' via netlink", w.ifce.GetInterfaceName())
addr, err := netlink.ParseAddr(w.ifce.GetLocalAddress())
if err != nil {