From bd9bdf73fec8d3b7a4efd2a0cef4055e9923712c Mon Sep 17 00:00:00 2001 From: "Xiaokang Wang (Shelikhoo)" Date: Wed, 8 Jul 2026 22:51:46 +0100 Subject: [PATCH] Add socks5ify engineering command (#3776) * Add socks5ify engineering command (Machine assisted.) * Add quiet and ipv4,ipv6 host, guest flag settings to engineering socks5ify command (Machine assisted.) * Fix lint errors (Machine assisted.) * fix typo in stun-nat-type-discovery command * Add support for keep uid for socks5ify, this will make some application that designed to not run as root happy --- app/tun/config.pb.go | 23 +- app/tun/config.proto | 3 +- app/tun/device/device.go | 6 +- app/tun/device/gvisor/gvisor_linux.go | 62 +++- app/tun/device/preopened_fd.go | 14 + app/tun/packetaddradaptar.go | 24 +- app/tun/tun.go | 41 ++- common/natTraversal/stun/stuncli/stuncli.go | 2 +- .../all/engineering/socks5ify/child.go | 332 ++++++++++++++++++ .../all/engineering/socks5ify/command.go | 260 ++++++++++++++ .../all/engineering/socks5ify/fdpass.go | 87 +++++ .../all/engineering/socks5ify/flags.go | 73 ++++ .../all/engineering/socks5ify/netlink.go | 138 ++++++++ .../all/engineering/socks5ify/parent.go | 110 ++++++ .../all/engineering/socks5ify/socks.go | 56 +++ .../all/engineering/socks5ify/types.go | 58 +++ .../all/engineering/socks5ify/v2ray.go | 130 +++++++ main/distro/all/socks5ify_linux.go | 6 + 18 files changed, 1399 insertions(+), 26 deletions(-) create mode 100644 app/tun/device/preopened_fd.go create mode 100644 main/commands/all/engineering/socks5ify/child.go create mode 100644 main/commands/all/engineering/socks5ify/command.go create mode 100644 main/commands/all/engineering/socks5ify/fdpass.go create mode 100644 main/commands/all/engineering/socks5ify/flags.go create mode 100644 main/commands/all/engineering/socks5ify/netlink.go create mode 100644 main/commands/all/engineering/socks5ify/parent.go create mode 100644 main/commands/all/engineering/socks5ify/socks.go create mode 100644 main/commands/all/engineering/socks5ify/types.go create mode 100644 main/commands/all/engineering/socks5ify/v2ray.go create mode 100644 main/distro/all/socks5ify_linux.go diff --git a/app/tun/config.pb.go b/app/tun/config.pb.go index edac3dd0b..f3d8301e6 100644 --- a/app/tun/config.pb.go +++ b/app/tun/config.pb.go @@ -1,3 +1,9 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.28.1 +// source: app/tun/config.proto + package tun import ( @@ -33,6 +39,7 @@ type Config struct { EnableSpoofing bool `protobuf:"varint,9,opt,name=enable_spoofing,json=enableSpoofing,proto3" json:"enable_spoofing,omitempty"` SocketSettings *internet.SocketConfig `protobuf:"bytes,10,opt,name=socket_settings,json=socketSettings,proto3" json:"socket_settings,omitempty"` SniffingSettings *proxyman.SniffingConfig `protobuf:"bytes,11,opt,name=sniffing_settings,json=sniffingSettings,proto3" json:"sniffing_settings,omitempty"` + PreopenedFd *int32 `protobuf:"varint,12,opt,name=preopened_fd,json=preopenedFd,proto3,oneof" json:"preopened_fd,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -144,11 +151,18 @@ func (x *Config) GetSniffingSettings() *proxyman.SniffingConfig { return nil } +func (x *Config) GetPreopenedFd() int32 { + if x != nil && x.PreopenedFd != nil { + return *x.PreopenedFd + } + return 0 +} + var File_app_tun_config_proto protoreflect.FileDescriptor const file_app_tun_config_proto_rawDesc = "" + "\n" + - "\x14app/tun/config.proto\x12\x12v2ray.core.app.tun\x1a\x19app/proxyman/config.proto\x1a$app/router/routercommon/common.proto\x1a common/protoext/extensions.proto\x1a\"common/net/packetaddr/config.proto\x1a\x1ftransport/internet/config.proto\"\xd2\x04\n" + + "\x14app/tun/config.proto\x12\x12v2ray.core.app.tun\x1a\x19app/proxyman/config.proto\x1a$app/router/routercommon/common.proto\x1a common/protoext/extensions.proto\x1a\"common/net/packetaddr/config.proto\x1a\x1ftransport/internet/config.proto\"\x8b\x05\n" + "\x06Config\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + "\x03mtu\x18\x02 \x01(\rR\x03mtu\x12\x1d\n" + @@ -162,8 +176,10 @@ const file_app_tun_config_proto_rawDesc = "" + "\x0fenable_spoofing\x18\t \x01(\bR\x0eenableSpoofing\x12T\n" + "\x0fsocket_settings\x18\n" + " \x01(\v2+.v2ray.core.transport.internet.SocketConfigR\x0esocketSettings\x12T\n" + - "\x11sniffing_settings\x18\v \x01(\v2'.v2ray.core.app.proxyman.SniffingConfigR\x10sniffingSettings:\x12\x82\xb5\x18\x0e\n" + - "\aservice\x12\x03tunBW\n" + + "\x11sniffing_settings\x18\v \x01(\v2'.v2ray.core.app.proxyman.SniffingConfigR\x10sniffingSettings\x12&\n" + + "\fpreopened_fd\x18\f \x01(\x05H\x00R\vpreopenedFd\x88\x01\x01:\x12\x82\xb5\x18\x0e\n" + + "\aservice\x12\x03tunB\x0f\n" + + "\r_preopened_fdBW\n" + "\x16com.v2ray.core.app.tunP\x01Z&github.com/v2fly/v2ray-core/v5/app/tun\xaa\x02\x12V2Ray.Core.App.Tunb\x06proto3" var ( @@ -204,6 +220,7 @@ func file_app_tun_config_proto_init() { if File_app_tun_config_proto != nil { return } + file_app_tun_config_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/app/tun/config.proto b/app/tun/config.proto index 494a42d4e..1acea99e7 100644 --- a/app/tun/config.proto +++ b/app/tun/config.proto @@ -27,4 +27,5 @@ message Config { bool enable_spoofing = 9; v2ray.core.transport.internet.SocketConfig socket_settings = 10; v2ray.core.app.proxyman.SniffingConfig sniffing_settings = 11; -} \ No newline at end of file + optional int32 preopened_fd = 12; +} diff --git a/app/tun/device/device.go b/app/tun/device/device.go index 1ed3575ff..fe6024f58 100644 --- a/app/tun/device/device.go +++ b/app/tun/device/device.go @@ -9,8 +9,10 @@ type Device interface { } type Options struct { - Name string - MTU uint32 + Name string + MTU uint32 + PreopenedFD int + PreopenedFDSet bool } type DeviceConstructor func(Options) (Device, error) diff --git a/app/tun/device/gvisor/gvisor_linux.go b/app/tun/device/gvisor/gvisor_linux.go index b46730526..5e3770d68 100644 --- a/app/tun/device/gvisor/gvisor_linux.go +++ b/app/tun/device/gvisor/gvisor_linux.go @@ -6,6 +6,7 @@ package gvisor import ( "fmt" + "sync" "unsafe" "golang.org/x/sys/unix" @@ -27,30 +28,59 @@ type GvisorTUN struct { options device.Options - fd int - mtu uint32 // real MTU + closeAccess sync.Mutex + fd int + mtu uint32 // real MTU } func New(options device.Options) (device.Device, error) { - t := &GvisorTUN{options: options} + t := &GvisorTUN{options: options, fd: -1} + + fd := -1 + preopened := options.PreopenedFDSet + if preopened { + if options.PreopenedFD < 0 { + return nil, newError("invalid preopened tun file descriptor").AtError() + } + fd = options.PreopenedFD + } if len(options.Name) > unix.IFNAMSIZ { + if preopened { + _ = unix.Close(fd) + } return nil, newError("name too long").AtError() } - fd, err := tun.Open(options.Name) - if err != nil { - return nil, newError("failed to open tun device").Base(err).AtError() + if preopened { + if err := unix.SetNonblock(fd, true); err != nil { + _ = unix.Close(fd) + return nil, newError("failed to set tun device non-blocking").Base(err).AtError() + } + } else { + var err error + fd, err = tun.Open(options.Name) + if err != nil { + return nil, newError("failed to open tun device").Base(err).AtError() + } + + if options.MTU > 0 { + _ = setMTU(options.Name, int(options.MTU)) + } } t.fd = fd - if options.MTU > 0 { - _ = setMTU(options.Name, int(options.MTU)) + mtu := options.MTU + if !preopened { + var err error + mtu, err = rawfile.GetMTU(options.Name) + if err != nil { + _ = unix.Close(fd) + return nil, newError("failed to get mtu").Base(err).AtError() + } } - - mtu, err := rawfile.GetMTU(options.Name) - if err != nil { - return nil, newError("failed to get mtu").Base(err).AtError() + if mtu == 0 { + mtu = 1500 } t.mtu = mtu @@ -66,6 +96,7 @@ func New(options device.Options) (device.Device, error) { MaxSyscallHeaderBytes: 0x00, }) if err != nil { + _ = unix.Close(fd) return nil, newError("failed to create link endpoint").Base(err).AtError() } t.LinkEndpoint = linkEndpoint @@ -74,7 +105,12 @@ func New(options device.Options) (device.Device, error) { } func (t *GvisorTUN) Close() { - _ = unix.Close(t.fd) + t.closeAccess.Lock() + defer t.closeAccess.Unlock() + if t.fd >= 0 { + _ = unix.Close(t.fd) + t.fd = -1 + } } // Modified from golang.zx2c4.com/wireguard/tun/tun_linux.go diff --git a/app/tun/device/preopened_fd.go b/app/tun/device/preopened_fd.go new file mode 100644 index 000000000..4c31f4fd0 --- /dev/null +++ b/app/tun/device/preopened_fd.go @@ -0,0 +1,14 @@ +package device + +import "os" + +func ClosePreopenedFD(fd int) error { + if fd < 0 { + return nil + } + file := os.NewFile(uintptr(fd), "preopened-tun") + if file == nil { + return nil + } + return file.Close() +} diff --git a/app/tun/packetaddradaptar.go b/app/tun/packetaddradaptar.go index 8f3afc420..1533cf13a 100644 --- a/app/tun/packetaddradaptar.go +++ b/app/tun/packetaddradaptar.go @@ -1,6 +1,8 @@ package tun import ( + "sync" + "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -19,6 +21,7 @@ type packetAddrDevice struct { device.Device sorter *tunsorter.TunSorter + dispatcherAccess sync.RWMutex secondaryDispatcher stack.NetworkDispatcher } @@ -26,16 +29,31 @@ func (p *packetAddrDevice) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNu buf := pkt.ToBuffer() _, err := p.sorter.OnPacketReceived(buf.Flatten()) if err != nil { - p.secondaryDispatcher.DeliverNetworkPacket(protocol, pkt) + p.dispatcherAccess.RLock() + dispatcher := p.secondaryDispatcher + p.dispatcherAccess.RUnlock() + if dispatcher != nil { + dispatcher.DeliverNetworkPacket(protocol, pkt) + } } } func (p *packetAddrDevice) DeliverLinkPacket(protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) { - // TODO implement me - panic("implement me") + p.dispatcherAccess.RLock() + dispatcher := p.secondaryDispatcher + p.dispatcherAccess.RUnlock() + if dispatcher != nil { + dispatcher.DeliverLinkPacket(protocol, pkt) + } } func (p *packetAddrDevice) Attach(dispatcher stack.NetworkDispatcher) { + p.dispatcherAccess.Lock() p.secondaryDispatcher = dispatcher + p.dispatcherAccess.Unlock() + if dispatcher == nil { + p.Device.Attach(nil) + return + } p.Device.Attach(p) } diff --git a/app/tun/tun.go b/app/tun/tun.go index ed86f146e..9e834c5ce 100644 --- a/app/tun/tun.go +++ b/app/tun/tun.go @@ -26,7 +26,10 @@ type TUN struct { policyManager policy.Manager config *Config - stack *stack.Stack + stack *stack.Stack + device device.Device + preopenedFD int + preopenedFDSet bool } func (t *TUN) Type() interface{} { @@ -35,13 +38,22 @@ func (t *TUN) Type() interface{} { func (t *TUN) Start() error { DeviceConstructor := gvisor.New - tunDevice, err := DeviceConstructor(device.Options{ + deviceOptions := device.Options{ Name: t.config.Name, MTU: t.config.Mtu, - }) + } + if t.preopenedFDSet { + deviceOptions.PreopenedFD = t.preopenedFD + deviceOptions.PreopenedFDSet = true + t.preopenedFD = -1 + t.preopenedFDSet = false + } + + tunDevice, err := DeviceConstructor(deviceOptions) if err != nil { return newError("failed to create device").Base(err).AtError() } + t.device = tunDevice if t.config.PacketEncoding != packetaddr.PacketAddrType_None { writer := device.NewLinkWriterToWriter(tunDevice) @@ -52,6 +64,10 @@ func (t *TUN) Start() error { stack, err := t.CreateStack(tunDevice) if err != nil { + if closer, ok := t.device.(interface{ Close() }); ok { + closer.Close() + } + t.device = nil return newError("failed to create stack").Base(err).AtError() } t.stack = stack @@ -63,6 +79,17 @@ func (t *TUN) Close() error { if t.stack != nil { t.stack.Close() t.stack.Wait() + t.stack = nil + } else if t.device != nil { + if closer, ok := t.device.(interface{ Close() }); ok { + closer.Close() + } + } + t.device = nil + if t.preopenedFDSet { + _ = device.ClosePreopenedFD(t.preopenedFD) + t.preopenedFD = -1 + t.preopenedFDSet = false } return nil } @@ -72,6 +99,14 @@ func (t *TUN) Init(ctx context.Context, config *Config, dispatcher routing.Dispa t.config = config t.dispatcher = dispatcher t.policyManager = policyManager + t.preopenedFD = -1 + if config.PreopenedFd != nil { + if *config.PreopenedFd < 0 { + return newError("invalid preopened_fd: ", *config.PreopenedFd).AtError() + } + t.preopenedFD = int(*config.PreopenedFd) + t.preopenedFDSet = true + } return nil } diff --git a/common/natTraversal/stun/stuncli/stuncli.go b/common/natTraversal/stun/stuncli/stuncli.go index 5dd0991dd..e8f61d3bc 100644 --- a/common/natTraversal/stun/stuncli/stuncli.go +++ b/common/natTraversal/stun/stuncli/stuncli.go @@ -38,7 +38,7 @@ The STUN server must support RFC 5780 (OTHER-ADDRESS and CHANGE-REQUEST) for full test coverage. Usage: - {{.Exec}} engineering stun-test -server [-server2 ] [-timeout ] [-attempts ] [-socks5udp ] + {{.Exec}} engineering stun-nat-type-discovery -server [-server2 ] [-timeout ] [-attempts ] [-socks5udp ] Options: -server diff --git a/main/commands/all/engineering/socks5ify/child.go b/main/commands/all/engineering/socks5ify/child.go new file mode 100644 index 000000000..3d7889d9f --- /dev/null +++ b/main/commands/all/engineering/socks5ify/child.go @@ -0,0 +1,332 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "strconv" + "strings" + "syscall" + + "golang.org/x/sys/unix" +) + +func runChildFromEnv() error { + sockFD, err := childSocketFD() + if err != nil { + return err + } + + cfg, err := decodeChildConfig(os.Getenv(childConfigEnv)) + if err != nil { + sendChildError(sockFD, err) + return err + } + + tunFD, err := setupChildNamespace(cfg) + if err != nil { + sendChildError(sockFD, err) + return err + } + + if err := sendFileDescriptor(sockFD, tunFD); err != nil { + _ = unix.Close(tunFD) + return err + } + _ = unix.Close(tunFD) + + ready := []byte{0} + if _, err := unix.Read(sockFD, ready); err != nil { + return fmt.Errorf("parent did not finish setup: %w", err) + } + if ready[0] != 1 { + return fmt.Errorf("parent reported setup failure") + } + + command := cfg.Command + if len(command) == 0 { + shell := os.Getenv("SHELL") + if shell == "" { + shell = "/bin/sh" + } + command = []string{shell} + } + + env := filteredChildEnv(os.Environ()) + argv0 := command[0] + if !strings.ContainsRune(argv0, '/') { + resolved, err := exec.LookPath(argv0) + if err != nil { + return err + } + argv0 = resolved + } + if cfg.KeepUID { + return runCommandWithCallerIdentity(cfg, argv0, command, env) + } + return syscall.Exec(argv0, command, env) +} + +func runCommandWithCallerIdentity(cfg childConfig, argv0 string, command []string, env []string) error { + if cfg.CallerUID <= 0 { + return fmt.Errorf("-keep-uid requires a non-root caller UID, got %d", cfg.CallerUID) + } + if cfg.CallerGID < 0 { + return fmt.Errorf("-keep-uid requires a valid caller GID, got %d", cfg.CallerGID) + } + + childCmd := exec.Command(argv0, command[1:]...) + childCmd.Args[0] = command[0] + childCmd.Stdin = os.Stdin + childCmd.Stdout = os.Stdout + childCmd.Stderr = os.Stderr + childCmd.Env = env + childCmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: unix.CLONE_NEWUSER, + UidMappings: []syscall.SysProcIDMap{ + {ContainerID: cfg.CallerUID, HostID: 0, Size: 1}, + }, + GidMappings: []syscall.SysProcIDMap{ + {ContainerID: cfg.CallerGID, HostID: 0, Size: 1}, + }, + GidMappingsEnableSetgroups: false, + Credential: &syscall.Credential{ + Uid: uint32(cfg.CallerUID), + Gid: uint32(cfg.CallerGID), + NoSetGroups: true, + }, + } + if err := childCmd.Start(); err != nil { + return fmt.Errorf("start command with caller UID/GID: %w", err) + } + waitForNestedCommand(childCmd) + return nil +} + +func waitForNestedCommand(childCmd *exec.Cmd) { + waitDone := make(chan error, 1) + go func() { + waitDone <- childCmd.Wait() + }() + + signals := make(chan os.Signal, 4) + signal.Notify(signals, os.Interrupt, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM) + defer signal.Stop(signals) + + for { + select { + case err := <-waitDone: + exitWithCommandStatus(err) + case sig := <-signals: + if sig == os.Interrupt { + continue + } + _ = childCmd.Process.Signal(sig) + } + } +} + +func exitWithCommandStatus(err error) { + if err == nil { + os.Exit(0) + } + if exitErr, ok := err.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + if status.Exited() { + os.Exit(status.ExitStatus()) + } + if status.Signaled() { + os.Exit(128 + int(status.Signal())) + } + } + } + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func setupChildNamespace(cfg childConfig) (int, error) { + if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil { + return -1, fmt.Errorf("failed to make mounts private: %w", err) + } + if err := applyMountOverrides(cfg); err != nil { + return -1, err + } + + tunFD, err := createTun(cfg.TunName) + if err != nil { + return -1, err + } + if err := configureInterface(cfg.TunName, cfg.MTU, cfg.IPv4, cfg.IPv6, cfg.IPv6Config); err != nil { + _ = unix.Close(tunFD) + return -1, err + } + return tunFD, nil +} + +func filteredChildEnv(env []string) []string { + out := make([]string, 0, len(env)) + for _, item := range env { + if strings.HasPrefix(item, childConfigEnv+"=") || strings.HasPrefix(item, childSockFDEnv+"=") { + continue + } + out = append(out, item) + } + return out +} + +func applyMountOverrides(cfg childConfig) error { + if len(cfg.DNS) > 0 { + resolvFile, err := writeGeneratedResolvConf(cfg.DNS) + if err != nil { + return err + } + defer os.Remove(resolvFile) + if err := bindMountFile(resolvFile, "/etc/resolv.conf"); err != nil { + return err + } + } + if cfg.ResolvConf != "" { + if err := bindMountFile(cfg.ResolvConf, "/etc/resolv.conf"); err != nil { + return err + } + } + for _, bind := range cfg.BindFiles { + if err := bindMountFile(bind.Source, bind.Target); err != nil { + return err + } + } + return nil +} + +func writeGeneratedResolvConf(servers []string) (string, error) { + var builder strings.Builder + for _, server := range servers { + server = strings.TrimSpace(server) + if server == "" { + continue + } + builder.WriteString("nameserver ") + builder.WriteString(server) + builder.WriteByte('\n') + } + if builder.Len() == 0 { + return "", fmt.Errorf("-dns did not contain any DNS server") + } + + file, err := os.CreateTemp("", "socks5ify-resolv-*.conf") + if err != nil { + return "", err + } + defer file.Close() + if _, err := file.WriteString(builder.String()); err != nil { + return "", err + } + return file.Name(), nil +} + +func bindMountFile(source string, target string) error { + if _, err := os.Stat(source); err != nil { + return fmt.Errorf("bind source %q: %w", source, err) + } + targetInfo, err := os.Stat(target) + if err != nil { + return fmt.Errorf("bind target %q: %w", target, err) + } + if targetInfo.IsDir() { + return fmt.Errorf("bind target %q is a directory", target) + } + if err := unix.Mount(source, target, "", unix.MS_BIND, ""); err != nil { + return fmt.Errorf("bind mount %q onto %q: %w", source, target, err) + } + return nil +} + +func createTun(name string) (int, error) { + fd, err := unix.Open("/dev/net/tun", unix.O_RDWR|unix.O_CLOEXEC, 0) + if err != nil { + return -1, err + } + ifr, err := unix.NewIfreq(name) + if err != nil { + _ = unix.Close(fd) + return -1, err + } + ifr.SetUint16(unix.IFF_TUN | unix.IFF_NO_PI) + if err := unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr); err != nil { + _ = unix.Close(fd) + return -1, err + } + return fd, nil +} + +func configureInterface(name string, mtu int, ipv4 tunProtocolConfig, enableIPv6 bool, ipv6 tunProtocolConfig) error { + if err := setLinkMTU(name, mtu); err != nil { + return err + } + if err := setLinkUp("lo"); err != nil { + return err + } + if err := setLinkUp(name); err != nil { + return err + } + iface, err := interfaceIndex(name) + if err != nil { + return err + } + if err := addAddress(iface, ipv4.Guest, ipv4.Prefix); err != nil { + return err + } + if err := addDefaultRoute(iface, false); err != nil { + return err + } + if enableIPv6 { + if err := addAddress(iface, ipv6.Guest, ipv6.Prefix); err != nil { + return err + } + if err := addDefaultRoute(iface, true); err != nil { + return err + } + } + return nil +} + +func setLinkMTU(name string, mtu int) error { + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, 0) + if err != nil { + return err + } + defer unix.Close(fd) + ifr, err := unix.NewIfreq(name) + if err != nil { + return err + } + ifr.SetUint32(uint32(mtu)) + if err := unix.IoctlIfreq(fd, unix.SIOCSIFMTU, ifr); err != nil { + return fmt.Errorf("set MTU on %s to %s: %w", name, strconv.Itoa(mtu), err) + } + return nil +} + +func setLinkUp(name string) error { + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, 0) + if err != nil { + return err + } + defer unix.Close(fd) + ifr, err := unix.NewIfreq(name) + if err != nil { + return err + } + if err := unix.IoctlIfreq(fd, unix.SIOCGIFFLAGS, ifr); err != nil { + return fmt.Errorf("get flags for %s: %w", name, err) + } + ifr.SetUint16(ifr.Uint16() | unix.IFF_UP) + if err := unix.IoctlIfreq(fd, unix.SIOCSIFFLAGS, ifr); err != nil { + return fmt.Errorf("set %s up: %w", name, err) + } + return nil +} diff --git a/main/commands/all/engineering/socks5ify/command.go b/main/commands/all/engineering/socks5ify/command.go new file mode 100644 index 000000000..49804b350 --- /dev/null +++ b/main/commands/all/engineering/socks5ify/command.go @@ -0,0 +1,260 @@ +//go:build linux && !confonly +// +build linux,!confonly + +/* +This feature is machine generated. +*/ + +package socks5ify + +import ( + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering" + "github.com/v2fly/v2ray-core/v5/main/commands/base" +) + +var ( + socksFlag *string + socksUserFlag *string + socksPassFlag *string + quietFlag *bool + keepUIDFlag *bool + tunNameFlag *string + mtuFlag *int + ipv4HostFlag *string + ipv4GuestFlag *string + ipv4PrefixFlag *int + ipv6Flag *bool + ipv6HostFlag *string + ipv6GuestFlag *string + ipv6PrefixFlag *int + dnsFlag *string + resolvConfFlag *string + bindFilesFlag bindFileFlags +) + +var cmdSocks5ify = &base.Command{ + UsageLine: "{{.Exec}} engineering socks5ify", + Short: "run a shell whose traffic is sent through a SOCKS5 proxy", + Long: ` +Create an unprivileged user, mount, and network namespace, configure a TUN +interface inside it, run V2Ray TUN outside the namespace, and start a shell or +command whose TCP and UDP traffic is proxied through SOCKS5. + +Arguments: + + -socks + Upstream SOCKS5 server. Required. + + -socks-user, -socks-pass + Optional SOCKS5 username and password. These override URL credentials. + + -quiet + Suppress non-error messages from the embedded V2Ray instance. + + -keep-uid + Run the command as the original non-root caller UID and primary GID + after namespace setup is complete. + + -tun-name + TUN interface name inside the network namespace. Default socks5ify0. + + -mtu + TUN MTU. Default 1500. + + -ipv6 + Also configure IPv6 address and default route. + + -tun-ipv4-host + V2Ray-side IPv4 address. Default 198.18.0.1. + + -tun-ipv4-guest + Namespace-side IPv4 address. Default 198.18.0.2. + + -tun-ipv4-prefix, -tun-ipv4-mask + IPv4 address prefix length. Default 30. + + -tun-ipv6-host + V2Ray-side IPv6 address. Default fd00:736f:636b:35::1. + + -tun-ipv6-guest + Namespace-side IPv6 address. Default fd00:736f:636b:35::2. + + -tun-ipv6-prefix, -tun-ipv6-mask + IPv6 address prefix length. Default 126. + + -dns + Opt-in generated /etc/resolv.conf override inside the mount namespace. + + -resolv-conf + Opt-in bind mount of an existing resolver file onto /etc/resolv.conf. + + -bind-file + Read-only bind mount for a single file. May be repeated. + +Examples: + + {{.Exec}} engineering socks5ify -socks 127.0.0.1:1080 + {{.Exec}} engineering socks5ify -socks socks5://user:pass@127.0.0.1:1080 -- curl https://example.com +`, + Flag: func() flag.FlagSet { + fs := flag.NewFlagSet("", flag.ExitOnError) + socksFlag = fs.String("socks", "", "") + socksUserFlag = fs.String("socks-user", "", "") + socksPassFlag = fs.String("socks-pass", "", "") + quietFlag = fs.Bool("quiet", false, "") + keepUIDFlag = fs.Bool("keep-uid", false, "") + tunNameFlag = fs.String("tun-name", defaultTunName, "") + mtuFlag = fs.Int("mtu", defaultMTU, "") + ipv4HostFlag = fs.String("tun-ipv4-host", defaultTunIPv4Host, "") + ipv4GuestFlag = fs.String("tun-ipv4-guest", defaultTunIPv4Guest, "") + ipv4PrefixFlag = fs.Int("tun-ipv4-prefix", defaultTunIPv4Prefix, "") + fs.IntVar(ipv4PrefixFlag, "tun-ipv4-mask", defaultTunIPv4Prefix, "") + ipv6Flag = fs.Bool("ipv6", false, "") + ipv6HostFlag = fs.String("tun-ipv6-host", defaultTunIPv6Host, "") + ipv6GuestFlag = fs.String("tun-ipv6-guest", defaultTunIPv6Guest, "") + ipv6PrefixFlag = fs.Int("tun-ipv6-prefix", defaultTunIPv6Prefix, "") + fs.IntVar(ipv6PrefixFlag, "tun-ipv6-mask", defaultTunIPv6Prefix, "") + dnsFlag = fs.String("dns", "", "") + resolvConfFlag = fs.String("resolv-conf", "", "") + fs.Var(&bindFilesFlag, "bind-file", "") + return *fs + }(), + Run: executeSocks5ify, +} + +func init() { + engineering.AddCommand(cmdSocks5ify) +} + +func executeSocks5ify(cmd *base.Command, args []string) { + if os.Getenv(childConfigEnv) != "" { + if err := runChildFromEnv(); err != nil { + base.Fatalf("socks5ify child failed: %v", err) + } + return + } + + if err := cmd.Flag.Parse(args); err != nil { + base.Fatalf("failed to parse flags: %v", err) + } + + opts, child, err := buildOptions(cmd.Flag.Args()) + if err != nil { + base.Fatalf("%v", err) + } + + if err := runParent(opts, child); err != nil { + base.Fatalf("socks5ify failed: %v", err) + } +} + +func buildOptions(command []string) (parentOptions, childConfig, error) { + if *socksFlag == "" { + return parentOptions{}, childConfig{}, fmt.Errorf("-socks is required") + } + if *mtuFlag <= 0 { + return parentOptions{}, childConfig{}, fmt.Errorf("-mtu must be positive, got %d", *mtuFlag) + } + if *tunNameFlag == "" { + return parentOptions{}, childConfig{}, fmt.Errorf("-tun-name must not be empty") + } + if *dnsFlag != "" && *resolvConfFlag != "" { + return parentOptions{}, childConfig{}, fmt.Errorf("-dns and -resolv-conf are mutually exclusive") + } + if !*ipv6Flag && ipv6AddressOptionsChanged() { + return parentOptions{}, childConfig{}, fmt.Errorf("IPv6 TUN address options require -ipv6") + } + + socksServer, err := parseSocksServer(*socksFlag, *socksUserFlag, *socksPassFlag) + if err != nil { + return parentOptions{}, childConfig{}, err + } + ipv4, err := parseTunProtocolConfig("tun-ipv4", *ipv4HostFlag, *ipv4GuestFlag, *ipv4PrefixFlag, false) + if err != nil { + return parentOptions{}, childConfig{}, err + } + ipv6, err := parseTunProtocolConfig("tun-ipv6", *ipv6HostFlag, *ipv6GuestFlag, *ipv6PrefixFlag, true) + if err != nil { + return parentOptions{}, childConfig{}, err + } + + child := childConfig{ + TunName: *tunNameFlag, + MTU: *mtuFlag, + KeepUID: *keepUIDFlag && os.Getuid() != 0, + CallerUID: os.Getuid(), + CallerGID: os.Getgid(), + IPv4: ipv4, + IPv6: *ipv6Flag, + IPv6Config: ipv6, + DNS: splitCommaList(*dnsFlag), + ResolvConf: *resolvConfFlag, + BindFiles: append([]bindFile(nil), bindFilesFlag...), + Command: append([]string(nil), command...), + } + return parentOptions{SOCKS: socksServer, Quiet: *quietFlag}, child, nil +} + +func ipv6AddressOptionsChanged() bool { + return *ipv6HostFlag != defaultTunIPv6Host || + *ipv6GuestFlag != defaultTunIPv6Guest || + *ipv6PrefixFlag != defaultTunIPv6Prefix +} + +func splitCommaList(raw string) []string { + if raw == "" { + return nil + } + parts := strings.Split(raw, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + out = append(out, part) + } + } + return out +} + +func encodeChildConfig(cfg childConfig) (string, error) { + raw, err := json.Marshal(cfg) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(raw), nil +} + +func decodeChildConfig(raw string) (childConfig, error) { + data, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return childConfig{}, err + } + var cfg childConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return childConfig{}, err + } + return cfg, nil +} + +func childSocketFD() (int, error) { + raw := os.Getenv(childSockFDEnv) + if raw == "" { + return -1, fmt.Errorf("%s is not set", childSockFDEnv) + } + fd, err := strconv.Atoi(raw) + if err != nil { + return -1, err + } + if fd < 0 { + return -1, fmt.Errorf("invalid child socket fd %d", fd) + } + return fd, nil +} diff --git a/main/commands/all/engineering/socks5ify/fdpass.go b/main/commands/all/engineering/socks5ify/fdpass.go new file mode 100644 index 000000000..e967f3260 --- /dev/null +++ b/main/commands/all/engineering/socks5ify/fdpass.go @@ -0,0 +1,87 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +import ( + "fmt" + + "golang.org/x/sys/unix" +) + +const ( + tunFDMessage = "tun" + childErrorPrefix = "error:" + maxReceivedFDs = 16 +) + +func sendFileDescriptor(sock int, fd int) error { + rights := unix.UnixRights(fd) + return unix.Sendmsg(sock, []byte(tunFDMessage), rights, nil, 0) +} + +func sendChildError(sock int, err error) { + _ = unix.Sendmsg(sock, []byte(childErrorPrefix+err.Error()), nil, nil, 0) +} + +func recvFileDescriptor(sock int) (int, error) { + payload := make([]byte, 256) + oob := make([]byte, unix.CmsgSpace(4*maxReceivedFDs)) + n, oobn, flags, _, err := unix.Recvmsg(sock, payload, oob, 0) + if err != nil { + return -1, err + } + + fds, err := parseFileDescriptors(oob[:oobn]) + if err != nil { + return -1, err + } + if flags&unix.MSG_CTRUNC != 0 { + closeFileDescriptors(fds) + return -1, fmt.Errorf("file descriptor message truncated") + } + if flags&unix.MSG_TRUNC != 0 { + closeFileDescriptors(fds) + return -1, fmt.Errorf("child message truncated") + } + + if n >= len(childErrorPrefix) && string(payload[:len(childErrorPrefix)]) == childErrorPrefix { + closeFileDescriptors(fds) + return -1, fmt.Errorf("%s", string(payload[len(childErrorPrefix):n])) + } + if string(payload[:n]) != tunFDMessage { + closeFileDescriptors(fds) + return -1, fmt.Errorf("unexpected child message %q", string(payload[:n])) + } + if len(fds) == 0 { + return -1, fmt.Errorf("child did not send a file descriptor") + } + closeFileDescriptors(fds[1:]) + return fds[0], nil +} + +func parseFileDescriptors(oob []byte) ([]int, error) { + messages, err := unix.ParseSocketControlMessage(oob) + if err != nil { + return nil, err + } + var allFDs []int + for _, message := range messages { + if message.Header.Level != unix.SOL_SOCKET || message.Header.Type != unix.SCM_RIGHTS { + continue + } + fds, err := unix.ParseUnixRights(&message) + if err != nil { + closeFileDescriptors(allFDs) + return nil, err + } + allFDs = append(allFDs, fds...) + } + return allFDs, nil +} + +func closeFileDescriptors(fds []int) { + for _, fd := range fds { + _ = unix.Close(fd) + } +} diff --git a/main/commands/all/engineering/socks5ify/flags.go b/main/commands/all/engineering/socks5ify/flags.go new file mode 100644 index 000000000..4b681f5f7 --- /dev/null +++ b/main/commands/all/engineering/socks5ify/flags.go @@ -0,0 +1,73 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +import ( + "fmt" + stdnet "net" + "strings" +) + +type bindFileFlags []bindFile + +func (f *bindFileFlags) String() string { + if f == nil { + return "" + } + parts := make([]string, 0, len(*f)) + for _, item := range *f { + parts = append(parts, item.Source+":"+item.Target) + } + return strings.Join(parts, ",") +} + +func (f *bindFileFlags) Set(raw string) error { + source, target, ok := strings.Cut(raw, ":") + if !ok || source == "" || target == "" { + return fmt.Errorf("bind-file must be source:target") + } + *f = append(*f, bindFile{Source: source, Target: target}) + return nil +} + +func parseTunProtocolConfig(name string, host string, guest string, prefix int, ipv6 bool) (tunProtocolConfig, error) { + hostIP, err := parseIPFlag(name+"-host", host, ipv6) + if err != nil { + return tunProtocolConfig{}, err + } + guestIP, err := parseIPFlag(name+"-guest", guest, ipv6) + if err != nil { + return tunProtocolConfig{}, err + } + maxPrefix := 32 + if ipv6 { + maxPrefix = 128 + } + if prefix < 0 || prefix > maxPrefix { + return tunProtocolConfig{}, fmt.Errorf("-%s-prefix must be between 0 and %d, got %d", name, maxPrefix, prefix) + } + return tunProtocolConfig{ + Host: hostIP, + Guest: guestIP, + Prefix: prefix, + }, nil +} + +func parseIPFlag(name string, raw string, ipv6 bool) (string, error) { + ip := stdnet.ParseIP(raw) + if ip == nil { + return "", fmt.Errorf("-%s must be a valid IP address, got %q", name, raw) + } + if ipv6 { + if ip.To4() != nil || ip.To16() == nil { + return "", fmt.Errorf("-%s must be an IPv6 address, got %q", name, raw) + } + return ip.String(), nil + } + ip4 := ip.To4() + if ip4 == nil { + return "", fmt.Errorf("-%s must be an IPv4 address, got %q", name, raw) + } + return ip4.String(), nil +} diff --git a/main/commands/all/engineering/socks5ify/netlink.go b/main/commands/all/engineering/socks5ify/netlink.go new file mode 100644 index 000000000..0841947e4 --- /dev/null +++ b/main/commands/all/engineering/socks5ify/netlink.go @@ -0,0 +1,138 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +import ( + "bytes" + "encoding/binary" + "fmt" + stdnet "net" + "syscall" + + "golang.org/x/sys/unix" +) + +func interfaceIndex(name string) (int, error) { + iface, err := stdnet.InterfaceByName(name) + if err != nil { + return 0, err + } + return iface.Index, nil +} + +func addAddress(ifindex int, ipText string, prefix int) error { + ip := stdnet.ParseIP(ipText) + if ip == nil { + return fmt.Errorf("invalid IP address %q", ipText) + } + + family := uint8(unix.AF_INET) + addr := ip.To4() + if addr == nil { + family = unix.AF_INET6 + addr = ip.To16() + } + if addr == nil { + return fmt.Errorf("invalid IP address %q", ipText) + } + + msg := unix.IfAddrmsg{ + Family: family, + Prefixlen: uint8(prefix), + Scope: unix.RT_SCOPE_UNIVERSE, + Index: uint32(ifindex), + } + payload := append(marshal(msg), + rtAttr(unix.IFA_LOCAL, addr)...) + payload = append(payload, rtAttr(unix.IFA_ADDRESS, addr)...) + return netlinkRequest(unix.RTM_NEWADDR, unix.NLM_F_REQUEST|unix.NLM_F_ACK|unix.NLM_F_CREATE|unix.NLM_F_REPLACE, payload) +} + +func addDefaultRoute(ifindex int, ipv6 bool) error { + family := uint8(unix.AF_INET) + if ipv6 { + family = unix.AF_INET6 + } + msg := unix.RtMsg{ + Family: family, + Table: unix.RT_TABLE_MAIN, + Protocol: unix.RTPROT_BOOT, + Scope: unix.RT_SCOPE_LINK, + Type: unix.RTN_UNICAST, + } + oif := make([]byte, 4) + binary.NativeEndian.PutUint32(oif, uint32(ifindex)) + payload := append(marshal(msg), rtAttr(unix.RTA_OIF, oif)...) + return netlinkRequest(unix.RTM_NEWROUTE, unix.NLM_F_REQUEST|unix.NLM_F_ACK|unix.NLM_F_CREATE|unix.NLM_F_REPLACE, payload) +} + +func netlinkRequest(messageType uint16, flags int, payload []byte) error { + fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE) + if err != nil { + return err + } + defer unix.Close(fd) + + seq := uint32(1) + header := unix.NlMsghdr{ + Len: uint32(unix.NLMSG_HDRLEN + len(payload)), + Type: messageType, + Flags: uint16(flags), + Seq: seq, + Pid: uint32(unix.Getpid()), + } + request := append(marshal(header), payload...) + if err := unix.Sendto(fd, request, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { + return err + } + + response := make([]byte, 8192) + for { + n, _, err := unix.Recvfrom(fd, response, 0) + if err != nil { + return err + } + messages, err := syscall.ParseNetlinkMessage(response[:n]) + if err != nil { + return err + } + for _, message := range messages { + if message.Header.Seq != seq { + continue + } + switch message.Header.Type { + case unix.NLMSG_ERROR: + var errMessage unix.NlMsgerr + if err := binary.Read(bytes.NewReader(message.Data), binary.NativeEndian, &errMessage); err != nil { + return err + } + if errMessage.Error == 0 { + return nil + } + return syscall.Errno(-errMessage.Error) + case unix.NLMSG_DONE: + return nil + } + } + } +} + +func rtAttr(attrType int, data []byte) []byte { + length := unix.SizeofRtAttr + len(data) + attr := unix.RtAttr{ + Len: uint16(length), + Type: uint16(attrType), + } + out := append(marshal(attr), data...) + for len(out)%4 != 0 { + out = append(out, 0) + } + return out +} + +func marshal(value interface{}) []byte { + var buf bytes.Buffer + _ = binary.Write(&buf, binary.NativeEndian, value) + return buf.Bytes() +} diff --git a/main/commands/all/engineering/socks5ify/parent.go b/main/commands/all/engineering/socks5ify/parent.go new file mode 100644 index 000000000..9eb2d187d --- /dev/null +++ b/main/commands/all/engineering/socks5ify/parent.go @@ -0,0 +1,110 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "syscall" + + "golang.org/x/sys/unix" +) + +func runParent(opts parentOptions, child childConfig) error { + socketPair, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_SEQPACKET, 0) + if err != nil { + return err + } + parentSock := socketPair[0] + childSock := socketPair[1] + defer unix.Close(parentSock) + + encodedChildConfig, err := encodeChildConfig(child) + if err != nil { + unix.Close(childSock) + return err + } + + childFile := os.NewFile(uintptr(childSock), "socks5ify-control") + childCmd := exec.Command(os.Args[0], "engineering", "socks5ify") + childCmd.Stdin = os.Stdin + childCmd.Stdout = os.Stdout + childCmd.Stderr = os.Stderr + childCmd.Env = append(os.Environ(), + childConfigEnv+"="+encodedChildConfig, + childSockFDEnv+"=3", + ) + childCmd.ExtraFiles = []*os.File{childFile} + childCmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: unix.CLONE_NEWUSER | unix.CLONE_NEWNS | unix.CLONE_NEWNET, + UidMappings: []syscall.SysProcIDMap{ + {ContainerID: 0, HostID: os.Getuid(), Size: 1}, + }, + GidMappings: []syscall.SysProcIDMap{ + {ContainerID: 0, HostID: os.Getgid(), Size: 1}, + }, + GidMappingsEnableSetgroups: false, + } + + if err := childCmd.Start(); err != nil { + childFile.Close() + return err + } + childFile.Close() + + tunFD, err := recvFileDescriptor(parentSock) + if err != nil { + _ = childCmd.Wait() + return err + } + + server, err := startV2RayWithTun(opts, child, tunFD) + if err != nil { + _ = childCmd.Process.Kill() + _ = childCmd.Wait() + return err + } + defer server.Close() + + if _, err := unix.Write(parentSock, []byte{1}); err != nil { + _ = childCmd.Process.Kill() + _ = childCmd.Wait() + return fmt.Errorf("failed to release child process: %w", err) + } + + return waitForChild(childCmd) +} + +func waitForChild(childCmd *exec.Cmd) error { + waitDone := make(chan error, 1) + go func() { + waitDone <- childCmd.Wait() + }() + + signals := make(chan os.Signal, 2) + signal.Notify(signals, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(signals) + + for { + select { + case err := <-waitDone: + if err == nil { + return nil + } + if exitErr, ok := err.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok && status.ExitStatus() == 0 { + return nil + } + } + return err + case sig := <-signals: + if sig == os.Interrupt { + continue + } + _ = childCmd.Process.Signal(sig) + } + } +} diff --git a/main/commands/all/engineering/socks5ify/socks.go b/main/commands/all/engineering/socks5ify/socks.go new file mode 100644 index 000000000..9c3c907e0 --- /dev/null +++ b/main/commands/all/engineering/socks5ify/socks.go @@ -0,0 +1,56 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +import ( + "fmt" + stdnet "net" + "net/url" + "strconv" + "strings" +) + +func parseSocksServer(raw string, userOverride string, passOverride string) (socksServer, error) { + if !strings.Contains(raw, "://") { + raw = "socks5://" + raw + } + parsed, err := url.Parse(raw) + if err != nil { + return socksServer{}, err + } + if parsed.Scheme != "socks5" && parsed.Scheme != "socks" { + return socksServer{}, fmt.Errorf("unsupported SOCKS scheme %q", parsed.Scheme) + } + if parsed.Path != "" && parsed.Path != "/" { + return socksServer{}, fmt.Errorf("SOCKS URL must not include a path") + } + host, portText, err := stdnet.SplitHostPort(parsed.Host) + if err != nil { + return socksServer{}, fmt.Errorf("invalid SOCKS address %q: %w", parsed.Host, err) + } + port, err := strconv.ParseUint(portText, 10, 16) + if err != nil || port == 0 { + return socksServer{}, fmt.Errorf("invalid SOCKS port %q", portText) + } + + username := "" + password := "" + if parsed.User != nil { + username = parsed.User.Username() + password, _ = parsed.User.Password() + } + if userOverride != "" { + username = userOverride + } + if passOverride != "" { + password = passOverride + } + + return socksServer{ + Host: host, + Port: uint32(port), + Username: username, + Password: password, + }, nil +} diff --git a/main/commands/all/engineering/socks5ify/types.go b/main/commands/all/engineering/socks5ify/types.go new file mode 100644 index 000000000..99a5a6238 --- /dev/null +++ b/main/commands/all/engineering/socks5ify/types.go @@ -0,0 +1,58 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +const ( + childConfigEnv = "V2RAY_SOCKS5IFY_CHILD_CONFIG" + childSockFDEnv = "V2RAY_SOCKS5IFY_CHILD_SOCK_FD" + + defaultTunName = "socks5ify0" + defaultMTU = 1500 + + defaultTunIPv4Host = "198.18.0.1" + defaultTunIPv4Guest = "198.18.0.2" + defaultTunIPv4Prefix = 30 + + defaultTunIPv6Host = "fd00:736f:636b:35::1" + defaultTunIPv6Guest = "fd00:736f:636b:35::2" + defaultTunIPv6Prefix = 126 +) + +type bindFile struct { + Source string `json:"source"` + Target string `json:"target"` +} + +type tunProtocolConfig struct { + Host string `json:"host"` + Guest string `json:"guest"` + Prefix int `json:"prefix"` +} + +type childConfig struct { + TunName string `json:"tun_name"` + MTU int `json:"mtu"` + KeepUID bool `json:"keep_uid"` + CallerUID int `json:"caller_uid"` + CallerGID int `json:"caller_gid"` + IPv4 tunProtocolConfig `json:"ipv4"` + IPv6 bool `json:"ipv6"` + IPv6Config tunProtocolConfig `json:"ipv6_config"` + DNS []string `json:"dns"` + ResolvConf string `json:"resolv_conf"` + BindFiles []bindFile `json:"bind_files"` + Command []string `json:"command"` +} + +type parentOptions struct { + SOCKS socksServer + Quiet bool +} + +type socksServer struct { + Host string + Port uint32 + Username string + Password string +} diff --git a/main/commands/all/engineering/socks5ify/v2ray.go b/main/commands/all/engineering/socks5ify/v2ray.go new file mode 100644 index 000000000..56ad7eabc --- /dev/null +++ b/main/commands/all/engineering/socks5ify/v2ray.go @@ -0,0 +1,130 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package socks5ify + +import ( + "fmt" + stdnet "net" + + "golang.org/x/sys/unix" + "google.golang.org/protobuf/types/known/anypb" + + core "github.com/v2fly/v2ray-core/v5" + "github.com/v2fly/v2ray-core/v5/app/dispatcher" + applog "github.com/v2fly/v2ray-core/v5/app/log" + "github.com/v2fly/v2ray-core/v5/app/proxyman" + "github.com/v2fly/v2ray-core/v5/app/router/routercommon" + tunapp "github.com/v2fly/v2ray-core/v5/app/tun" + clog "github.com/v2fly/v2ray-core/v5/common/log" + v2net "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/net/packetaddr" + "github.com/v2fly/v2ray-core/v5/common/protocol" + "github.com/v2fly/v2ray-core/v5/common/serial" + "github.com/v2fly/v2ray-core/v5/proxy/socks" +) + +func startV2RayWithTun(opts parentOptions, child childConfig, tunFD int) (*core.Instance, error) { + if int(int32(tunFD)) != tunFD { + _ = unix.Close(tunFD) + return nil, fmt.Errorf("invalid TUN fd %d", tunFD) + } + + config := buildCoreConfig(opts, child, int32(tunFD)) + server, err := core.New(config) + if err != nil { + _ = unix.Close(tunFD) + return nil, err + } + if err := server.Start(); err != nil { + _ = server.Close() + return nil, err + } + return server, nil +} + +func buildCoreConfig(opts parentOptions, child childConfig, tunFD int32) *core.Config { + tunConfig := &tunapp.Config{ + Name: child.TunName, + Mtu: uint32(child.MTU), + PreopenedFd: &tunFD, + Tag: "socks5ify-tun", + PacketEncoding: packetaddr.PacketAddrType_Packet, + EnablePromiscuousMode: true, + EnableSpoofing: true, + Ips: []*routercommon.CIDR{ + cidr(child.IPv4.Host, child.IPv4.Prefix), + }, + Routes: []*routercommon.CIDR{ + cidr("0.0.0.0", 0), + }, + } + if child.IPv6 { + tunConfig.Ips = append(tunConfig.Ips, cidr(child.IPv6Config.Host, child.IPv6Config.Prefix)) + tunConfig.Routes = append(tunConfig.Routes, cidr("::", 0)) + } + apps := []*anypb.Any{ + serial.ToTypedMessage(&dispatcher.Config{}), + serial.ToTypedMessage(&proxyman.InboundConfig{}), + serial.ToTypedMessage(&proxyman.OutboundConfig{}), + serial.ToTypedMessage(tunConfig), + } + if opts.Quiet { + apps = append([]*anypb.Any{serial.ToTypedMessage(quietLogConfig())}, apps...) + } + + socksConfig := &socks.ClientConfig{ + Version: socks.Version_SOCKS5, + Server: []*protocol.ServerEndpoint{ + { + Address: v2net.NewIPOrDomain(v2net.ParseAddress(opts.SOCKS.Host)), + Port: opts.SOCKS.Port, + User: socksUsers(opts.SOCKS), + }, + }, + } + + return &core.Config{ + App: apps, + Outbound: []*core.OutboundHandlerConfig{ + { + Tag: "socks5ify-socks", + ProxySettings: serial.ToTypedMessage(socksConfig), + }, + }, + } +} + +func quietLogConfig() *applog.Config { + return &applog.Config{ + Error: &applog.LogSpecification{ + Type: applog.LogType_Console, + Level: clog.Severity_Error, + }, + Access: &applog.LogSpecification{ + Type: applog.LogType_None, + }, + } +} + +func socksUsers(server socksServer) []*protocol.User { + if server.Username == "" && server.Password == "" { + return nil + } + return []*protocol.User{ + { + Account: serial.ToTypedMessage(&socks.Account{ + Username: server.Username, + Password: server.Password, + }), + }, + } +} + +func cidr(ipText string, prefix int) *routercommon.CIDR { + ip := stdnet.ParseIP(ipText) + if ip4 := ip.To4(); ip4 != nil { + return &routercommon.CIDR{Ip: ip4, Prefix: uint32(prefix)} + } + return &routercommon.CIDR{Ip: ip.To16(), Prefix: uint32(prefix)} +} diff --git a/main/distro/all/socks5ify_linux.go b/main/distro/all/socks5ify_linux.go new file mode 100644 index 000000000..24765c011 --- /dev/null +++ b/main/distro/all/socks5ify_linux.go @@ -0,0 +1,6 @@ +//go:build linux && !confonly +// +build linux,!confonly + +package all + +import _ "github.com/v2fly/v2ray-core/v5/main/commands/all/engineering/socks5ify"