├── .github └── workflows │ └── go-cross-build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── README_cn.md ├── bridge_test.go ├── bridger.go ├── chain ├── bridge.go ├── chain.go ├── default.go └── env.go ├── cmd └── bridge │ ├── main.go │ ├── main_other.go │ └── main_windows.go ├── config └── config.go ├── go.mod ├── go.sum ├── internal ├── dump │ └── dump.go ├── idle │ ├── idle.go │ └── idle_manager.go ├── netutils │ ├── command.go │ ├── net.go │ └── virtual.go ├── pool │ └── bytes.go └── scheme │ ├── scheme.go │ └── scheme_test.go ├── logger └── logger.go └── protocols ├── command ├── command.go └── init.go ├── connect ├── connect.go └── init.go ├── emux ├── init.go └── smux.go ├── local ├── local.go └── local_windows.go ├── netcat ├── init.go └── netcat.go ├── permuteproxy ├── init.go └── permuteproxy.go ├── shadowsocks ├── init.go └── shadowsocks.go ├── snappy ├── init.go └── snappy.go ├── socks4 ├── init.go └── socks4.go ├── socks5 ├── init.go └── socks5.go ├── ssh ├── init.go └── ssh.go └── tls ├── init.go └── tls.go /.github/workflows/go-cross-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.24 16 | - name: Build Cross Platform 17 | uses: wzshiming/action-go-build-cross-plantform@v1 18 | - name: Upload Release Assets 19 | uses: wzshiming/action-upload-release-assets@v1 20 | env: 21 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Log into registry 23 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 24 | - name: Upload Release Images 25 | uses: wzshiming/action-upload-release-images@v1 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | WORKDIR /go/src/github.com/wzshiming/bridge/ 3 | COPY . . 4 | ENV CGO_ENABLED=0 5 | RUN go install ./cmd/bridge 6 | 7 | FROM alpine 8 | COPY --from=builder /go/bin/bridge /usr/local/bin/ 9 | ENTRYPOINT [ "/usr/local/bin/bridge" ] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wzshiming 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 | # Bridge 2 | 3 | Bridge is a TCP proxy tool Support http(s)-connect socks4/4a/5/5h ssh proxycommand 4 | 5 | [![Build](https://github.com/wzshiming/bridge/actions/workflows/go-cross-build.yml/badge.svg)](https://github.com/wzshiming/bridge/actions/workflows/go-cross-build.yml) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/wzshiming/bridge)](https://goreportcard.com/report/github.com/wzshiming/bridge) 7 | [![GoDoc](https://godoc.org/github.com/wzshiming/bridge?status.svg)](https://godoc.org/github.com/wzshiming/bridge) 8 | [![Docker Automated build](https://img.shields.io/docker/cloud/automated/wzshiming/bridge.svg)](https://hub.docker.com/r/wzshiming/bridge) 9 | [![GitHub license](https://img.shields.io/github/license/wzshiming/bridge.svg)](https://github.com/wzshiming/bridge/blob/master/LICENSE) 10 | 11 | - [English](https://github.com/wzshiming/bridge/blob/master/README.md) 12 | - [简体中文](https://github.com/wzshiming/bridge/blob/master/README_cn.md) 13 | 14 | ## Supported protocols 15 | 16 | - [Socks4](https://github.com/wzshiming/socks4) 17 | - [Socks5](https://github.com/wzshiming/socks5) 18 | - [HTTP Proxy](https://github.com/wzshiming/httpproxy) 19 | - [Shadow Socks](https://github.com/wzshiming/shadowsocks) 20 | - [SSH Proxy](https://github.com/wzshiming/sshproxy) 21 | - [Any Proxy](https://github.com/wzshiming/anyproxy) 22 | - [Emux](https://github.com/wzshiming/emux) 23 | 24 | ## Example 25 | 26 | Mapping example.org:80 TCP port to 8080 port of the local machines. 27 | 28 | ``` shell 29 | bridge -b :8080 -p example.org:80 30 | # `curl -H 'Host: example.org' 127.0.0.1:8080` will return to the target page 31 | ``` 32 | 33 | Proxy that can go through various protocols. 34 | 35 | ``` shell 36 | bridge -b :8080 -p example.org:80 -p ssh://username:password@my_server:22 37 | bridge -b :8080 -p example.org:80 -p ssh://username@my_server:22?identity_file=~/.ssh/id_rsa 38 | bridge -b :8080 -p example.org:80 -p socks5://username:password@my_server:1080 39 | bridge -b :8080 -p example.org:80 -p http://username:password@my_server:8080 40 | bridge -b :8080 -p example.org:80 -p 'cmd:nc %h %p' 41 | bridge -b :8080 -p example.org:80 -p 'cmd:ssh sshserver nc %h %p' 42 | ``` 43 | 44 | It can also go through multi-level proxy. 45 | 46 | ``` shell 47 | bridge -b :8080 -p example.org:80 -p http://username:password@my_server2:8080 -p http://username:password@my_server1:8080 48 | ``` 49 | 50 | Using proxy protocol(http/socks4/socks5) instead of direct TCP forwarding. 51 | 52 | ``` shell 53 | bridge -b :8080 -p - 54 | bridge -b :8080 -p - -p http://username:password@my_server1:8080 55 | # `http_proxy=http://127.0.0.1:8080 curl example.org` Will be the proxy 56 | ``` 57 | 58 | You can also use ssh to listen for port mapping from local port to server port, 59 | due to the limitation of sshd, only 127.0.0.1 ports can be monitored. 60 | if you want to provide external services, 61 | you need to change the 'GatewayPorts no' in /etc/ssh/sshd_config to yes 62 | and then reload sshd. 63 | 64 | ``` shell 65 | bridge -b :8080 -b ssh://username:password@my_server:22 -p 127.0.0.1:80 66 | ``` 67 | 68 | More of the time I'm acting as a ssh proxy. 69 | 70 | ``` text 71 | # in ~/.ssh/config 72 | ProxyCommand bridge -p %h:%p -p "ssh://username@my_server?identity_file=~/.ssh/id_rsa" 73 | ``` 74 | 75 | ## Usage 76 | 77 | ``` text 78 | Usage: bridge [-d] \ 79 | [-b=[[tcp://]bind_address]:bind_port \ 80 | [-b=ssh://bridge_bind_address:bridge_bind_port [-b=(socks4://|socks4a://|socks5://|socks5h://|https://|http://|ssh://|cmd:)bridge_bind_address:bridge_bind_port ...]]] \ // 81 | -p=([tcp://]proxy_address:proxy_port|-) \ 82 | [-p=(socks4://|socks4a://|socks5://|socks5h://|https://|http://|ssh://|cmd:)bridge_proxy_address:bridge_proxy_port ...] 83 | -b, --bind strings The first is the listening address, and then the proxy through which the listening address passes. 84 | If it is not filled in, it is redirected to the pipeline. 85 | only SSH and local support listening, so the last proxy must be ssh. 86 | -d, --debug Output the communication data. 87 | -p, --proxy strings The first is the dial-up address, followed by the proxy through which the dial-up address passes. 88 | ``` 89 | 90 | ## Installation 91 | 92 | ``` shell 93 | go install github.com/wzshiming/bridge/cmd/bridge@latest 94 | ``` 95 | 96 | or 97 | 98 | [Download releases](https://github.com/wzshiming/bridge/releases) 99 | 100 | or 101 | 102 | [Image](https://github.com/wzshiming/bridge/pkgs/container/bridge%2Fbridge) 103 | 104 | ## License 105 | 106 | Licensed under the MIT License. See [LICENSE](https://github.com/wzshiming/bridge/blob/master/LICENSE) for the full license text. 107 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # Bridge 2 | 3 | Bridge 是一个支持 http(s)-connect socks4/4a/5/5h ssh proxycommand 的tcp代理工具 4 | 5 | [![Build](https://github.com/wzshiming/bridge/actions/workflows/go-cross-build.yml/badge.svg)](https://github.com/wzshiming/bridge/actions/workflows/go-cross-build.yml) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/wzshiming/bridge)](https://goreportcard.com/report/github.com/wzshiming/bridge) 7 | [![GoDoc](https://godoc.org/github.com/wzshiming/bridge?status.svg)](https://godoc.org/github.com/wzshiming/bridge) 8 | [![Docker Automated build](https://img.shields.io/docker/cloud/automated/wzshiming/bridge.svg)](https://hub.docker.com/r/wzshiming/bridge) 9 | [![GitHub license](https://img.shields.io/github/license/wzshiming/bridge.svg)](https://github.com/wzshiming/bridge/blob/master/LICENSE) 10 | 11 | - [English](https://github.com/wzshiming/bridge/blob/master/README.md) 12 | - [简体中文](https://github.com/wzshiming/bridge/blob/master/README_cn.md) 13 | 14 | ## 支持的协议 15 | 16 | - [Socks4](https://github.com/wzshiming/socks4) 17 | - [Socks5](https://github.com/wzshiming/socks5) 18 | - [HTTP Proxy](https://github.com/wzshiming/httpproxy) 19 | - [Shadow Socks](https://github.com/wzshiming/shadowsocks) 20 | - [SSH Proxy](https://github.com/wzshiming/sshproxy) 21 | - [Any Proxy](https://github.com/wzshiming/anyproxy) 22 | - [Emux](https://github.com/wzshiming/emux) 23 | 24 | ## 示例 25 | 26 | 映射 example.org:80 tcp 端口到本机的 8080 端口. 27 | 28 | ``` shell 29 | bridge -b :8080 -p example.org:80 30 | # `curl -H 'Host: example.org' 127.0.0.1:8080` 将返回目标的页面 31 | ``` 32 | 33 | 可以经过各种协议的代理. 34 | 35 | ``` shell 36 | bridge -b :8080 -p example.org:80 -p ssh://username:password@my_server:22 37 | bridge -b :8080 -p example.org:80 -p ssh://username@my_server:22?identity_file=~/.ssh/id_rsa 38 | bridge -b :8080 -p example.org:80 -p socks5://username:password@my_server:1080 39 | bridge -b :8080 -p example.org:80 -p http://username:password@my_server:8080 40 | bridge -b :8080 -p example.org:80 -p 'cmd:nc %h %p' 41 | bridge -b :8080 -p example.org:80 -p 'cmd:ssh sshserver nc %h %p' 42 | ``` 43 | 44 | 也可以经过多级代理 45 | 46 | ``` shell 47 | bridge -b :8080 -p example.org:80 -p http://username:password@my_server2:8080 -p http://username:password@my_server1:8080 48 | ``` 49 | 50 | 使用代理协议(http/socks4/socks5)代替直接TCP转发. 51 | 52 | ``` shell 53 | bridge -b :8080 -p - 54 | bridge -b :8080 -p - -p http://username:password@my_server1:8080 55 | # `http_proxy=http://127.0.0.1:8080 curl example.org` 将经过代理 56 | ``` 57 | 58 | 也可以通过 ssh 监听端口 本地的端口映射到服务器的端口, 59 | 由于 sshd 的限制只能监听 127.0.0.1 的端口, 60 | 如果想提供对外的服务需要把 /etc/ssh/sshd_config 里的 GatewayPorts no 改成 yes 然后重新加载 sshd. 61 | 62 | ``` shell 63 | bridge -b :8080 -b ssh://username:password@my_server:22 -p 127.0.0.1:80 64 | ``` 65 | 66 | 更多的时候我是用作 ssh 代理的. 67 | 68 | ``` text 69 | # 在 ~/.ssh/config 70 | ProxyCommand bridge -p %h:%p -p "ssh://username@my_server?identity_file=~/.ssh/id_rsa" 71 | ``` 72 | 73 | ## 用法 74 | 75 | ``` text 76 | Usage: bridge [-d] \ 77 | [-b=[[tcp://]bind_address]:bind_port \ 78 | [-b=ssh://bridge_bind_address:bridge_bind_port [-b=(socks4://|socks4a://|socks5://|socks5h://|https://|http://|ssh://|cmd:)bridge_bind_address:bridge_bind_port ...]]] \ // 79 | -p=([tcp://]proxy_address:proxy_port|-) \ 80 | [-p=(socks4://|socks4a://|socks5://|socks5h://|https://|http://|ssh://|cmd:)bridge_proxy_address:bridge_proxy_port ...] 81 | -b, --bind strings 第一个是侦听地址,然后是侦听地址通过的代理。 82 | 如果未填写,则重定向到管道。 83 | 只有ssh和本地支持监听,所以最后一个代理必须是ssh。 84 | -d, --debug 输出通信数据。 85 | -p, --proxy strings 第一个是拨号地址,然后是拨号地址通过的代理。 86 | ``` 87 | 88 | ## 安装 89 | 90 | ``` shell 91 | go install github.com/wzshiming/bridge/cmd/bridge@latest 92 | ``` 93 | 94 | or 95 | 96 | [Download releases](https://github.com/wzshiming/bridge/releases) 97 | 98 | or 99 | 100 | [Image](https://github.com/wzshiming/bridge/pkgs/container/bridge%2Fbridge) 101 | 102 | ## 许可证 103 | 104 | 软包根据MIT License。有关完整的许可证文本,请参阅[LICENSE](https://github.com/wzshiming/bridge/blob/master/LICENSE)。 105 | -------------------------------------------------------------------------------- /bridge_test.go: -------------------------------------------------------------------------------- 1 | package bridge_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "net" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | _ "github.com/wzshiming/bridge/protocols/command" 17 | _ "github.com/wzshiming/bridge/protocols/connect" 18 | _ "github.com/wzshiming/bridge/protocols/emux" 19 | _ "github.com/wzshiming/bridge/protocols/netcat" 20 | _ "github.com/wzshiming/bridge/protocols/permuteproxy" 21 | _ "github.com/wzshiming/bridge/protocols/shadowsocks" 22 | _ "github.com/wzshiming/bridge/protocols/snappy" 23 | _ "github.com/wzshiming/bridge/protocols/socks4" 24 | _ "github.com/wzshiming/bridge/protocols/socks5" 25 | _ "github.com/wzshiming/bridge/protocols/ssh" 26 | _ "github.com/wzshiming/bridge/protocols/tls" 27 | 28 | _ "github.com/wzshiming/anyproxy/proxies/httpproxy" 29 | _ "github.com/wzshiming/anyproxy/proxies/shadowsocks" 30 | _ "github.com/wzshiming/anyproxy/proxies/socks4" 31 | _ "github.com/wzshiming/anyproxy/proxies/socks5" 32 | _ "github.com/wzshiming/anyproxy/proxies/sshproxy" 33 | 34 | "github.com/wzshiming/anyproxy" 35 | "github.com/wzshiming/permuteproxy" 36 | 37 | "github.com/wzshiming/bridge/chain" 38 | "github.com/wzshiming/bridge/logger" 39 | ) 40 | 41 | var ctx = context.Background() 42 | 43 | func bridge(ctx context.Context, listens, dials []string) error { 44 | b := chain.NewBridge(logger.Std, false) 45 | return b.Bridge(ctx, listens, dials) 46 | } 47 | 48 | func MustProxy(addr string) (uri string) { 49 | uri, err := newProxy(addr) 50 | if err != nil { 51 | panic(err) 52 | } 53 | return uri 54 | } 55 | 56 | func newProxy(addr string) (uri string, err error) { 57 | u, err := url.Parse(addr) 58 | if err != nil { 59 | return "", err 60 | } 61 | if strings.Contains(u.Scheme, "+") { 62 | l := &permuteproxy.Proxy{ 63 | ListenConfig: &net.ListenConfig{}, 64 | } 65 | dc, err := l.NewRunner(addr) 66 | if err != nil { 67 | return "", err 68 | } 69 | go func() { 70 | err = dc.Run(ctx) 71 | if err != nil { 72 | slog.Error("run", "err", err) 73 | return 74 | } 75 | }() 76 | return addr, nil 77 | } else { 78 | proxy, err := anyproxy.NewAnyProxy(ctx, []string{addr}, &anyproxy.Config{ 79 | Dialer: &net.Dialer{}, 80 | ListenConfig: &net.ListenConfig{}, 81 | }) 82 | if err != nil { 83 | return "", err 84 | } 85 | host := proxy.Match(u.Host) 86 | listener, err := net.Listen("tcp", u.Host) 87 | if err != nil { 88 | return "", err 89 | } 90 | u.Host = listener.Addr().String() 91 | go func() { 92 | for { 93 | conn, err := listener.Accept() 94 | if err != nil { 95 | slog.Error("accept", "err", err) 96 | return 97 | } 98 | go host.ServeConn(conn) 99 | } 100 | }() 101 | return u.String(), nil 102 | } 103 | } 104 | 105 | var ProxyServer = []string{ 106 | "socks5://127.0.0.1:0", 107 | "socks4://127.0.0.1:0", 108 | "http://127.0.0.1:0", 109 | "ssh://127.0.0.1:0", 110 | "http://h:p@127.0.0.1:0", 111 | "socks4://s4@127.0.0.1:0", 112 | "socks5://s5:p@127.0.0.1:0", 113 | "ssh://s:p@127.0.0.1:0", 114 | "http+snappy://127.0.0.1:45670", 115 | "socks4+snappy://127.0.0.1:45671", 116 | "socks5+snappy://127.0.0.1:45672", 117 | } 118 | 119 | func init() { 120 | for i, proxy := range ProxyServer { 121 | ProxyServer[i] = MustProxy(proxy) 122 | logger.Std.Info(ProxyServer[i]) 123 | } 124 | } 125 | 126 | func TestPortForward(t *testing.T) { 127 | want := "OK" 128 | ser := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 129 | rw.Write([]byte(want)) 130 | })) 131 | 132 | u, err := url.Parse(ser.URL) 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | proxy := getRandomAddress() 138 | 139 | ctx, cancel := context.WithCancel(ctx) 140 | defer cancel() 141 | go func() { 142 | err := bridge(ctx, []string{proxy}, append([]string{u.Host}, ProxyServer...)) 143 | if err != nil { 144 | t.Log(err) 145 | } 146 | }() 147 | 148 | cli := http.Client{} 149 | 150 | for i := 0; i != 10; i++ { 151 | resp, e := cli.Get("http://" + proxy) 152 | if e != nil { 153 | err = e 154 | time.Sleep(time.Second) 155 | continue 156 | } 157 | data, e := io.ReadAll(resp.Body) 158 | if err != nil { 159 | err = e 160 | time.Sleep(time.Second) 161 | continue 162 | } 163 | resp.Body.Close() 164 | if string(data) != want { 165 | err = fmt.Errorf("want %q, got %q", want, data) 166 | time.Sleep(time.Second) 167 | continue 168 | } 169 | err = nil 170 | break 171 | } 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | } 176 | 177 | func TestPortForwardWithRemoteListen(t *testing.T) { 178 | want := "OK" 179 | ser := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 180 | rw.Write([]byte(want)) 181 | })) 182 | 183 | u, err := url.Parse(ser.URL) 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | 188 | proxy := getRandomAddress() 189 | 190 | ctx, cancel := context.WithCancel(ctx) 191 | defer cancel() 192 | go func() { 193 | err := bridge(ctx, append([]string{proxy}, ProxyServer...), []string{u.Host}) 194 | if err != nil { 195 | t.Log(err) 196 | } 197 | }() 198 | 199 | cli := http.Client{} 200 | 201 | for i := 0; i != 10; i++ { 202 | resp, e := cli.Get("http://" + proxy) 203 | if e != nil { 204 | err = e 205 | time.Sleep(time.Second) 206 | continue 207 | } 208 | data, e := io.ReadAll(resp.Body) 209 | if err != nil { 210 | err = e 211 | time.Sleep(time.Second) 212 | continue 213 | } 214 | resp.Body.Close() 215 | if string(data) != want { 216 | err = fmt.Errorf("want %q, got %q", want, data) 217 | time.Sleep(time.Second) 218 | continue 219 | } 220 | err = nil 221 | break 222 | } 223 | if err != nil { 224 | t.Fatal(err) 225 | } 226 | } 227 | 228 | func TestProxy(t *testing.T) { 229 | want := "OK" 230 | ser := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 231 | rw.Write([]byte(want)) 232 | })) 233 | 234 | u, err := url.Parse(ser.URL) 235 | if err != nil { 236 | t.Fatal(err) 237 | } 238 | 239 | proxy := getRandomAddress() 240 | 241 | ctx, cancel := context.WithCancel(ctx) 242 | defer cancel() 243 | go func() { 244 | err := bridge(ctx, []string{proxy}, append([]string{"-"}, ProxyServer...)) 245 | if err != nil { 246 | t.Log(err) 247 | } 248 | }() 249 | 250 | transport := http.DefaultTransport.(*http.Transport).Clone() 251 | transport.Proxy = func(request *http.Request) (*url.URL, error) { 252 | return url.Parse("http://" + proxy) 253 | } 254 | cli := http.Client{ 255 | Transport: transport, 256 | } 257 | for i := 0; i != 10; i++ { 258 | resp, e := cli.Get("http://" + u.Host) 259 | if e != nil { 260 | err = e 261 | time.Sleep(time.Second) 262 | continue 263 | } 264 | data, e := io.ReadAll(resp.Body) 265 | if err != nil { 266 | err = e 267 | time.Sleep(time.Second) 268 | continue 269 | } 270 | resp.Body.Close() 271 | if string(data) != want { 272 | err = fmt.Errorf("want %q, got %q", want, data) 273 | time.Sleep(time.Second) 274 | continue 275 | } 276 | err = nil 277 | break 278 | } 279 | if err != nil { 280 | t.Fatal(err) 281 | } 282 | } 283 | 284 | func TestProxyWithRemoteListen(t *testing.T) { 285 | want := "OK" 286 | ser := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 287 | rw.Write([]byte(want)) 288 | })) 289 | 290 | u, err := url.Parse(ser.URL) 291 | if err != nil { 292 | t.Fatal(err) 293 | } 294 | 295 | proxy := getRandomAddress() 296 | 297 | ctx, cancel := context.WithCancel(ctx) 298 | defer cancel() 299 | go func() { 300 | err := bridge(ctx, append([]string{proxy}, ProxyServer...), []string{"-"}) 301 | if err != nil { 302 | t.Log(err) 303 | } 304 | }() 305 | 306 | transport := http.DefaultTransport.(*http.Transport).Clone() 307 | transport.Proxy = func(request *http.Request) (*url.URL, error) { 308 | return url.Parse("http://" + proxy) 309 | } 310 | cli := http.Client{ 311 | Transport: transport, 312 | } 313 | for i := 0; i != 10; i++ { 314 | resp, e := cli.Get("http://" + u.Host) 315 | if e != nil { 316 | err = e 317 | time.Sleep(time.Second) 318 | continue 319 | } 320 | data, e := io.ReadAll(resp.Body) 321 | if err != nil { 322 | err = e 323 | time.Sleep(time.Second) 324 | continue 325 | } 326 | resp.Body.Close() 327 | if string(data) != want { 328 | err = fmt.Errorf("want %q, got %q", want, data) 329 | time.Sleep(time.Second) 330 | continue 331 | } 332 | err = nil 333 | break 334 | } 335 | if err != nil { 336 | t.Fatal(err) 337 | } 338 | } 339 | 340 | func getRandomAddress() string { 341 | addr, err := net.Listen("tcp", ":0") 342 | if err != nil { 343 | return "" 344 | } 345 | defer addr.Close() 346 | return addr.Addr().String() 347 | } 348 | -------------------------------------------------------------------------------- /bridger.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | // ListenConfig contains options for listening to an address. 9 | type ListenConfig interface { 10 | Listen(ctx context.Context, network, address string) (net.Listener, error) 11 | } 12 | 13 | // ListenConfigFunc type is an adapter for ListenConfig. 14 | type ListenConfigFunc func(ctx context.Context, network, address string) (net.Listener, error) 15 | 16 | // Listen calls b(ctx, network, address) 17 | func (l ListenConfigFunc) Listen(ctx context.Context, network, address string) (net.Listener, error) { 18 | return l(ctx, network, address) 19 | } 20 | 21 | // Dialer contains options for connecting to an address. 22 | type Dialer interface { 23 | DialContext(ctx context.Context, network, address string) (net.Conn, error) 24 | } 25 | 26 | // DialFunc type is an adapter for Dialer. 27 | type DialFunc func(ctx context.Context, network, address string) (net.Conn, error) 28 | 29 | // DialContext calls d(ctx, network, address) 30 | func (d DialFunc) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 31 | return d(ctx, network, address) 32 | } 33 | 34 | // Bridger contains options for crossing a bridge address. 35 | type Bridger interface { 36 | Bridge(ctx context.Context, dialer Dialer, address string) (Dialer, error) 37 | } 38 | 39 | // BridgeFunc type is an adapter for Bridger. 40 | type BridgeFunc func(ctx context.Context, dialer Dialer, address string) (Dialer, error) 41 | 42 | // Bridge calls b(dialer, address) 43 | func (b BridgeFunc) Bridge(ctx context.Context, dialer Dialer, address string) (Dialer, error) { 44 | return b(ctx, dialer, address) 45 | } 46 | 47 | // CommandDialer contains options for connecting to an address with command. 48 | type CommandDialer interface { 49 | CommandDialContext(ctx context.Context, name string, args ...string) (net.Conn, error) 50 | } 51 | 52 | // CommandDialFunc type is an adapter for Dialer with command. 53 | type CommandDialFunc func(ctx context.Context, name string, args ...string) (net.Conn, error) 54 | 55 | // CommandDialContext calls d(ctx, name, args...) 56 | func (d CommandDialFunc) CommandDialContext(ctx context.Context, name string, args ...string) (net.Conn, error) { 57 | return d(ctx, name, args...) 58 | } 59 | 60 | // CommandListenConfig contains options for listening to an address with command. 61 | type CommandListenConfig interface { 62 | CommandListen(ctx context.Context, name string, args ...string) (net.Listener, error) 63 | } 64 | 65 | // CommandListenConfigFunc type is an adapter for ListenConfig with command. 66 | type CommandListenConfigFunc func(ctx context.Context, name string, args ...string) (net.Listener, error) 67 | 68 | // CommandListen calls b(ctx, network, address) 69 | func (l CommandListenConfigFunc) CommandListen(ctx context.Context, name string, args ...string) (net.Listener, error) { 70 | return l(ctx, name, args...) 71 | } 72 | -------------------------------------------------------------------------------- /chain/bridge.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "math/rand" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/wzshiming/anyproxy" 17 | "github.com/wzshiming/bridge" 18 | "github.com/wzshiming/bridge/config" 19 | "github.com/wzshiming/bridge/internal/dump" 20 | "github.com/wzshiming/bridge/internal/idle" 21 | "github.com/wzshiming/bridge/internal/netutils" 22 | "github.com/wzshiming/bridge/internal/pool" 23 | "github.com/wzshiming/bridge/internal/scheme" 24 | "github.com/wzshiming/bridge/logger" 25 | "github.com/wzshiming/bridge/protocols/local" 26 | "github.com/wzshiming/commandproxy" 27 | ) 28 | 29 | type Bridge struct { 30 | logger *slog.Logger 31 | dump bool 32 | chain *BridgeChain 33 | } 34 | 35 | func NewBridge(logger *slog.Logger, dump bool) *Bridge { 36 | return &Bridge{ 37 | logger: logger, 38 | dump: dump, 39 | chain: Default, 40 | } 41 | } 42 | 43 | func (b *Bridge) BridgeWithConfig(ctx context.Context, config config.Chain) error { 44 | var ( 45 | dialer bridge.Dialer = local.LOCAL 46 | listenConfig bridge.ListenConfig = local.LOCAL 47 | ) 48 | dial := config.Proxy[0] 49 | dials := config.Proxy[1:] 50 | 51 | if len(dials) != 0 { 52 | d, err := b.chain.BridgeChainWithConfig(ctx, local.LOCAL, dials...) 53 | if err != nil { 54 | return err 55 | } 56 | dialer = d 57 | } 58 | 59 | // No listener is set, use stdio. 60 | if len(config.Bind) == 0 { 61 | var raw io.ReadWriteCloser = struct { 62 | io.ReadCloser 63 | io.Writer 64 | }{ 65 | ReadCloser: io.NopCloser(os.Stdin), 66 | Writer: os.Stdout, 67 | } 68 | 69 | if b.dump { 70 | raw = dump.NewDumpReadWriteCloser(raw, true, "STDIO", strings.Join(dial.LB, "|")) 71 | } 72 | 73 | return step(ctx, dialer, raw, dial.LB) 74 | } 75 | 76 | listen := config.Bind[0] 77 | listens := config.Bind[1:] 78 | 79 | if len(listens) != 0 { 80 | d, err := b.chain.BridgeChainWithConfig(ctx, local.LOCAL, listens...) 81 | if err != nil { 82 | return err 83 | } 84 | l, ok := d.(bridge.ListenConfig) 85 | if !ok || l == nil { 86 | return fmt.Errorf("the last proxy could not listen") 87 | } 88 | listenConfig = l 89 | } 90 | 91 | if len(dial.LB) != 0 && dial.LB[0] == "-" { 92 | return b.bridgeProxy(ctx, listenConfig, dialer, config.IdleTimeout, listen.LB) 93 | } else { 94 | return b.bridgeStream(ctx, listenConfig, dialer, config.IdleTimeout, listen.LB, dial.LB) 95 | } 96 | } 97 | 98 | func (b *Bridge) Bridge(ctx context.Context, listens, dials []string) error { 99 | conf, err := config.LoadConfigWithArgs(listens, dials) 100 | if err != nil { 101 | return err 102 | } 103 | return b.BridgeWithConfig(ctx, conf[0]) 104 | } 105 | 106 | func (b *Bridge) bridgeStream(ctx context.Context, listenConfig bridge.ListenConfig, dialer bridge.Dialer, idleTimeout time.Duration, listens []string, dials []string) error { 107 | wg := sync.WaitGroup{} 108 | listeners := make([]net.Listener, 0, len(listens)) 109 | for _, l := range listens { 110 | network, listen, ok := scheme.SplitSchemeAddr(l) 111 | if !ok { 112 | err := fmt.Errorf("unsupported protocol format %q", l) 113 | b.logger.Error("SplitSchemeAddr", "err", err) 114 | return err 115 | } 116 | listener, err := netutils.Listen(ctx, listenConfig, network, listen) 117 | if err != nil { 118 | b.logger.Error("Listen", "err", err) 119 | return err 120 | } 121 | listeners = append(listeners, listener) 122 | } 123 | 124 | if ctx != context.Background() { 125 | go func() { 126 | <-ctx.Done() 127 | b.logger.Info("Close all listeners") 128 | for _, listener := range listeners { 129 | if listener == nil { 130 | continue 131 | } 132 | listener.Close() 133 | } 134 | }() 135 | } 136 | 137 | wg.Add(len(listens)) 138 | for i, l := range listens { 139 | go func(i int, l string) { 140 | defer func() { 141 | b.logger.Info("Close listener", "listen", l) 142 | wg.Done() 143 | }() 144 | listener := listeners[i] 145 | 146 | backoff := time.Second / 10 147 | loop: 148 | for ctx.Err() == nil { 149 | raw, err := listener.Accept() 150 | if err != nil { 151 | if ignoreClosedErr(err) != nil { 152 | b.logger.Error("Accept", "err", err) 153 | } 154 | 155 | for ctx.Err() == nil { 156 | backoff <<= 1 157 | if backoff > time.Second*30 { 158 | backoff = time.Second * 30 159 | } 160 | b.logger.Info("Relisten", "backoff", backoff) 161 | time.Sleep(backoff) 162 | 163 | network, listen, ok := scheme.SplitSchemeAddr(l) 164 | if !ok { 165 | b.logger.Error("unsupported protocol", "protocol", l) 166 | return 167 | } 168 | listener, err = netutils.Listen(ctx, listenConfig, network, listen) 169 | if err == nil { 170 | listeners[i] = listener 171 | continue loop 172 | } 173 | b.logger.Error("Relisten", "err", err) 174 | } 175 | return 176 | } 177 | if b.dump { 178 | raw = dump.NewDumpConn(raw, true, raw.RemoteAddr().String(), strings.Join(dials, "|")) 179 | } 180 | if idleTimeout != 0 { 181 | raw = idle.NewIdleConn(raw, idleTimeout) 182 | } 183 | backoff = time.Second / 10 184 | go b.stepIgnoreErr(ctx, dialer, raw, dials) 185 | } 186 | }(i, l) 187 | } 188 | wg.Wait() 189 | return nil 190 | } 191 | 192 | func (b *Bridge) bridgeProxy(ctx context.Context, listenConfig bridge.ListenConfig, dialer bridge.Dialer, idleTimeout time.Duration, listens []string) error { 193 | wg := sync.WaitGroup{} 194 | svc, err := anyproxy.NewAnyProxy(ctx, listens, &anyproxy.Config{ 195 | Dialer: dialer, 196 | ListenConfig: listenConfig, 197 | Logger: logger.Wrap(b.logger, "anyproxy"), 198 | BytesPool: pool.Bytes, 199 | }) 200 | if err != nil { 201 | return err 202 | } 203 | hosts := svc.Hosts() 204 | 205 | listeners := make([]net.Listener, 0, len(listens)) 206 | for _, host := range hosts { 207 | listener, err := netutils.Listen(ctx, listenConfig, "tcp", host) 208 | if err != nil { 209 | b.logger.Error("Listen", "err", err) 210 | return err 211 | } 212 | listeners = append(listeners, listener) 213 | } 214 | 215 | if ctx != context.Background() { 216 | go func() { 217 | <-ctx.Done() 218 | b.logger.Info("Close all listeners") 219 | for _, listener := range listeners { 220 | if listener == nil { 221 | continue 222 | } 223 | listener.Close() 224 | } 225 | }() 226 | } 227 | 228 | wg.Add(len(hosts)) 229 | for i, host := range hosts { 230 | go func(i int, host string) { 231 | defer func() { 232 | b.logger.Info("Close listener", "listen", host) 233 | wg.Done() 234 | }() 235 | 236 | listener := listeners[i] 237 | h := svc.Match(host) 238 | 239 | backoff := time.Second / 10 240 | loop: 241 | for ctx.Err() == nil { 242 | raw, err := listener.Accept() 243 | if err != nil { 244 | if ignoreClosedErr(err) != nil { 245 | b.logger.Error("Accept", "err", err) 246 | } 247 | for ctx.Err() == nil { 248 | backoff <<= 1 249 | if backoff > time.Second*30 { 250 | backoff = time.Second * 30 251 | } 252 | b.logger.Info("Relisten", "backoff", backoff) 253 | time.Sleep(backoff) 254 | 255 | listener, err = netutils.Listen(ctx, listenConfig, "tcp", host) 256 | if err == nil { 257 | listeners[i] = listener 258 | continue loop 259 | } 260 | b.logger.Error("Relisten", "err", err) 261 | } 262 | return 263 | } 264 | h := h 265 | if b.dump { 266 | // In dubug mode, need to know the address of the client. 267 | // Because it is debug, performance is not considered here. 268 | dial := bridge.DialFunc(func(ctx context.Context, network, address string) (c net.Conn, err error) { 269 | c, err = netutils.Dial(ctx, dialer, network, address) 270 | if err != nil { 271 | return nil, err 272 | } 273 | return dump.NewDumpConn(c, false, raw.RemoteAddr().String(), address), nil 274 | }) 275 | svc, err := anyproxy.NewAnyProxy(ctx, listens, &anyproxy.Config{ 276 | Dialer: dial, 277 | ListenConfig: listenConfig, 278 | Logger: logger.Wrap(b.logger, "anyproxy"), 279 | BytesPool: pool.Bytes, 280 | }) 281 | if err != nil { 282 | b.logger.Error("NewAnyProxy", "err", err) 283 | return 284 | } 285 | h = svc.Match(host) 286 | } 287 | if idleTimeout != 0 { 288 | raw = idle.NewIdleConn(raw, idleTimeout) 289 | } 290 | backoff = time.Second / 10 291 | go h.ServeConn(raw) 292 | } 293 | }(i, host) 294 | } 295 | wg.Wait() 296 | return nil 297 | } 298 | 299 | func ignoreClosedErr(err error) error { 300 | if err != nil && err != io.EOF && err != io.ErrClosedPipe && !netutils.IsClosedConnError(err) { 301 | return err 302 | } 303 | return nil 304 | } 305 | 306 | func (b *Bridge) stepIgnoreErr(ctx context.Context, dialer bridge.Dialer, raw io.ReadWriteCloser, dials []string) { 307 | err := step(ctx, dialer, raw, dials) 308 | if ignoreClosedErr(err) != nil { 309 | b.logger.Error("Step", "err", err) 310 | } 311 | } 312 | 313 | func step(ctx context.Context, dialer bridge.Dialer, raw io.ReadWriteCloser, dials []string) error { 314 | defer raw.Close() 315 | 316 | dial := dials[0] 317 | if len(dials) > 1 { 318 | dial = dials[rand.Int()%len(dials)] 319 | } 320 | network, address, ok := scheme.SplitSchemeAddr(dial) 321 | if !ok { 322 | return fmt.Errorf("unsupported protocol format %q", address) 323 | } 324 | 325 | conn, err := netutils.Dial(ctx, dialer, network, address) 326 | if err != nil { 327 | return err 328 | } 329 | buf1 := pool.Bytes.Get() 330 | buf2 := pool.Bytes.Get() 331 | defer func() { 332 | pool.Bytes.Put(buf1) 333 | pool.Bytes.Put(buf2) 334 | }() 335 | return commandproxy.Tunnel(context.Background(), conn, raw, buf1, buf2) 336 | } 337 | 338 | func ShowChainWithConfig(config config.Chain) string { 339 | dials := make([]string, 0, len(config.Proxy)) 340 | listens := make([]string, 0, len(config.Bind)) 341 | for _, proxy := range config.Proxy { 342 | dials = append(dials, strings.Join(proxy.LB, "|")) 343 | } 344 | for _, bind := range config.Bind { 345 | listens = append(listens, strings.Join(bind.LB, "|")) 346 | } 347 | return ShowChain(dials, listens) 348 | } 349 | 350 | func ShowChain(dials, listens []string) string { 351 | dials = removeUserInfo(dials) 352 | listens = reverse(removeUserInfo(listens)) 353 | 354 | if len(listens) == 0 { 355 | return fmt.Sprintln("DIAL", strings.Join(dials, " <- "), "<- LOCAL <- STDIO") 356 | } 357 | return fmt.Sprintln("DIAL", strings.Join(dials, " <- "), "<- LOCAL <-", strings.Join(listens, " <- "), "LISTEN") 358 | } 359 | 360 | func removeUserInfo(addresses []string) []string { 361 | addresses = stringsClone(addresses) 362 | for i := 0; i != len(addresses); i++ { 363 | address := strings.Split(addresses[i], "|") 364 | for j := 0; j != len(address); j++ { 365 | sch, addr, ok := scheme.SplitSchemeAddr(address[j]) 366 | if !ok { 367 | continue 368 | } 369 | p, ok := scheme.JoinSchemeAddr(sch, addr) 370 | if !ok { 371 | continue 372 | } 373 | address[j] = p 374 | } 375 | addresses[i] = strings.Join(address, "|") 376 | } 377 | for i := 0; i != len(addresses); i++ { 378 | addresses[i] = strconv.Quote(addresses[i]) 379 | } 380 | return addresses 381 | } 382 | 383 | func stringsClone(s []string) []string { 384 | n := make([]string, len(s)) 385 | copy(n, s) 386 | return n 387 | } 388 | 389 | func reverse(s []string) []string { 390 | if len(s) < 2 { 391 | return s 392 | } 393 | for i := 0; i != len(s)/2; i++ { 394 | s[i], s[len(s)-1] = s[len(s)-1], s[i] 395 | } 396 | return s 397 | } 398 | -------------------------------------------------------------------------------- /chain/chain.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "net" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/wzshiming/bridge" 13 | "github.com/wzshiming/bridge/config" 14 | "github.com/wzshiming/bridge/internal/scheme" 15 | "github.com/wzshiming/bridge/logger" 16 | ) 17 | 18 | // BridgeChain is a bridger that supports multiple crossing of bridger. 19 | type BridgeChain struct { 20 | DialerFunc func(dialer bridge.Dialer) bridge.Dialer 21 | proto map[string]bridge.Bridger 22 | defaultProto bridge.Bridger 23 | } 24 | 25 | // NewBridgeChain create a new BridgeChain. 26 | func NewBridgeChain() *BridgeChain { 27 | return &BridgeChain{ 28 | proto: map[string]bridge.Bridger{}, 29 | DialerFunc: NewEnvDialer, 30 | } 31 | } 32 | 33 | // BridgeChain is multiple crossing of bridge. 34 | func (b *BridgeChain) BridgeChain(ctx context.Context, dialer bridge.Dialer, addresses ...string) (bridge.Dialer, error) { 35 | if len(addresses) == 0 { 36 | return dialer, nil 37 | } 38 | address := addresses[len(addresses)-1] 39 | d := b.multiDial(dialer, strings.Split(address, "|")) 40 | 41 | addresses = addresses[:len(addresses)-1] 42 | if len(addresses) == 0 { 43 | return d, nil 44 | } 45 | return b.BridgeChain(ctx, d, addresses...) 46 | } 47 | 48 | // BridgeChainWithConfig is multiple crossing of bridge. 49 | func (b *BridgeChain) BridgeChainWithConfig(ctx context.Context, dialer bridge.Dialer, addresses ...config.Node) (bridge.Dialer, error) { 50 | if len(addresses) == 0 { 51 | return dialer, nil 52 | } 53 | d, err := b.bridgeChainWithConfig(ctx, dialer, addresses...) 54 | if err != nil { 55 | return nil, err 56 | } 57 | if b.DialerFunc != nil { 58 | d = b.DialerFunc(d) 59 | } 60 | return d, nil 61 | } 62 | func (b *BridgeChain) bridgeChainWithConfig(ctx context.Context, dialer bridge.Dialer, addresses ...config.Node) (bridge.Dialer, error) { 63 | if len(addresses) == 0 { 64 | return dialer, nil 65 | } 66 | address := addresses[len(addresses)-1] 67 | d := b.multiDial(dialer, address.LB) 68 | 69 | addresses = addresses[:len(addresses)-1] 70 | if len(addresses) == 0 { 71 | return d, nil 72 | } 73 | return b.bridgeChainWithConfig(ctx, d, addresses...) 74 | } 75 | 76 | func (b *BridgeChain) multiDial(dialer bridge.Dialer, addresses []string) bridge.Dialer { 77 | return newBackoffManager(dialer, b.singleDial, addresses) 78 | } 79 | 80 | func (b *BridgeChain) singleDial(ctx context.Context, dialer bridge.Dialer, address string) (bridge.Dialer, error) { 81 | sch, _, ok := scheme.SplitSchemeAddr(address) 82 | if !ok { 83 | return nil, fmt.Errorf("unsupported protocol format %q", address) 84 | } 85 | bridger, ok := b.proto[sch] 86 | if !ok { 87 | if b.defaultProto == nil { 88 | return nil, fmt.Errorf("unsupported protocol %q", sch) 89 | } 90 | bridger = b.defaultProto 91 | } 92 | return bridger.Bridge(ctx, dialer, address) 93 | } 94 | 95 | // Register is register a new bridger for BridgeChain. 96 | func (b *BridgeChain) Register(name string, bridger bridge.Bridger) error { 97 | b.proto[name] = bridger 98 | return nil 99 | } 100 | 101 | // RegisterDefault is register a default bridger for BridgeChain. 102 | func (b *BridgeChain) RegisterDefault(bridger bridge.Bridger) { 103 | b.defaultProto = bridger 104 | } 105 | 106 | type backoffManager struct { 107 | addresses []string 108 | dialers []bridge.Dialer 109 | 110 | baseDialer bridge.Dialer 111 | 112 | bridgeFunc bridge.BridgeFunc 113 | 114 | backoffCount map[int]uint64 115 | 116 | mut sync.Mutex 117 | } 118 | 119 | func newBackoffManager(baseDialer bridge.Dialer, bridgeFunc bridge.BridgeFunc, addresses []string) *backoffManager { 120 | return &backoffManager{ 121 | addresses: addresses, 122 | dialers: make([]bridge.Dialer, len(addresses)), 123 | baseDialer: baseDialer, 124 | bridgeFunc: bridgeFunc, 125 | backoffCount: map[int]uint64{}, 126 | } 127 | } 128 | 129 | func (u *backoffManager) useLeastIndex() int { 130 | min := uint64(math.MaxUint64) 131 | 132 | var index int 133 | for i := range u.addresses { 134 | if u.backoffCount[i] < min { 135 | min = u.backoffCount[i] 136 | index = i 137 | } 138 | } 139 | 140 | u.backoffCount[index]++ 141 | 142 | if min > math.MaxInt32 { 143 | for i := range u.backoffCount { 144 | u.backoffCount[i] -= math.MaxInt32 145 | } 146 | } 147 | return index 148 | } 149 | 150 | func (u *backoffManager) backoff(index int, count uint64) { 151 | u.mut.Lock() 152 | defer u.mut.Unlock() 153 | u.backoffCount[index] += count 154 | } 155 | 156 | func (u *backoffManager) dialContext(ctx context.Context, network, address string) (net.Conn, error) { 157 | u.mut.Lock() 158 | index := u.useLeastIndex() 159 | addr := u.addresses[index] 160 | dialer := u.dialers[index] 161 | u.mut.Unlock() 162 | 163 | if dialer == nil { 164 | d, err := u.bridgeFunc(ctx, u.baseDialer, addr) 165 | if err != nil { 166 | logger.Std.Warn("failed dial", "err", err, "previous", addr) 167 | u.backoff(index, 16) 168 | return nil, err 169 | } 170 | dialer = d 171 | 172 | u.mut.Lock() 173 | u.dialers[index] = d 174 | u.mut.Unlock() 175 | } 176 | 177 | conn, err := dialer.DialContext(ctx, network, address) 178 | if err != nil { 179 | logger.Std.Warn("failed dial target", "err", err, "previous", addr, "target", address) 180 | u.backoff(index, 8) 181 | return nil, err 182 | } 183 | 184 | logger.Std.Info("success dial target", "previous", addr, "target", address) 185 | return conn, nil 186 | } 187 | 188 | func (u *backoffManager) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 189 | var errs []error 190 | tryTimes := len(u.addresses)/2 + 1 191 | for i := 0; i < tryTimes; i++ { 192 | conn, err := u.dialContext(ctx, network, address) 193 | if err != nil { 194 | errs = append(errs, err) 195 | continue 196 | } 197 | return conn, nil 198 | } 199 | return nil, errors.Join(errs...) 200 | } 201 | -------------------------------------------------------------------------------- /chain/default.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | var Default = NewBridgeChain() 4 | -------------------------------------------------------------------------------- /chain/env.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | "github.com/wzshiming/bridge" 10 | "github.com/wzshiming/bridge/protocols/local" 11 | "github.com/wzshiming/hostmatcher" 12 | ) 13 | 14 | var ( 15 | NoProxy hostmatcher.Matcher 16 | OnlyProxy hostmatcher.Matcher 17 | ) 18 | 19 | func init() { 20 | noProxy, ok := os.LookupEnv("no_proxy") 21 | if !ok { 22 | noProxy, ok = os.LookupEnv("NO_PROXY") 23 | } 24 | if ok && noProxy != "" { 25 | list := strings.Split(noProxy, ",") 26 | if len(list) != 0 { 27 | NoProxy = hostmatcher.NewMatcher(list) 28 | } 29 | } 30 | 31 | onlyProxy, ok := os.LookupEnv("only_proxy") 32 | if !ok { 33 | onlyProxy, ok = os.LookupEnv("ONLY_PROXY") 34 | } 35 | if ok && onlyProxy != "" { 36 | list := strings.Split(onlyProxy, ",") 37 | if len(list) != 0 { 38 | OnlyProxy = hostmatcher.NewMatcher(list) 39 | } 40 | } 41 | } 42 | 43 | func NewEnvDialer(dialer bridge.Dialer) bridge.Dialer { 44 | if OnlyProxy == nil && NoProxy == nil { 45 | return dialer 46 | } 47 | if OnlyProxy != nil { 48 | dialer = NewShuntDialer(local.LOCAL, dialer, OnlyProxy) 49 | } 50 | if NoProxy != nil { 51 | dialer = NewShuntDialer(dialer, local.LOCAL, NoProxy) 52 | } 53 | if l, ok := dialer.(bridge.ListenConfig); ok { 54 | return struct { 55 | bridge.Dialer 56 | bridge.ListenConfig 57 | }{ 58 | dialer, 59 | l, 60 | } 61 | } 62 | return dialer 63 | } 64 | 65 | type shuntDialer struct { 66 | dialer bridge.Dialer 67 | matchDialer bridge.Dialer 68 | matcher hostmatcher.Matcher 69 | } 70 | 71 | func NewShuntDialer(dialer bridge.Dialer, matchDialer bridge.Dialer, matcher hostmatcher.Matcher) bridge.Dialer { 72 | if matcher == nil || matchDialer == nil { 73 | return dialer 74 | } 75 | return &shuntDialer{ 76 | dialer: dialer, 77 | matchDialer: matchDialer, 78 | matcher: matcher, 79 | } 80 | } 81 | 82 | func (s *shuntDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 83 | if s.matcher.Match(address) { 84 | return s.matchDialer.DialContext(ctx, network, address) 85 | } 86 | return s.dialer.DialContext(ctx, network, address) 87 | } 88 | -------------------------------------------------------------------------------- /cmd/bridge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "sync" 11 | "syscall" 12 | "time" 13 | 14 | _ "github.com/wzshiming/bridge/protocols/command" 15 | _ "github.com/wzshiming/bridge/protocols/connect" 16 | _ "github.com/wzshiming/bridge/protocols/emux" 17 | _ "github.com/wzshiming/bridge/protocols/netcat" 18 | _ "github.com/wzshiming/bridge/protocols/permuteproxy" 19 | _ "github.com/wzshiming/bridge/protocols/shadowsocks" 20 | _ "github.com/wzshiming/bridge/protocols/snappy" 21 | _ "github.com/wzshiming/bridge/protocols/socks4" 22 | _ "github.com/wzshiming/bridge/protocols/socks5" 23 | _ "github.com/wzshiming/bridge/protocols/ssh" 24 | _ "github.com/wzshiming/bridge/protocols/tls" 25 | 26 | _ "github.com/wzshiming/anyproxy/pprof" 27 | _ "github.com/wzshiming/anyproxy/proxies/httpproxy" 28 | _ "github.com/wzshiming/anyproxy/proxies/shadowsocks" 29 | _ "github.com/wzshiming/anyproxy/proxies/socks4" 30 | _ "github.com/wzshiming/anyproxy/proxies/socks5" 31 | _ "github.com/wzshiming/anyproxy/proxies/sshproxy" 32 | 33 | flag "github.com/spf13/pflag" 34 | "github.com/wzshiming/bridge/chain" 35 | "github.com/wzshiming/bridge/config" 36 | "github.com/wzshiming/bridge/logger" 37 | "github.com/wzshiming/notify" 38 | ) 39 | 40 | var ( 41 | ctx, globalCancel = context.WithCancel(context.Background()) 42 | configs []string 43 | toConfig bool 44 | listens []string 45 | idleTimeout time.Duration 46 | dials []string 47 | dump bool 48 | pprofAddress string 49 | ) 50 | 51 | const defaults = `Bridge is a TCP proxy tool Support http(s)-connect socks4/4a/5/5h ssh proxycommand 52 | More information, please go to https://github.com/wzshiming/bridge 53 | 54 | Usage: bridge [-f path/to/config] [-t] [-d] \ 55 | [-b=[[(tcp://|unix://)]bind_address]:bind_port \ 56 | [-b=ssh://bridge_bind_address:bridge_bind_port [-b=(socks4://|socks4a://|socks5://|socks5h://|https://|http://|ssh://|cmd:)bridge_bind_address:bridge_bind_port ...]]] \ // 57 | -p=([(tcp://|unix://)]proxy_address:proxy_port|-) \ 58 | [-p=(socks4://|socks4a://|socks5://|socks5h://|https://|http://|ssh://|cmd:)bridge_proxy_address:bridge_proxy_port ...] 59 | ` 60 | 61 | func init() { 62 | flag.StringSliceVarP(&configs, "config", "c", nil, "load from config and ignore --bind and --proxy") 63 | flag.BoolVarP(&toConfig, "to-config", "t", false, "args to config") 64 | flag.StringSliceVarP(&listens, "bind", "b", nil, "The first is the listening address, and then the proxy through which the listening address passes.\nIf it is not filled in, it is redirected to the pipeline.\nonly ssh and local support listening, so the last proxy must be ssh.") 65 | flag.StringSliceVarP(&dials, "proxy", "p", nil, "The first is the dial-up address, followed by the proxy through which the dial-up address passes.") 66 | flag.DurationVar(&idleTimeout, "idle-timeout", 0, "The idle timeout for connections.") 67 | flag.StringVar(&pprofAddress, "pprof", "", "The pprof address.") 68 | flag.BoolVarP(&dump, "debug", "d", dump, "Output the communication data.") 69 | flag.Parse() 70 | 71 | signals := []os.Signal{syscall.SIGINT, syscall.SIGTERM} 72 | notify.OnceSlice(signals, func() { 73 | globalCancel() 74 | logger.Std.Info("Wait for the existing task to complete, and exit directly if the signal occurs again") 75 | notify.OnceSlice(signals, func() { 76 | os.Exit(1) 77 | }) 78 | }) 79 | } 80 | 81 | func printDefaults() { 82 | fmt.Fprintf(os.Stderr, defaults) 83 | flag.PrintDefaults() 84 | } 85 | 86 | func main() { 87 | if pprofAddress != "" { 88 | go func() { 89 | err := http.ListenAndServe(pprofAddress, http.DefaultServeMux) 90 | if err != nil { 91 | logger.Std.Error("ListenAndServe", "err", err) 92 | } 93 | }() 94 | } 95 | var tasks []config.Chain 96 | var err error 97 | if len(configs) != 0 { 98 | tasks, err = config.LoadConfig(configs...) 99 | if err != nil { 100 | printDefaults() 101 | logger.Std.Error("LoadConfig", "err", err) 102 | return 103 | } 104 | } else { 105 | tasks, err = config.LoadConfigWithArgs(listens, dials) 106 | if err != nil { 107 | printDefaults() 108 | logger.Std.Error("LoadConfigWithArgs", "err", err) 109 | return 110 | } 111 | } 112 | 113 | if toConfig { 114 | encoder := json.NewEncoder(os.Stdout) 115 | encoder.SetIndent("", " ") 116 | encoder.Encode(config.Config{ 117 | Chains: tasks, 118 | }) 119 | return 120 | } 121 | 122 | if len(configs) != 0 { 123 | runWithReload(ctx, logger.Std, tasks, configs) 124 | } else { 125 | run(ctx, logger.Std, tasks) 126 | } 127 | return 128 | } 129 | 130 | func run(ctx context.Context, log *slog.Logger, tasks []config.Chain) { 131 | var wg sync.WaitGroup 132 | wg.Add(len(tasks)) 133 | for _, task := range tasks { 134 | if task.IdleTimeout == 0 { 135 | task.IdleTimeout = idleTimeout 136 | } 137 | go func(task config.Chain) { 138 | defer wg.Done() 139 | log.Info(chain.ShowChainWithConfig(task)) 140 | b := chain.NewBridge(log, dump) 141 | err := b.BridgeWithConfig(ctx, task) 142 | if err != nil { 143 | log.Error("BridgeWithConfig", "err", err) 144 | } 145 | }(task) 146 | } 147 | wg.Wait() 148 | } 149 | -------------------------------------------------------------------------------- /cmd/bridge/main_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "log/slog" 9 | "sync" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/wzshiming/bridge/chain" 14 | "github.com/wzshiming/bridge/config" 15 | "github.com/wzshiming/notify" 16 | ) 17 | 18 | func runWithReload(ctx context.Context, log *slog.Logger, tasks []config.Chain, configs []string) { 19 | reloadCn := make(chan struct{}, 1) 20 | notify.On(syscall.SIGHUP, func() { 21 | select { 22 | case reloadCn <- struct{}{}: 23 | default: 24 | } 25 | }) 26 | wg := sync.WaitGroup{} 27 | defer wg.Wait() 28 | var lastWorking = map[string]func(){} 29 | var cleanups []func() 30 | count := 1 31 | reloadCn <- struct{}{} 32 | for { 33 | select { 34 | case <-ctx.Done(): 35 | return 36 | case <-reloadCn: 37 | } 38 | log := log.With("reload_count", count) 39 | tasks, err := config.LoadConfig(configs...) 40 | if err != nil { 41 | for { 42 | log.Error("LoadConfig", "err", err) 43 | log.Info("Try reload again after 1 second") 44 | time.Sleep(time.Second) 45 | tasks, err = config.LoadConfig(configs...) 46 | if err == nil { 47 | break 48 | } 49 | } 50 | } 51 | working := map[string]func(){} 52 | for _, task := range tasks { 53 | uniq := task.Unique() 54 | 55 | cleanup := lastWorking[uniq] 56 | if cleanup != nil { 57 | working[uniq] = cleanup 58 | continue 59 | } 60 | 61 | ctx, cancel := context.WithCancel(ctx) 62 | working[uniq] = cancel 63 | wg.Add(1) 64 | go func(ctx context.Context, task config.Chain) { 65 | defer wg.Done() 66 | log.Info(chain.ShowChainWithConfig(task)) 67 | for ctx.Err() == nil { 68 | b := chain.NewBridge(log, dump) 69 | err := b.BridgeWithConfig(ctx, task) 70 | if err != nil { 71 | log.Error("BridgeWithConfig", "err", err) 72 | } 73 | time.Sleep(time.Second) 74 | } 75 | }(ctx, task) 76 | } 77 | 78 | for uniq := range lastWorking { 79 | if _, ok := working[uniq]; !ok { 80 | cancel := lastWorking[uniq] 81 | if cancel != nil { 82 | cleanups = append(cleanups, cancel) 83 | } 84 | } 85 | } 86 | lastWorking = working 87 | 88 | // TODO: wait for all task is working 89 | select { 90 | case <-ctx.Done(): 91 | return 92 | case <-time.After(time.Second): 93 | } 94 | 95 | if len(cleanups) > 0 { 96 | for _, cleanup := range cleanups { 97 | cleanup() 98 | } 99 | cleanups = cleanups[:0] 100 | } 101 | count++ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cmd/bridge/main_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "log/slog" 9 | 10 | "github.com/wzshiming/bridge/config" 11 | ) 12 | 13 | func runWithReload(ctx context.Context, log *slog.Logger, tasks []config.Chain, configs []string) { 14 | run(ctx, log, tasks) 15 | } 16 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/wzshiming/bridge/internal/scheme" 12 | "github.com/wzshiming/bridge/logger" 13 | ) 14 | 15 | func LoadConfigWithArgs(listens []string, dials []string) ([]Chain, error) { 16 | if len(dials) > 0 && len(listens) > 0 && dials[0] == "-" { 17 | proxies := strings.Split(listens[0], "|") 18 | if len(proxies) == 1 { 19 | network, address, _ := scheme.SplitSchemeAddr(proxies[0]) 20 | if network == "tcp" { 21 | proxies = anyProxy(address) 22 | } 23 | } 24 | listens[0] = strings.Join(proxies, "|") 25 | } 26 | data := struct { 27 | Bind []string `json:"bind"` 28 | Proxy []string `json:"proxy"` 29 | }{ 30 | Bind: listens, 31 | Proxy: dials, 32 | } 33 | rawJson, err := json.Marshal(data) 34 | if err != nil { 35 | return nil, err 36 | } 37 | conf := Chain{} 38 | err = json.Unmarshal(rawJson, &conf) 39 | if err != nil { 40 | return nil, err 41 | } 42 | err = conf.Verification() 43 | if err != nil { 44 | return nil, err 45 | } 46 | return []Chain{conf}, nil 47 | } 48 | 49 | func anyProxy(address string) []string { 50 | return []string{"http://" + address, "socks5://" + address, "socks4://" + address, "ssh://" + address} 51 | } 52 | 53 | func LoadConfig(configs ...string) ([]Chain, error) { 54 | tasks := []Chain{} 55 | for _, confPath := range configs { 56 | data, err := os.ReadFile(confPath) 57 | if err != nil { 58 | logger.Std.Error("LoadConfig", "err", err, "path", confPath) 59 | continue 60 | } 61 | conf := Config{} 62 | err = json.Unmarshal(data, &conf) 63 | if err != nil { 64 | return nil, err 65 | } 66 | for _, ch := range conf.Chains { 67 | err := ch.Verification() 68 | if err != nil { 69 | return nil, fmt.Errorf("%s: %w", confPath, err) 70 | } 71 | tasks = append(tasks, ch) 72 | } 73 | } 74 | return tasks, nil 75 | } 76 | 77 | type Config struct { 78 | Chains []Chain `json:"chains"` 79 | } 80 | 81 | type Chain struct { 82 | Bind []Node `json:"bind"` 83 | Proxy []Node `json:"proxy"` 84 | IdleTimeout time.Duration `json:"idle_timeout"` 85 | } 86 | 87 | func (c Chain) Verification() error { 88 | if len(c.Proxy) == 0 { 89 | return fmt.Errorf("must has proxy") 90 | } 91 | return nil 92 | } 93 | 94 | func (c Chain) Unique() string { 95 | d, err := json.Marshal(c) 96 | if err != nil { 97 | return "" 98 | } 99 | return string(d) 100 | } 101 | 102 | type Node struct { 103 | LB []string `json:"lb"` 104 | } 105 | 106 | func (m Node) MarshalJSON() ([]byte, error) { 107 | if len(m.LB) == 1 { 108 | return json.Marshal(m.LB[0]) 109 | } 110 | type node Node 111 | return json.Marshal(node(m)) 112 | } 113 | 114 | func (m *Node) UnmarshalJSON(data []byte) error { 115 | if m == nil { 116 | return fmt.Errorf("node: UnmarshalJSON on nil pointer") 117 | } 118 | if len(data) > 0 { 119 | switch data[0] { 120 | case '"': 121 | str, err := strconv.Unquote(string(data)) 122 | if err != nil { 123 | return err 124 | } 125 | m.LB = strings.Split(str, "|") 126 | return nil 127 | case '[': 128 | return json.Unmarshal(data, &m.LB) 129 | } 130 | } 131 | type node Node 132 | return json.Unmarshal(data, (*node)(m)) 133 | } 134 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wzshiming/bridge 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/Microsoft/go-winio v0.6.2 7 | github.com/golang/snappy v1.0.0 8 | github.com/spf13/pflag v1.0.6 9 | github.com/wzshiming/anyproxy v0.7.19 10 | github.com/wzshiming/cmux v0.4.2 11 | github.com/wzshiming/commandproxy v0.2.1 12 | github.com/wzshiming/emux v0.2.1 13 | github.com/wzshiming/hostmatcher v0.0.3 14 | github.com/wzshiming/httpproxy v0.5.7 15 | github.com/wzshiming/notify v0.1.1 16 | github.com/wzshiming/permuteproxy v0.0.2 17 | github.com/wzshiming/shadowsocks v0.4.2 18 | github.com/wzshiming/socks4 v0.3.3 19 | github.com/wzshiming/socks5 v0.5.2 20 | github.com/wzshiming/sshproxy v0.5.4 21 | ) 22 | 23 | require ( 24 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect 25 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 26 | github.com/wzshiming/sshd v0.2.5 // indirect 27 | github.com/wzshiming/trie v0.3.1 // indirect 28 | golang.org/x/crypto v0.38.0 // indirect 29 | golang.org/x/net v0.40.0 // indirect 30 | golang.org/x/sync v0.14.0 // indirect 31 | golang.org/x/sys v0.33.0 // indirect 32 | golang.org/x/text v0.25.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 2 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 3 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 4 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 5 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 6 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 7 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 8 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 12 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 13 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 14 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 15 | github.com/wzshiming/anyproxy v0.7.19 h1:zSnPiFej7MEx+otPkS6pguuiVBGkmBD3+OfpvdvEQWY= 16 | github.com/wzshiming/anyproxy v0.7.19/go.mod h1:2wtEEJIcJCKWulzEOcn6rMoU6qQZdfT2jFYKErXjCBk= 17 | github.com/wzshiming/cmux v0.4.2 h1:tI73lL5ztVfiqw7R5m5BkxT1+vQ2PBo/oV6qPbNGPiA= 18 | github.com/wzshiming/cmux v0.4.2/go.mod h1:JgE61QfZAjEyNMX0iZo9zIKY6pr9bHVY132yYPwHW5U= 19 | github.com/wzshiming/commandproxy v0.2.0 h1:uPVhgIj2YSncRUo6g9smGR6OMzsIg7lwklcMHPPmEeM= 20 | github.com/wzshiming/commandproxy v0.2.0/go.mod h1:wS6+aJ9KMHciqYX3xmDO0W+QVY0zvngeBvmoIFMfq8A= 21 | github.com/wzshiming/commandproxy v0.2.1 h1:3LbKbNX0JS0CHZH2DpRmhzT+JZLXG6ZgNclL9vpZNCc= 22 | github.com/wzshiming/commandproxy v0.2.1/go.mod h1:wS6+aJ9KMHciqYX3xmDO0W+QVY0zvngeBvmoIFMfq8A= 23 | github.com/wzshiming/emux v0.2.1 h1:pu0oV9PpAJ5cVO8tzkqUXcCqc8xC452vNzQK9cghUis= 24 | github.com/wzshiming/emux v0.2.1/go.mod h1:VQF6NoR4nfm3+OrKZLx47JuxuDeWemHDc0a4qDNtFtg= 25 | github.com/wzshiming/hostmatcher v0.0.3 h1:+JYAq6vUZXDEQ1Ipfdc/D7HmaIMngcc71ftonyCQVQk= 26 | github.com/wzshiming/hostmatcher v0.0.3/go.mod h1:F04RIvIWEvOIrIKOlQlMuR8vQMKAVf2YhpU6l31Wwz4= 27 | github.com/wzshiming/httpproxy v0.5.7 h1:eAdbzsnr0JcXVLst9vp4oHL1rTaUgTYQeCerzkOAP3o= 28 | github.com/wzshiming/httpproxy v0.5.7/go.mod h1:vw/jA1IzuGBj+LndQ8h00IkU8OSS0lOZYw0HtjnnGZw= 29 | github.com/wzshiming/notify v0.1.1 h1:rJXoszrkNglhCVyn/IfW500f5cW03q1q7YzL8hsLchI= 30 | github.com/wzshiming/notify v0.1.1/go.mod h1:SFhsQKZJznzsDcj/Qfo9A65k5IRcpUrpgbLRzZEa/DI= 31 | github.com/wzshiming/permuteproxy v0.0.2 h1:svedMueotlxJk9oJfA0gs8WzRYOdgd0DER9XvKpjwlY= 32 | github.com/wzshiming/permuteproxy v0.0.2/go.mod h1:Ny08A1JbuljB8FeJAOiB7dfvRGCVD8PB9hwrALIvYI8= 33 | github.com/wzshiming/shadowsocks v0.4.2 h1:f3nVW20I/cpRXxHojRoosCM2DgCAGL8zv7T7QFe0Olw= 34 | github.com/wzshiming/shadowsocks v0.4.2/go.mod h1:4VlBH5YAkDUaU/f3rcuk3m+625Hyz3VajJJI2iRlK8E= 35 | github.com/wzshiming/socks4 v0.3.3 h1:IsuqRbDrYfJKCrEXYNW3Nxi5l+4xKpoN7Z18b6xhE1s= 36 | github.com/wzshiming/socks4 v0.3.3/go.mod h1:YEPfhjf/4JezwdTmgXZU+UX+A2KvD05quzhsUBVMNA0= 37 | github.com/wzshiming/socks5 v0.5.2 h1:LtoowVNwAmkIQSkP1r1Wg435xUmC+tfRxorNW30KtnM= 38 | github.com/wzshiming/socks5 v0.5.2/go.mod h1:BvCAqlzocQN5xwLjBZDBbvWlrx8sCYSSbHEOf2wZgT0= 39 | github.com/wzshiming/sshd v0.2.4 h1:GGhZx8qA1GjjY76C2JCwJZHAJ+Kd1Qrm6ykQQueKidg= 40 | github.com/wzshiming/sshd v0.2.4/go.mod h1:dT2CiVtyiXxEdbTTQ46tAzeT9opon98T263ZtV6xU84= 41 | github.com/wzshiming/sshd v0.2.5 h1:tJ4CukRzIHrOjNZscxnHgIt/qh7gggzHnMbkkoOMB5I= 42 | github.com/wzshiming/sshd v0.2.5/go.mod h1:IwV2OzhzyaphzOT7ee/+OF/X42Ht3A9tqk72UOOO3VY= 43 | github.com/wzshiming/sshproxy v0.5.2 h1:vV6nX2xVZNUP68gkuw6rOTOPzyV3pJmXNFtZNX8UAc4= 44 | github.com/wzshiming/sshproxy v0.5.2/go.mod h1:VvDPPaMcGav87evkR25DxF7xGx20E9fMhxYfRoZpiCM= 45 | github.com/wzshiming/sshproxy v0.5.4 h1:RiV9dQ1SMI19we0cZwybY7tx0M+fPHVSow6yHnOrkzw= 46 | github.com/wzshiming/sshproxy v0.5.4/go.mod h1:AIz6ABcDiuN/nA7DSeihsme/6jw/6B4oxSzqD1gAmgs= 47 | github.com/wzshiming/trie v0.3.1 h1:YpuoqmEQFJiW0mns/mM6Qk4kdWrXc8kc28/KR1vn0m8= 48 | github.com/wzshiming/trie v0.3.1/go.mod h1:c9thxXTh4KcGkejt4sUsO4c5GUmWpxeWzOJ7AZJaI+8= 49 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 50 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 51 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 52 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 53 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 54 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 55 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 56 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 57 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 58 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 59 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 60 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 61 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 62 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 63 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 64 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 67 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 68 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 69 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 70 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 71 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 72 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 73 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 74 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 75 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 76 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 77 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 78 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 79 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 81 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 82 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 83 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 84 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 85 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 86 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 87 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 88 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 97 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 98 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 99 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 100 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 101 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 102 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 103 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 104 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 105 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 106 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 107 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 108 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 109 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 110 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 111 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 112 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 113 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 114 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 115 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 116 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 117 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 118 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 119 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 120 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 121 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 122 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 123 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 124 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 125 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 126 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 127 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 128 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 129 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 130 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 131 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 132 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | -------------------------------------------------------------------------------- /internal/dump/dump.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "sync" 10 | ) 11 | 12 | var mut = sync.Mutex{} 13 | 14 | // syncDumper the asynchronous output is locked only for debug with no performance considerations 15 | type syncDumper struct { 16 | Prefix string 17 | Count int64 18 | } 19 | 20 | func (s *syncDumper) Dump(p []byte) (n int, err error) { 21 | if len(p) == 0 { 22 | return 0, nil 23 | } 24 | mut.Lock() 25 | defer mut.Unlock() 26 | s.Count++ 27 | fmt.Printf("# %d. %s \n", s.Count, s.Prefix) 28 | w := hex.Dumper(os.Stderr) 29 | defer w.Close() 30 | return w.Write(p) 31 | } 32 | 33 | type dumpConn struct { 34 | net.Conn 35 | R syncDumper 36 | W syncDumper 37 | } 38 | 39 | func NewDumpConn(conn net.Conn, rev bool, from, to string) net.Conn { 40 | w := syncDumper{Prefix: fmt.Sprintf("Send: %s -> %s", from, to)} 41 | r := syncDumper{Prefix: fmt.Sprintf("Receive: %s <- %s", from, to)} 42 | if rev { 43 | r, w = w, r 44 | } 45 | return &dumpConn{ 46 | Conn: conn, 47 | W: w, 48 | R: r, 49 | } 50 | } 51 | 52 | func (s *dumpConn) Write(p []byte) (n int, err error) { 53 | n, err = s.Conn.Write(p) 54 | s.W.Dump(p[:n]) 55 | return n, err 56 | } 57 | 58 | func (s *dumpConn) Read(p []byte) (n int, err error) { 59 | n, err = s.Conn.Read(p) 60 | s.R.Dump(p[:n]) 61 | return n, err 62 | } 63 | 64 | type dumpReadWriteCloser struct { 65 | io.ReadWriteCloser 66 | R syncDumper 67 | W syncDumper 68 | } 69 | 70 | func NewDumpReadWriteCloser(rwc io.ReadWriteCloser, rev bool, from, to string) io.ReadWriteCloser { 71 | w := syncDumper{Prefix: fmt.Sprintf("Send: %s -> %s", from, to)} 72 | r := syncDumper{Prefix: fmt.Sprintf("Receive: %s <- %s", from, to)} 73 | if rev { 74 | r, w = w, r 75 | } 76 | return &dumpReadWriteCloser{ 77 | ReadWriteCloser: rwc, 78 | W: w, 79 | R: r, 80 | } 81 | } 82 | 83 | func (s *dumpReadWriteCloser) Write(p []byte) (n int, err error) { 84 | n, err = s.ReadWriteCloser.Write(p) 85 | s.W.Dump(p[:n]) 86 | return n, err 87 | } 88 | 89 | func (s *dumpReadWriteCloser) Read(p []byte) (n int, err error) { 90 | n, err = s.ReadWriteCloser.Read(p) 91 | s.R.Dump(p[:n]) 92 | return n, err 93 | } 94 | -------------------------------------------------------------------------------- /internal/idle/idle.go: -------------------------------------------------------------------------------- 1 | package idle 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type idleConn struct { 9 | timeout time.Duration 10 | last time.Time 11 | net.Conn 12 | } 13 | 14 | // NewIdleConn wraps a net.Conn with idle timeout. 15 | func NewIdleConn(conn net.Conn, timeout time.Duration) net.Conn { 16 | c := &idleConn{ 17 | timeout: timeout, 18 | last: time.Now(), 19 | Conn: conn, 20 | } 21 | _ = connManager.add(c) 22 | return c 23 | } 24 | 25 | func (c *idleConn) Read(b []byte) (int, error) { 26 | n, err := c.Conn.Read(b) 27 | if err != nil { 28 | connManager.remove(c) 29 | } else { 30 | c.last = time.Now() 31 | } 32 | return n, err 33 | } 34 | 35 | func (c *idleConn) Write(b []byte) (int, error) { 36 | c.last = time.Now() 37 | n, err := c.Conn.Write(b) 38 | if err != nil { 39 | connManager.remove(c) 40 | } else { 41 | c.last = time.Now() 42 | } 43 | return n, err 44 | } 45 | 46 | func (c *idleConn) Close() error { 47 | if !connManager.remove(c) { 48 | return nil 49 | } 50 | return c.Conn.Close() 51 | } 52 | -------------------------------------------------------------------------------- /internal/idle/idle_manager.go: -------------------------------------------------------------------------------- 1 | package idle 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/wzshiming/bridge/logger" 8 | ) 9 | 10 | var ( 11 | connManager = newIdleConnManager() 12 | once sync.Once 13 | ) 14 | 15 | type idleConnManager struct { 16 | list map[*idleConn]struct{} 17 | mut sync.RWMutex 18 | } 19 | 20 | func newIdleConnManager() *idleConnManager { 21 | return &idleConnManager{ 22 | list: map[*idleConn]struct{}{}, 23 | } 24 | } 25 | 26 | func (m *idleConnManager) add(conn *idleConn) bool { 27 | once.Do(func() { 28 | go connManager.run() 29 | }) 30 | m.mut.Lock() 31 | defer m.mut.Unlock() 32 | if _, ok := m.list[conn]; ok { 33 | return false 34 | } 35 | 36 | m.list[conn] = struct{}{} 37 | return true 38 | } 39 | 40 | func (m *idleConnManager) remove(conn *idleConn) bool { 41 | m.mut.Lock() 42 | defer m.mut.Unlock() 43 | if _, ok := m.list[conn]; !ok { 44 | return false 45 | } 46 | 47 | delete(m.list, conn) 48 | return true 49 | } 50 | 51 | func (m *idleConnManager) Clear() { 52 | now := time.Now() 53 | conns := make([]*idleConn, 0, len(m.list)) 54 | 55 | m.mut.RLock() 56 | for conn := range m.list { 57 | if conn.last.Add(conn.timeout).Before(now) { 58 | conns = append(conns, conn) 59 | } 60 | } 61 | m.mut.RUnlock() 62 | 63 | if len(conns) == 0 { 64 | return 65 | } 66 | 67 | logger.Std.Info("Clear idle connections", "count", len(conns)) 68 | for _, conn := range conns { 69 | conn.Close() 70 | } 71 | } 72 | 73 | func (m *idleConnManager) run() { 74 | ticker := time.NewTicker(time.Second) 75 | for range ticker.C { 76 | m.Clear() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/netutils/command.go: -------------------------------------------------------------------------------- 1 | package netutils 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/wzshiming/bridge" 10 | "github.com/wzshiming/cmux" 11 | ) 12 | 13 | func ConnWithCloser(conn net.Conn, closer func() error) net.Conn { 14 | return &connCloser{Conn: conn, closer: closer} 15 | } 16 | 17 | type connCloser struct { 18 | net.Conn 19 | closer func() error 20 | } 21 | 22 | func (w *connCloser) Close() error { 23 | return w.closer() 24 | } 25 | 26 | func ConnWithAddr(conn net.Conn, localAddr, remoteAddr net.Addr) net.Conn { 27 | return &connAddr{Conn: conn, localAddr: localAddr, remoteAddr: remoteAddr} 28 | } 29 | 30 | type connAddr struct { 31 | net.Conn 32 | localAddr net.Addr 33 | remoteAddr net.Addr 34 | } 35 | 36 | func (w *connAddr) LocalAddr() net.Addr { 37 | if w.localAddr == nil { 38 | return w.Conn.LocalAddr() 39 | } 40 | return w.localAddr 41 | } 42 | 43 | func (w *connAddr) RemoteAddr() net.Addr { 44 | if w.remoteAddr == nil { 45 | return w.Conn.RemoteAddr() 46 | } 47 | return w.remoteAddr 48 | } 49 | 50 | func NewNetAddr(network, address string) net.Addr { 51 | return &addr{network: network, address: address} 52 | } 53 | 54 | type addr struct { 55 | network string 56 | address string 57 | } 58 | 59 | func (a *addr) Network() string { 60 | return a.network 61 | } 62 | func (a *addr) String() string { 63 | return a.address 64 | } 65 | 66 | func NewCommandDialContext(ctx context.Context, commandDialer bridge.CommandDialer, localAddr, remoteAddr net.Addr, proxy []string) (net.Conn, error) { 67 | conn, err := commandDialer.CommandDialContext(ctx, proxy[0], proxy[1:]...) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | conn = ConnWithAddr(conn, localAddr, remoteAddr) 73 | return conn, nil 74 | } 75 | 76 | func NewCommandListener(ctx context.Context, commandDialer bridge.CommandDialer, localAddr net.Addr, remoteAddr net.Addr, proxy []string) (net.Listener, error) { 77 | return &listener{ 78 | ctx: ctx, 79 | commandDialer: commandDialer, 80 | localAddr: localAddr, 81 | remoteAddr: remoteAddr, 82 | proxy: proxy, 83 | }, nil 84 | } 85 | 86 | type listener struct { 87 | ctx context.Context 88 | commandDialer bridge.CommandDialer 89 | proxy []string 90 | localAddr net.Addr 91 | remoteAddr net.Addr 92 | isClose uint32 93 | mux sync.Mutex 94 | } 95 | 96 | func (l *listener) Accept() (net.Conn, error) { 97 | l.mux.Lock() 98 | defer l.mux.Unlock() 99 | if atomic.LoadUint32(&l.isClose) == 1 { 100 | return nil, ErrClosedConn 101 | } 102 | 103 | n, err := NewCommandDialContext(l.ctx, l.commandDialer, l.localAddr, l.remoteAddr, l.proxy) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | // Because there is no way to tell if there is a connection coming in from the command line, 109 | // the next listen can only be performed if the data is read or closed 110 | var tmp [1]byte 111 | _, err = n.Read(tmp[:]) 112 | if err != nil { 113 | return nil, err 114 | } 115 | n = cmux.UnreadConn(n, tmp[:]) 116 | return n, nil 117 | } 118 | 119 | func (l *listener) Close() error { 120 | atomic.StoreUint32(&l.isClose, 1) 121 | return nil 122 | } 123 | 124 | func (l *listener) Addr() net.Addr { 125 | return l.localAddr 126 | } 127 | -------------------------------------------------------------------------------- /internal/netutils/net.go: -------------------------------------------------------------------------------- 1 | package netutils 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | "reflect" 11 | "runtime" 12 | "strings" 13 | 14 | "github.com/wzshiming/bridge" 15 | "github.com/wzshiming/commandproxy" 16 | ) 17 | 18 | var ErrServerClosed = errors.New("server closed") 19 | 20 | // IsServerClosedError reports whether err is an error from server closed. 21 | func IsServerClosedError(err error) bool { 22 | if err == nil { 23 | return false 24 | } 25 | 26 | if err == http.ErrServerClosed || err == ErrServerClosed || strings.Contains(strings.ToLower(err.Error()), ErrServerClosed.Error()) { 27 | return true 28 | } 29 | 30 | return false 31 | } 32 | 33 | var ErrClosedConn = errors.New("use of closed network connection") 34 | 35 | // IsClosedConnError reports whether err is an error from use of a closed network connection. 36 | func IsClosedConnError(err error) bool { 37 | if err == nil { 38 | return false 39 | } 40 | 41 | if err == ErrClosedConn || strings.Contains(strings.ToLower(err.Error()), ErrClosedConn.Error()) { 42 | return true 43 | } 44 | 45 | if runtime.GOOS == "windows" { 46 | if oe, ok := err.(*net.OpError); ok && oe.Op == "read" { 47 | if se, ok := oe.Err.(*os.SyscallError); ok && se.Syscall == "wsarecv" { 48 | const WSAECONNABORTED = 10053 49 | const WSAECONNRESET = 10054 50 | if n := errno(se.Err); n == WSAECONNRESET || n == WSAECONNABORTED { 51 | return true 52 | } 53 | } 54 | } 55 | } 56 | return false 57 | } 58 | 59 | func errno(v error) uintptr { 60 | if rv := reflect.ValueOf(v); rv.Kind() == reflect.Uintptr { 61 | return uintptr(rv.Uint()) 62 | } 63 | return 0 64 | } 65 | 66 | var ErrAcceptTimeout = errors.New("i/o timeout") 67 | 68 | // IsAcceptTimeoutError reports whether err is an error from use of a accept timeout. 69 | func IsAcceptTimeoutError(err error) bool { 70 | if err == nil { 71 | return false 72 | } 73 | 74 | if err == ErrAcceptTimeout || strings.Contains(err.Error(), ErrAcceptTimeout.Error()) { 75 | return true 76 | } 77 | 78 | if oe, ok := err.(*net.OpError); ok && oe.Op == "accept" { 79 | return IsAcceptTimeoutError(oe.Err) 80 | } 81 | 82 | return false 83 | } 84 | 85 | func Dial(ctx context.Context, dialer bridge.Dialer, network, address string) (net.Conn, error) { 86 | if network == "cmd" || network == "command" { 87 | d, ok := dialer.(bridge.CommandDialer) 88 | if !ok { 89 | return nil, fmt.Errorf("protocol %q unsupported cmd %q", network, address) 90 | } 91 | cmd, err := commandproxy.SplitCommand(address) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return d.CommandDialContext(ctx, cmd[0], cmd[1:]...) 96 | } 97 | if network == "virtual" { 98 | return virtualNetwork.DialContext(ctx, "tcp", address) 99 | } 100 | return dialer.DialContext(ctx, network, address) 101 | } 102 | 103 | func Listen(ctx context.Context, listener bridge.ListenConfig, network, address string) (net.Listener, error) { 104 | if network == "cmd" || network == "command" { 105 | l, ok := listener.(bridge.CommandListenConfig) 106 | if !ok { 107 | return nil, fmt.Errorf("protocol %q unsupported cmd %q", network, address) 108 | } 109 | cmd, err := commandproxy.SplitCommand(address) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return l.CommandListen(ctx, cmd[0], cmd[1:]...) 114 | } 115 | if network == "virtual" { 116 | return virtualNetwork.Listen(ctx, "tcp", address) 117 | } 118 | return listener.Listen(ctx, network, address) 119 | } 120 | -------------------------------------------------------------------------------- /internal/netutils/virtual.go: -------------------------------------------------------------------------------- 1 | package netutils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "sync/atomic" 9 | ) 10 | 11 | var virtualNetwork = newVirtualNetworkManager() 12 | 13 | type virtualNetworkManager struct { 14 | address map[string]*VirtualNetwork 15 | mut sync.RWMutex 16 | } 17 | 18 | func newVirtualNetworkManager() *virtualNetworkManager { 19 | return &virtualNetworkManager{ 20 | address: map[string]*VirtualNetwork{}, 21 | } 22 | } 23 | 24 | type Addr string 25 | 26 | func (a Addr) Network() string { 27 | return "virtual" 28 | } 29 | 30 | func (a Addr) String() string { 31 | return string(a) 32 | } 33 | 34 | func (v *virtualNetworkManager) Listen(ctx context.Context, network, address string) (net.Listener, error) { 35 | addr := Addr(address) 36 | listener := newVirtualNetwork(v, addr) 37 | 38 | v.mut.Lock() 39 | defer v.mut.Unlock() 40 | l, ok := v.address[address] 41 | if ok { 42 | old := l 43 | defer old.Close() 44 | } 45 | v.address[address] = listener 46 | return listener, nil 47 | } 48 | 49 | func (v *virtualNetworkManager) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 50 | v.mut.RLock() 51 | defer v.mut.RUnlock() 52 | l, ok := v.address[address] 53 | if ok { 54 | return l.Conn(Addr(address)) 55 | } 56 | return nil, fmt.Errorf("couldn't connect to virtual server %s://%s", network, address) 57 | } 58 | 59 | func (v *virtualNetworkManager) close(listener *VirtualNetwork) error { 60 | address := listener.Addr() 61 | addr := address.String() 62 | 63 | v.mut.Lock() 64 | defer v.mut.Unlock() 65 | l, ok := v.address[addr] 66 | if ok && l == listener { 67 | delete(v.address, addr) 68 | } 69 | return nil 70 | } 71 | 72 | type VirtualNetwork struct { 73 | parent *virtualNetworkManager 74 | serverAddr net.Addr 75 | ch chan net.Conn 76 | isClose uint32 77 | } 78 | 79 | func newVirtualNetwork(parent *virtualNetworkManager, serverAddr net.Addr) *VirtualNetwork { 80 | return &VirtualNetwork{ 81 | parent: parent, 82 | serverAddr: serverAddr, 83 | ch: make(chan net.Conn), 84 | } 85 | } 86 | 87 | func (l *VirtualNetwork) Accept() (net.Conn, error) { 88 | conn, ok := <-l.ch 89 | if !ok { 90 | return nil, ErrClosedConn 91 | } 92 | return conn, nil 93 | } 94 | 95 | func (l *VirtualNetwork) Close() error { 96 | if atomic.CompareAndSwapUint32(&l.isClose, 0, 1) { 97 | close(l.ch) 98 | if l.parent != nil { 99 | l.parent.close(l) 100 | } 101 | } 102 | return nil 103 | } 104 | 105 | func (l *VirtualNetwork) Addr() net.Addr { 106 | return l.serverAddr 107 | } 108 | 109 | func (l *VirtualNetwork) Conn(clientAddr net.Addr) (net.Conn, error) { 110 | if atomic.LoadUint32(&l.isClose) == 1 { 111 | return nil, ErrClosedConn 112 | } 113 | c, s := net.Pipe() 114 | s = &pipeConn{ 115 | Conn: s, 116 | remoteAddr: clientAddr, 117 | localAddr: l.serverAddr, 118 | } 119 | c = &pipeConn{ 120 | Conn: c, 121 | remoteAddr: l.serverAddr, 122 | localAddr: clientAddr, 123 | } 124 | l.ch <- s 125 | return c, nil 126 | } 127 | 128 | type pipeConn struct { 129 | net.Conn 130 | localAddr net.Addr 131 | remoteAddr net.Addr 132 | } 133 | 134 | func (c *pipeConn) LocalAddr() net.Addr { 135 | return c.localAddr 136 | } 137 | 138 | func (c *pipeConn) RemoteAddr() net.Addr { 139 | return c.remoteAddr 140 | } 141 | -------------------------------------------------------------------------------- /internal/pool/bytes.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var DefaultSize = 32 * 1024 8 | 9 | type bytesPool struct { 10 | sync.Pool 11 | } 12 | 13 | func (b *bytesPool) Get() []byte { 14 | buf := b.Pool.Get().([]byte) 15 | buf = buf[:cap(buf)] 16 | return buf 17 | } 18 | 19 | func (b *bytesPool) Put(d []byte) { 20 | if d == nil || len(d) < DefaultSize { 21 | return 22 | } 23 | b.Pool.Put(d) 24 | } 25 | 26 | var Bytes = &bytesPool{ 27 | Pool: sync.Pool{ 28 | New: func() interface{} { 29 | return make([]byte, DefaultSize) 30 | }, 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /internal/scheme/scheme.go: -------------------------------------------------------------------------------- 1 | package scheme 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | func JoinSchemeAddr(sch, addr string) (string, bool) { 11 | if sch == "" && addr == "" { 12 | return "", false 13 | } 14 | if addr == "" { 15 | return fmt.Sprintf("%s:", sch), true 16 | } 17 | if sch == "" { 18 | return fmt.Sprintf("tcp://%s", addr), true 19 | } 20 | return fmt.Sprintf("%s://%s", sch, addr), true 21 | } 22 | 23 | func SplitSchemeAddr(addr string) (string, string, bool) { 24 | // scheme: 25 | if strings.HasSuffix(addr, ":") { 26 | return addr[:len(addr)-1], "", true 27 | } 28 | // :port 29 | if strings.HasPrefix(addr, ":") { 30 | return "tcp", addr, true 31 | } 32 | // ./path/to/socks 33 | if strings.HasPrefix(addr, "./") || strings.HasPrefix(addr, "/") { 34 | return "unix", addr, true 35 | } 36 | 37 | u, _ := url.Parse(addr) 38 | if u != nil && u.Scheme != "" { 39 | // scheme://host 40 | if u.Opaque == "" { 41 | if u.Host != "" { 42 | return u.Scheme, u.Host, true 43 | } 44 | if u.Path != "" { 45 | return u.Scheme, u.Path, true 46 | } 47 | // scheme:?args=... 48 | if u.ForceQuery || u.RawQuery != "" { 49 | return u.Scheme, u.RawQuery, true 50 | } 51 | } 52 | 53 | // scheme: other 54 | if strings.ContainsAny(u.Opaque, " ./") { 55 | return u.Scheme, strings.TrimSpace(u.Opaque), true 56 | } 57 | 58 | // ip:port or host:port 59 | if strings.Contains(u.Scheme, ".") { 60 | return "tcp", net.JoinHostPort(u.Scheme, strings.TrimSpace(u.Opaque)), true 61 | } 62 | } 63 | 64 | if strings.Contains(addr, ":") { 65 | return "tcp", addr, true 66 | } 67 | 68 | return "", "", false 69 | } 70 | -------------------------------------------------------------------------------- /internal/scheme/scheme_test.go: -------------------------------------------------------------------------------- 1 | package scheme 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_ResolveProtocol(t *testing.T) { 8 | type args struct { 9 | addr string 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | wantNetwork string 15 | wantAddress string 16 | wantOk bool 17 | }{ 18 | { 19 | args: args{ 20 | addr: "nc:", 21 | }, 22 | wantNetwork: "nc", 23 | wantAddress: "", 24 | wantOk: true, 25 | }, 26 | { 27 | args: args{ 28 | addr: "nc:?", 29 | }, 30 | wantNetwork: "nc", 31 | wantAddress: "", 32 | wantOk: true, 33 | }, 34 | { 35 | args: args{ 36 | addr: "nc:?k=v", 37 | }, 38 | wantNetwork: "nc", 39 | wantAddress: "k=v", 40 | wantOk: true, 41 | }, 42 | { 43 | args: args{ 44 | addr: "nc:cmd --", 45 | }, 46 | wantNetwork: "nc", 47 | wantAddress: "cmd --", 48 | wantOk: true, 49 | }, 50 | { 51 | args: args{ 52 | addr: "cmd: nc %h %p", 53 | }, 54 | wantNetwork: "cmd", 55 | wantAddress: "nc %h %p", 56 | wantOk: true, 57 | }, 58 | { 59 | args: args{ 60 | addr: ":1111", 61 | }, 62 | wantNetwork: "tcp", 63 | wantAddress: ":1111", 64 | wantOk: true, 65 | }, 66 | { 67 | args: args{ 68 | addr: "tcp://:1111", 69 | }, 70 | wantNetwork: "tcp", 71 | wantAddress: ":1111", 72 | wantOk: true, 73 | }, 74 | { 75 | args: args{ 76 | addr: "domain:1111", 77 | }, 78 | wantNetwork: "tcp", 79 | wantAddress: "domain:1111", 80 | wantOk: true, 81 | }, 82 | { 83 | args: args{ 84 | addr: "tcp://domain:1111", 85 | }, 86 | wantNetwork: "tcp", 87 | wantAddress: "domain:1111", 88 | wantOk: true, 89 | }, 90 | { 91 | args: args{ 92 | addr: "domain.local:1111", 93 | }, 94 | wantNetwork: "tcp", 95 | wantAddress: "domain.local:1111", 96 | wantOk: true, 97 | }, 98 | { 99 | args: args{ 100 | addr: "tcp://domain.local:1111", 101 | }, 102 | wantNetwork: "tcp", 103 | wantAddress: "domain.local:1111", 104 | wantOk: true, 105 | }, 106 | { 107 | args: args{ 108 | addr: "127.0.0.1:1111", 109 | }, 110 | wantNetwork: "tcp", 111 | wantAddress: "127.0.0.1:1111", 112 | wantOk: true, 113 | }, 114 | { 115 | args: args{ 116 | addr: "tcp://127.0.0.1:1111", 117 | }, 118 | wantNetwork: "tcp", 119 | wantAddress: "127.0.0.1:1111", 120 | wantOk: true, 121 | }, 122 | { 123 | args: args{ 124 | addr: "./xxx.socks", 125 | }, 126 | wantNetwork: "unix", 127 | wantAddress: "./xxx.socks", 128 | wantOk: true, 129 | }, 130 | { 131 | args: args{ 132 | addr: "unix:./xxx.socks", 133 | }, 134 | wantNetwork: "unix", 135 | wantAddress: "./xxx.socks", 136 | wantOk: true, 137 | }, 138 | { 139 | args: args{ 140 | addr: "unix:/xxx.socks", 141 | }, 142 | wantNetwork: "unix", 143 | wantAddress: "/xxx.socks", 144 | wantOk: true, 145 | }, 146 | { 147 | args: args{ 148 | addr: "ssh://username@my_server?identity_file=~/.ssh/id_rsa", 149 | }, 150 | wantNetwork: "ssh", 151 | wantAddress: "my_server", 152 | wantOk: true, 153 | }, 154 | { 155 | args: args{ 156 | addr: "virtual://xxxx", 157 | }, 158 | wantNetwork: "virtual", 159 | wantAddress: "xxxx", 160 | wantOk: true, 161 | }, 162 | } 163 | for _, tt := range tests { 164 | t.Run(tt.name, func(t *testing.T) { 165 | gotNetwork, gotAddress, gotOk := SplitSchemeAddr(tt.args.addr) 166 | if gotNetwork != tt.wantNetwork { 167 | t.Errorf("SplitSchemeAddr() gotNetwork = %v, want %v", gotNetwork, tt.wantNetwork) 168 | } 169 | if gotAddress != tt.wantAddress { 170 | t.Errorf("SplitSchemeAddr() gotAddress = %v, want %v", gotAddress, tt.wantAddress) 171 | } 172 | if gotOk != tt.wantOk { 173 | t.Errorf("SplitSchemeAddr() wantOk = %v, want %v", gotOk, tt.wantOk) 174 | } 175 | }) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | ) 9 | 10 | var Std = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 11 | Level: slog.LevelInfo, 12 | })) 13 | 14 | func Wrap(logger *slog.Logger, name string) *wrap { 15 | return &wrap{ 16 | Logger: logger.WithGroup(name), 17 | } 18 | } 19 | 20 | type wrap struct { 21 | *slog.Logger 22 | } 23 | 24 | func (w wrap) Println(v ...interface{}) { 25 | w.Logger.Log(context.Background(), slog.LevelWarn, "print", "message", fmt.Sprint(v...)) 26 | } 27 | -------------------------------------------------------------------------------- /protocols/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/url" 8 | 9 | "github.com/wzshiming/bridge" 10 | "github.com/wzshiming/bridge/internal/netutils" 11 | "github.com/wzshiming/bridge/protocols/local" 12 | "github.com/wzshiming/commandproxy" 13 | ) 14 | 15 | var ( 16 | ErrNotSupported = fmt.Errorf("is not supported 'cmd'") 17 | ) 18 | 19 | // COMMAND cmd:shell 20 | func COMMAND(ctx context.Context, dialer bridge.Dialer, cmd string) (bridge.Dialer, error) { 21 | if dialer == nil { 22 | dialer = local.LOCAL 23 | } 24 | cd, ok := dialer.(bridge.CommandDialer) 25 | if !ok { 26 | return nil, ErrNotSupported 27 | } 28 | pd, err := newProxyDialer(cmd) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return &command{ 33 | pd: pd, 34 | CommandDialer: cd, 35 | }, nil 36 | } 37 | 38 | func newProxyDialer(cmd string) (*proxyDialer, error) { 39 | uri, err := url.Parse(cmd) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | scmd, err := commandproxy.SplitCommand(uri.Opaque) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return &proxyDialer{ 49 | proxyCommand: scmd, 50 | localAddr: netutils.NewNetAddr(uri.Scheme, uri.Opaque), 51 | }, nil 52 | } 53 | 54 | type proxyDialer struct { 55 | proxyCommand commandproxy.DialProxyCommand 56 | localAddr net.Addr 57 | } 58 | 59 | type command struct { 60 | pd *proxyDialer 61 | bridge.CommandDialer 62 | } 63 | 64 | func (c *command) Listen(ctx context.Context, network, address string) (net.Listener, error) { 65 | proxy := c.pd.proxyCommand.Format(network, address) 66 | remoteAddr := netutils.NewNetAddr(network, address) 67 | return netutils.NewCommandListener(ctx, c.CommandDialer, c.pd.localAddr, remoteAddr, proxy) 68 | } 69 | 70 | func (c *command) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 71 | proxy := c.pd.proxyCommand.Format(network, address) 72 | remoteAddr := netutils.NewNetAddr(network, address) 73 | return netutils.NewCommandDialContext(ctx, c.CommandDialer, c.pd.localAddr, remoteAddr, proxy) 74 | } 75 | -------------------------------------------------------------------------------- /protocols/command/init.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("cmd", bridge.BridgeFunc(COMMAND)) 10 | chain.Default.Register("command", bridge.BridgeFunc(COMMAND)) 11 | } 12 | -------------------------------------------------------------------------------- /protocols/connect/connect.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wzshiming/bridge" 7 | "github.com/wzshiming/httpproxy" 8 | ) 9 | 10 | // CONNECT https?://[username:password@]{address} 11 | func CONNECT(ctx context.Context, dialer bridge.Dialer, address string) (bridge.Dialer, error) { 12 | d, err := httpproxy.NewDialer(address) 13 | if err != nil { 14 | return nil, err 15 | } 16 | if dialer != nil { 17 | d.ProxyDial = dialer.DialContext 18 | } 19 | return d, nil 20 | } 21 | -------------------------------------------------------------------------------- /protocols/connect/init.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("http", bridge.BridgeFunc(CONNECT)) 10 | chain.Default.Register("https", bridge.BridgeFunc(CONNECT)) 11 | } 12 | -------------------------------------------------------------------------------- /protocols/emux/init.go: -------------------------------------------------------------------------------- 1 | package emux 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("emux", bridge.BridgeFunc(EMux)) 10 | } 11 | -------------------------------------------------------------------------------- /protocols/emux/smux.go: -------------------------------------------------------------------------------- 1 | package emux 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "strconv" 7 | 8 | "github.com/wzshiming/bridge" 9 | "github.com/wzshiming/bridge/internal/pool" 10 | "github.com/wzshiming/bridge/logger" 11 | "github.com/wzshiming/bridge/protocols/local" 12 | "github.com/wzshiming/emux" 13 | ) 14 | 15 | // EMux emux:?handshake=EMUX%20 16 | func EMux(ctx context.Context, dialer bridge.Dialer, addr string) (bridge.Dialer, error) { 17 | if dialer == nil { 18 | dialer = local.LOCAL 19 | } 20 | handshake, instruction, err := parseConfig(addr) 21 | if err != nil { 22 | return nil, err 23 | } 24 | d := emux.NewDialer(ctx, dialer) 25 | d.Instruction = *instruction 26 | d.BytesPool = pool.Bytes 27 | d.Logger = logger.Wrap(logger.Std, "emux") 28 | if handshake != nil { 29 | if len(handshake) == 0 { 30 | d.Handshake = nil 31 | } else { 32 | d.Handshake = emux.NewHandshake(handshake, true) 33 | } 34 | } 35 | if listenConfig, ok := dialer.(bridge.ListenConfig); ok { 36 | l := emux.NewListenConfig(ctx, listenConfig) 37 | l.Instruction = *instruction 38 | l.BytesPool = pool.Bytes 39 | l.Logger = logger.Wrap(logger.Std, "emux") 40 | if handshake != nil { 41 | if len(handshake) == 0 { 42 | d.Handshake = nil 43 | } else { 44 | l.Handshake = emux.NewHandshake(handshake, false) 45 | } 46 | } 47 | return struct { 48 | bridge.Dialer 49 | bridge.ListenConfig 50 | }{ 51 | Dialer: d, 52 | ListenConfig: l, 53 | }, nil 54 | } 55 | return d, nil 56 | } 57 | 58 | func parseConfig(addr string) ([]byte, *emux.Instruction, error) { 59 | u, err := url.Parse(addr) 60 | if err != nil { 61 | return nil, nil, err 62 | } 63 | var handshake []byte 64 | instruction := emux.DefaultInstruction 65 | query := u.Query() 66 | for key := range query { 67 | switch key { 68 | case "handshake": 69 | handshake = []byte(query.Get(key)) 70 | case "close": 71 | u, err := strconv.ParseUint(query.Get(key), 0, 8) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | instruction.Close = uint8(u) 76 | case "connect": 77 | u, err := strconv.ParseUint(query.Get(key), 0, 8) 78 | if err != nil { 79 | return nil, nil, err 80 | } 81 | instruction.Connect = uint8(u) 82 | case "connected": 83 | u, err := strconv.ParseUint(query.Get(key), 0, 8) 84 | if err != nil { 85 | return nil, nil, err 86 | } 87 | instruction.Connected = uint8(u) 88 | case "disconnect": 89 | u, err := strconv.ParseUint(query.Get(key), 0, 8) 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | instruction.Disconnect = uint8(u) 94 | case "disconnected": 95 | u, err := strconv.ParseUint(query.Get(key), 0, 8) 96 | if err != nil { 97 | return nil, nil, err 98 | } 99 | instruction.Disconnected = uint8(u) 100 | case "data": 101 | u, err := strconv.ParseUint(query.Get(key), 0, 8) 102 | if err != nil { 103 | return nil, nil, err 104 | } 105 | instruction.Data = uint8(u) 106 | } 107 | } 108 | 109 | return handshake, &instruction, nil 110 | } 111 | -------------------------------------------------------------------------------- /protocols/local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | "github.com/wzshiming/bridge" 10 | "github.com/wzshiming/bridge/internal/netutils" 11 | "github.com/wzshiming/bridge/logger" 12 | "github.com/wzshiming/commandproxy" 13 | ) 14 | 15 | var LOCAL = &Local{ 16 | Dialer: &net.Dialer{}, 17 | ListenConfig: &net.ListenConfig{}, 18 | LocalAddr: netutils.NewNetAddr("local", "local"), 19 | } 20 | 21 | type Local struct { 22 | Dialer bridge.Dialer 23 | ListenConfig bridge.ListenConfig 24 | LocalAddr net.Addr 25 | } 26 | 27 | func (l *Local) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 28 | logger.Std.Debug("Dial", "network", network, "address", address) 29 | return l.Dialer.DialContext(ctx, network, address) 30 | } 31 | 32 | func (l *Local) Listen(ctx context.Context, network, address string) (net.Listener, error) { 33 | logger.Std.Debug("Listen", "network", network, "address", address) 34 | return l.ListenConfig.Listen(ctx, network, address) 35 | } 36 | 37 | func (l *Local) CommandDialContext(ctx context.Context, name string, args ...string) (net.Conn, error) { 38 | logger.Std.Debug("CommandDial", "name", name, "args", args) 39 | proxy := commandproxy.ProxyCommand(ctx, name, args...) 40 | proxy.Stderr = os.Stderr 41 | conn, err := proxy.Stdio() 42 | if err != nil { 43 | return nil, err 44 | } 45 | remoteAddr := netutils.NewNetAddr("cmd", strings.Join(append([]string{name}, args...), " ")) 46 | conn = netutils.ConnWithAddr(conn, l.LocalAddr, remoteAddr) 47 | return conn, nil 48 | } 49 | 50 | func (l *Local) CommandListen(ctx context.Context, name string, args ...string) (net.Listener, error) { 51 | logger.Std.Debug("CommandListen", "name", name, "args", args) 52 | proxy := append([]string{name}, args...) 53 | remoteAddr := netutils.NewNetAddr("cmd", strings.Join(proxy, " ")) 54 | return netutils.NewCommandListener(ctx, l, l.LocalAddr, remoteAddr, proxy) 55 | } 56 | -------------------------------------------------------------------------------- /protocols/local/local_windows.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | winio "github.com/Microsoft/go-winio" 8 | "github.com/wzshiming/bridge" 9 | ) 10 | 11 | const ( 12 | pipeName = "pipe" 13 | ) 14 | 15 | func init() { 16 | LOCAL.Dialer = &pipeDialer{LOCAL.Dialer} 17 | LOCAL.ListenConfig = &pipeListenConfig{LOCAL.ListenConfig} 18 | } 19 | 20 | type pipeListenConfig struct { 21 | bridge.ListenConfig 22 | } 23 | 24 | func (p *pipeListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error) { 25 | if network != pipeName { 26 | return p.ListenConfig.Listen(ctx, network, address) 27 | } 28 | var conf winio.PipeConfig 29 | return winio.ListenPipe(address, &conf) 30 | } 31 | 32 | type pipeDialer struct { 33 | bridge.Dialer 34 | } 35 | 36 | func (p *pipeDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 37 | if network != pipeName { 38 | return p.Dialer.DialContext(ctx, network, address) 39 | } 40 | return winio.DialPipeContext(ctx, address) 41 | } 42 | -------------------------------------------------------------------------------- /protocols/netcat/init.go: -------------------------------------------------------------------------------- 1 | package netcat 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("nc", bridge.BridgeFunc(NetCat)) 10 | chain.Default.Register("netcat", bridge.BridgeFunc(NetCat)) 11 | } 12 | -------------------------------------------------------------------------------- /protocols/netcat/netcat.go: -------------------------------------------------------------------------------- 1 | package netcat 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/wzshiming/bridge" 10 | "github.com/wzshiming/bridge/protocols/command" 11 | "github.com/wzshiming/bridge/protocols/local" 12 | ) 13 | 14 | // NetCat nc: [prefix] 15 | func NetCat(ctx context.Context, dialer bridge.Dialer, cmd string) (bridge.Dialer, error) { 16 | if dialer == nil { 17 | dialer = local.LOCAL 18 | } 19 | var prefix string 20 | u, err := url.Parse(cmd) 21 | if err == nil { 22 | prefix = u.Opaque 23 | } 24 | return &netCat{ 25 | prefix: prefix, 26 | dialer: dialer, 27 | command: command.COMMAND, 28 | }, nil 29 | } 30 | 31 | type netCat struct { 32 | prefix string 33 | dialer bridge.Dialer 34 | tcpDialer bridge.Dialer 35 | unixDialer bridge.Dialer 36 | tcpListener bridge.ListenConfig 37 | unixListener bridge.ListenConfig 38 | command func(ctx context.Context, dialer bridge.Dialer, cmd string) (bridge.Dialer, error) 39 | } 40 | 41 | func (n *netCat) exec(ctx context.Context, cmd string) (bridge.Dialer, error) { 42 | return n.command(ctx, n.dialer, strings.Join([]string{"cmd:", n.prefix, cmd}, " ")) 43 | } 44 | 45 | func (n *netCat) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 46 | if network == "unix" { 47 | if n.unixDialer == nil { 48 | d, err := n.exec(ctx, "nc -U %h") 49 | if err != nil { 50 | return nil, err 51 | } 52 | n.unixDialer = d 53 | } 54 | return n.unixDialer.DialContext(ctx, network, address) 55 | } 56 | if n.tcpDialer == nil { 57 | cmd := "nc %h %p" 58 | switch network { 59 | case "tcp4": 60 | cmd = "nc -4 %h %p" 61 | case "tcp6": 62 | cmd = "nc -6 %h %p" 63 | } 64 | d, err := n.exec(ctx, cmd) 65 | if err != nil { 66 | return nil, err 67 | } 68 | n.tcpDialer = d 69 | } 70 | return n.tcpDialer.DialContext(ctx, network, address) 71 | } 72 | 73 | func (n *netCat) Listen(ctx context.Context, network, address string) (net.Listener, error) { 74 | if network == "unix" { 75 | if n.unixListener == nil { 76 | d, err := n.exec(ctx, "nc -Ul %h") 77 | if err != nil { 78 | return nil, err 79 | } 80 | n.unixListener = d.(bridge.ListenConfig) 81 | } 82 | return n.unixListener.Listen(ctx, network, address) 83 | } 84 | if n.tcpListener == nil { 85 | cmd := "nc -l %h %p" 86 | switch network { 87 | case "tcp4": 88 | cmd = "nc -4l %h %p" 89 | case "tcp6": 90 | cmd = "nc -6l %h %p" 91 | } 92 | d, err := n.exec(ctx, cmd) 93 | if err != nil { 94 | return nil, err 95 | } 96 | n.tcpListener = d.(bridge.ListenConfig) 97 | } 98 | return n.tcpListener.Listen(ctx, network, address) 99 | } 100 | -------------------------------------------------------------------------------- /protocols/permuteproxy/init.go: -------------------------------------------------------------------------------- 1 | package permuteproxy 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.RegisterDefault(bridge.BridgeFunc(PermuteProxy)) 10 | } 11 | -------------------------------------------------------------------------------- /protocols/permuteproxy/permuteproxy.go: -------------------------------------------------------------------------------- 1 | package permuteproxy 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wzshiming/permuteproxy" 7 | "github.com/wzshiming/permuteproxy/protocols/local" 8 | 9 | _ "github.com/wzshiming/permuteproxy/protocols/httpproxy" 10 | _ "github.com/wzshiming/permuteproxy/protocols/local" 11 | _ "github.com/wzshiming/permuteproxy/protocols/shadowsocks" 12 | _ "github.com/wzshiming/permuteproxy/protocols/snappy" 13 | _ "github.com/wzshiming/permuteproxy/protocols/socks4" 14 | _ "github.com/wzshiming/permuteproxy/protocols/socks5" 15 | _ "github.com/wzshiming/permuteproxy/protocols/sshproxy" 16 | _ "github.com/wzshiming/permuteproxy/protocols/tls" 17 | 18 | "github.com/wzshiming/bridge" 19 | ) 20 | 21 | func PermuteProxy(ctx context.Context, dialer bridge.Dialer, addr string) (bridge.Dialer, error) { 22 | l := &permuteproxy.Proxy{ 23 | Dialer: local.LOCAL, 24 | } 25 | if dialer != nil { 26 | l.Dialer = dialer 27 | } 28 | return l.NewDialer(addr) 29 | } 30 | -------------------------------------------------------------------------------- /protocols/shadowsocks/init.go: -------------------------------------------------------------------------------- 1 | package shadowsocks 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("ss", bridge.BridgeFunc(ShadowSocks)) 10 | chain.Default.Register("shadowsocks", bridge.BridgeFunc(ShadowSocks)) 11 | } 12 | -------------------------------------------------------------------------------- /protocols/shadowsocks/shadowsocks.go: -------------------------------------------------------------------------------- 1 | package shadowsocks 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wzshiming/bridge" 7 | "github.com/wzshiming/bridge/protocols/local" 8 | "github.com/wzshiming/shadowsocks" 9 | _ "github.com/wzshiming/shadowsocks/init" 10 | ) 11 | 12 | // ShadowSocks ss://{cipher}:{password}@{address} 13 | func ShadowSocks(ctx context.Context, dialer bridge.Dialer, addr string) (bridge.Dialer, error) { 14 | if dialer == nil { 15 | dialer = local.LOCAL 16 | } 17 | d, err := shadowsocks.NewDialer(addr) 18 | if err != nil { 19 | return nil, err 20 | } 21 | d.ProxyDial = dialer.DialContext 22 | return d, nil 23 | } 24 | -------------------------------------------------------------------------------- /protocols/snappy/init.go: -------------------------------------------------------------------------------- 1 | package snappy 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("snappy", bridge.BridgeFunc(Snappy)) 10 | } 11 | -------------------------------------------------------------------------------- /protocols/snappy/snappy.go: -------------------------------------------------------------------------------- 1 | package snappy 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/golang/snappy" 8 | "github.com/wzshiming/bridge" 9 | "github.com/wzshiming/bridge/protocols/local" 10 | ) 11 | 12 | // Snappy snappy: 13 | func Snappy(ctx context.Context, dialer bridge.Dialer, cmd string) (bridge.Dialer, error) { 14 | if dialer == nil { 15 | dialer = local.LOCAL 16 | } 17 | 18 | if l, ok := dialer.(bridge.ListenConfig); ok { 19 | return struct { 20 | bridge.Dialer 21 | bridge.ListenConfig 22 | }{ 23 | snappyDialer{dialer}, 24 | snappyListenConfig{l}, 25 | }, nil 26 | } 27 | return snappyDialer{dialer}, nil 28 | } 29 | 30 | type snappyDialer struct { 31 | dialer bridge.Dialer 32 | } 33 | 34 | func (n snappyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 35 | c, err := n.dialer.DialContext(ctx, network, address) 36 | if err != nil { 37 | return nil, err 38 | } 39 | conn := newWarpConn(c) 40 | return conn, nil 41 | } 42 | 43 | type snappyListenConfig struct { 44 | listenConfig bridge.ListenConfig 45 | } 46 | 47 | func (n snappyListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error) { 48 | l, err := n.listenConfig.Listen(ctx, network, address) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return wrapListener{l}, nil 53 | } 54 | 55 | func newWarpConn(conn net.Conn) net.Conn { 56 | w := snappy.NewWriter(conn) 57 | r := snappy.NewReader(conn) 58 | return wrapConn{ 59 | Conn: conn, 60 | w: w, 61 | r: r, 62 | } 63 | } 64 | 65 | type wrapConn struct { 66 | net.Conn 67 | w *snappy.Writer 68 | r *snappy.Reader 69 | } 70 | 71 | func (w wrapConn) Read(b []byte) (int, error) { 72 | return w.r.Read(b) 73 | } 74 | 75 | func (w wrapConn) Write(b []byte) (int, error) { 76 | n, err := w.w.Write(b) 77 | if err != nil { 78 | return n, err 79 | } 80 | err = w.w.Flush() 81 | if err != nil { 82 | return n, err 83 | } 84 | return n, nil 85 | } 86 | 87 | type wrapListener struct { 88 | net.Listener 89 | } 90 | 91 | func (w wrapListener) Accept() (net.Conn, error) { 92 | c, err := w.Listener.Accept() 93 | if err != nil { 94 | return nil, err 95 | } 96 | return newWarpConn(c), nil 97 | } 98 | -------------------------------------------------------------------------------- /protocols/socks4/init.go: -------------------------------------------------------------------------------- 1 | package socks4 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("socks4", bridge.BridgeFunc(SOCKS4)) 10 | chain.Default.Register("socks4a", bridge.BridgeFunc(SOCKS4)) 11 | } 12 | -------------------------------------------------------------------------------- /protocols/socks4/socks4.go: -------------------------------------------------------------------------------- 1 | package socks4 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wzshiming/bridge" 7 | "github.com/wzshiming/socks4" 8 | ) 9 | 10 | // SOCKS4 socks4://[username@]{address} 11 | func SOCKS4(ctx context.Context, dialer bridge.Dialer, addr string) (bridge.Dialer, error) { 12 | d, err := socks4.NewDialer(addr) 13 | if err != nil { 14 | return nil, err 15 | } 16 | if dialer != nil { 17 | d.ProxyDial = dialer.DialContext 18 | } 19 | return d, nil 20 | } 21 | -------------------------------------------------------------------------------- /protocols/socks5/init.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("socks5", bridge.BridgeFunc(SOCKS5)) 10 | chain.Default.Register("socks5h", bridge.BridgeFunc(SOCKS5)) 11 | } 12 | -------------------------------------------------------------------------------- /protocols/socks5/socks5.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wzshiming/bridge" 7 | "github.com/wzshiming/socks5" 8 | ) 9 | 10 | // SOCKS5 socks5://[username:password@]{address} 11 | func SOCKS5(ctx context.Context, dialer bridge.Dialer, addr string) (bridge.Dialer, error) { 12 | d, err := socks5.NewDialer(addr) 13 | if err != nil { 14 | return nil, err 15 | } 16 | if dialer != nil { 17 | d.ProxyDial = dialer.DialContext 18 | } 19 | return d, nil 20 | } 21 | -------------------------------------------------------------------------------- /protocols/ssh/init.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("ssh", bridge.BridgeFunc(SSH)) 10 | } 11 | -------------------------------------------------------------------------------- /protocols/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wzshiming/bridge" 7 | "github.com/wzshiming/sshproxy" 8 | ) 9 | 10 | // SSH ssh://[username:password@]{address}[?identity_file=path/to/file] 11 | func SSH(ctx context.Context, dialer bridge.Dialer, addr string) (bridge.Dialer, error) { 12 | d, err := sshproxy.NewDialer(addr) 13 | if err != nil { 14 | return nil, err 15 | } 16 | if dialer != nil { 17 | d.ProxyDial = dialer.DialContext 18 | } 19 | return d, nil 20 | } 21 | -------------------------------------------------------------------------------- /protocols/tls/init.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "github.com/wzshiming/bridge" 5 | "github.com/wzshiming/bridge/chain" 6 | ) 7 | 8 | func init() { 9 | chain.Default.Register("tls", bridge.BridgeFunc(TLS)) 10 | } 11 | -------------------------------------------------------------------------------- /protocols/tls/tls.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net" 7 | "net/url" 8 | 9 | "github.com/wzshiming/bridge" 10 | "github.com/wzshiming/bridge/protocols/local" 11 | ) 12 | 13 | // TLS tls:[opaque] 14 | func TLS(ctx context.Context, dialer bridge.Dialer, addr string) (bridge.Dialer, error) { 15 | if dialer == nil { 16 | dialer = local.LOCAL 17 | } 18 | uri, err := url.Parse(addr) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return bridge.DialFunc(func(ctx context.Context, network, addr string) (c net.Conn, err error) { 23 | c, err = dialer.DialContext(ctx, network, addr) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | conf := &tls.Config{} 29 | if uri.Opaque == "" || net.ParseIP(uri.Opaque) != nil { 30 | conf.InsecureSkipVerify = true 31 | } else { 32 | conf.ServerName = uri.Opaque 33 | } 34 | 35 | tc := tls.Client(c, conf) 36 | err = tc.Handshake() 37 | if err != nil { 38 | return nil, err 39 | } 40 | return tc, nil 41 | }), nil 42 | } 43 | --------------------------------------------------------------------------------