├── .drone.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── command ├── config.go ├── config_test.go ├── root.go ├── run.go ├── run_container.go ├── run_container_other.go ├── run_other.go └── server.go ├── go.mod ├── go.sum ├── main.go ├── main_test.go └── pkg └── connect ├── connect.go ├── connect_test.go ├── network_stack.go ├── server.go └── transport.go /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: wirez-ci 5 | 6 | trigger: 7 | ref: 8 | - refs/heads/main 9 | - refs/pull/*/head 10 | - refs/tags/* 11 | event: 12 | - push 13 | - tag 14 | - pull_request 15 | 16 | clone: 17 | depth: 1 18 | 19 | steps: 20 | - name: lint 21 | image: golangci/golangci-lint:v1.50-alpine 22 | volumes: 23 | - name: deps 24 | path: /go 25 | commands: 26 | - golangci-lint run -v 27 | 28 | - name: test & build 29 | image: golang:1.19-alpine 30 | environment: 31 | CGO_ENABLED: "0" 32 | volumes: 33 | - name: deps 34 | path: /go 35 | commands: 36 | - go test ./... -v -cover 37 | - go build -ldflags "-w -s" 38 | 39 | - name: goreleaser-snapshot 40 | image: golang:1.19-alpine 41 | volumes: 42 | - name: deps 43 | path: /go 44 | commands: 45 | - apk add curl git 46 | - curl -s https://raw.githubusercontent.com/goreleaser/get/master/get > goreleaser.sh 47 | - chmod +x goreleaser.sh && ./goreleaser.sh --snapshot 48 | when: 49 | event: push 50 | 51 | volumes: 52 | - name: deps 53 | temp: {} 54 | 55 | --- 56 | kind: pipeline 57 | type: docker 58 | name: release 59 | 60 | depends_on: 61 | - wirez-ci 62 | 63 | trigger: 64 | event: 65 | - tag 66 | 67 | steps: 68 | - name: release 69 | image: golang:1.19-alpine 70 | environment: 71 | GITHUB_TOKEN: 72 | from_secret: github_token 73 | commands: 74 | - apk add curl git 75 | - git fetch --tags 76 | - curl -s https://raw.githubusercontent.com/goreleaser/get/master/get | sh 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | wirez 2 | proxies.txt 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - dogsled 4 | - exportloopref 5 | - gocognit 6 | - goconst 7 | - gocritic 8 | - gocyclo 9 | - gofmt 10 | - goimports 11 | - staticcheck 12 | - govet 13 | - misspell 14 | - nestif 15 | - prealloc 16 | - revive 17 | - unconvert 18 | - unparam 19 | 20 | linters-settings: 21 | revive: 22 | max-open-files: 2048 23 | ignore-generated-header: true 24 | severity: warning 25 | confidence: 0.8 26 | 27 | run: 28 | timeout: 5m -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: wirez 2 | gomod: 3 | proxy: true 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - linux 9 | goarch: 10 | - amd64 11 | flags: 12 | - -trimpath 13 | - -buildvcs=false 14 | ldflags: 15 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} 16 | mod_timestamp: '{{ .CommitTimestamp }}' 17 | archives: 18 | - files: 19 | - none* 20 | checksum: 21 | algorithm: sha256 22 | changelog: 23 | sort: asc 24 | filters: 25 | exclude: 26 | - '^(\s)*docs:' 27 | - '^(\s)*test:' 28 | - Merge pull request 29 | - update README 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 v-byte-cpu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wirez 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/v-byte-cpu/wirez/blob/main/LICENSE) 4 | [![Build Status](https://cloud.drone.io/api/badges/v-byte-cpu/wirez/status.svg)](https://cloud.drone.io/v-byte-cpu/wirez) 5 | [![GoReportCard Status](https://goreportcard.com/badge/github.com/v-byte-cpu/wirez)](https://goreportcard.com/report/github.com/v-byte-cpu/wirez) 6 | 7 | **wirez** can redirect all TCP/UDP traffic made by **any** given program (application, script, shell, etc.) to SOCKS5 proxy 8 | and block other IP traffic (ICMP, SCTP etc). 9 | 10 | Compared with [tsocks](https://linux.die.net/man/8/tsocks), [proxychains](http://proxychains.sourceforge.net/) or 11 | [proxychains-ng](https://github.com/rofl0r/proxychains-ng), `wirez` is not using the [LD_PRELOAD hack](https://stackoverflow.com/questions/426230/what-is-the-ld-preload-trick) 12 | that only works for dynamically linked programs, e.g., [applications built by Go can not be hooked by proxychains-ng](https://github.com/rofl0r/proxychains-ng/issues/199). 13 | 14 | Instead, `wirez` is based on the rootless container technology and the userspace network stack, that is much more robust and secure. 15 | See [how does it work](#how-does-it-work) for more details. 16 | 17 | Also, wirez can act as a simple SOCKS5 load balancer server. 18 | 19 | https://user-images.githubusercontent.com/65545655/200089415-fc04e91e-e933-43b6-a3b1-7243c5171f9d.mp4 20 | 21 | 22 | --- 23 | 24 | - [Installation](#installation) 25 | - [Quick Start](#quick-start) 26 | - [Load Balancing](#load-balancing) 27 | - [How does it work?](#how-does-it-work) 28 | - [License](#license) 29 | 30 | --- 31 | 32 | ## Installation 33 | 34 | `wirez` is a cross-platform application. However, `run` subcommand is available only on Linux. 35 | 36 | ### From source 37 | 38 | From the root of the source tree, run: 39 | 40 | ``` 41 | go build 42 | ``` 43 | 44 | ## Quick Start 45 | 46 | start bash shell and redirect all TCP and UDP traffic through socks5 proxy server that is listening on `127.0.0.1:1234` address: 47 | 48 | ``` 49 | wirez run -F 127.0.0.1:1234 bash 50 | ``` 51 | 52 | proxy a curl request to `example.com`: 53 | 54 | ``` 55 | wirez run -F 127.0.0.1:1234 -- curl example.com 56 | ``` 57 | 58 | By default, all UDP traffic is forwarded to SOCKS5 proxy using UDP ASSOCIATE request. 59 | If SOCKS5 proxy doesn't support this method (like ssh and Tor) you can use local port forwarding option `-L`. 60 | It specifies that connections to the target host and TCP/UDP port are to be directly forwarded to the given host and port. 61 | 62 | For instance, forward all TCP traffic through proxy, but all UDP traffic directly to 1.1.1.1 DNS server: 63 | 64 | ``` 65 | wirez run -F 127.0.0.1:1234 -L 53:1.1.1.1:53/udp -- curl example.com 66 | ``` 67 | 68 | forward all TCP and UDP traffic through the proxy, but redirect TCP traffic targeted to `10.10.10.10:2345` directly to `127.0.0.1:4567`: 69 | 70 | ``` 71 | wirez run -F 127.0.0.1:1234 -L 10.10.10.10:2345:127.0.0.1:4567/tcp bash 72 | ``` 73 | 74 | ## Load Balancing 75 | 76 | Create a plain text file with one socks5 proxy per line. For demonstration purposes, here is an example file `proxies.txt`: 77 | 78 | ``` 79 | 10.1.1.1:1035 80 | 10.2.2.2:1037 81 | ``` 82 | 83 | Start **wirez** on the localhost on port 1080: 84 | 85 | ``` 86 | wirez server -f proxies.txt -l 127.0.0.1:1080 87 | ``` 88 | 89 | Now every socks5 request on 1080 port will be load balanced between socks5 proxies in the `proxies.txt` file. Enjoy! 90 | 91 | ## Usage 92 | 93 | ``` 94 | wirez help 95 | ``` 96 | 97 | ## How does it work? 98 | 99 | First of all, `run` command creates a new unix socket pair for parent/child process communication. 100 | 101 | ``` 102 | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0) 103 | ``` 104 | 105 | After that we start a new child process in a new Linux user namespace, see `user_namespaces(7)`: 106 | 107 | ``` 108 | proc.SysProcAttr = &syscall.SysProcAttr{ 109 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNET | syscall.CLONE_NEWUSER, 110 | ... 111 | } 112 | ``` 113 | 114 | Inside the new namespace, the child process has root capabilities, so we open a new tun network device: 115 | 116 | ``` 117 | tunFd, err := tun.Open(tunDevice) 118 | ``` 119 | 120 | and send this tun file descriptor to the parent process using the unix socket pair: 121 | 122 | ``` 123 | rights := unix.UnixRights(fd) 124 | return unix.Sendmsg(c.socketFd, nil, rights, nil, 0) 125 | ``` 126 | 127 | in the parent process we receive this tun file descriptor: 128 | 129 | ``` 130 | func (c *parentUnixSocketConn) ReceiveFd() (fd int, err error) { 131 | // receive socket control message 132 | b := make([]byte, unix.CmsgSpace(4)) 133 | if _, _, _, _, err = unix.Recvmsg(c.socketFd, nil, b, 0); err != nil { 134 | return 135 | } 136 | 137 | // parse socket control message 138 | cmsgs, err := unix.ParseSocketControlMessage(b) 139 | if err != nil { 140 | return 0, fmt.Errorf("parse socket control message: %w", err) 141 | } 142 | 143 | tunFds, err := unix.ParseUnixRights(&cmsgs[0]) 144 | if err != nil { 145 | return 0, err 146 | } 147 | if len(tunFds) == 0 { 148 | return 0, errors.New("tun fds slice is empty") 149 | } 150 | return tunFds[0], nil 151 | } 152 | ``` 153 | 154 | and initialize a gVisor userspace network stack on top of it. Then in the child process we set up the tun device as default IP gateway 155 | and, finally, start a target process specified in cli args. That's it! 156 | 157 | ## License 158 | 159 | This project is licensed under the MIT License. See the [LICENSE](https://github.com/v-byte-cpu/wirez/blob/main/LICENSE) file for the full license text. 160 | -------------------------------------------------------------------------------- /command/config.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/rs/zerolog" 14 | "github.com/spf13/pflag" 15 | "github.com/v-byte-cpu/wirez/pkg/connect" 16 | ) 17 | 18 | func parseProxyFile(proxyFile io.Reader) (socksAddrs []*connect.SocksAddr, err error) { 19 | bs := bufio.NewScanner(proxyFile) 20 | for bs.Scan() { 21 | rawSocksAddr := strings.Trim(bs.Text(), " ") 22 | if rawSocksAddr == "" || rawSocksAddr[0] == '#' { 23 | continue 24 | } 25 | socksAddr, err := parseProxyURL(rawSocksAddr) 26 | if err != nil { 27 | return nil, err 28 | } 29 | socksAddrs = append(socksAddrs, socksAddr) 30 | } 31 | err = bs.Err() 32 | return 33 | } 34 | 35 | func parseProxyURL(proxyURL string) (*connect.SocksAddr, error) { 36 | proxyURL = strings.Trim(proxyURL, " ") 37 | if !strings.Contains(proxyURL, "//") { 38 | proxyURL = "socks5://" + proxyURL 39 | } 40 | socksURL, err := url.Parse(proxyURL) 41 | if err != nil { 42 | return nil, err 43 | } 44 | if socksURL.Scheme != "socks5" { 45 | return nil, errors.New("invalid socks5 scheme") 46 | } 47 | if _, _, err := net.SplitHostPort(socksURL.Host); err != nil { 48 | return nil, err 49 | } 50 | return &connect.SocksAddr{Address: socksURL.Host, Auth: socksURL.User}, nil 51 | } 52 | 53 | func parseProxyURLs(proxyURLs []string) ([]*connect.SocksAddr, error) { 54 | result := make([]*connect.SocksAddr, 0, len(proxyURLs)) 55 | for _, proxyURL := range proxyURLs { 56 | socksAddr, err := parseProxyURL(proxyURL) 57 | if err != nil { 58 | return nil, err 59 | } 60 | result = append(result, socksAddr) 61 | } 62 | return result, nil 63 | } 64 | 65 | func parseAddressMapper(addressMappings []string) (connect.AddressMapper, error) { 66 | m := connect.NewAddressMapper() 67 | for _, mapping := range addressMappings { 68 | network, fromAddress, targetAddress, err := parseMapping(mapping) 69 | if err != nil { 70 | return nil, err 71 | } 72 | if err = m.AddAddressMapping(network, fromAddress, targetAddress); err != nil { 73 | return nil, err 74 | } 75 | } 76 | return m, nil 77 | } 78 | 79 | func parseMapping(mapping string) (network, fromAddress, targetAddress string, err error) { 80 | parts := strings.Split(mapping, "/") 81 | if len(parts) < 2 { 82 | network = "tcp" 83 | } else { 84 | network = parts[1] 85 | } 86 | targetPort, rest, err := takeLastPort(parts[0]) 87 | if err != nil { 88 | err = fmt.Errorf("invalid target port in mapping %s: %w", mapping, err) 89 | return 90 | } 91 | targetHost, rest, err := takeLastHost(rest) 92 | if err != nil { 93 | err = fmt.Errorf("invalid target host in mapping %s: %w", mapping, err) 94 | return 95 | } 96 | if len(targetHost) == 0 { 97 | err = fmt.Errorf("empty target host in mapping %s", mapping) 98 | return 99 | } 100 | fromPort, rest, err := takeLastPort(rest) 101 | if err != nil { 102 | err = fmt.Errorf("invalid source port in mapping %s: %w", mapping, err) 103 | return 104 | } 105 | fromHost, rest, err := takeLastHost(rest) 106 | if err != nil { 107 | err = fmt.Errorf("invalid source host in mapping %s: %w", mapping, err) 108 | return 109 | } 110 | if len(rest) > 0 { 111 | err = fmt.Errorf("invalid source address in mapping %s", mapping) 112 | return 113 | } 114 | fromAddress = net.JoinHostPort(fromHost, fromPort) 115 | targetAddress = net.JoinHostPort(targetHost, targetPort) 116 | return 117 | } 118 | 119 | func takeLastHost(input string) (host, rest string, err error) { 120 | if len(input) == 0 { 121 | return 122 | } 123 | if input[len(input)-1] == ']' { 124 | return takeLastIPv6Host(input) 125 | } 126 | idx := strings.LastIndex(input, ":") 127 | host = input[idx+1:] 128 | if idx > 0 { 129 | rest = input[:idx] 130 | } 131 | return host, rest, err 132 | } 133 | 134 | func takeLastIPv6Host(input string) (host, rest string, err error) { 135 | idx := strings.LastIndex(input, "[") 136 | if idx == -1 { 137 | return "", "", errors.New("invalid IPv6 address") 138 | } 139 | host = input[idx+1 : len(input)-1] 140 | if idx > 0 { 141 | if input[idx-1] != ':' { 142 | return "", "", errors.New("missing colon before host") 143 | } 144 | rest = input[:idx-1] 145 | } 146 | if ip := net.ParseIP(host); ip == nil { 147 | err = errors.New("invalid IPv6 address") 148 | } 149 | return host, rest, err 150 | } 151 | 152 | func takeLastPort(input string) (port, rest string, err error) { 153 | idx := strings.LastIndex(input, ":") 154 | port = input[idx+1:] 155 | if idx > 0 { 156 | rest = input[:idx] 157 | } 158 | _, err = strconv.ParseUint(port, 10, 16) 159 | return 160 | } 161 | 162 | type renamedTypeFlagValue struct { 163 | pflag.Value 164 | name string 165 | hideDefault bool 166 | } 167 | 168 | func (v *renamedTypeFlagValue) Type() string { 169 | return v.name 170 | } 171 | 172 | func (v *renamedTypeFlagValue) String() string { 173 | if v.hideDefault { 174 | return "" 175 | } 176 | return v.Value.String() 177 | } 178 | 179 | func setLogLevel(log *zerolog.Logger, verboseLevel int) *zerolog.Logger { 180 | level := zerolog.InfoLevel 181 | switch { 182 | case verboseLevel < 0: 183 | level = zerolog.Disabled 184 | case verboseLevel == 1: 185 | level = zerolog.DebugLevel 186 | case verboseLevel >= 2: 187 | level = zerolog.TraceLevel 188 | } 189 | result := log.Level(level) 190 | return &result 191 | } 192 | -------------------------------------------------------------------------------- /command/config_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/v-byte-cpu/wirez/pkg/connect" 10 | ) 11 | 12 | func TestParseProxyURL(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | input string 16 | expected *connect.SocksAddr 17 | expectedErr bool 18 | }{ 19 | { 20 | name: "EmptyString", 21 | input: "", 22 | expectedErr: true, 23 | }, 24 | { 25 | name: "StringWithSpaces", 26 | input: " ", 27 | expectedErr: true, 28 | }, 29 | { 30 | name: "CommentSign", 31 | input: "#", 32 | expectedErr: true, 33 | }, 34 | { 35 | name: "OneIPPort", 36 | input: "10.10.10.10:1111", 37 | expected: &connect.SocksAddr{Address: "10.10.10.10:1111"}, 38 | }, 39 | { 40 | name: "OneIPPortWithSpaces", 41 | input: " 10.10.10.10:1111 ", 42 | expected: &connect.SocksAddr{Address: "10.10.10.10:1111"}, 43 | }, 44 | { 45 | name: "OneHostPort", 46 | input: "example.com:1111", 47 | expected: &connect.SocksAddr{Address: "example.com:1111"}, 48 | }, 49 | { 50 | name: "OneIPPortWithUsername", 51 | input: "abc@10.10.10.10:1111", 52 | expected: &connect.SocksAddr{Address: "10.10.10.10:1111", Auth: url.User("abc")}, 53 | }, 54 | { 55 | name: "OneIPPortWithUsernamePassword", 56 | input: "abc:def@10.10.10.10:1111", 57 | expected: &connect.SocksAddr{Address: "10.10.10.10:1111", Auth: url.UserPassword("abc", "def")}, 58 | }, 59 | { 60 | name: "OneHostPortWithUsernamePassword", 61 | input: "abc:def@example.com:1111", 62 | expected: &connect.SocksAddr{Address: "example.com:1111", Auth: url.UserPassword("abc", "def")}, 63 | }, 64 | { 65 | name: "WithSocks5Scheme", 66 | input: "socks5://10.10.10.10:1111", 67 | expected: &connect.SocksAddr{Address: "10.10.10.10:1111"}, 68 | }, 69 | { 70 | name: "WithInvalidScheme", 71 | input: "socks3://10.10.10.10:1111", 72 | expectedErr: true, 73 | }, 74 | { 75 | name: "OneIPPortWithInvalidColons", 76 | input: "abc@def:10.10.10.10:1111", 77 | expectedErr: true, 78 | }, 79 | { 80 | name: "OneIPPortWithComment", 81 | input: "10.10.10.10:1111 #hello", 82 | expectedErr: true, 83 | }, 84 | { 85 | name: "OneIPPortWithInvalidPort", 86 | input: "10.10.10.10:abc", 87 | expectedErr: true, 88 | }, 89 | { 90 | name: "OneIP", 91 | input: "10.10.10.10", 92 | expectedErr: true, 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | socksAddr, err := parseProxyURL(tt.input) 98 | if tt.expectedErr { 99 | require.Error(t, err) 100 | } else { 101 | require.NoError(t, err) 102 | } 103 | require.Equal(t, tt.expected, socksAddr) 104 | }) 105 | } 106 | } 107 | 108 | func TestParseProxyURLs(t *testing.T) { 109 | tests := []struct { 110 | name string 111 | input []string 112 | expected []*connect.SocksAddr 113 | expectedErr bool 114 | }{ 115 | { 116 | name: "OneAddress", 117 | input: []string{"10.10.10.10:1111"}, 118 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111"}}, 119 | }, 120 | { 121 | name: "TwoAddresses", 122 | input: []string{"10.10.10.10:1111", "socks5://20.20.20.20:2221"}, 123 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111"}, {Address: "20.20.20.20:2221"}}, 124 | }, 125 | { 126 | name: "Error", 127 | input: []string{"socks3://10.10.10.10:1111"}, 128 | expectedErr: true, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | socksAddrs, err := parseProxyURLs(tt.input) 134 | if tt.expectedErr { 135 | require.Error(t, err) 136 | } else { 137 | require.NoError(t, err) 138 | } 139 | require.Equal(t, tt.expected, socksAddrs) 140 | }) 141 | } 142 | } 143 | 144 | func TestParseProxyFile(t *testing.T) { 145 | tests := []struct { 146 | name string 147 | input string 148 | expected []*connect.SocksAddr 149 | expectedErr bool 150 | }{ 151 | { 152 | name: "EmptyString", 153 | input: "", 154 | }, 155 | { 156 | name: "StringWithSpaces", 157 | input: " ", 158 | }, 159 | { 160 | name: "Newline", 161 | input: "\n", 162 | }, 163 | { 164 | name: "TwoLinesWithSpaces", 165 | input: " \n ", 166 | }, 167 | { 168 | name: "CommentSign", 169 | input: "#", 170 | }, 171 | { 172 | name: "CommentSignWithSpaces", 173 | input: " # ", 174 | }, 175 | { 176 | name: "TwoLinesWithComments", 177 | input: " # \n# ", 178 | }, 179 | { 180 | name: "OneIPPort", 181 | input: "10.10.10.10:1111", 182 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111"}}, 183 | }, 184 | { 185 | name: "OneIPPortWithSpaces", 186 | input: " 10.10.10.10:1111 ", 187 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111"}}, 188 | }, 189 | { 190 | name: "OneHostPort", 191 | input: "example.com:1111", 192 | expected: []*connect.SocksAddr{{Address: "example.com:1111"}}, 193 | }, 194 | { 195 | name: "TwoIPPortLines", 196 | input: "10.10.10.10:1111\n20.20.20.20:2222", 197 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111"}, {Address: "20.20.20.20:2222"}}, 198 | }, 199 | { 200 | name: "TwoHostPortLines", 201 | input: "example.com:1111\nexample.org:2222", 202 | expected: []*connect.SocksAddr{{Address: "example.com:1111"}, {Address: "example.org:2222"}}, 203 | }, 204 | { 205 | name: "OneIPPortWithUsername", 206 | input: "abc@10.10.10.10:1111", 207 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111", Auth: url.User("abc")}}, 208 | }, 209 | { 210 | name: "OneIPPortWithUsernamePassword", 211 | input: "abc:def@10.10.10.10:1111", 212 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111", Auth: url.UserPassword("abc", "def")}}, 213 | }, 214 | { 215 | name: "OneHostPortWithUsernamePassword", 216 | input: "abc:def@example.com:1111", 217 | expected: []*connect.SocksAddr{{Address: "example.com:1111", Auth: url.UserPassword("abc", "def")}}, 218 | }, 219 | { 220 | name: "WithSocks5Scheme", 221 | input: "socks5://10.10.10.10:1111", 222 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111"}}, 223 | }, 224 | { 225 | name: "TwoWithSocks5Schemes", 226 | input: "socks5://10.10.10.10:1111\nsocks5://20.20.20.20:2221", 227 | expected: []*connect.SocksAddr{{Address: "10.10.10.10:1111"}, {Address: "20.20.20.20:2221"}}, 228 | }, 229 | { 230 | name: "WithInvalidScheme", 231 | input: "socks3://10.10.10.10:1111", 232 | expectedErr: true, 233 | }, 234 | { 235 | name: "OneIPPortWithInvalidColons", 236 | input: "abc@def:10.10.10.10:1111", 237 | expectedErr: true, 238 | }, 239 | { 240 | name: "OneIPPortWithComment", 241 | input: "10.10.10.10:1111 #hello", 242 | expectedErr: true, 243 | }, 244 | { 245 | name: "OneIPPortWithInvalidPort", 246 | input: "10.10.10.10:abc", 247 | expectedErr: true, 248 | }, 249 | { 250 | name: "OneIP", 251 | input: "10.10.10.10", 252 | expectedErr: true, 253 | }, 254 | { 255 | name: "ParsingErrorAfterOneValid", 256 | input: "10.10.10.10:1111\n10.10.10.13", 257 | expectedErr: true, 258 | }, 259 | { 260 | name: "ParsingErrorBeforeOneValid", 261 | input: " 10.10.10.13\n10.10.10.10:1111", 262 | expectedErr: true, 263 | }, 264 | } 265 | for _, tt := range tests { 266 | t.Run(tt.name, func(t *testing.T) { 267 | socksAddrs, err := parseProxyFile(strings.NewReader(tt.input)) 268 | if tt.expectedErr { 269 | require.Error(t, err) 270 | } else { 271 | require.NoError(t, err) 272 | } 273 | require.Equal(t, tt.expected, socksAddrs) 274 | }) 275 | } 276 | } 277 | 278 | func TestParseMapping(t *testing.T) { 279 | t.Run("OneFullUDPMapping", func(t *testing.T) { 280 | network, fromAddress, toAddress, err := parseMapping("1.1.1.1:53:127.0.0.1:5341/udp") 281 | require.NoError(t, err) 282 | require.Equal(t, "udp", network) 283 | require.Equal(t, "1.1.1.1:53", fromAddress) 284 | require.Equal(t, "127.0.0.1:5341", toAddress) 285 | }) 286 | t.Run("OneFullTCPMapping", func(t *testing.T) { 287 | network, fromAddress, toAddress, err := parseMapping("1.1.1.1:53:127.0.0.1:5341/tcp") 288 | require.NoError(t, err) 289 | require.Equal(t, "tcp", network) 290 | require.Equal(t, "1.1.1.1:53", fromAddress) 291 | require.Equal(t, "127.0.0.1:5341", toAddress) 292 | }) 293 | t.Run("OnePortUDPMapping", func(t *testing.T) { 294 | network, fromAddress, toAddress, err := parseMapping("53:127.0.0.1:5341/udp") 295 | require.NoError(t, err) 296 | require.Equal(t, "udp", network) 297 | require.Equal(t, ":53", fromAddress) 298 | require.Equal(t, "127.0.0.1:5341", toAddress) 299 | }) 300 | t.Run("InvalidTargetPortMapping", func(t *testing.T) { 301 | _, _, _, err := parseMapping("53:127.0.0.1:abc/udp") 302 | require.Error(t, err) 303 | }) 304 | t.Run("InvalidFromPortMapping", func(t *testing.T) { 305 | _, _, _, err := parseMapping("1.1.1.1:abc:127.0.0.1:5353/udp") 306 | require.Error(t, err) 307 | }) 308 | t.Run("InvalidEmptyFromPortMapping", func(t *testing.T) { 309 | _, _, _, err := parseMapping("127.0.0.1:5353/udp") 310 | require.Error(t, err) 311 | }) 312 | t.Run("OneMappingWithoutNetwork", func(t *testing.T) { 313 | network, fromAddress, toAddress, err := parseMapping("2.2.2.2:8080:127.0.0.1:5341") 314 | require.NoError(t, err) 315 | require.Equal(t, "tcp", network) 316 | require.Equal(t, "2.2.2.2:8080", fromAddress) 317 | require.Equal(t, "127.0.0.1:5341", toAddress) 318 | }) 319 | t.Run("InvalidMappingWithDoubledSourceIP", func(t *testing.T) { 320 | _, _, _, err := parseMapping("2.2.2.2:2.2.2.2:8080:127.0.0.1:5341") 321 | require.Error(t, err) 322 | }) 323 | t.Run("OneTargetIPv6Mapping", func(t *testing.T) { 324 | network, fromAddress, toAddress, err := parseMapping("1.1.1.1:53:[::1]:5353/udp") 325 | require.NoError(t, err) 326 | require.Equal(t, "udp", network) 327 | require.Equal(t, "1.1.1.1:53", fromAddress) 328 | require.Equal(t, "[::1]:5353", toAddress) 329 | }) 330 | t.Run("OneSourceIPv6Mapping", func(t *testing.T) { 331 | network, fromAddress, toAddress, err := parseMapping("[2001:db8::2:1]:5353:1.1.1.1:53/udp") 332 | require.NoError(t, err) 333 | require.Equal(t, "udp", network) 334 | require.Equal(t, "[2001:db8::2:1]:5353", fromAddress) 335 | require.Equal(t, "1.1.1.1:53", toAddress) 336 | }) 337 | t.Run("InvalidTargetEmptyMapping", func(t *testing.T) { 338 | _, _, _, err := parseMapping("1.1.1.1:53::5353/udp") 339 | require.Error(t, err) 340 | }) 341 | t.Run("InvalidTargetIPv6MissingBracketMapping", func(t *testing.T) { 342 | _, _, _, err := parseMapping("1.1.1.1:53:1]:5353/udp") 343 | require.Error(t, err) 344 | }) 345 | t.Run("InvalidTargetIPv6Mapping", func(t *testing.T) { 346 | _, _, _, err := parseMapping("1.1.1.1:53:[abc]:5353/udp") 347 | require.Error(t, err) 348 | }) 349 | t.Run("InvalidTargetEmptyIPv6Mapping", func(t *testing.T) { 350 | _, _, _, err := parseMapping("1.1.1.1:53:[]:5353/udp") 351 | require.Error(t, err) 352 | }) 353 | t.Run("MissingColonBetweenFromPortAndTargetHostIPv6", func(t *testing.T) { 354 | _, _, _, err := parseMapping("1.1.1.1:53[2001:db8::2:1]:5353/udp") 355 | require.Error(t, err) 356 | }) 357 | t.Run("InvalidSourceIPv6MissingBracketMapping", func(t *testing.T) { 358 | _, _, _, err := parseMapping("1]:5353:1.1.1.1:53/udp") 359 | require.Error(t, err) 360 | }) 361 | t.Run("InvalidSourceIPv6Mapping", func(t *testing.T) { 362 | _, _, _, err := parseMapping("[abc]:5353:1.1.1.1:53/udp") 363 | require.Error(t, err) 364 | }) 365 | t.Run("InvalidSourceEmptyIPv6Mapping", func(t *testing.T) { 366 | _, _, _, err := parseMapping("[]:5353:1.1.1.1:53/udp") 367 | require.Error(t, err) 368 | }) 369 | } 370 | 371 | func TestParseAddressMapper(t *testing.T) { 372 | t.Run("EmptyMapper", func(t *testing.T) { 373 | m, err := parseAddressMapper(nil) 374 | require.NoError(t, err) 375 | _, exists := m.MapAddress("udp", "8.8.8.8:53") 376 | require.False(t, exists) 377 | }) 378 | t.Run("OneFullUDPMapping", func(t *testing.T) { 379 | m, err := parseAddressMapper([]string{"8.8.8.8:53:127.0.0.1:5341/udp"}) 380 | require.NoError(t, err) 381 | targetAddress, exists := m.MapAddress("udp", "8.8.8.8:53") 382 | require.True(t, exists) 383 | require.Equal(t, "127.0.0.1:5341", targetAddress) 384 | }) 385 | t.Run("OneFullTCPMapping", func(t *testing.T) { 386 | m, err := parseAddressMapper([]string{"1.1.1.1:53:127.0.0.1:5341/tcp"}) 387 | require.NoError(t, err) 388 | targetAddress, exists := m.MapAddress("tcp", "1.1.1.1:53") 389 | require.True(t, exists) 390 | require.Equal(t, "127.0.0.1:5341", targetAddress) 391 | }) 392 | t.Run("OnePortUDPMapping", func(t *testing.T) { 393 | m, err := parseAddressMapper([]string{"53:127.0.0.1:5341/udp"}) 394 | require.NoError(t, err) 395 | targetAddress, exists := m.MapAddress("udp", "8.8.8.8:53") 396 | require.True(t, exists) 397 | require.Equal(t, "127.0.0.1:5341", targetAddress) 398 | }) 399 | t.Run("OneMappingWithError", func(t *testing.T) { 400 | _, err := parseAddressMapper([]string{"1.1.1.1:abc:127.0.0.1:5353/udp"}) 401 | require.Error(t, err) 402 | }) 403 | t.Run("TwoFullAddressMappings", func(t *testing.T) { 404 | m, err := parseAddressMapper([]string{"1.1.1.1:53:127.0.0.1:5341/udp", "2.2.2.2:53:1.1.1.1:5341/udp"}) 405 | require.NoError(t, err) 406 | targetAddress, exists := m.MapAddress("udp", "1.1.1.1:53") 407 | require.True(t, exists) 408 | require.Equal(t, "127.0.0.1:5341", targetAddress) 409 | targetAddress, exists = m.MapAddress("udp", "2.2.2.2:53") 410 | require.True(t, exists) 411 | require.Equal(t, "1.1.1.1:5341", targetAddress) 412 | }) 413 | t.Run("TwoFullUDPAndTCPSameAddressMappings", func(t *testing.T) { 414 | m, err := parseAddressMapper([]string{"1.1.1.1:53:127.0.0.1:5341/udp", "1.1.1.1:53:8.8.8.8:1234/tcp"}) 415 | require.NoError(t, err) 416 | targetAddress, exists := m.MapAddress("udp", "1.1.1.1:53") 417 | require.True(t, exists) 418 | require.Equal(t, "127.0.0.1:5341", targetAddress) 419 | targetAddress, exists = m.MapAddress("tcp", "1.1.1.1:53") 420 | require.True(t, exists) 421 | require.Equal(t, "8.8.8.8:1234", targetAddress) 422 | }) 423 | t.Run("TwoFullAddressMappingsWithError", func(t *testing.T) { 424 | _, err := parseAddressMapper([]string{"1.1.1.1:abc:127.0.0.1:5341/udp", "2.2.2.2:53:1.1.1.1:5341/udp"}) 425 | require.Error(t, err) 426 | }) 427 | t.Run("TwoPortMappings", func(t *testing.T) { 428 | m, err := parseAddressMapper([]string{"53:127.0.0.1:5341/udp", "8080:127.0.0.1:4444/udp"}) 429 | require.NoError(t, err) 430 | targetAddress, exists := m.MapAddress("udp", "1.1.1.1:53") 431 | require.True(t, exists) 432 | require.Equal(t, "127.0.0.1:5341", targetAddress) 433 | targetAddress, exists = m.MapAddress("udp", "2.2.2.2:8080") 434 | require.True(t, exists) 435 | require.Equal(t, "127.0.0.1:4444", targetAddress) 436 | }) 437 | } 438 | -------------------------------------------------------------------------------- /command/root.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | "time" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func Main(version string) { 14 | log := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger() 15 | if err := newRootCmd(&log, version).Execute(); err != nil { 16 | var exitError *exec.ExitError 17 | if errors.As(err, &exitError) { 18 | os.Exit(exitError.ExitCode()) 19 | } 20 | log.Error().Err(err).Msg("") 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | func newRootCmd(log *zerolog.Logger, version string) *cobra.Command { 26 | cmd := &cobra.Command{ 27 | Use: "wirez", 28 | Short: "socks5 proxy rotator", 29 | Version: version, 30 | SilenceUsage: true, 31 | SilenceErrors: true, 32 | } 33 | 34 | cmd.AddCommand( 35 | newServerCmd(log).cmd, 36 | newRunCmd(log).cmd, 37 | newRunContainerCmd().cmd, 38 | ) 39 | 40 | return cmd 41 | } 42 | -------------------------------------------------------------------------------- /command/run.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package command 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "strconv" 12 | "strings" 13 | "syscall" 14 | 15 | "github.com/rs/zerolog" 16 | "github.com/spf13/cobra" 17 | "github.com/v-byte-cpu/wirez/pkg/connect" 18 | "go.uber.org/multierr" 19 | "golang.org/x/sys/unix" 20 | ) 21 | 22 | func newRunCmd(log *zerolog.Logger) *runCmd { 23 | c := &runCmd{} 24 | 25 | cmd := &cobra.Command{ 26 | Use: "run [flags] command", 27 | Example: strings.Join([]string{ 28 | "wirez run -F 127.0.0.1:1234 bash", 29 | "wirez run -F 127.0.0.1:1234 -L 53:1.1.1.1:53/udp -- curl example.com"}, "\n"), 30 | Short: "Proxy application traffic through the socks5 server", 31 | Long: "Run a command in an unprivileged container that transparently proxies application traffic through the socks5 server", 32 | RunE: func(cmd *cobra.Command, args []string) (err error) { 33 | if c.opts.ContainerUID < 0 { 34 | return errors.New("uid is negative") 35 | } 36 | if c.opts.ContainerGID < 0 { 37 | return errors.New("gid is negative") 38 | } 39 | if len(c.opts.ForwardProxies) == 0 { 40 | return errors.New("forward proxies list is empty") 41 | } 42 | log = setLogLevel(log, c.opts.VerboseLevel) 43 | log.Debug().Strs("forward", c.opts.ForwardProxies).Msg("") 44 | log.Debug().Strs("local_address_mappings", c.opts.LocalAddressMappings).Msg("") 45 | forwardProxies, err := parseProxyURLs(c.opts.ForwardProxies) 46 | if err != nil { 47 | return 48 | } 49 | 50 | nat, err := parseAddressMapper(c.opts.LocalAddressMappings) 51 | if err != nil { 52 | return 53 | } 54 | 55 | parentFd, childFd, err := newUnixSocketPair() 56 | if err != nil { 57 | return 58 | } 59 | defer unix.Close(parentFd) 60 | defer unix.Close(childFd) 61 | 62 | privileged := os.Geteuid() == 0 63 | proc := exec.Command("/proc/self/exe", append([]string{"runc", 64 | "--unix-fd", strconv.Itoa(childFd), fmt.Sprintf("--privileged=%t", privileged), 65 | "--uid", strconv.Itoa(c.opts.ContainerUID), "--gid", strconv.Itoa(c.opts.ContainerGID), "--"}, args...)...) 66 | proc.Stdin = os.Stdin 67 | proc.Stdout = os.Stdout 68 | proc.Stderr = os.Stderr 69 | 70 | if privileged { 71 | proc.SysProcAttr = &syscall.SysProcAttr{ 72 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNET, 73 | } 74 | } else { 75 | proc.SysProcAttr = &syscall.SysProcAttr{ 76 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNET | syscall.CLONE_NEWUSER, 77 | Credential: &syscall.Credential{Uid: 0, Gid: uint32(c.opts.ContainerGID)}, 78 | UidMappings: []syscall.SysProcIDMap{ 79 | {ContainerID: 0, HostID: os.Geteuid(), Size: 1}, 80 | }, 81 | GidMappings: []syscall.SysProcIDMap{ 82 | {ContainerID: c.opts.ContainerGID, HostID: os.Getegid(), Size: 1}, 83 | }, 84 | } 85 | } 86 | if err = proc.Start(); err != nil { 87 | return err 88 | } 89 | 90 | parentConn := newParentUnixSocketConn(parentFd) 91 | tunFd, err := parentConn.ReceiveFd() 92 | if err != nil { 93 | return err 94 | } 95 | log.Debug().Int("fd", tunFd).Msg("got tun device") 96 | defer unix.Close(tunFd) 97 | 98 | tunMTU, err := parentConn.ReceiveMTU() 99 | if err != nil { 100 | return err 101 | } 102 | log.Debug().Uint32("mtu", tunMTU).Msg("") 103 | 104 | dconn := connect.NewDirectConnector() 105 | socksTCPConn := dconn 106 | socksTCPConns := make([]connect.Connector, 0, len(c.opts.ForwardProxies)+1) 107 | socksTCPConns = append(socksTCPConns, dconn) 108 | for _, proxyAddr := range forwardProxies { 109 | socksTCPConn = connect.NewSOCKS5Connector(socksTCPConn, proxyAddr) 110 | socksTCPConns = append(socksTCPConns, socksTCPConn) 111 | } 112 | socksUDPConn := dconn 113 | for i, proxyAddr := range forwardProxies { 114 | socksUDPConn = connect.NewSOCKS5UDPConnector(log, socksTCPConns[i], socksUDPConn, proxyAddr) 115 | } 116 | socksTCPConn = connect.NewLocalForwardingConnector(dconn, socksTCPConn, nat) 117 | socksUDPConn = connect.NewLocalForwardingConnector(dconn, socksUDPConn, nat) 118 | 119 | stack, err := connect.NewNetworkStack(log, tunFd, tunMTU, tunNetworkAddr, 120 | socksTCPConn, socksUDPConn, connect.NewTransporter(log)) 121 | if err != nil { 122 | return err 123 | } 124 | defer stack.Close() 125 | 126 | if err = parentConn.SendACK(); err != nil { 127 | return err 128 | } 129 | 130 | return proc.Wait() 131 | }, 132 | } 133 | 134 | c.opts.initCliFlags(cmd) 135 | 136 | c.cmd = cmd 137 | return c 138 | } 139 | 140 | type runCmd struct { 141 | cmd *cobra.Command 142 | opts runCmdOpts 143 | } 144 | 145 | type runCmdOpts struct { 146 | ForwardProxies []string 147 | LocalAddressMappings []string 148 | VerboseLevel int 149 | ContainerUID int 150 | ContainerGID int 151 | } 152 | 153 | func (o *runCmdOpts) initCliFlags(cmd *cobra.Command) { 154 | cmd.Flags().StringArrayVarP(&o.ForwardProxies, "forward", "F", nil, "set socks5 proxy address to forward TCP/UDP packets") 155 | forwardFlag := cmd.Flags().Lookup("forward") 156 | forwardFlag.Value = &renamedTypeFlagValue{Value: forwardFlag.Value, name: "address", hideDefault: true} 157 | 158 | cmd.Flags().CountVarP(&o.VerboseLevel, "verbose", "v", "log verbose level") 159 | verboseFlag := cmd.Flags().Lookup("verbose") 160 | verboseFlag.Value = &renamedTypeFlagValue{Value: verboseFlag.Value} 161 | 162 | cmd.Flags().StringArrayVarP(&o.LocalAddressMappings, "local", "L", nil, "specifies that connections to the target host and TCP/UDP port are to be directly forwarded to the given host and port") 163 | localFlag := cmd.Flags().Lookup("local") 164 | localFlag.Value = &renamedTypeFlagValue{Value: localFlag.Value, name: "[target_host:]port:host:hostport[/proto]", hideDefault: true} 165 | 166 | cmd.Flags().IntVar(&o.ContainerUID, "uid", os.Geteuid(), "set uid of container process") 167 | cmd.Flags().IntVar(&o.ContainerGID, "gid", os.Getegid(), "set gid of container process") 168 | } 169 | 170 | func newUnixSocketPair() (parentFd, childFd int, err error) { 171 | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0) 172 | if err != nil { 173 | return 174 | } 175 | parentFd = fds[0] 176 | childFd = fds[1] 177 | 178 | // set clo_exec flag on parent file descriptor 179 | _, err = unix.FcntlInt(uintptr(parentFd), unix.F_SETFD, unix.FD_CLOEXEC) 180 | if err != nil { 181 | err = multierr.Append(err, unix.Close(parentFd)) 182 | err = multierr.Append(err, unix.Close(childFd)) 183 | } 184 | return 185 | } 186 | 187 | type parentUnixSocketConn struct { 188 | socketFd int 189 | socketFile *os.File 190 | } 191 | 192 | func newParentUnixSocketConn(socketFd int) *parentUnixSocketConn { 193 | return &parentUnixSocketConn{ 194 | socketFd: socketFd, 195 | socketFile: os.NewFile(uintptr(socketFd), "parentPipe"), 196 | } 197 | } 198 | 199 | func (c *parentUnixSocketConn) Close() error { 200 | return unix.Close(c.socketFd) 201 | } 202 | 203 | func (c *parentUnixSocketConn) ReceiveFd() (fd int, err error) { 204 | // receive socket control message 205 | b := make([]byte, unix.CmsgSpace(4)) 206 | if _, _, _, _, err = unix.Recvmsg(c.socketFd, nil, b, 0); err != nil { 207 | return 208 | } 209 | 210 | // parse socket control message 211 | cmsgs, err := unix.ParseSocketControlMessage(b) 212 | if err != nil { 213 | return 0, fmt.Errorf("parse socket control message: %w", err) 214 | } 215 | 216 | tunFds, err := unix.ParseUnixRights(&cmsgs[0]) 217 | if err != nil { 218 | return 0, err 219 | } 220 | if len(tunFds) == 0 { 221 | return 0, errors.New("tun fds slice is empty") 222 | } 223 | return tunFds[0], nil 224 | } 225 | 226 | func (c *parentUnixSocketConn) ReceiveMTU() (mtu uint32, err error) { 227 | var msg MTUMessage 228 | if err = json.NewDecoder(c.socketFile).Decode(&msg); err != nil { 229 | return 230 | } 231 | return msg.MTU, nil 232 | } 233 | 234 | func (c *parentUnixSocketConn) SendACK() error { 235 | return json.NewEncoder(c.socketFile).Encode(&ACKMessage{ACK: true}) 236 | } 237 | 238 | type ACKMessage struct { 239 | ACK bool 240 | } 241 | -------------------------------------------------------------------------------- /command/run_container.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package command 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "syscall" 12 | 13 | "github.com/spf13/cobra" 14 | "github.com/vishvananda/netlink" 15 | "golang.org/x/sys/unix" 16 | "gvisor.dev/gvisor/pkg/tcpip/link/rawfile" 17 | "gvisor.dev/gvisor/pkg/tcpip/link/tun" 18 | ) 19 | 20 | const ( 21 | loDevice = "lo" 22 | tunDevice = "tun0" 23 | tunNetworkAddr = "10.1.1.1/24" 24 | ) 25 | 26 | func newRunContainerCmd() *runContainerCmd { 27 | c := &runContainerCmd{} 28 | 29 | cmd := &cobra.Command{ 30 | Use: "runc [flags] command", 31 | Example: "runc --hostname=wirez --unix-fd=10 bash", 32 | Short: "Internal command to run a new process inside an isolated container", 33 | Hidden: true, 34 | RunE: func(cmd *cobra.Command, args []string) (err error) { 35 | if err = syscall.Sethostname([]byte(c.opts.Hostname)); err != nil { 36 | return 37 | } 38 | 39 | childConn := newChildUnixSocketConn(c.opts.PipeFd) 40 | defer childConn.Close() 41 | 42 | tunFd, err := tun.Open(tunDevice) 43 | if err != nil { 44 | return err 45 | } 46 | defer unix.Close(tunFd) 47 | 48 | if err = childConn.SendFd(tunFd); err != nil { 49 | return 50 | } 51 | 52 | mtu, err := rawfile.GetMTU(tunDevice) 53 | if err != nil { 54 | return fmt.Errorf("get mtu: %w", err) 55 | } 56 | 57 | if err = childConn.SendMTU(mtu); err != nil { 58 | return 59 | } 60 | 61 | // wait for starting network stack 62 | if err = childConn.ReceiveACK(); err != nil { 63 | return 64 | } 65 | 66 | if err = setupIPNetwork(); err != nil { 67 | return err 68 | } 69 | 70 | proc := exec.Command(args[0], args[1:]...) 71 | proc.Stdin = os.Stdin 72 | proc.Stdout = os.Stdout 73 | proc.Stderr = os.Stderr 74 | 75 | if c.opts.Privileged { 76 | proc.SysProcAttr = &syscall.SysProcAttr{ 77 | Credential: &syscall.Credential{Uid: uint32(c.opts.ContainerUID), Gid: uint32(c.opts.ContainerGID)}, 78 | } 79 | } else if c.opts.ContainerUID != 0 { 80 | proc.SysProcAttr = &syscall.SysProcAttr{ 81 | Cloneflags: syscall.CLONE_NEWUSER, 82 | Credential: &syscall.Credential{Uid: uint32(c.opts.ContainerUID), Gid: uint32(c.opts.ContainerGID)}, 83 | UidMappings: []syscall.SysProcIDMap{ 84 | {ContainerID: c.opts.ContainerUID, HostID: os.Geteuid(), Size: 1}, 85 | }, 86 | GidMappings: []syscall.SysProcIDMap{ 87 | {ContainerID: c.opts.ContainerGID, HostID: os.Getegid(), Size: 1}, 88 | }, 89 | } 90 | } 91 | 92 | return proc.Run() 93 | }, 94 | } 95 | 96 | c.opts.initCliFlags(cmd) 97 | 98 | c.cmd = cmd 99 | return c 100 | } 101 | 102 | type runContainerCmd struct { 103 | cmd *cobra.Command 104 | opts runContainerCmdOpts 105 | } 106 | 107 | type runContainerCmdOpts struct { 108 | Hostname string 109 | PipeFd int 110 | ContainerUID int 111 | ContainerGID int 112 | Privileged bool 113 | } 114 | 115 | func (o *runContainerCmdOpts) initCliFlags(cmd *cobra.Command) { 116 | cmd.Flags().StringVar(&o.Hostname, "hostname", "wirez", "set container hostname") 117 | cmd.Flags().IntVar(&o.PipeFd, "unix-fd", 0, "set unix pipe fd") 118 | cmd.Flags().IntVar(&o.ContainerUID, "uid", os.Geteuid(), "set uid of container process") 119 | cmd.Flags().IntVar(&o.ContainerGID, "gid", os.Getegid(), "set gid of container process") 120 | cmd.Flags().BoolVar(&o.Privileged, "privileged", false, "indicates if started with root privileges") 121 | } 122 | 123 | type childUnixSocketConn struct { 124 | socketFd int 125 | socketFile *os.File 126 | } 127 | 128 | func newChildUnixSocketConn(socketFd int) *childUnixSocketConn { 129 | return &childUnixSocketConn{ 130 | socketFd: socketFd, 131 | socketFile: os.NewFile(uintptr(socketFd), "childPipe"), 132 | } 133 | } 134 | 135 | func (c *childUnixSocketConn) Close() error { 136 | return unix.Close(c.socketFd) 137 | } 138 | 139 | func (c *childUnixSocketConn) SendFd(fd int) error { 140 | rights := unix.UnixRights(fd) 141 | return unix.Sendmsg(c.socketFd, nil, rights, nil, 0) 142 | } 143 | 144 | func (c *childUnixSocketConn) SendMTU(mtu uint32) error { 145 | data, err := json.Marshal(&MTUMessage{MTU: mtu}) 146 | if err != nil { 147 | return err 148 | } 149 | _, err = c.socketFile.Write(data) 150 | return err 151 | } 152 | 153 | func (c *childUnixSocketConn) ReceiveACK() (err error) { 154 | var msg ACKMessage 155 | if err = json.NewDecoder(c.socketFile).Decode(&msg); err != nil { 156 | return 157 | } 158 | if !msg.ACK { 159 | return errors.New("network stack initialization is not acknowledged") 160 | } 161 | return 162 | } 163 | 164 | type MTUMessage struct { 165 | MTU uint32 `json:"mtu"` 166 | } 167 | 168 | func setupIPNetwork() error { 169 | lo, err := netlink.LinkByName(loDevice) 170 | if err != nil { 171 | return err 172 | } 173 | if err = netlink.LinkSetUp(lo); err != nil { 174 | return err 175 | } 176 | 177 | tun0, tunAddr, err := setupIPAddress(tunDevice, tunNetworkAddr) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | return netlink.RouteAdd(&netlink.Route{ 183 | Gw: tunAddr.IP, 184 | LinkIndex: tun0.Attrs().Index, 185 | }) 186 | } 187 | 188 | func setupIPAddress(device, networkAddr string) (dev netlink.Link, addr *netlink.Addr, err error) { 189 | dev, err = netlink.LinkByName(device) 190 | if err != nil { 191 | return 192 | } 193 | if err = netlink.LinkSetUp(dev); err != nil { 194 | return 195 | } 196 | addr, err = netlink.ParseAddr(networkAddr) 197 | if err != nil { 198 | return 199 | } 200 | err = netlink.AddrAdd(dev, addr) 201 | return 202 | } 203 | -------------------------------------------------------------------------------- /command/run_container_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package command 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | const ( 12 | loDevice = "lo" 13 | tunDevice = "tun0" 14 | tunNetworkAddr = "10.1.1.1/24" 15 | ) 16 | 17 | func newRunContainerCmd() *runContainerCmd { 18 | c := &runContainerCmd{} 19 | 20 | cmd := &cobra.Command{ 21 | Use: "runc [flags] command", 22 | Short: "Internal command to run a new process inside an isolated container", 23 | Hidden: true, 24 | RunE: func(cmd *cobra.Command, args []string) (err error) { 25 | return errors.New("this command is not supported by your OS") 26 | }, 27 | } 28 | 29 | c.cmd = cmd 30 | return c 31 | } 32 | 33 | type runContainerCmd struct { 34 | cmd *cobra.Command 35 | } 36 | -------------------------------------------------------------------------------- /command/run_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package command 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/rs/zerolog" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func newRunCmd(log *zerolog.Logger) *runCmd { 13 | c := &runCmd{} 14 | 15 | cmd := &cobra.Command{ 16 | Use: "run [flags] command", 17 | Short: "Proxy application traffic through the socks5 server", 18 | Long: "Run a command in an unprivileged container that transparently proxies application traffic through the socks5 server", 19 | Hidden: true, 20 | RunE: func(cmd *cobra.Command, args []string) (err error) { 21 | return errors.New("this command is not supported by your OS") 22 | }, 23 | } 24 | 25 | c.cmd = cmd 26 | return c 27 | } 28 | 29 | type runCmd struct { 30 | cmd *cobra.Command 31 | } 32 | -------------------------------------------------------------------------------- /command/server.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/ginuerzh/gosocks5/server" 12 | "github.com/rs/zerolog" 13 | "github.com/spf13/cobra" 14 | "github.com/v-byte-cpu/wirez/pkg/connect" 15 | ) 16 | 17 | func newServerCmd(log *zerolog.Logger) *serverCmd { 18 | c := &serverCmd{} 19 | 20 | cmd := &cobra.Command{ 21 | Use: "server [flags]", 22 | Example: "server -l 127.0.0.1:1080 -f proxies.txt", 23 | Short: "Start SOCKS5 server to load-balance requests", 24 | RunE: func(cmd *cobra.Command, args []string) (err error) { 25 | f, err := os.Open(c.opts.proxyFile) 26 | if err != nil { 27 | return err 28 | } 29 | defer f.Close() 30 | socksAddrs, err := parseProxyFile(f) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | log.Info().Msgf("starting listening on %s...", c.opts.listenAddr) 36 | ln, err := net.Listen("tcp", c.opts.listenAddr) 37 | if err != nil { 38 | return err 39 | } 40 | srv := &server.Server{ 41 | Listener: ln, 42 | } 43 | 44 | dconn := connect.NewDirectConnector() 45 | tcpProxies := make([]connect.Connector, 0, len(socksAddrs)) 46 | for _, socksAddr := range socksAddrs { 47 | socksConn := connect.NewSOCKS5Connector(dconn, socksAddr) 48 | tcpProxies = append(tcpProxies, socksConn) 49 | } 50 | rotationTCPConn := connect.NewRotationConnector(tcpProxies) 51 | 52 | udpProxies := make([]connect.Connector, 0, len(socksAddrs)) 53 | for _, socksAddr := range socksAddrs { 54 | socksConn := connect.NewSOCKS5UDPConnector(log, dconn, dconn, socksAddr) 55 | udpProxies = append(udpProxies, socksConn) 56 | } 57 | rotationUDPConn := connect.NewRotationConnector(udpProxies) 58 | 59 | go func() { 60 | ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 61 | defer cancel() 62 | <-ctx.Done() 63 | if err := srv.Close(); err != nil { 64 | log.Error().Err(err).Msg("") 65 | } 66 | }() 67 | 68 | err = srv.Serve(connect.NewSOCKS5ServerHandler(log, rotationTCPConn, rotationUDPConn, connect.NewTransporter(log))) 69 | if err != nil && !errors.Is(err, net.ErrClosed) { 70 | return err 71 | } 72 | return nil 73 | }, 74 | } 75 | 76 | c.opts.initCliFlags(cmd) 77 | 78 | c.cmd = cmd 79 | return c 80 | } 81 | 82 | type serverCmd struct { 83 | cmd *cobra.Command 84 | opts serverCmdOpts 85 | } 86 | 87 | type serverCmdOpts struct { 88 | listenAddr string 89 | proxyFile string 90 | } 91 | 92 | func (o *serverCmdOpts) initCliFlags(cmd *cobra.Command) { 93 | cmd.Flags().StringVarP(&o.listenAddr, "listen", "l", ":1080", "SOCKS5 server address") 94 | cmd.Flags().StringVarP(&o.proxyFile, "file", "f", "proxies.txt", "SOCKS5 proxies file") 95 | } 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/v-byte-cpu/wirez 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/ginuerzh/gosocks5 v0.2.0 7 | github.com/rs/zerolog v1.28.0 8 | github.com/spf13/cobra v1.5.0 9 | github.com/spf13/pflag v1.0.5 10 | github.com/stretchr/testify v1.7.0 11 | github.com/vishvananda/netlink v1.1.0 12 | go.uber.org/multierr v1.7.0 13 | golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 14 | gvisor.dev/gvisor v0.0.0-20220816193615-632fd54acfb3 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/google/btree v1.0.1 // indirect 20 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 21 | github.com/mattn/go-colorable v0.1.12 // indirect 22 | github.com/mattn/go-isatty v0.0.14 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect 25 | go.uber.org/atomic v1.7.0 // indirect 26 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect 27 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/ginuerzh/gosocks5 v0.2.0 h1:K0Ua23U9LU3BZrf3XpGDcs0mP8DiEpa6PJE4TA/MU3s= 7 | github.com/ginuerzh/gosocks5 v0.2.0/go.mod h1:qp22mr6tH/prEoaN0pFukq76LlScIE+F2rP2ZP5ZHno= 8 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 9 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 10 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 11 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 12 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 13 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 14 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 15 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 16 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 21 | github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= 22 | github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 23 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 24 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= 25 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 26 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 27 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 30 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 31 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= 33 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 34 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 35 | github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= 36 | github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 37 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 38 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 39 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 40 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 41 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU= 46 | golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= 48 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 54 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | gvisor.dev/gvisor v0.0.0-20220816193615-632fd54acfb3 h1:tUIBK8FT792eTihrOgrENtwO5ODoa3Mhj9ruZJ7anZo= 56 | gvisor.dev/gvisor v0.0.0-20220816193615-632fd54acfb3/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/v-byte-cpu/wirez/command" 7 | ) 8 | 9 | // will be injected during release 10 | var ( 11 | version = "dev" 12 | commit = "" 13 | ) 14 | 15 | func main() { 16 | command.Main(buildVersion(version, commit)) 17 | } 18 | 19 | func buildVersion(version, commit string) string { 20 | result := version 21 | if commit != "" { 22 | result = fmt.Sprintf("%s\ncommit: %s", result, commit) 23 | } 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestBuildVersion(t *testing.T) { 10 | t.Parallel() 11 | 12 | tests := []struct { 13 | name string 14 | version string 15 | commit string 16 | expected string 17 | }{ 18 | { 19 | name: "VersionOnly", 20 | version: "0.1.0", 21 | expected: "0.1.0", 22 | }, 23 | { 24 | name: "VersionAndCommit", 25 | version: "0.1.0", 26 | commit: "1234567", 27 | expected: "0.1.0\ncommit: 1234567", 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | result := buildVersion(tt.version, tt.commit) 33 | require.Equal(t, tt.expected, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/connect/connect.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "sync/atomic" 15 | "time" 16 | 17 | "github.com/ginuerzh/gosocks5" 18 | "github.com/ginuerzh/gosocks5/client" 19 | "github.com/rs/zerolog" 20 | "github.com/rs/zerolog/log" 21 | "go.uber.org/multierr" 22 | ) 23 | 24 | const ( 25 | // tcpIOTimeout is the default timeout for each TCP i/o operation. 26 | tcpIOTimeout = 1 * time.Minute 27 | // udpIOTimeout is the default timeout for each UDP i/o operation. 28 | udpIOTimeout = 15 * time.Second 29 | // connectTimeout is the default timeout for TCP/UDP dial connect 30 | connectTimeout = 3 * time.Second 31 | ) 32 | 33 | // Connector is responsible for connecting to the destination address. 34 | type Connector interface { 35 | DialContext(ctx context.Context, network, address string) (net.Conn, error) 36 | } 37 | 38 | func NewDirectConnector() Connector { 39 | return &net.Dialer{} 40 | } 41 | 42 | type SocksAddr struct { 43 | Address string 44 | Auth *url.Userinfo 45 | } 46 | 47 | func NewSOCKS5Connector(connector Connector, socksAddr *SocksAddr) Connector { 48 | selector := client.DefaultSelector 49 | if socksAddr.Auth != nil { 50 | selector = client.NewClientSelector(socksAddr.Auth, gosocks5.MethodUserPass, gosocks5.MethodNoAuth) 51 | } 52 | return &socks5Connector{ 53 | tcpConnector: connector, 54 | selector: selector, 55 | socksAddress: socksAddr.Address, 56 | } 57 | } 58 | 59 | type socks5Connector struct { 60 | tcpConnector Connector 61 | selector gosocks5.Selector 62 | socksAddress string 63 | } 64 | 65 | func (c *socks5Connector) DialContext(ctx context.Context, network, address string) (conn net.Conn, err error) { 66 | if network != "tcp" { 67 | return nil, fmt.Errorf("network %s is not supported", network) 68 | } 69 | dstAddr, err := gosocks5.NewAddr(address) 70 | if err != nil { 71 | return 72 | } 73 | 74 | if conn, err = c.tcpConnector.DialContext(ctx, "tcp", c.socksAddress); err != nil { 75 | return 76 | } 77 | if err = conn.SetDeadline(time.Now().Add(connectTimeout)); err != nil { 78 | return 79 | } 80 | defer func() { 81 | err = multierr.Append(err, conn.SetDeadline(time.Time{})) 82 | }() 83 | 84 | cc := gosocks5.ClientConn(conn, c.selector) 85 | if err = cc.Handleshake(); err != nil { 86 | return 87 | } 88 | conn = cc 89 | 90 | req := gosocks5.NewRequest(gosocks5.CmdConnect, dstAddr) 91 | if err = req.Write(conn); err != nil { 92 | return 93 | } 94 | reply, err := gosocks5.ReadReply(conn) 95 | if err != nil { 96 | return 97 | } 98 | if reply.Rep != gosocks5.Succeeded { 99 | return conn, fmt.Errorf("destination address [%s] is unavailable", dstAddr) 100 | } 101 | return 102 | } 103 | 104 | func NewSOCKS5UDPConnector(log *zerolog.Logger, tcpConnector Connector, udpConnector Connector, socksAddr *SocksAddr) Connector { 105 | selector := client.DefaultSelector 106 | if socksAddr.Auth != nil { 107 | selector = client.NewClientSelector(socksAddr.Auth, gosocks5.MethodUserPass, gosocks5.MethodNoAuth) 108 | } 109 | return &socks5UDPConnector{ 110 | log: log, 111 | tcpConnector: tcpConnector, 112 | udpConnector: udpConnector, 113 | selector: selector, 114 | socksAddress: socksAddr.Address, 115 | } 116 | } 117 | 118 | type socks5UDPConnector struct { 119 | log *zerolog.Logger 120 | tcpConnector Connector 121 | udpConnector Connector 122 | selector gosocks5.Selector 123 | socksAddress string 124 | } 125 | 126 | func (c *socks5UDPConnector) DialContext(ctx context.Context, network, address string) (_ net.Conn, err error) { 127 | if network != "udp" { 128 | return nil, fmt.Errorf("network %s is not supported", network) 129 | } 130 | dstAddr, err := gosocks5.NewAddr(address) 131 | if err != nil { 132 | return 133 | } 134 | dstUDPAddr, err := net.ResolveUDPAddr("udp", address) 135 | if err != nil { 136 | return 137 | } 138 | 139 | socksConn, err := c.tcpConnector.DialContext(ctx, "tcp", c.socksAddress) 140 | if err != nil { 141 | return 142 | } 143 | defer func() { 144 | if err != nil { 145 | err = multierr.Append(err, socksConn.Close()) 146 | } 147 | }() 148 | if err = socksConn.SetDeadline(time.Now().Add(connectTimeout)); err != nil { 149 | return 150 | } 151 | defer func() { 152 | err = multierr.Append(err, socksConn.SetDeadline(time.Time{})) 153 | }() 154 | 155 | cc := gosocks5.ClientConn(socksConn, c.selector) 156 | if err = cc.Handleshake(); err != nil { 157 | return 158 | } 159 | socksConn = cc 160 | 161 | req := gosocks5.NewRequest(gosocks5.CmdUdp, &gosocks5.Addr{Type: dstAddr.Type}) 162 | if err = req.Write(socksConn); err != nil { 163 | return 164 | } 165 | c.log.Debug().Str("dstAddr", address).Msg("udp cmd request write success") 166 | reply, err := gosocks5.ReadReply(socksConn) 167 | if err != nil { 168 | return 169 | } 170 | if reply.Rep != gosocks5.Succeeded { 171 | return nil, errors.New("service unavailable") 172 | } 173 | replyAddr := reply.Addr.String() 174 | c.log.Debug().Str("dstAddr", address).Str("replyAddr", replyAddr).Msg("udp cmd reply success") 175 | 176 | uc, err := c.udpConnector.DialContext(ctx, "udp", replyAddr) 177 | if err != nil { 178 | return 179 | } 180 | c.log.Debug().Str("local udp addr", uc.LocalAddr().String()) 181 | //nolint:errcheck 182 | go func() { 183 | io.Copy(io.Discard, socksConn) 184 | socksConn.Close() 185 | // A UDP association terminates when the TCP connection that the UDP 186 | // ASSOCIATE request arrived on terminates. RFC1928 187 | uc.Close() 188 | }() 189 | 190 | if dstUDPAddr.IP.IsUnspecified() { 191 | return newSocksRawUDPConn(uc, socksConn), nil 192 | } 193 | return newSocksUDPConn(uc, socksConn, dstUDPAddr), nil 194 | } 195 | 196 | func newSocksRawUDPConn(udpConn net.Conn, tcpConn net.Conn) *socksRawUDPConn { 197 | return &socksRawUDPConn{Conn: udpConn, tcpConn: tcpConn} 198 | } 199 | 200 | type socksRawUDPConn struct { 201 | net.Conn 202 | tcpConn net.Conn 203 | } 204 | 205 | func (c *socksRawUDPConn) Write(b []byte) (n int, err error) { 206 | n, err = c.Conn.Write(b) 207 | if err != nil { 208 | log.Print("rawUDPConn error: ", err) 209 | } 210 | return n, err 211 | } 212 | 213 | func (c *socksRawUDPConn) Close() error { 214 | err := c.Conn.Close() 215 | return multierr.Append(err, c.tcpConn.Close()) 216 | } 217 | 218 | func newSocksUDPConn(udpConn net.Conn, tcpConn net.Conn, dstAddr *net.UDPAddr) *socksUDPConn { 219 | return &socksUDPConn{Conn: udpConn, tcpConn: tcpConn, dstAddr: dstAddr} 220 | } 221 | 222 | type socksUDPConn struct { 223 | net.Conn 224 | tcpConn net.Conn 225 | dstAddr *net.UDPAddr 226 | } 227 | 228 | var _ net.PacketConn = (*socksUDPConn)(nil) 229 | var _ net.Conn = (*socksUDPConn)(nil) 230 | 231 | func (c *socksUDPConn) Read(b []byte) (n int, err error) { 232 | n, _, err = c.ReadFrom(b) 233 | return 234 | } 235 | 236 | func (c *socksUDPConn) Write(b []byte) (n int, err error) { 237 | n, err = c.WriteTo(b, c.dstAddr) 238 | return n, err 239 | } 240 | 241 | func (c *socksUDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { 242 | toAddr, err := gosocks5.NewAddr(addr.String()) 243 | if err != nil { 244 | return 245 | } 246 | // TODO buffer pool 247 | buf := &bytes.Buffer{} 248 | h := &gosocks5.UDPHeader{Addr: toAddr} 249 | if err = h.Write(buf); err != nil { 250 | return 251 | } 252 | if _, err = buf.Write(b); err != nil { 253 | return 254 | } 255 | _, err = c.Conn.Write(buf.Bytes()) 256 | return len(b), err 257 | } 258 | 259 | func (c *socksUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { 260 | n, err := c.Conn.Read(b) 261 | if err != nil { 262 | return 0, nil, err 263 | } 264 | buf := bytes.NewBuffer(b[:n]) 265 | packet, err := gosocks5.ReadUDPDatagram(buf) 266 | if err != nil { 267 | return 0, nil, err 268 | } 269 | copy(b, packet.Data) 270 | fromAddr, err := net.ResolveUDPAddr("udp", packet.Header.Addr.String()) 271 | if err != nil { 272 | return 0, nil, err 273 | } 274 | return len(packet.Data), fromAddr, nil 275 | } 276 | 277 | func (c *socksUDPConn) Close() error { 278 | err := c.Conn.Close() 279 | return multierr.Append(err, c.tcpConn.Close()) 280 | } 281 | 282 | // TODO performance metrics 283 | // TODO add/remove dynamic connectors 284 | 285 | func NewRotationConnector(connectors []Connector) Connector { 286 | return &rotationConnector{connectors: connectors} 287 | } 288 | 289 | type rotationConnector struct { 290 | connectors []Connector 291 | robin uint32 292 | } 293 | 294 | func (c *rotationConnector) DialContext(ctx context.Context, network, address string) (conn net.Conn, err error) { 295 | i := int(atomic.AddUint32(&c.robin, 1) % uint32(len(c.connectors))) 296 | return c.connectors[i].DialContext(ctx, network, address) 297 | } 298 | 299 | type localForwardingConnector struct { 300 | directConnector Connector 301 | socksConnector Connector 302 | nat AddressMapper 303 | } 304 | 305 | func NewLocalForwardingConnector(directConnector Connector, socksConnector Connector, nat AddressMapper) Connector { 306 | return &localForwardingConnector{ 307 | directConnector: directConnector, 308 | socksConnector: socksConnector, 309 | nat: nat, 310 | } 311 | } 312 | 313 | func (c *localForwardingConnector) DialContext(ctx context.Context, network, address string) (conn net.Conn, err error) { 314 | if newAddress, ok := c.nat.MapAddress(network, address); ok { 315 | return c.directConnector.DialContext(ctx, network, newAddress) 316 | } 317 | return c.socksConnector.DialContext(ctx, network, address) 318 | } 319 | 320 | type AddressMapper interface { 321 | MapAddress(network, address string) (mappedAddress string, exists bool) 322 | AddAddressMapping(network, fromAddress, toAddress string) error 323 | } 324 | 325 | type addressMapper struct { 326 | mu sync.RWMutex 327 | nat map[string]map[string]string 328 | } 329 | 330 | func NewAddressMapper() AddressMapper { 331 | return &addressMapper{ 332 | nat: make(map[string]map[string]string), 333 | } 334 | } 335 | 336 | func (m *addressMapper) MapAddress(network, address string) (mappedAddress string, exists bool) { 337 | m.mu.RLock() 338 | defer m.mu.RUnlock() 339 | if mappedAddress, exists = m.nat[network][address]; exists { 340 | return 341 | } 342 | port := address[strings.LastIndex(address, ":")+1:] 343 | mappedAddress, exists = m.nat[network][port] 344 | return 345 | } 346 | 347 | func (m *addressMapper) AddAddressMapping(network, fromAddress, toAddress string) error { 348 | m.mu.Lock() 349 | defer m.mu.Unlock() 350 | if _, ok := m.nat[network]; !ok { 351 | m.nat[network] = make(map[string]string) 352 | } 353 | if !strings.Contains(fromAddress, ":") { 354 | fromAddress = ":" + fromAddress 355 | } 356 | host, port, err := net.SplitHostPort(fromAddress) 357 | if err != nil { 358 | return err 359 | } 360 | if _, err = strconv.ParseUint(port, 10, 16); err != nil { 361 | return err 362 | } 363 | if host == "" || host == "0.0.0.0" { 364 | fromAddress = port 365 | } 366 | m.nat[network][fromAddress] = toAddress 367 | return nil 368 | } 369 | -------------------------------------------------------------------------------- /pkg/connect/connect_test.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestAddressMapper(t *testing.T) { 10 | t.Run("EmptyMapper", func(t *testing.T) { 11 | m := NewAddressMapper() 12 | _, ok := m.MapAddress("tcp", "1.1.1.1:53") 13 | require.False(t, ok) 14 | }) 15 | t.Run("OneFullAddressMapped", func(t *testing.T) { 16 | m := NewAddressMapper() 17 | err := m.AddAddressMapping("tcp", "1.1.1.1:53", "127.0.0.1:5353") 18 | require.NoError(t, err) 19 | 20 | mappedAddress, ok := m.MapAddress("tcp", "1.1.1.1:53") 21 | require.True(t, ok) 22 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 23 | }) 24 | t.Run("OneFullAddressNotMapped", func(t *testing.T) { 25 | m := NewAddressMapper() 26 | err := m.AddAddressMapping("tcp", "1.1.1.1:53", "127.0.0.1:5353") 27 | require.NoError(t, err) 28 | 29 | _, ok := m.MapAddress("tcp", "2.2.2.2:53") 30 | require.False(t, ok) 31 | }) 32 | t.Run("TwoFullAddressesMapped", func(t *testing.T) { 33 | m := NewAddressMapper() 34 | err := m.AddAddressMapping("tcp", "1.1.1.1:53", "127.0.0.1:5353") 35 | require.NoError(t, err) 36 | err = m.AddAddressMapping("tcp", "8.8.8.8:1031", "2.2.2.2:5454") 37 | require.NoError(t, err) 38 | 39 | mappedAddress, ok := m.MapAddress("tcp", "1.1.1.1:53") 40 | require.True(t, ok) 41 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 42 | mappedAddress, ok = m.MapAddress("tcp", "8.8.8.8:1031") 43 | require.True(t, ok) 44 | require.Equal(t, "2.2.2.2:5454", mappedAddress) 45 | }) 46 | t.Run("TCPAndUDPFullAddressesMapped", func(t *testing.T) { 47 | m := NewAddressMapper() 48 | err := m.AddAddressMapping("tcp", "1.1.1.1:53", "127.0.0.1:5353") 49 | require.NoError(t, err) 50 | err = m.AddAddressMapping("udp", "1.1.1.1:53", "2.2.2.2:5454") 51 | require.NoError(t, err) 52 | 53 | mappedAddress, ok := m.MapAddress("tcp", "1.1.1.1:53") 54 | require.True(t, ok) 55 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 56 | mappedAddress, ok = m.MapAddress("udp", "1.1.1.1:53") 57 | require.True(t, ok) 58 | require.Equal(t, "2.2.2.2:5454", mappedAddress) 59 | }) 60 | t.Run("TCPPortMapped", func(t *testing.T) { 61 | m := NewAddressMapper() 62 | err := m.AddAddressMapping("tcp", "53", "127.0.0.1:5353") 63 | require.NoError(t, err) 64 | 65 | mappedAddress, ok := m.MapAddress("tcp", "1.1.1.1:53") 66 | require.True(t, ok) 67 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 68 | mappedAddress, ok = m.MapAddress("tcp", "2.2.2.2:53") 69 | require.True(t, ok) 70 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 71 | }) 72 | t.Run("TCPPortWithColonMapped", func(t *testing.T) { 73 | m := NewAddressMapper() 74 | err := m.AddAddressMapping("tcp", ":53", "127.0.0.1:5353") 75 | require.NoError(t, err) 76 | 77 | mappedAddress, ok := m.MapAddress("tcp", "1.1.1.1:53") 78 | require.True(t, ok) 79 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 80 | mappedAddress, ok = m.MapAddress("tcp", "2.2.2.2:53") 81 | require.True(t, ok) 82 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 83 | }) 84 | t.Run("TCPPortWithAnyIPMapped", func(t *testing.T) { 85 | m := NewAddressMapper() 86 | err := m.AddAddressMapping("tcp", "0.0.0.0:53", "127.0.0.1:5353") 87 | require.NoError(t, err) 88 | 89 | mappedAddress, ok := m.MapAddress("tcp", "1.1.1.1:53") 90 | require.True(t, ok) 91 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 92 | mappedAddress, ok = m.MapAddress("tcp", "2.2.2.2:53") 93 | require.True(t, ok) 94 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 95 | }) 96 | t.Run("TCPAndUDPPortMapped", func(t *testing.T) { 97 | m := NewAddressMapper() 98 | err := m.AddAddressMapping("tcp", "53", "127.0.0.1:5353") 99 | require.NoError(t, err) 100 | err = m.AddAddressMapping("udp", "53", "1.2.3.4:5454") 101 | require.NoError(t, err) 102 | 103 | mappedAddress, ok := m.MapAddress("tcp", "1.1.1.1:53") 104 | require.True(t, ok) 105 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 106 | mappedAddress, ok = m.MapAddress("udp", "2.2.2.2:53") 107 | require.True(t, ok) 108 | require.Equal(t, "1.2.3.4:5454", mappedAddress) 109 | }) 110 | t.Run("InvalidAddressAndPortError", func(t *testing.T) { 111 | m := NewAddressMapper() 112 | err := m.AddAddressMapping("tcp", "1.1.1.1:53:53", "127.0.0.1:5353") 113 | require.Error(t, err) 114 | err = m.AddAddressMapping("tcp", "1.1.1.1:abc", "127.0.0.1:5353") 115 | require.Error(t, err) 116 | err = m.AddAddressMapping("tcp", "abc", "127.0.0.1:5353") 117 | require.Error(t, err) 118 | }) 119 | t.Run("OneFullIPv6AddressMapped", func(t *testing.T) { 120 | m := NewAddressMapper() 121 | err := m.AddAddressMapping("tcp", "[2001:db8::2:1]:53", "127.0.0.1:5353") 122 | require.NoError(t, err) 123 | 124 | mappedAddress, ok := m.MapAddress("tcp", "[2001:db8::2:1]:53") 125 | require.True(t, ok) 126 | require.Equal(t, "127.0.0.1:5353", mappedAddress) 127 | }) 128 | 129 | } 130 | -------------------------------------------------------------------------------- /pkg/connect/network_stack.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package connect 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "math/rand" 10 | "net" 11 | "time" 12 | 13 | "github.com/rs/zerolog" 14 | "gvisor.dev/gvisor/pkg/tcpip" 15 | "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" 16 | "gvisor.dev/gvisor/pkg/tcpip/header" 17 | "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" 18 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" 19 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" 20 | "gvisor.dev/gvisor/pkg/tcpip/stack" 21 | "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" 22 | "gvisor.dev/gvisor/pkg/tcpip/transport/udp" 23 | "gvisor.dev/gvisor/pkg/waiter" 24 | ) 25 | 26 | type NetworkStack struct { 27 | *stack.Stack 28 | log *zerolog.Logger 29 | socksTCPConn Connector 30 | socksUDPConn Connector 31 | transporter Transporter 32 | TcpIOTimeout time.Duration 33 | UdpIOTimeout time.Duration 34 | ConnectTimeout time.Duration 35 | } 36 | 37 | func NewNetworkStack(log *zerolog.Logger, fd int, mtu uint32, tunNetworkAddr string, 38 | socksTCPConn Connector, socksUDPConn Connector, transporter Transporter) (*NetworkStack, error) { 39 | s := &NetworkStack{ 40 | log: log, 41 | socksTCPConn: socksTCPConn, 42 | socksUDPConn: socksUDPConn, 43 | TcpIOTimeout: tcpIOTimeout, 44 | UdpIOTimeout: udpIOTimeout, 45 | ConnectTimeout: connectTimeout, 46 | transporter: transporter, 47 | Stack: stack.New(stack.Options{ 48 | NetworkProtocols: []stack.NetworkProtocolFactory{ 49 | ipv4.NewProtocol, 50 | ipv6.NewProtocol, 51 | }, 52 | TransportProtocols: []stack.TransportProtocolFactory{ 53 | tcp.NewProtocol, 54 | udp.NewProtocol, 55 | }, 56 | DefaultIPTables: defaultIPTables, 57 | }), 58 | } 59 | 60 | ep, err := fdbased.New(&fdbased.Options{ 61 | MTU: mtu, 62 | FDs: []int{fd}, 63 | // TUN only 64 | EthernetHeader: false, 65 | }) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | var defaultNICID tcpip.NICID = 0x01 71 | if err := s.CreateNIC(defaultNICID, ep); err != nil { 72 | return nil, errors.New(err.String()) 73 | } 74 | 75 | if err := s.SetPromiscuousMode(defaultNICID, true); err != nil { 76 | return nil, errors.New(err.String()) 77 | } 78 | if err := s.SetSpoofing(defaultNICID, true); err != nil { 79 | return nil, errors.New(err.String()) 80 | } 81 | if err = s.SetupRouting(defaultNICID, tunNetworkAddr); err != nil { 82 | return nil, err 83 | } 84 | 85 | s.setTCPHandler() 86 | s.setUDPHandler() 87 | return s, nil 88 | } 89 | 90 | func (s *NetworkStack) SetupRouting(nic tcpip.NICID, assignNet string) error { 91 | _, ipNet, err := net.ParseCIDR(assignNet) 92 | if err != nil { 93 | return fmt.Errorf("unable to ParseCIDR(%s): %w", assignNet, err) 94 | } 95 | 96 | subnet, err := tcpip.NewSubnet(tcpip.Address(ipNet.IP), tcpip.AddressMask(ipNet.Mask)) 97 | if err != nil { 98 | return fmt.Errorf("unable to NewSubnet(%s): %w", ipNet, err) 99 | } 100 | 101 | rt := s.GetRouteTable() 102 | rt = append(rt, tcpip.Route{ 103 | Destination: subnet, 104 | NIC: nic, 105 | }) 106 | s.SetRouteTable(rt) 107 | return nil 108 | } 109 | 110 | func (s *NetworkStack) setTCPHandler() { 111 | tcpForwarder := tcp.NewForwarder(s.Stack, 0, 2<<10, func(r *tcp.ForwarderRequest) { 112 | var wq waiter.Queue 113 | id := r.ID() 114 | s.log.Debug().Str("handler", "tcp"). 115 | Stringer("localAddress", id.LocalAddress).Uint16("localPort", id.LocalPort). 116 | Stringer("fromAddress", id.RemoteAddress).Uint16("fromPort", id.RemotePort).Msg("received request") 117 | ep, err := r.CreateEndpoint(&wq) 118 | if err != nil { 119 | s.log.Error().Str("handler", "tcp").Stringer("error", err).Msg("") 120 | // prevent potential half-open TCP connection leak. 121 | r.Complete(true) 122 | return 123 | } 124 | r.Complete(false) 125 | 126 | go func() { 127 | if err := s.handleTCP(gonet.NewTCPConn(&wq, ep), &id); err != nil { 128 | s.log.Error().Str("handler", "tcp").Err(err).Msg("") 129 | } 130 | }() 131 | }) 132 | s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) 133 | } 134 | 135 | func (s *NetworkStack) setUDPHandler() { 136 | udpForwarder := udp.NewForwarder(s.Stack, func(r *udp.ForwarderRequest) { 137 | var wq waiter.Queue 138 | id := r.ID() 139 | s.log.Debug().Str("handler", "udp"). 140 | Stringer("localAddress", id.LocalAddress).Uint16("localPort", id.LocalPort). 141 | Stringer("fromAddress", id.RemoteAddress).Uint16("fromPort", id.RemotePort).Msg("received request") 142 | ep, err := r.CreateEndpoint(&wq) 143 | if err != nil { 144 | s.log.Error().Str("handler", "udp").Stringer("error", err).Msg("") 145 | return 146 | } 147 | go func() { 148 | if err := s.handleUDP(gonet.NewUDPConn(s.Stack, &wq, ep), &id); err != nil { 149 | s.log.Error().Str("handler", "udp").Err(err).Msg("") 150 | } 151 | }() 152 | }) 153 | s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) 154 | } 155 | 156 | func (s *NetworkStack) handleTCP(localConn net.Conn, id *stack.TransportEndpointID) (err error) { 157 | defer localConn.Close() 158 | 159 | address := fmt.Sprintf("%s:%v", id.LocalAddress, id.LocalPort) 160 | 161 | ctx, cancel := context.WithTimeout(context.Background(), s.ConnectTimeout) 162 | defer cancel() 163 | dstConn, err := s.socksTCPConn.DialContext(ctx, "tcp", address) 164 | if err != nil { 165 | return 166 | } 167 | defer dstConn.Close() 168 | 169 | localConn = NewTimeoutConn(localConn, s.TcpIOTimeout) 170 | dstConn = NewTimeoutConn(dstConn, s.TcpIOTimeout) 171 | // relay TCP connections 172 | return s.transporter.Transport(localConn, dstConn) 173 | } 174 | 175 | func (s *NetworkStack) handleUDP(localConn net.Conn, id *stack.TransportEndpointID) (err error) { 176 | defer localConn.Close() 177 | 178 | dstAddress := fmt.Sprintf("%s:%v", id.LocalAddress, id.LocalPort) 179 | s.log.Debug().Str("dstAddr", dstAddress).Msg("handleUDP called") 180 | 181 | ctx, cancel := context.WithTimeout(context.Background(), s.ConnectTimeout) 182 | defer cancel() 183 | dstConn, err := s.socksUDPConn.DialContext(ctx, "udp", dstAddress) 184 | if err != nil { 185 | return 186 | } 187 | defer dstConn.Close() 188 | 189 | localConn = NewTimeoutConn(localConn, s.UdpIOTimeout) 190 | dstConn = NewTimeoutConn(dstConn, s.UdpIOTimeout) 191 | // relay UDP connections 192 | return s.transporter.Transport(localConn, dstConn) 193 | } 194 | 195 | // defaultIPTables creates iptables rules that allow only TCP and UDP traffic 196 | func defaultIPTables(clock tcpip.Clock, rand *rand.Rand) *stack.IPTables { 197 | const ( 198 | TCPAllowRuleNum = iota 199 | _ 200 | DropRuleNum 201 | AllowRuleNum 202 | ) 203 | iptables := stack.DefaultTables(clock, rand) 204 | ipv4filter := iptables.GetTable(stack.FilterID, false) 205 | ipv4filter.Rules = []stack.Rule{ 206 | { 207 | Filter: stack.IPHeaderFilter{ 208 | Protocol: header.TCPProtocolNumber, 209 | CheckProtocol: true, 210 | }, 211 | Target: &stack.AcceptTarget{NetworkProtocol: header.IPv4ProtocolNumber}, 212 | }, 213 | { 214 | Filter: stack.IPHeaderFilter{ 215 | Protocol: header.UDPProtocolNumber, 216 | CheckProtocol: true, 217 | }, 218 | Target: &stack.AcceptTarget{NetworkProtocol: header.IPv4ProtocolNumber}, 219 | }, 220 | {Target: &stack.DropTarget{NetworkProtocol: header.IPv4ProtocolNumber}}, 221 | {Target: &stack.AcceptTarget{NetworkProtocol: header.IPv4ProtocolNumber}}, 222 | } 223 | ipv4filter.BuiltinChains = [stack.NumHooks]int{ 224 | stack.Prerouting: TCPAllowRuleNum, 225 | stack.Input: TCPAllowRuleNum, 226 | stack.Forward: TCPAllowRuleNum, 227 | stack.Output: TCPAllowRuleNum, 228 | stack.Postrouting: AllowRuleNum, 229 | } 230 | ipv4filter.Underflows = [stack.NumHooks]int{ 231 | stack.Prerouting: DropRuleNum, 232 | stack.Input: DropRuleNum, 233 | stack.Forward: DropRuleNum, 234 | stack.Output: DropRuleNum, 235 | stack.Postrouting: DropRuleNum, 236 | } 237 | iptables.ReplaceTable(stack.FilterID, ipv4filter, false) 238 | 239 | ipv6filter := iptables.GetTable(stack.FilterID, true) 240 | ipv6filter.Rules = []stack.Rule{ 241 | { 242 | Filter: stack.IPHeaderFilter{ 243 | Protocol: header.TCPProtocolNumber, 244 | CheckProtocol: true, 245 | }, 246 | Target: &stack.AcceptTarget{NetworkProtocol: header.IPv6ProtocolNumber}, 247 | }, 248 | { 249 | Filter: stack.IPHeaderFilter{ 250 | Protocol: header.UDPProtocolNumber, 251 | CheckProtocol: true, 252 | }, 253 | Target: &stack.AcceptTarget{NetworkProtocol: header.IPv6ProtocolNumber}, 254 | }, 255 | {Target: &stack.DropTarget{NetworkProtocol: header.IPv6ProtocolNumber}}, 256 | {Target: &stack.AcceptTarget{NetworkProtocol: header.IPv6ProtocolNumber}}, 257 | } 258 | ipv6filter.BuiltinChains = [stack.NumHooks]int{ 259 | stack.Prerouting: TCPAllowRuleNum, 260 | stack.Input: TCPAllowRuleNum, 261 | stack.Forward: TCPAllowRuleNum, 262 | stack.Output: TCPAllowRuleNum, 263 | stack.Postrouting: AllowRuleNum, 264 | } 265 | ipv6filter.Underflows = [stack.NumHooks]int{ 266 | stack.Prerouting: DropRuleNum, 267 | stack.Input: DropRuleNum, 268 | stack.Forward: DropRuleNum, 269 | stack.Output: DropRuleNum, 270 | stack.Postrouting: DropRuleNum, 271 | } 272 | iptables.ReplaceTable(stack.FilterID, ipv6filter, true) 273 | 274 | return iptables 275 | } 276 | -------------------------------------------------------------------------------- /pkg/connect/server.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "github.com/ginuerzh/gosocks5" 11 | "github.com/ginuerzh/gosocks5/server" 12 | "github.com/rs/zerolog" 13 | "go.uber.org/multierr" 14 | ) 15 | 16 | func NewSOCKS5ServerHandler(log *zerolog.Logger, socksTCPConn Connector, socksUDPConn Connector, transporter Transporter) server.Handler { 17 | return &serverHandler{ 18 | log: log, selector: server.DefaultSelector, 19 | socksTCPConn: socksTCPConn, socksUDPConn: socksUDPConn, transporter: transporter, 20 | tcpIOTimeout: tcpIOTimeout, 21 | udpIOTimeout: udpIOTimeout, 22 | connectTimeout: connectTimeout, 23 | } 24 | } 25 | 26 | type serverHandler struct { 27 | log *zerolog.Logger 28 | selector gosocks5.Selector 29 | socksTCPConn Connector 30 | socksUDPConn Connector 31 | transporter Transporter 32 | tcpIOTimeout time.Duration 33 | udpIOTimeout time.Duration 34 | connectTimeout time.Duration 35 | } 36 | 37 | func (h *serverHandler) Handle(conn net.Conn) (err error) { 38 | defer func() { 39 | if err != nil { 40 | h.log.Error().Err(err).Msg("") 41 | } 42 | }() 43 | conn = gosocks5.ServerConn(conn, h.selector) 44 | defer conn.Close() 45 | req, err := gosocks5.ReadRequest(conn) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | switch req.Cmd { 51 | case gosocks5.CmdConnect: 52 | return h.handleConnect(conn, req) 53 | case gosocks5.CmdUdp: 54 | return h.handleUDPAssociate(conn, req) 55 | default: 56 | return fmt.Errorf("%d: unsupported command", gosocks5.CmdUnsupported) 57 | } 58 | } 59 | 60 | func (h *serverHandler) handleConnect(localConn net.Conn, req *gosocks5.Request) error { 61 | ctx, cancel := context.WithTimeout(context.Background(), h.connectTimeout) 62 | defer cancel() 63 | dstConn, err := h.socksTCPConn.DialContext(ctx, "tcp", req.Addr.String()) 64 | if err != nil { 65 | return multierr.Append(err, gosocks5.NewReply(gosocks5.HostUnreachable, nil).Write(localConn)) 66 | } 67 | defer dstConn.Close() 68 | 69 | rep := gosocks5.NewReply(gosocks5.Succeeded, nil) 70 | if err := rep.Write(localConn); err != nil { 71 | return err 72 | } 73 | 74 | localConn = NewTimeoutConn(localConn, h.tcpIOTimeout) 75 | dstConn = NewTimeoutConn(dstConn, h.tcpIOTimeout) 76 | return h.transporter.Transport(localConn, dstConn) 77 | } 78 | 79 | func (h *serverHandler) handleUDPAssociate(localConn net.Conn, req *gosocks5.Request) error { 80 | 81 | localHost, _, err := net.SplitHostPort(localConn.LocalAddr().String()) 82 | if err != nil { 83 | return err 84 | } 85 | listenAddr, err := net.ResolveUDPAddr("udp", localHost+":") 86 | if err != nil { 87 | return err 88 | } 89 | listenConn, err := net.ListenUDP("udp", listenAddr) 90 | if err != nil { 91 | return err 92 | } 93 | defer listenConn.Close() 94 | socksListenAddr, err := gosocks5.NewAddr(listenConn.LocalAddr().String()) 95 | if err != nil { 96 | return err 97 | } 98 | rep := gosocks5.NewReply(gosocks5.Succeeded, socksListenAddr) 99 | if err := rep.Write(localConn); err != nil { 100 | return err 101 | } 102 | 103 | buf := trPool.Get().([]byte) 104 | n, sourceAddr, err := listenConn.ReadFromUDP(buf) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | ctx, cancel := context.WithTimeout(context.Background(), h.connectTimeout) 110 | defer cancel() 111 | dstAddr := net.IPv4zero 112 | if req.Addr.Type == gosocks5.AddrIPv6 { 113 | dstAddr = net.IPv6zero 114 | } 115 | dstConn, err := h.socksUDPConn.DialContext(ctx, "udp", dstAddr.String()+":0") 116 | if err != nil { 117 | return err 118 | } 119 | dstConn = NewTimeoutConn(dstConn, h.udpIOTimeout) 120 | if _, err = dstConn.Write(buf[:n]); err != nil { 121 | return err 122 | } 123 | trPool.Put(buf) //nolint:staticcheck 124 | 125 | localUDPConn := &firstConnectUDPConn{UDPConn: listenConn, targetAddr: sourceAddr} 126 | localConn = NewTimeoutConn(localConn, h.udpIOTimeout) 127 | return h.transporter.Transport(localUDPConn, dstConn) 128 | } 129 | 130 | type firstConnectUDPConn struct { 131 | *net.UDPConn 132 | targetAddr *net.UDPAddr 133 | } 134 | 135 | func (c *firstConnectUDPConn) Read(b []byte) (n int, err error) { 136 | n, addr, err := c.UDPConn.ReadFromUDP(b) 137 | if err != nil { 138 | return 139 | } 140 | if !addr.IP.Equal(c.targetAddr.IP) || addr.Port != c.targetAddr.Port { 141 | return 0, errors.New("source ip address is invalid") 142 | } 143 | return 144 | } 145 | 146 | func (c *firstConnectUDPConn) Write(b []byte) (n int, err error) { 147 | return c.UDPConn.WriteToUDP(b, c.targetAddr) 148 | } 149 | -------------------------------------------------------------------------------- /pkg/connect/transport.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/rs/zerolog" 11 | ) 12 | 13 | var ( 14 | trPool = sync.Pool{ 15 | New: func() interface{} { 16 | return make([]byte, 1<<16) 17 | }, 18 | } 19 | ) 20 | 21 | type TimeoutConn struct { 22 | net.Conn 23 | // specifies max amount of time to wait for Read/Write calls to complete 24 | IOTimeout time.Duration 25 | } 26 | 27 | func NewTimeoutConn(conn net.Conn, ioTimeout time.Duration) *TimeoutConn { 28 | return &TimeoutConn{Conn: conn, IOTimeout: ioTimeout} 29 | } 30 | 31 | func (c *TimeoutConn) Read(b []byte) (n int, err error) { 32 | if err = c.SetDeadline(time.Now().Add(c.IOTimeout)); err != nil { 33 | return 34 | } 35 | return c.Conn.Read(b) 36 | } 37 | 38 | func (c *TimeoutConn) Write(b []byte) (n int, err error) { 39 | if err = c.SetDeadline(time.Now().Add(c.IOTimeout)); err != nil { 40 | return 41 | } 42 | return c.Conn.Write(b) 43 | } 44 | 45 | type Transporter interface { 46 | Transport(rw1, rw2 io.ReadWriter) error 47 | } 48 | 49 | func NewTransporter(log *zerolog.Logger) Transporter { 50 | return &transporter{log} 51 | } 52 | 53 | type transporter struct { 54 | log *zerolog.Logger 55 | } 56 | 57 | func (t *transporter) Transport(rw1, rw2 io.ReadWriter) error { 58 | errc := make(chan error, 1) 59 | copyBuf := func(w io.Writer, r io.Reader) { 60 | buf := trPool.Get().([]byte) 61 | defer trPool.Put(buf) //nolint:staticcheck 62 | 63 | _, err := io.CopyBuffer(w, r, buf) 64 | errc <- err 65 | } 66 | go copyBuf(rw1, rw2) 67 | go copyBuf(rw2, rw1) 68 | 69 | err := <-errc 70 | t.log.Debug().Err(err).Msg("close connection") 71 | var terr timeoutError 72 | if err == io.EOF || (errors.As(err, &terr) && terr.Timeout()) { 73 | err = nil 74 | } 75 | return err 76 | } 77 | 78 | type timeoutError interface { 79 | error 80 | Timeout() bool 81 | } 82 | --------------------------------------------------------------------------------