diff --git a/internal/web/service/xray.go b/internal/web/service/xray.go index c1e205e09..d235f9368 100644 --- a/internal/web/service/xray.go +++ b/internal/web/service/xray.go @@ -189,6 +189,9 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { if err := transformAmneziaWGOutbounds(xrayConfig); err != nil { return nil, err } + if xray.KernelTunBlocked() { + xrayConfig.OutboundConfigs = forceUserspaceWireGuardTun(xrayConfig.OutboundConfigs) + } _, _, _ = s.inboundService.AddTraffic(nil, nil) @@ -1716,3 +1719,38 @@ func upgradeStreamLegacyUDPHop(stream map[string]any) bool { func normalizeStreamUDPHop(stream map[string]any) bool { return maskcompat.NormalizeUDPHop(stream["finalmask"]) } + +// forceUserspaceWireGuardTun moves every WireGuard outbound to the gVisor TUN (#6796). Values +// stay raw so the template's other keys reach the core byte for byte. +func forceUserspaceWireGuardTun(raw json_util.RawMessage) json_util.RawMessage { + var outbounds []map[string]json.RawMessage + if err := json.Unmarshal(raw, &outbounds); err != nil { + return raw + } + for _, ob := range outbounds { + var protocol, tag string + if json.Unmarshal(ob["protocol"], &protocol) != nil || !strings.EqualFold(protocol, "wireguard") { + continue + } + var settings map[string]json.RawMessage + if json.Unmarshal(ob["settings"], &settings) != nil || settings == nil { + continue + } + wasUserspace := string(settings["noKernelTun"]) == "true" + settings["noKernelTun"] = json.RawMessage("true") + rewritten, err := json.Marshal(settings) + if err != nil { + continue + } + ob["settings"] = rewritten + if !wasUserspace { + _ = json.Unmarshal(ob["tag"], &tag) + logger.Warningf("Outbound %q: kernel TUN cannot be set up here, running WireGuard on the userspace TUN instead (#6796)", tag) + } + } + rewritten, err := json.Marshal(outbounds) + if err != nil { + return raw + } + return rewritten +} diff --git a/internal/web/service/xray_wireguard_config_test.go b/internal/web/service/xray_wireguard_config_test.go index 874f3cbc0..f38aa2367 100644 --- a/internal/web/service/xray_wireguard_config_test.go +++ b/internal/web/service/xray_wireguard_config_test.go @@ -7,6 +7,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/util/json_util" ) func wgTestSecretKey() string { @@ -270,3 +271,35 @@ func TestGetXrayConfigWireGuardNoClientsEmitsEmptyPeers(t *testing.T) { t.Fatalf("expected empty peers, got %v", peers) } } + +// #6796: where the core would pick a kernel TUN it cannot set up (Docker, a sandboxed unit), +// one WireGuard outbound aborts the whole start unless it is moved to the userspace TUN. +func TestForceUserspaceWireGuardTun(t *testing.T) { + cases := []struct { + name, protocol, settings string + }{ + {"kernel TUN requested", "wireguard", `{"noKernelTun":false,"mtu":1420}`}, + {"kernel TUN by default, protocol cased like the core accepts", "WireGuard", `{"mtu":1420}`}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + raw := json_util.RawMessage(`[{"protocol":"freedom","tag":"direct","settings":{}},` + + `{"protocol":"` + tc.protocol + `","tag":"warp","settings":` + tc.settings + `}]`) + var outbounds []struct { + Settings map[string]any `json:"settings"` + } + if err := json.Unmarshal(forceUserspaceWireGuardTun(raw), &outbounds); err != nil { + t.Fatal(err) + } + if got := outbounds[1].Settings["noKernelTun"]; got != true { + t.Fatalf("wireguard noKernelTun = %v, want true", got) + } + if got := outbounds[1].Settings["mtu"]; got != float64(1420) { + t.Fatalf("wireguard mtu = %v, want the template's 1420 kept", got) + } + if _, ok := outbounds[0].Settings["noKernelTun"]; ok { + t.Fatalf("freedom outbound gained noKernelTun: %v", outbounds[0].Settings) + } + }) + } +} diff --git a/internal/xray/kernel_tun_linux.go b/internal/xray/kernel_tun_linux.go new file mode 100644 index 000000000..f675345c3 --- /dev/null +++ b/internal/xray/kernel_tun_linux.go @@ -0,0 +1,52 @@ +package xray + +import ( + "errors" + "io/fs" + "os" + "sync" + + "golang.org/x/sys/unix" +) + +// The files the core's WireGuard kernel TUN opens before it creates the device +// (proxy/wireguard/tun_linux.go); a missing one is skipped there too. +var kernelTunSysctls = []string{ + "/proc/sys/net/ipv4/conf/all/rp_filter", + "/proc/sys/net/ipv6/conf/all/disable_ipv6", + "/proc/sys/net/ipv6/conf/all/rp_filter", +} + +// KernelTunBlocked reports that the core would put a WireGuard outbound on a kernel TUN +// (it holds CAP_NET_ADMIN) where a read-only /proc/sys or a missing /dev/net/tun aborts its start. +var KernelTunBlocked = sync.OnceValue(func() bool { + return hasEffectiveNetAdmin() && !kernelTunReachable() +}) + +func hasEffectiveNetAdmin() bool { + hdr := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3} + var data [2]unix.CapUserData + if err := unix.Capget(&hdr, &data[0]); err != nil { + return false + } + return data[0].Effective&(1<