mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-16 20:45:50 +00:00
1. **Fixed XPadding Placement Dropdown**: - Added the missing `cookie` and `query` options to `xPaddingPlacement` (`stream_xhttp.html`). - *Why:* Previously, users wanting `cookie` obfuscation were forced to use the `header` placement string. This caused Xray-core to blindly intercept the entire monolithic HTTP Cookie header, failing internal padding-length validations and causing the inbound to silently drop the connection. 2. **Fixed Uplink Data Placement Validation**: - Replaced the unsupported `query` option with `cookie` in `uplinkDataPlacement`. - *Why:* Xray-core's `transport_internet.go` explicitly forbids `query` as an uplink placement option. Selecting it from the UI previously sent a payload that would cause Xray-core to instantly throw an `unsupported uplink data placement: query` panic. Adding `cookie` perfectly aligns the UI with Xray-core restrictions. ### Related Issues - Resolves #3992
354 lines
9.2 KiB
Go
354 lines
9.2 KiB
Go
//go:build darwin
|
|
|
|
package tun
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"github.com/xtls/xray-core/common/buf"
|
|
"github.com/xtls/xray-core/common/platform"
|
|
"golang.org/x/sys/unix"
|
|
"gvisor.dev/gvisor/pkg/buffer"
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
|
)
|
|
|
|
const (
|
|
utunControlName = "com.apple.net.utun_control"
|
|
sysprotoControl = 2
|
|
gateway = "169.254.10.1/30"
|
|
utunHeaderSize = 4
|
|
)
|
|
|
|
const (
|
|
SIOCAIFADDR6 = 2155899162 // netinet6/in6_var.h
|
|
IN6_IFF_NODAD = 0x0020 // netinet6/in6_var.h
|
|
IN6_IFF_SECURED = 0x0400 // netinet6/in6_var.h
|
|
ND6_INFINITE_LIFETIME = 0xFFFFFFFF // netinet6/nd6.h
|
|
)
|
|
|
|
//go:linkname procyield runtime.procyield
|
|
func procyield(cycles uint32)
|
|
|
|
type DarwinTun struct {
|
|
tunFile *os.File
|
|
options TunOptions
|
|
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) {
|
|
// Check if fd is provided via environment (iOS mode)
|
|
fdStr := platform.NewEnvFlag(platform.TunFdKey).GetValue(func() string { return "" })
|
|
if fdStr != "" {
|
|
// iOS: use provided fd from NetworkExtension
|
|
fd, err := strconv.Atoi(fdStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = unix.SetNonblock(fd, true); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &DarwinTun{
|
|
tunFile: os.NewFile(uintptr(fd), "utun"),
|
|
options: options,
|
|
ownsFd: false,
|
|
}, nil
|
|
}
|
|
|
|
// macOS: create our own utun interface
|
|
tunFile, err := open(options.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = setup(options.Name, options.MTU)
|
|
if err != nil {
|
|
_ = tunFile.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return &DarwinTun{
|
|
tunFile: tunFile,
|
|
options: options,
|
|
ownsFd: true,
|
|
}, nil
|
|
}
|
|
|
|
func (t *DarwinTun) Start() error {
|
|
return nil
|
|
}
|
|
|
|
func (t *DarwinTun) Close() error {
|
|
if t.ownsFd {
|
|
return t.tunFile.Close()
|
|
}
|
|
// iOS: don't close the fd, it's owned by NetworkExtension
|
|
return 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)
|
|
defer b.Release()
|
|
|
|
// prepare Darwin specific packet header
|
|
_, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0})
|
|
// copy the bytes of slices that compose the packet into the allocated buffer
|
|
for _, packetElement := range packet.AsSlices() {
|
|
_, _ = b.Write(packetElement)
|
|
}
|
|
// fill Darwin specific header from the first raw packet byte, that we can access now
|
|
var family byte
|
|
switch b.Byte(4) >> 4 {
|
|
case 4:
|
|
family = unix.AF_INET
|
|
case 6:
|
|
family = unix.AF_INET6
|
|
default:
|
|
return &tcpip.ErrAborted{}
|
|
}
|
|
b.SetByte(3, family)
|
|
|
|
if _, err := t.tunFile.Write(b.Bytes()); err != nil {
|
|
if errors.Is(err, unix.EAGAIN) {
|
|
return &tcpip.ErrWouldBlock{}
|
|
}
|
|
return &tcpip.ErrAborted{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadPacket implements GVisorDevice method to read one packet from the tun device
|
|
// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line,
|
|
// 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)
|
|
|
|
// read the bytes to the interface file
|
|
n, err := b.ReadFrom(t.tunFile)
|
|
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
|
|
b.Release()
|
|
return 0, nil, ErrQueueEmpty
|
|
}
|
|
if err != nil {
|
|
b.Release()
|
|
return 0, nil, err
|
|
}
|
|
|
|
// discard empty or sub-empty packets
|
|
if n <= utunHeaderSize {
|
|
b.Release()
|
|
return 0, nil, ErrQueueEmpty
|
|
}
|
|
|
|
// network protocol version from first byte of the raw packet, the one that follows Darwin specific header
|
|
version := b.Byte(utunHeaderSize) >> 4
|
|
packetBuffer := buffer.MakeWithData(b.BytesFrom(utunHeaderSize))
|
|
return version, stack.NewPacketBuffer(stack.PacketBufferOptions{
|
|
Payload: packetBuffer,
|
|
IsForwardedPacket: true,
|
|
OnRelease: func() {
|
|
b.Release()
|
|
},
|
|
}), nil
|
|
}
|
|
|
|
// Wait some cpu cycles
|
|
func (t *DarwinTun) Wait() {
|
|
procyield(1)
|
|
}
|
|
|
|
func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) {
|
|
return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil
|
|
}
|
|
|
|
// open the interface, by creating new utunN if in the system and returning its file descriptor
|
|
func open(name string) (*os.File, error) {
|
|
ifIndex := -1
|
|
_, err := fmt.Sscanf(name, "utun%d", &ifIndex)
|
|
if err != nil || ifIndex < 0 {
|
|
return nil, errors.New("interface name must be utunN, where N is a number, e.g. utun9, utun11 and so on")
|
|
}
|
|
|
|
fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, sysprotoControl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctlInfo := &unix.CtlInfo{}
|
|
copy(ctlInfo.Name[:], utunControlName)
|
|
if err := unix.IoctlCtlInfo(fd, ctlInfo); err != nil {
|
|
_ = unix.Close(fd)
|
|
return nil, err
|
|
}
|
|
|
|
sockaddr := &unix.SockaddrCtl{
|
|
ID: ctlInfo.Id,
|
|
Unit: uint32(ifIndex) + 1,
|
|
}
|
|
if err := unix.Connect(fd, sockaddr); err != nil {
|
|
_ = unix.Close(fd)
|
|
return nil, err
|
|
}
|
|
|
|
if err := unix.SetNonblock(fd, true); err != nil {
|
|
_ = unix.Close(fd)
|
|
return nil, err
|
|
}
|
|
|
|
return os.NewFile(uintptr(fd), name), nil
|
|
}
|
|
|
|
// setup the interface by name
|
|
func setup(name string, MTU uint32) error {
|
|
if err := setMTU(name, MTU); err != nil {
|
|
return err
|
|
}
|
|
|
|
/*
|
|
* Darwin routing require tunnel type interface to have local and remote address, to be routable.
|
|
* To simplify inevitable task, assign the interface static ip address, which in current implementation
|
|
* is just some random ip from link-local pool, allowing to not bother about existing routing intersection.
|
|
*/
|
|
syntheticIP, _ := netip.ParsePrefix(gateway)
|
|
if err := setIPAddress(name, syntheticIP); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setMTU sets MTU on the interface by given name
|
|
func setMTU(name string, mtu uint32) error {
|
|
socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(socket)
|
|
|
|
ifr := unix.IfreqMTU{MTU: int32(mtu)}
|
|
copy(ifr.Name[:], name)
|
|
return unix.IoctlSetIfreqMTU(socket, &ifr)
|
|
}
|
|
|
|
type ifAliasReq4 struct {
|
|
Name [unix.IFNAMSIZ]byte
|
|
Addr unix.RawSockaddrInet4
|
|
Dstaddr unix.RawSockaddrInet4
|
|
Mask unix.RawSockaddrInet4
|
|
}
|
|
|
|
type ifAliasReq6 struct {
|
|
Name [unix.IFNAMSIZ]byte
|
|
Addr unix.RawSockaddrInet6
|
|
Dstaddr unix.RawSockaddrInet6
|
|
Mask unix.RawSockaddrInet6
|
|
Flags uint32
|
|
Lifetime addrLifetime6
|
|
}
|
|
|
|
type addrLifetime6 struct {
|
|
Expire float64
|
|
Preferred float64
|
|
Vltime uint32
|
|
Pltime uint32
|
|
}
|
|
|
|
// setIPAddress sets ipv4 and ipv6 addresses to the interface, required for the routing to work
|
|
func setIPAddress(name string, gateway netip.Prefix) error {
|
|
socket4, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(socket4)
|
|
|
|
// assume local ip address is next one from the remote address
|
|
local4 := gateway.Addr().As4()
|
|
local4[3]++
|
|
|
|
// fill the configuration for ipv4
|
|
ifReq4 := ifAliasReq4{
|
|
Addr: unix.RawSockaddrInet4{
|
|
Len: unix.SizeofSockaddrInet4,
|
|
Family: unix.AF_INET,
|
|
Addr: local4,
|
|
},
|
|
Dstaddr: unix.RawSockaddrInet4{
|
|
Len: unix.SizeofSockaddrInet4,
|
|
Family: unix.AF_INET,
|
|
Addr: gateway.Addr().As4(),
|
|
},
|
|
Mask: unix.RawSockaddrInet4{
|
|
Len: unix.SizeofSockaddrInet4,
|
|
Family: unix.AF_INET,
|
|
Addr: netip.MustParseAddr(net.IP(net.CIDRMask(gateway.Bits(), 32)).String()).As4(),
|
|
},
|
|
}
|
|
copy(ifReq4.Name[:], name)
|
|
if err = ioctlPtr(socket4, unix.SIOCAIFADDR, unsafe.Pointer(&ifReq4)); err != nil {
|
|
return os.NewSyscallError("SIOCAIFADDR", err)
|
|
}
|
|
|
|
socket6, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(socket6)
|
|
|
|
// link-local ipv6 address with suffix from ipv6
|
|
local6 := netip.AddrFrom16([16]byte{0: 0xfe, 1: 0x80, 12: local4[0], 13: local4[1], 14: local4[2], 15: local4[3]})
|
|
|
|
// fill the configuration for ipv6
|
|
// only link-local address without the destination is enough for it
|
|
ifReq6 := ifAliasReq6{
|
|
Addr: unix.RawSockaddrInet6{
|
|
Len: unix.SizeofSockaddrInet6,
|
|
Family: unix.AF_INET6,
|
|
Addr: local6.As16(),
|
|
},
|
|
Mask: unix.RawSockaddrInet6{
|
|
Len: unix.SizeofSockaddrInet6,
|
|
Family: unix.AF_INET6,
|
|
Addr: netip.MustParseAddr(net.IP(net.CIDRMask(64, 128)).String()).As16(),
|
|
},
|
|
Flags: IN6_IFF_NODAD,
|
|
Lifetime: addrLifetime6{
|
|
Vltime: ND6_INFINITE_LIFETIME,
|
|
Pltime: ND6_INFINITE_LIFETIME,
|
|
},
|
|
}
|
|
// assign link-local ipv6 address to the interface.
|
|
// this will additionally trigger OS level autoconfiguration, which will result two different link-local
|
|
// addresses - the requested one, and autoconfigured one.
|
|
// this really has no known side effects, just look excessive. and actually considered pretty normal way to
|
|
// enable the ipv6 on the interface by macOS concepts.
|
|
copy(ifReq6.Name[:], name)
|
|
if err = ioctlPtr(socket6, SIOCAIFADDR6, unsafe.Pointer(&ifReq6)); err != nil {
|
|
return os.NewSyscallError("SIOCAIFADDR6", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
|
|
_, _, errno := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
return nil
|
|
}
|