mirror of
https://github.com/v2fly/v2ray-core.git
synced 2026-07-29 19:20:39 -04:00
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:
+20
-3
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user