mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-05-08 14:13:22 +00:00
TUN inbound: Add gateway, dns, autoSystemRoutingTable, autoOutboundsInterface for Windows (#5887)
And refactor `mtu` to support setting IPv4/v6 separately Example: https://github.com/XTLS/Xray-core/pull/5887#issue-4198837696
This commit is contained in:
@@ -1,12 +1,21 @@
|
||||
package localdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
// Client is an implementation of dns.Client, which queries localhost for DNS.
|
||||
type Client struct{}
|
||||
type Client struct {
|
||||
d *net.Dialer
|
||||
r *net.Resolver
|
||||
}
|
||||
|
||||
// Type implements common.HasType.
|
||||
func (*Client) Type() interface{} {
|
||||
@@ -20,8 +29,14 @@ func (*Client) Start() error { return nil }
|
||||
func (*Client) Close() error { return nil }
|
||||
|
||||
// LookupIP implements Client.
|
||||
func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, uint32, error) {
|
||||
ips, err := net.LookupIP(host)
|
||||
func (c *Client) LookupIP(host string, option dns.IPOption) ([]net.IP, uint32, error) {
|
||||
var ips []net.IP
|
||||
var err error
|
||||
if len(internet.Controllers) > 0 {
|
||||
ips, err = c.r.LookupIP(context.Background(), "ip", host)
|
||||
} else {
|
||||
ips, err = net.LookupIP(host)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -62,5 +77,28 @@ func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, uint32, err
|
||||
|
||||
// New create a new dns.Client that queries localhost for DNS.
|
||||
func New() *Client {
|
||||
return &Client{}
|
||||
d := &net.Dialer{
|
||||
Timeout: time.Second * 16,
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range internet.Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "failed to apply external controller")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return d.DialContext(ctx, network, address)
|
||||
},
|
||||
}
|
||||
|
||||
return &Client{
|
||||
d: d,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
1
go.mod
1
go.mod
@@ -27,6 +27,7 @@ require (
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -131,6 +131,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||
|
||||
@@ -7,24 +7,38 @@ import (
|
||||
|
||||
type TunConfig struct {
|
||||
Name string `json:"name"`
|
||||
MTU uint32 `json:"MTU"`
|
||||
MTU []uint32 `json:"mtu"`
|
||||
Gateway []string `json:"gateway"`
|
||||
DNS []string `json:"dns"`
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"`
|
||||
AutoOutboundsInterface *string `json:"autoOutboundsInterface"`
|
||||
}
|
||||
|
||||
func (v *TunConfig) Build() (proto.Message, error) {
|
||||
config := &tun.Config{
|
||||
Name: v.Name,
|
||||
MTU: v.MTU,
|
||||
Gateway: v.Gateway,
|
||||
DNS: v.DNS,
|
||||
UserLevel: v.UserLevel,
|
||||
AutoSystemRoutingTable: v.AutoSystemRoutingTable,
|
||||
}
|
||||
if v.AutoOutboundsInterface != nil {
|
||||
config.AutoOutboundsInterface = *v.AutoOutboundsInterface
|
||||
}
|
||||
if len(v.AutoSystemRoutingTable) > 0 && v.AutoOutboundsInterface == nil {
|
||||
config.AutoOutboundsInterface = "auto"
|
||||
}
|
||||
|
||||
if v.Name == "" {
|
||||
if config.Name == "" {
|
||||
config.Name = "xray0"
|
||||
}
|
||||
|
||||
if v.MTU == 0 {
|
||||
config.MTU = 1500
|
||||
if len(config.MTU) == 0 {
|
||||
config.MTU = []uint32{1500, 1280}
|
||||
}
|
||||
if len(config.MTU) == 1 {
|
||||
config.MTU = append(config.MTU, config.MTU[0])
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -1 +1,78 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
type InterfaceUpdater struct {
|
||||
sync.Mutex
|
||||
|
||||
tunIndex int
|
||||
fixedName string
|
||||
iface *net.Interface
|
||||
}
|
||||
|
||||
var updater *InterfaceUpdater
|
||||
|
||||
func (updater *InterfaceUpdater) Get() *net.Interface {
|
||||
updater.Lock()
|
||||
defer updater.Unlock()
|
||||
|
||||
return updater.iface
|
||||
}
|
||||
|
||||
func (updater *InterfaceUpdater) Update() {
|
||||
updater.Lock()
|
||||
defer updater.Unlock()
|
||||
|
||||
if updater.iface != nil {
|
||||
iface, err := net.InterfaceByIndex(updater.iface.Index)
|
||||
if err == nil && iface.Name == updater.iface.Name {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updater.iface = nil
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "[tun] failed to update interface")
|
||||
return
|
||||
}
|
||||
|
||||
var got *net.Interface
|
||||
for _, iface := range interfaces {
|
||||
if iface.Index == updater.tunIndex {
|
||||
continue
|
||||
}
|
||||
if updater.fixedName != "" {
|
||||
if iface.Name == updater.fixedName {
|
||||
got = &iface
|
||||
break
|
||||
}
|
||||
} else {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if (iface.Flags&net.FlagUp != 0) &&
|
||||
(iface.Flags&net.FlagLoopback == 0) &&
|
||||
len(addrs) > 0 {
|
||||
got = &iface
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if got == nil {
|
||||
errors.LogInfo(context.Background(), "[tun] failed to update interface > got == nil")
|
||||
return
|
||||
}
|
||||
|
||||
updater.iface = got
|
||||
errors.LogInfo(context.Background(), "[tun] update interface ", got.Name, " ", got.Index)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,12 @@ const (
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
MTU uint32 `protobuf:"varint,2,opt,name=MTU,proto3" json:"MTU,omitempty"`
|
||||
UserLevel uint32 `protobuf:"varint,3,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
MTU []uint32 `protobuf:"varint,2,rep,packed,name=MTU,proto3" json:"MTU,omitempty"`
|
||||
Gateway []string `protobuf:"bytes,3,rep,name=gateway,proto3" json:"gateway,omitempty"`
|
||||
DNS []string `protobuf:"bytes,4,rep,name=DNS,proto3" json:"DNS,omitempty"`
|
||||
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
|
||||
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -67,11 +71,25 @@ func (x *Config) GetName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetMTU() uint32 {
|
||||
func (x *Config) GetMTU() []uint32 {
|
||||
if x != nil {
|
||||
return x.MTU
|
||||
}
|
||||
return 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetGateway() []string {
|
||||
if x != nil {
|
||||
return x.Gateway
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetDNS() []string {
|
||||
if x != nil {
|
||||
return x.DNS
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetUserLevel() uint32 {
|
||||
@@ -81,16 +99,34 @@ func (x *Config) GetUserLevel() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetAutoSystemRoutingTable() []string {
|
||||
if x != nil {
|
||||
return x.AutoSystemRoutingTable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetAutoOutboundsInterface() string {
|
||||
if x != nil {
|
||||
return x.AutoOutboundsInterface
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proxy_tun_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"M\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x1d\n" +
|
||||
"\x03MTU\x18\x02 \x03(\rR\x03MTU\x12\x18\n" +
|
||||
"\agateway\x18\x03 \x03(\tR\agateway\x12\x10\n" +
|
||||
"\x03DNS\x18\x04 \x03(\tR\x03DNS\x12\x1d\n" +
|
||||
"\n" +
|
||||
"user_level\x18\x03 \x01(\rR\tuserLevelBL\n" +
|
||||
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
|
||||
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -8,6 +8,10 @@ option java_multiple_files = true;
|
||||
|
||||
message Config {
|
||||
string name = 1;
|
||||
uint32 MTU = 2;
|
||||
uint32 user_level = 3;
|
||||
repeated uint32 MTU = 2;
|
||||
repeated string gateway = 3;
|
||||
repeated string DNS = 4;
|
||||
uint32 user_level = 5;
|
||||
repeated string auto_system_routing_table = 6;
|
||||
string auto_outbounds_interface = 7;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
|
||||
@@ -38,11 +40,6 @@ type ConnectionHandler interface {
|
||||
// Handler implements ConnectionHandler
|
||||
var _ ConnectionHandler = (*Handler)(nil)
|
||||
|
||||
func (t *Handler) policy() policy.Session {
|
||||
p := t.policyManager.ForLevel(t.config.UserLevel)
|
||||
return p
|
||||
}
|
||||
|
||||
// Init the Handler instance with necessary parameters
|
||||
func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routing.Dispatcher) error {
|
||||
var err error
|
||||
@@ -60,15 +57,37 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
t.dispatcher = dispatcher
|
||||
|
||||
tunName := t.config.Name
|
||||
tunOptions := TunOptions{
|
||||
Name: tunName,
|
||||
MTU: t.config.MTU,
|
||||
}
|
||||
tunInterface, err := NewTun(tunOptions)
|
||||
tunInterface, err := NewTun(t.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.config.AutoOutboundsInterface != "" {
|
||||
tunIndex, err := tunInterface.Index()
|
||||
if err != nil {
|
||||
_ = tunInterface.Close()
|
||||
return err
|
||||
}
|
||||
if t.config.AutoOutboundsInterface == "auto" {
|
||||
t.config.AutoOutboundsInterface = ""
|
||||
}
|
||||
updater = &InterfaceUpdater{tunIndex: tunIndex, fixedName: t.config.AutoOutboundsInterface}
|
||||
updater.Update()
|
||||
internet.RegisterDialerController(func(network, address string, c syscall.RawConn) error {
|
||||
iface := updater.Get()
|
||||
if iface == nil {
|
||||
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
|
||||
return nil
|
||||
}
|
||||
return c.Control(func(fd uintptr) {
|
||||
err := setinterface(network, address, fd, iface)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
errors.LogInfo(t.ctx, tunName, " created")
|
||||
|
||||
tunStackOptions := StackOptions{
|
||||
|
||||
@@ -34,23 +34,18 @@ const (
|
||||
// stackGVisor is ip stack implemented by gVisor package
|
||||
type stackGVisor struct {
|
||||
ctx context.Context
|
||||
tun GVisorTun
|
||||
tun Tun
|
||||
idleTimeout time.Duration
|
||||
handler *Handler
|
||||
stack *stack.Stack
|
||||
endpoint stack.LinkEndpoint
|
||||
}
|
||||
|
||||
// GVisorTun implements a bridge to connect gVisor ip stack to tun interface
|
||||
type GVisorTun interface {
|
||||
newEndpoint() (stack.LinkEndpoint, error)
|
||||
}
|
||||
|
||||
// NewStack builds new ip stack (using gVisor)
|
||||
func NewStack(ctx context.Context, options StackOptions, handler *Handler) (Stack, error) {
|
||||
gStack := &stackGVisor{
|
||||
ctx: ctx,
|
||||
tun: options.Tun.(GVisorTun),
|
||||
tun: options.Tun,
|
||||
idleTimeout: options.IdleTimeout,
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package tun
|
||||
|
||||
import "gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
|
||||
// Tun interface implements tun interface interaction
|
||||
type Tun interface {
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// TunOptions for tun interface implementation
|
||||
type TunOptions struct {
|
||||
Name string
|
||||
MTU uint32
|
||||
Name() (string, error)
|
||||
Index() (int, error)
|
||||
newEndpoint() (stack.LinkEndpoint, error)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -15,17 +16,14 @@ import (
|
||||
|
||||
type AndroidTun struct {
|
||||
tunFd int
|
||||
options TunOptions
|
||||
options *Config
|
||||
}
|
||||
|
||||
// DefaultTun implements Tun
|
||||
var _ Tun = (*AndroidTun)(nil)
|
||||
|
||||
// DefaultTun implements GVisorTun
|
||||
var _ GVisorTun = (*AndroidTun)(nil)
|
||||
|
||||
// NewTun builds new tun interface handler
|
||||
func NewTun(options TunOptions) (Tun, error) {
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
fd, err := strconv.Atoi(platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "0" }))
|
||||
errors.LogInfo(context.Background(), "read Android Tun Fd ", fd, err)
|
||||
|
||||
@@ -49,10 +47,37 @@ func (t *AndroidTun) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *AndroidTun) Name() (string, error) {
|
||||
ifr, err := unix.NewIfreq("")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = unix.IoctlIfreq(t.tunFd, unix.TUNGETIFF, ifr); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ifr.Name(), nil
|
||||
}
|
||||
|
||||
func (t *AndroidTun) Index() (int, error) {
|
||||
name, err := t.Name()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return iface.Index, nil
|
||||
}
|
||||
|
||||
func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return fdbased.New(&fdbased.Options{
|
||||
FDs: []int{t.tunFd},
|
||||
MTU: t.options.MTU,
|
||||
MTU: t.options.MTU[0],
|
||||
RXChecksumOffload: true,
|
||||
})
|
||||
}
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
return unix.BindToDevice(int(fd), iface.Name)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
go_errors "errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
sysprotoControl = 2
|
||||
gateway = "169.254.10.1/30"
|
||||
utunHeaderSize = 4
|
||||
UTUN_OPT_IFNAME = 2
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,15 +40,15 @@ func procyield(cycles uint32)
|
||||
|
||||
type DarwinTun struct {
|
||||
tunFile *os.File
|
||||
options TunOptions
|
||||
options *Config
|
||||
tunFd int
|
||||
ownsFd bool // true for macOS (we created the fd), false for iOS (fd from system)
|
||||
}
|
||||
|
||||
var _ Tun = (*DarwinTun)(nil)
|
||||
var _ GVisorTun = (*DarwinTun)(nil)
|
||||
var _ GVisorDevice = (*DarwinTun)(nil)
|
||||
|
||||
func NewTun(options TunOptions) (Tun, error) {
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
// Check if fd is provided via environment (iOS mode)
|
||||
fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" })
|
||||
if fdStr != "" {
|
||||
@@ -64,6 +65,7 @@ func NewTun(options TunOptions) (Tun, error) {
|
||||
return &DarwinTun{
|
||||
tunFile: os.NewFile(uintptr(fd), "utun"),
|
||||
options: options,
|
||||
tunFd: fd,
|
||||
ownsFd: false,
|
||||
}, nil
|
||||
}
|
||||
@@ -74,7 +76,7 @@ func NewTun(options TunOptions) (Tun, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = setup(options.Name, options.MTU)
|
||||
err = setup(options.Name, options.MTU[0])
|
||||
if err != nil {
|
||||
_ = tunFile.Close()
|
||||
return nil, err
|
||||
@@ -83,6 +85,7 @@ func NewTun(options TunOptions) (Tun, error) {
|
||||
return &DarwinTun{
|
||||
tunFile: tunFile,
|
||||
options: options,
|
||||
tunFd: int(tunFile.Fd()),
|
||||
ownsFd: true,
|
||||
}, nil
|
||||
}
|
||||
@@ -99,10 +102,26 @@ func (t *DarwinTun) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *DarwinTun) Name() (string, error) {
|
||||
return unix.GetsockoptString(t.tunFd, sysprotoControl, UTUN_OPT_IFNAME)
|
||||
}
|
||||
|
||||
func (t *DarwinTun) Index() (int, error) {
|
||||
name, err := t.Name()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return iface.Index, nil
|
||||
}
|
||||
|
||||
// WritePacket implements GVisorDevice method to write one packet to the tun device
|
||||
func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize)
|
||||
defer b.Release()
|
||||
|
||||
// prepare Darwin specific packet header
|
||||
@@ -124,7 +143,7 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
b.SetByte(3, family)
|
||||
|
||||
if _, err := t.tunFile.Write(b.Bytes()); err != nil {
|
||||
if errors.Is(err, unix.EAGAIN) {
|
||||
if go_errors.Is(err, unix.EAGAIN) {
|
||||
return &tcpip.ErrWouldBlock{}
|
||||
}
|
||||
return &tcpip.ErrAborted{}
|
||||
@@ -137,11 +156,11 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error {
|
||||
// which will make the stack call Wait which should implement desired push-back
|
||||
func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
// request memory to write from reusable buffer pool
|
||||
b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize)
|
||||
b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize)
|
||||
|
||||
// read the bytes to the interface file
|
||||
n, err := b.ReadFrom(t.tunFile)
|
||||
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
|
||||
if go_errors.Is(err, unix.EAGAIN) || go_errors.Is(err, unix.EINTR) {
|
||||
b.Release()
|
||||
return 0, nil, ErrQueueEmpty
|
||||
}
|
||||
@@ -174,7 +193,7 @@ func (t *DarwinTun) Wait() {
|
||||
}
|
||||
|
||||
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU[0], device: t}, nil
|
||||
}
|
||||
|
||||
// open the interface, by creating new utunN if in the system and returning its file descriptor
|
||||
@@ -346,9 +365,20 @@ func setIPAddress(name string, gateway netip.Prefix) error {
|
||||
}
|
||||
|
||||
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
|
||||
_, _, errno := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
switch network {
|
||||
case "tcp4", "udp4", "ip4":
|
||||
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
|
||||
case "tcp6", "udp6", "ip6":
|
||||
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
|
||||
default:
|
||||
return errors.New("unknown network ", network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
)
|
||||
@@ -13,11 +15,8 @@ type DefaultTun struct {
|
||||
// DefaultTun implements Tun
|
||||
var _ Tun = (*DefaultTun)(nil)
|
||||
|
||||
// DefaultTun implements GVisorTun
|
||||
var _ GVisorTun = (*DefaultTun)(nil)
|
||||
|
||||
// NewTun builds new tun interface handler
|
||||
func NewTun(options TunOptions) (Tun, error) {
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
return nil, errors.New("Tun is not supported on your platform")
|
||||
}
|
||||
|
||||
@@ -29,6 +28,18 @@ func (t *DefaultTun) Close() error {
|
||||
return errors.New("Tun is not supported on your platform")
|
||||
}
|
||||
|
||||
func (t *DefaultTun) Name() (string, error) {
|
||||
return "", errors.New("Tun is not supported on your platform")
|
||||
}
|
||||
|
||||
func (t *DefaultTun) Index() (int, error) {
|
||||
return 0, errors.New("Tun is not supported on your platform")
|
||||
}
|
||||
|
||||
func (t *DefaultTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return nil, errors.New("Tun is not supported on your platform")
|
||||
}
|
||||
|
||||
func setinterface(string, string, uintptr, *net.Interface) error {
|
||||
return errors.New("Tun is not supported on your platform")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
|
||||
@@ -15,23 +17,20 @@ import (
|
||||
type LinuxTun struct {
|
||||
tunFd int
|
||||
tunLink netlink.Link
|
||||
options TunOptions
|
||||
options *Config
|
||||
}
|
||||
|
||||
// LinuxTun implements Tun
|
||||
var _ Tun = (*LinuxTun)(nil)
|
||||
|
||||
// LinuxTun implements GVisorTun
|
||||
var _ GVisorTun = (*LinuxTun)(nil)
|
||||
|
||||
// NewTun builds new tun interface handler (linux specific)
|
||||
func NewTun(options TunOptions) (Tun, error) {
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
tunFd, err := open(options.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tunLink, err := setup(options.Name, int(options.MTU))
|
||||
tunLink, err := setup(options.Name, int(options.MTU[0]))
|
||||
if err != nil {
|
||||
_ = unix.Close(tunFd)
|
||||
return nil, err
|
||||
@@ -110,11 +109,23 @@ func (t *LinuxTun) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *LinuxTun) Name() (string, error) {
|
||||
return t.tunLink.Attrs().Name, nil
|
||||
}
|
||||
|
||||
func (t *LinuxTun) Index() (int, error) {
|
||||
return t.tunLink.Attrs().Index, nil
|
||||
}
|
||||
|
||||
// newEndpoint builds new gVisor stack.LinkEndpoint from the tun interface file descriptor
|
||||
func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return fdbased.New(&fdbased.Options{
|
||||
FDs: []int{t.tunFd},
|
||||
MTU: t.options.MTU,
|
||||
MTU: t.options.MTU[0],
|
||||
RXChecksumOffload: true,
|
||||
})
|
||||
}
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
return unix.BindToDevice(int(fd), iface.Name)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,17 @@ package tun
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"encoding/binary"
|
||||
go_errors "errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wintun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
@@ -21,25 +27,26 @@ func procyield(cycles uint32)
|
||||
// current version is heavily stripped to do nothing more,
|
||||
// then create a network interface, to be provided as endpoint to gVisor ip stack
|
||||
type WindowsTun struct {
|
||||
options TunOptions
|
||||
sync.RWMutex
|
||||
|
||||
options *Config
|
||||
adapter *wintun.Adapter
|
||||
session wintun.Session
|
||||
readWait windows.Handle
|
||||
MTU uint32
|
||||
luid winipcfg.LUID
|
||||
changeCallback winipcfg.ChangeCallback
|
||||
closed bool
|
||||
}
|
||||
|
||||
// WindowsTun implements Tun
|
||||
var _ Tun = (*WindowsTun)(nil)
|
||||
|
||||
// WindowsTun implements GVisorTun
|
||||
var _ GVisorTun = (*WindowsTun)(nil)
|
||||
|
||||
// WindowsTun implements GVisorDevice
|
||||
var _ GVisorDevice = (*WindowsTun)(nil)
|
||||
|
||||
// NewTun creates a Wintun interface with the given name. Should a Wintun
|
||||
// interface with the same name exist, it tried to be reused.
|
||||
func NewTun(options TunOptions) (Tun, error) {
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
// instantiate wintun adapter
|
||||
adapter, err := open(options.Name)
|
||||
if err != nil {
|
||||
@@ -58,9 +65,7 @@ func NewTun(options TunOptions) (Tun, error) {
|
||||
adapter: adapter,
|
||||
session: session,
|
||||
readWait: session.ReadWaitEvent(),
|
||||
// there is currently no iphndl.dll support, which is the netlink library for windows
|
||||
// so there is nowhere to change MTU for the Wintun interface, and we take its default value
|
||||
MTU: wintun.PacketSizeMax,
|
||||
luid: winipcfg.LUID(adapter.LUID()),
|
||||
}
|
||||
|
||||
return tun, nil
|
||||
@@ -70,13 +75,8 @@ func open(name string) (*wintun.Adapter, error) {
|
||||
// generate a deterministic GUID from the adapter name
|
||||
id := md5.Sum([]byte(name))
|
||||
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
|
||||
// try to open existing adapter by name
|
||||
adapter, err := wintun.OpenAdapter(name)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
// try to create adapter anew
|
||||
adapter, err = wintun.CreateAdapter(name, "Xray", guid)
|
||||
adapter, err := wintun.CreateAdapter(name, "Xray", guid)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
@@ -84,18 +84,153 @@ func open(name string) (*wintun.Adapter, error) {
|
||||
}
|
||||
|
||||
func (t *WindowsTun) Start() error {
|
||||
var has4, has6 bool
|
||||
allowedIPs := make([]netip.Prefix, 0, len(t.options.AutoSystemRoutingTable))
|
||||
for _, route := range t.options.AutoSystemRoutingTable {
|
||||
allowedIPs = append(allowedIPs, netip.MustParsePrefix(route))
|
||||
}
|
||||
routesMap := make(map[winipcfg.RouteData]struct{})
|
||||
for _, ip := range allowedIPs {
|
||||
route := winipcfg.RouteData{
|
||||
Destination: ip.Masked(),
|
||||
Metric: 0,
|
||||
}
|
||||
if ip.Addr().Is4() {
|
||||
has4 = true
|
||||
route.NextHop = netip.IPv4Unspecified()
|
||||
} else {
|
||||
has6 = true
|
||||
route.NextHop = netip.IPv6Unspecified()
|
||||
}
|
||||
routesMap[route] = struct{}{}
|
||||
}
|
||||
routesData := make([]*winipcfg.RouteData, 0, len(routesMap))
|
||||
for route := range routesMap {
|
||||
r := route
|
||||
routesData = append(routesData, &r)
|
||||
}
|
||||
err := t.luid.SetRoutes(routesData)
|
||||
if err != nil {
|
||||
return errors.New("unable to set routes").Base(err)
|
||||
}
|
||||
|
||||
if len(t.options.Gateway) > 0 {
|
||||
addresses := make([]netip.Prefix, 0, len(t.options.Gateway))
|
||||
for _, address := range t.options.Gateway {
|
||||
addresses = append(addresses, netip.MustParsePrefix(address))
|
||||
}
|
||||
err := t.luid.SetIPAddresses(addresses)
|
||||
if err != nil {
|
||||
return errors.New("unable to set ips").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if has4 {
|
||||
ipif, err := t.luid.IPInterface(windows.AF_INET)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
|
||||
ipif.DadTransmits = 0
|
||||
ipif.ManagedAddressConfigurationSupported = false
|
||||
ipif.OtherStatefulConfigurationSupported = false
|
||||
ipif.NLMTU = t.options.MTU[0]
|
||||
ipif.UseAutomaticMetric = false
|
||||
ipif.Metric = 0
|
||||
err = ipif.Set()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if has6 {
|
||||
ipif, err := t.luid.IPInterface(windows.AF_INET6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
|
||||
ipif.DadTransmits = 0
|
||||
ipif.ManagedAddressConfigurationSupported = false
|
||||
ipif.OtherStatefulConfigurationSupported = false
|
||||
ipif.NLMTU = t.options.MTU[1]
|
||||
ipif.UseAutomaticMetric = false
|
||||
ipif.Metric = 0
|
||||
err = ipif.Set()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.options.DNS) > 0 {
|
||||
dns := make([]netip.Addr, 0, len(t.options.DNS))
|
||||
for _, ip := range t.options.DNS {
|
||||
dns = append(dns, netip.MustParseAddr(ip))
|
||||
}
|
||||
err := t.luid.SetDNS(windows.AF_INET, dns, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = t.luid.SetDNS(windows.AF_INET6, dns, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if updater != nil {
|
||||
t.changeCallback, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
|
||||
if notificationType != winipcfg.MibDeleteInstance {
|
||||
return
|
||||
}
|
||||
updater.Update()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *WindowsTun) Close() error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
if t.closed {
|
||||
return nil
|
||||
}
|
||||
t.closed = true
|
||||
|
||||
if t.changeCallback != nil {
|
||||
t.changeCallback.Unregister()
|
||||
}
|
||||
t.session.End()
|
||||
_ = t.adapter.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *WindowsTun) Name() (string, error) {
|
||||
row, err := t.luid.Interface()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return row.Alias(), nil
|
||||
}
|
||||
|
||||
func (t *WindowsTun) Index() (int, error) {
|
||||
row, err := t.luid.Interface()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(row.InterfaceIndex), nil
|
||||
}
|
||||
|
||||
// WritePacket implements GVisorDevice method to write one packet to the tun device
|
||||
func (t *WindowsTun) WritePacket(packetBuffer *stack.PacketBuffer) tcpip.Error {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
if t.closed {
|
||||
return &tcpip.ErrClosedForSend{}
|
||||
}
|
||||
|
||||
// request buffer from Wintun
|
||||
packet, err := t.session.AllocateSendPacket(packetBuffer.Size())
|
||||
if err != nil {
|
||||
@@ -119,7 +254,7 @@ func (t *WindowsTun) WritePacket(packetBuffer *stack.PacketBuffer) tcpip.Error {
|
||||
// which will make the stack call Wait which should implement desired push-back
|
||||
func (t *WindowsTun) ReadPacket() (byte, *stack.PacketBuffer, error) {
|
||||
packet, err := t.session.ReceivePacket()
|
||||
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
|
||||
if go_errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
|
||||
return 0, nil, ErrQueueEmpty
|
||||
}
|
||||
if err != nil {
|
||||
@@ -143,5 +278,38 @@ func (t *WindowsTun) Wait() {
|
||||
}
|
||||
|
||||
func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) {
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
|
||||
return &LinkEndpoint{deviceMTU: t.options.MTU[0], device: t}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
IP_UNICAST_IF = 31
|
||||
IPV6_UNICAST_IF = 31
|
||||
)
|
||||
|
||||
func setinterface(network, address string, fd uintptr, iface *net.Interface) error {
|
||||
var index [4]byte
|
||||
binary.BigEndian.PutUint32(index[:], uint32(iface.Index))
|
||||
|
||||
switch network {
|
||||
case "tcp4", "udp4", "ip4":
|
||||
err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if network == "udp4" {
|
||||
return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, windows.IP_MULTICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
|
||||
}
|
||||
case "tcp6", "udp6", "ip6":
|
||||
err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if network == "udp6" {
|
||||
return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index)
|
||||
}
|
||||
default:
|
||||
return errors.New("unknown network ", network)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package internet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"github.com/xtls/xray-core/features/outbound"
|
||||
)
|
||||
|
||||
var Controllers []func(network, address string, c syscall.RawConn) error
|
||||
var ControllersLock sync.Mutex
|
||||
var effectiveSystemDialer SystemDialer = &DefaultSystemDialer{}
|
||||
|
||||
type SystemDialer interface {
|
||||
@@ -19,7 +22,6 @@ type SystemDialer interface {
|
||||
}
|
||||
|
||||
type DefaultSystemDialer struct {
|
||||
controllers []func(network, address string, c syscall.RawConn) error
|
||||
dns dns.Client
|
||||
obm outbound.Manager
|
||||
}
|
||||
@@ -63,7 +65,7 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
|
||||
return nil, err
|
||||
}
|
||||
lc.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range d.controllers {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to apply external controller")
|
||||
}
|
||||
@@ -115,12 +117,12 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
|
||||
KeepAliveConfig: keepAliveConfig,
|
||||
}
|
||||
|
||||
if sockopt != nil || len(d.controllers) > 0 {
|
||||
if sockopt != nil || len(Controllers) > 0 {
|
||||
if sockopt != nil && sockopt.TcpMptcp {
|
||||
dialer.SetMultipathTCP(true)
|
||||
}
|
||||
dialer.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range d.controllers {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to apply external controller")
|
||||
}
|
||||
@@ -208,12 +210,15 @@ func RegisterDialerController(ctl func(network, address string, c syscall.RawCon
|
||||
return errors.New("nil listener controller")
|
||||
}
|
||||
|
||||
dialer, ok := effectiveSystemDialer.(*DefaultSystemDialer)
|
||||
ControllersLock.Lock()
|
||||
Controllers = append(Controllers, ctl)
|
||||
ControllersLock.Unlock()
|
||||
|
||||
_, ok := effectiveSystemDialer.(*DefaultSystemDialer)
|
||||
if !ok {
|
||||
return errors.New("RegisterListenerController not supported in custom dialer")
|
||||
}
|
||||
|
||||
dialer.controllers = append(dialer.controllers, ctl)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user