From 0f3d884786f9ec73d3be8899cb6b9e1e6dd3c41e Mon Sep 17 00:00:00 2001 From: T3ST3ST3R0N Date: Fri, 16 Oct 2026 13:20:47 +0330 Subject: [PATCH] WireGuard outbound: Keep the device when its setup fails (#7151) https://github.com/XTLS/Xray-core/pull/7151#issuecomment-6186150131 Fixes https://github.com/XTLS/Xray-core/pull/7151#issuecomment-6190189547 --------- Co-authored-by: LjhAUMEM --- proxy/wireguard/bind.go | 25 ++- proxy/wireguard/client.go | 314 ++++++++++++++++++++------------------ proxy/wireguard/server.go | 2 +- 3 files changed, 177 insertions(+), 164 deletions(-) diff --git a/proxy/wireguard/bind.go b/proxy/wireguard/bind.go index fda540d4d..ab2596daa 100644 --- a/proxy/wireguard/bind.go +++ b/proxy/wireguard/bind.go @@ -16,8 +16,9 @@ import ( ) type bind struct { + ctx context.Context resolveFunc func(host string) (net.IP, error) - listenFunc func() (net.PacketConn, error) + listenFunc func(ctx context.Context) (net.PacketConn, error) downFunc func() error reserved []byte @@ -26,6 +27,12 @@ type bind struct { mu sync.Mutex } +func (b *bind) SetCtx(ctx context.Context) { + b.mu.Lock() + defer b.mu.Unlock() + b.ctx = ctx +} + func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) { b.mu.Lock() defer b.mu.Unlock() @@ -34,7 +41,7 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err return nil, 0, conn.ErrBindAlreadyOpen } - c, err := b.listenFunc() + c, err := b.listenFunc(b.ctx) if err != nil { return nil, 0, err } @@ -52,12 +59,9 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err case <-ch: default: errors.LogErrorInner(context.Background(), err, "unexpected closed") - b.mu.Lock() - downFunc := b.downFunc - b.mu.Unlock() - if downFunc != nil { + if b.downFunc != nil { go func() { - common.Must(downFunc()) + common.Must(b.downFunc()) }() } } @@ -79,13 +83,6 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err }, uint16(c.LocalAddr().(*net.UDPAddr).Port), nil } -// setDownFunc sets downFunc after the device is created, since the device may already be using the bind. -func (b *bind) setDownFunc(f func() error) { - b.mu.Lock() - defer b.mu.Unlock() - b.downFunc = f -} - func (b *bind) Close() error { b.mu.Lock() defer b.mu.Unlock() diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index 862fb4e99..506ec4e92 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -7,6 +7,7 @@ import ( "reflect" "strings" "sync" + "sync/atomic" "time" "golang.zx2c4.com/wireguard/tun" @@ -31,18 +32,14 @@ import ( ) type Handler struct { - conf *DeviceConfig - policyManager policy.Manager - dns dns.Client + p policy.Manager - streamSettings *internet.MemoryStreamConfig - uplinkCounter stats.Counter - downlinkCounter stats.Counter - - tun tun.Device - tnet *Net - dev *device.Device - mu sync.Mutex + canup *atomic.Bool + tnet *Net + bind *bind + dev *device.Device + ipc string + mu sync.Mutex } func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { @@ -124,17 +121,86 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { return nil, err } + blockup := newblockup(tun) + bind := &bind{} + logger := &device.Logger{ + Verbosef: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Debug, + Content: fmt.Sprintf(format, args...), + }) + }, + Errorf: func(format string, args ...any) { + log.Record(&log.GeneralMessage{ + Severity: log.Severity_Error, + Content: fmt.Sprintf(format, args...), + }) + }, + } + dev := device.NewDevice(blockup, bind, logger) + bind.ctx = context.Background() + bind.resolveFunc = MakeResolveLocal(d, streamSettings) + bind.listenFunc = func(ctx context.Context) (net.PacketConn, error) { + dest, err := net.ParseDestination("udp:" + conf.Peers[0].Endpoint) + if err != nil { + return nil, err + } + var pktConn net.PacketConn + if streamSettings.FinalMask != nil { + conn, err := streamSettings.FinalMask.DialUDP(ctx, dest) + if err != nil { + return nil, errors.New("failed to dial to dest").Base(err) + } + pktConn = conn.(*net.PacketConnWrapper).PacketConn + } else { + conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings) + if err != nil { + return nil, errors.New("failed to dial to dest").Base(err) + } + switch c := conn.(type) { + case *net.PacketConnWrapper: + pktConn = c.PacketConn + case *cnc.Connection: + pktConn = &internet.FakePacketConn{Conn: c} + default: + panic(reflect.TypeOf(c)) + } + } + if uplinkCounter != nil || downlinkCounter != nil { + pktConn = &PacketCounterConnection{ + PacketConn: pktConn, + ReadCounter: downlinkCounter, + WriteCounter: uplinkCounter, + } + } + return pktConn, nil + } + bind.downFunc = dev.Down + bind.reserved = conf.Reserved + var cfg strings.Builder + cfg.WriteString("private_key=" + conf.SecretKey + "\n") + for _, peer := range conf.Peers { + cfg.WriteString("public_key=" + peer.PublicKey + "\n") + if peer.PreSharedKey != "" { + cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n") + } + cfg.WriteString("endpoint=" + peer.Endpoint + "\n") + for _, ip := range peer.AllowedIps { + cfg.WriteString("allowed_ip=" + ip + "\n") + } + if peer.KeepAlive != "" { + cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n") + } + } + return &Handler{ - conf: conf, - policyManager: p, - dns: d, + p: p, - streamSettings: streamSettings, - uplinkCounter: uplinkCounter, - downlinkCounter: downlinkCounter, - - tun: tun, - tnet: tnet, + canup: blockup.canup, + tnet: tnet, + bind: bind, + dev: dev, + ipc: cfg.String(), }, nil } @@ -149,7 +215,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte ob.CanSpliceCopy = 3 dialer.SetOutboundGateway(ctx, ob) - if err := h.init(ctx); err != nil { + if err := h.up(ctx); err != nil { return err } @@ -159,7 +225,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte newCtx, newCancel = context.WithCancel(context.Background()) } - sessionPolicy := h.policyManager.ForLevel(0) + sessionPolicy := h.p.ForLevel(0) ctx, cancel := context.WithCancel(ctx) timer := signal.CancelAfterInactivity(ctx, func() { cancel() @@ -234,150 +300,72 @@ func (h *Handler) Close() (err error) { if h.dev != nil { h.dev.Close() h.dev = nil - h.tun = nil - } else if h.tun != nil { - h.tun.Close() - h.tun = nil } return nil } -func (h *Handler) init(ctx context.Context) error { +func (h *Handler) up(ctx context.Context) error { h.mu.Lock() defer h.mu.Unlock() - if h.tun == nil { + if h.dev == nil { return errors.New("closed") } - if h.dev != nil { - return h.dev.Up() + h.bind.SetCtx(ctx) + if !h.canup.Load() { + h.canup.Store(true) } - resolveFunc := h.resolveLocal - listenFunc := func() (net.PacketConn, error) { - dest, err := net.ParseDestination("udp:" + h.conf.Peers[0].Endpoint) + if len(h.ipc) > 0 { + if err := h.dev.IpcSet(h.ipc); err != nil { + return err + } + h.ipc = "" + } + return h.dev.Up() +} + +func MakeResolveLocal(d dns.Client, streamSettings *internet.MemoryStreamConfig) func(host string) (net.IP, error) { + return func(host string) (net.IP, error) { + if ip := net.ParseIP(host); ip != nil { + return ip, nil + } + ips, _, err := d.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) if err != nil { return nil, err } - var pktConn net.PacketConn - if h.streamSettings.FinalMask != nil { - conn, err := h.streamSettings.FinalMask.DialUDP(ctx, dest) - if err != nil { - return nil, errors.New("failed to dial to dest").Base(err) + got := ips + if streamSettings.SocketSettings != nil { + var got4, got6 []net.IP + for _, ip := range ips { + if ip.To4() != nil { + got4 = append(got4, ip) + } else { + got6 = append(got6, ip) + } } - pktConn = conn.(*net.PacketConnWrapper).PacketConn - } else { - conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings) - if err != nil { - return nil, errors.New("failed to dial to dest").Base(err) - } - switch c := conn.(type) { - case *net.PacketConnWrapper: - pktConn = c.PacketConn - case *cnc.Connection: - pktConn = &internet.FakePacketConn{Conn: c} - default: - panic(reflect.TypeOf(c)) - } - } - if h.uplinkCounter != nil || h.downlinkCounter != nil { - pktConn = &PacketCounterConnection{ - PacketConn: pktConn, - ReadCounter: h.downlinkCounter, - WriteCounter: h.uplinkCounter, - } - } - return pktConn, nil - } - // device.NewDevice may use the bind right away (Up -> BindUpdate -> Open), - // so everything it reads must be set before creating the device. - bind := &bind{ - resolveFunc: resolveFunc, - listenFunc: listenFunc, - reserved: h.conf.Reserved, - } - logger := &device.Logger{ - Verbosef: func(format string, args ...any) { - log.Record(&log.GeneralMessage{ - Severity: log.Severity_Debug, - Content: fmt.Sprintf(format, args...), - }) - }, - Errorf: func(format string, args ...any) { - log.Record(&log.GeneralMessage{ - Severity: log.Severity_Error, - Content: fmt.Sprintf(format, args...), - }) - }, - } - dev := device.NewDevice(h.tun, bind, logger) - bind.setDownFunc(dev.Down) - var cfg strings.Builder - cfg.WriteString("private_key=" + h.conf.SecretKey + "\n") - for _, peer := range h.conf.Peers { - cfg.WriteString("public_key=" + peer.PublicKey + "\n") - if peer.PreSharedKey != "" { - cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n") - } - cfg.WriteString("endpoint=" + peer.Endpoint + "\n") - for _, ip := range peer.AllowedIps { - cfg.WriteString("allowed_ip=" + ip + "\n") - } - if peer.KeepAlive != "" { - cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n") - } - } - err := dev.IpcSet(cfg.String()) - if err != nil { - return err - } - err = dev.Up() - if err != nil { - return err - } - h.dev = dev - return nil -} - -func (h *Handler) resolveLocal(host string) (net.IP, error) { - if ip := net.ParseIP(host); ip != nil { - return ip, nil - } - ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) - if err != nil { - return nil, err - } - got := ips - if h.streamSettings.SocketSettings != nil { - var got4, got6 []net.IP - for _, ip := range ips { - if ip.To4() != nil { - got4 = append(got4, ip) - } else { - got6 = append(got6, ip) - } - } - switch h.streamSettings.SocketSettings.DomainStrategy { - case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP: - got = ips - case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4: - got = got4 - case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6: - got = got6 - case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46: - got = got4 - if len(got) == 0 { - got = got6 - } - case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64: - got = got6 - if len(got) == 0 { + switch streamSettings.SocketSettings.DomainStrategy { + case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP: + got = ips + case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4: got = got4 + case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6: + got = got6 + case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46: + got = got4 + if len(got) == 0 { + got = got6 + } + case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64: + got = got6 + if len(got) == 0 { + got = got4 + } + } + if len(got) == 0 { + return nil, dns.ErrEmptyResponse } } - if len(got) == 0 { - return nil, dns.ErrEmptyResponse - } + return got[dice.Roll(len(got))], nil } - return got[dice.Roll(len(got))], nil } type UDPConnClient struct { @@ -510,3 +498,31 @@ func (c *cache) Cache(host string, saddr []string, ttl uint32) { deadline: time.Now().Add(time.Second * time.Duration(ttl)), } } + +type blockup struct { + tun.Device + ch chan tun.Event + canup *atomic.Bool +} + +func newblockup(t tun.Device) *blockup { + ch := make(chan tun.Event, cap(t.Events())) + canup := &atomic.Bool{} + go func() { + for e := range t.Events() { + if e&tun.EventUp != 0 && !canup.Load() { + continue + } + select { + case ch <- e: + default: + } + } + close(ch) + }() + return &blockup{Device: t, ch: ch, canup: canup} +} + +func (b *blockup) Events() <-chan tun.Event { + return b.ch +} diff --git a/proxy/wireguard/server.go b/proxy/wireguard/server.go index fda0a837a..0e7ff867c 100644 --- a/proxy/wireguard/server.go +++ b/proxy/wireguard/server.go @@ -255,7 +255,7 @@ func (s *Server) Start() error { if s.src.Address.Family().IsDomain() { return errors.New("address is domain") } - listenFunc := func() (net.PacketConn, error) { + listenFunc := func(context.Context) (net.PacketConn, error) { var pktConn net.PacketConn var err error if s.streamSettings.FinalMask != nil {