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
This commit is contained in:
Xiaokang Wang (Shelikhoo)
2026-07-08 22:51:46 +01:00
committed by GitHub
parent ce48e2f9ec
commit bd9bdf73fe
18 changed files with 1399 additions and 26 deletions
+20 -3
View File
@@ -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{
+2 -1
View File
@@ -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;
}
optional int32 preopened_fd = 12;
}
+4 -2
View File
@@ -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)
+49 -13
View File
@@ -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
+14
View File
@@ -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()
}
+21 -3
View File
@@ -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)
}
+38 -3
View File
@@ -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
}
+1 -1
View File
@@ -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 <host:port> [-server2 <host:port>] [-timeout <ms>] [-attempts <n>] [-socks5udp <host:port>]
{{.Exec}} engineering stun-nat-type-discovery -server <host:port> [-server2 <host:port>] [-timeout <ms>] [-attempts <n>] [-socks5udp <host:port>]
Options:
-server <host:port>
@@ -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
}
@@ -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 <host:port|socks5://[user[:pass]@]host:port>
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 <name>
TUN interface name inside the network namespace. Default socks5ify0.
-mtu <bytes>
TUN MTU. Default 1500.
-ipv6
Also configure IPv6 address and default route.
-tun-ipv4-host <ip>
V2Ray-side IPv4 address. Default 198.18.0.1.
-tun-ipv4-guest <ip>
Namespace-side IPv4 address. Default 198.18.0.2.
-tun-ipv4-prefix, -tun-ipv4-mask <bits>
IPv4 address prefix length. Default 30.
-tun-ipv6-host <ip>
V2Ray-side IPv6 address. Default fd00:736f:636b:35::1.
-tun-ipv6-guest <ip>
Namespace-side IPv6 address. Default fd00:736f:636b:35::2.
-tun-ipv6-prefix, -tun-ipv6-mask <bits>
IPv6 address prefix length. Default 126.
-dns <ip[,ip...]>
Opt-in generated /etc/resolv.conf override inside the mount namespace.
-resolv-conf <path>
Opt-in bind mount of an existing resolver file onto /etc/resolv.conf.
-bind-file <source:target>
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
}
@@ -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)
}
}
@@ -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
}
@@ -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()
}
@@ -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)
}
}
}
@@ -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
}
@@ -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
}
@@ -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)}
}
+6
View File
@@ -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"