├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── conf.go ├── conn.go ├── docker-bake.hcl ├── docs ├── install.md ├── systemd │ └── wghttp.service └── usage.md ├── go.mod ├── go.sum ├── internal ├── proxy │ ├── proxy.go │ └── proxy_test.go ├── resolver │ ├── doh.go │ ├── resolver.go │ └── resolver_test.go └── third_party │ └── tailscale │ ├── LICENSE │ ├── httpproxy │ └── proxy.go │ ├── proxymux │ └── mux.go │ └── socks5 │ └── socks5.go ├── main.go ├── option.go └── stats.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "master" 12 | 13 | jobs: 14 | docker: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | 25 | - name: Docker meta 26 | id: meta 27 | uses: docker/metadata-action@v5 28 | with: 29 | images: zhusj/wghttp,ghcr.io/zhsj/wghttp 30 | tags: | 31 | type=ref,event=branch 32 | type=ref,event=pr 33 | type=semver,pattern={{version}} 34 | 35 | - name: Login to DockerHub 36 | if: github.event_name != 'pull_request' 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKERHUB_USERNAME }} 40 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 41 | 42 | - name: Login to GitHub Container Registry 43 | if: github.event_name != 'pull_request' 44 | uses: docker/login-action@v3 45 | with: 46 | registry: ghcr.io 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Build 51 | uses: docker/bake-action@v4 52 | with: 53 | files: | 54 | ./docker-bake.hcl 55 | ${{ steps.meta.outputs.bake-file }} 56 | targets: build 57 | push: ${{ github.event_name != 'pull_request' }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | wghttp 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM --platform=$BUILDPLATFORM golang as builder 4 | WORKDIR /app 5 | 6 | COPY go.mod . 7 | RUN go mod download 8 | 9 | COPY . . 10 | RUN < "$file_prefix".info 17 | cp go.mod "$file_prefix".mod 18 | git archive --prefix=github.com/zhsj/wghttp@"$tag"/ -o "$file_prefix".zip HEAD 19 | EOF 20 | 21 | ARG TARGETARCH TARGETVARIANT 22 | RUN <. 15 | -------------------------------------------------------------------------------- /conf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/netip" 8 | "time" 9 | 10 | "github.com/zhsj/wghttp/internal/resolver" 11 | "golang.zx2c4.com/wireguard/device" 12 | ) 13 | 14 | type peer struct { 15 | resolver *resolver.Resolver 16 | 17 | pubKey keyT 18 | psk keyT 19 | 20 | host string 21 | ip netip.Addr 22 | port uint16 23 | } 24 | 25 | func newPeerEndpoint() (*peer, error) { 26 | p := &peer{ 27 | pubKey: opts.PeerKey, 28 | psk: opts.PresharedKey, 29 | host: opts.PeerEndpoint.host, 30 | port: opts.PeerEndpoint.port, 31 | } 32 | var err error 33 | p.ip, err = netip.ParseAddr(p.host) 34 | if err == nil { 35 | return p, nil 36 | } 37 | 38 | p.resolver = resolver.New( 39 | opts.ResolveDNS, 40 | func(ctx context.Context, network, address string) (net.Conn, error) { 41 | netConn, err := (&net.Dialer{}).DialContext(ctx, network, address) 42 | logger.Verbosef("Using %s to resolve peer endpoint: %v", opts.ResolveDNS, err) 43 | return netConn, err 44 | }, 45 | ) 46 | 47 | p.ip, err = p.resolveHost() 48 | if err != nil { 49 | return nil, fmt.Errorf("resolve peer endpoint ip: %w", err) 50 | } 51 | 52 | return p, err 53 | } 54 | 55 | func (p *peer) initConf() string { 56 | conf := fmt.Sprintf("public_key=%s\n", p.pubKey) 57 | conf += fmt.Sprintf("endpoint=%s\n", netip.AddrPortFrom(p.ip, p.port)) 58 | conf += "allowed_ip=0.0.0.0/0\n" 59 | conf += "allowed_ip=::/0\n" 60 | 61 | if opts.KeepaliveInterval > 0 { 62 | conf += fmt.Sprintf("persistent_keepalive_interval=%d\n", opts.KeepaliveInterval) 63 | } 64 | if p.psk != "" { 65 | conf += fmt.Sprintf("preshared_key=%s\n", p.psk) 66 | } 67 | 68 | return conf 69 | } 70 | 71 | func (p *peer) updateConf() (string, bool) { 72 | newIP, err := p.resolveHost() 73 | if err != nil { 74 | logger.Verbosef("Resolve peer endpoint: %v", err) 75 | return "", false 76 | } 77 | if p.ip == newIP { 78 | return "", false 79 | } 80 | p.ip = newIP 81 | logger.Verbosef("PeerEndpoint is changed to: %s", p.ip) 82 | 83 | conf := fmt.Sprintf("public_key=%s\n", p.pubKey) 84 | conf += "update_only=true\n" 85 | conf += fmt.Sprintf("endpoint=%s\n", netip.AddrPortFrom(p.ip, p.port)) 86 | return conf, true 87 | } 88 | 89 | func (p *peer) resolveHost() (netip.Addr, error) { 90 | ips, err := p.resolver.LookupNetIP(context.Background(), "ip", p.host) 91 | if err != nil { 92 | return netip.Addr{}, fmt.Errorf("resolve ip for %s: %w", p.host, err) 93 | } 94 | for _, ip := range ips { 95 | // netstack doesn't seem to understand IPv4-mapped IPv6 addresses. 96 | ip = ip.Unmap() 97 | conn, err := net.DialUDP("udp", nil, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, p.port))) 98 | if err == nil { 99 | conn.Close() 100 | return ip, nil 101 | } else { 102 | logger.Verbosef("Dial %s: %s", ip, err) 103 | } 104 | } 105 | return netip.Addr{}, fmt.Errorf("no available ip for %s", p.host) 106 | } 107 | 108 | func ipcSet(dev *device.Device) error { 109 | conf := fmt.Sprintf("private_key=%s\n", opts.PrivateKey) 110 | if opts.ClientPort != 0 { 111 | conf += fmt.Sprintf("listen_port=%d\n", opts.ClientPort) 112 | } 113 | 114 | peer, err := newPeerEndpoint() 115 | if err != nil { 116 | return err 117 | } 118 | conf += peer.initConf() 119 | logger.Verbosef("Device config:\n%s", conf) 120 | 121 | if err := dev.IpcSet(conf); err != nil { 122 | return err 123 | } 124 | 125 | if peer.resolver != nil { 126 | go func() { 127 | c := time.Tick(time.Duration(opts.ResolveInterval) * time.Second) 128 | 129 | for range c { 130 | conf, needUpdate := peer.updateConf() 131 | if !needUpdate { 132 | continue 133 | } 134 | 135 | if err := dev.IpcSet(conf); err != nil { 136 | logger.Errorf("Config device: %v", err) 137 | } 138 | } 139 | }() 140 | } 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "golang.zx2c4.com/wireguard/conn" 7 | ) 8 | 9 | type connBind struct { 10 | // magic 3 bytes in wireguard header reserved section. 11 | clientID []uint8 12 | defaultBind conn.Bind 13 | } 14 | 15 | func newConnBind(clientID string) conn.Bind { 16 | defaultBind := conn.NewDefaultBind() 17 | if clientID == "" { 18 | return defaultBind 19 | } 20 | parsed, err := base64.StdEncoding.DecodeString(clientID) 21 | if err != nil { 22 | logger.Errorf("Invalid client id: %v, fallback to default", err) 23 | return defaultBind 24 | } 25 | return &connBind{clientID: parsed, defaultBind: defaultBind} 26 | } 27 | 28 | func (c *connBind) Open(port uint16) ([]conn.ReceiveFunc, uint16, error) { 29 | fns, actualPort, err := c.defaultBind.Open(port) 30 | newFNs := make([]conn.ReceiveFunc, 0, len(fns)) 31 | for i := range fns { 32 | f := fns[i] 33 | newFNs = append(newFNs, func(packets [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) { 34 | n, err = f(packets, sizes, eps) 35 | for i := range packets { 36 | if len(packets[i]) > 4 { 37 | copy(packets[i][1:4], []byte{0, 0, 0}) 38 | } 39 | } 40 | return 41 | }) 42 | } 43 | return newFNs, actualPort, err 44 | } 45 | 46 | func (c *connBind) BatchSize() int { 47 | return c.defaultBind.BatchSize() 48 | } 49 | 50 | func (c *connBind) Close() error { return c.defaultBind.Close() } 51 | 52 | func (c *connBind) SetMark(mark uint32) error { return c.defaultBind.SetMark(mark) } 53 | 54 | func (c *connBind) Send(bufs [][]byte, ep conn.Endpoint) error { 55 | for i := range bufs { 56 | if len(bufs[i]) > 4 { 57 | copy(bufs[i][1:4], c.clientID) 58 | } 59 | } 60 | return c.defaultBind.Send(bufs, ep) 61 | } 62 | 63 | func (c *connBind) ParseEndpoint(s string) (conn.Endpoint, error) { 64 | return c.defaultBind.ParseEndpoint(s) 65 | } 66 | -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | target "docker-metadata-action" {} 2 | 3 | target "build" { 4 | inherits = ["docker-metadata-action"] 5 | context = "./" 6 | platforms = [ 7 | "linux/386", 8 | "linux/amd64", 9 | "linux/arm/v5", 10 | "linux/arm/v7", 11 | "linux/arm64/v8", 12 | "linux/mips", 13 | "linux/mips64", 14 | "linux/mips64le", 15 | "linux/mipsle", 16 | "linux/ppc64", 17 | "linux/ppc64le", 18 | "linux/riscv64", 19 | "linux/s390x", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | ## From source 4 | 5 | ```bash 6 | go install github.com/zhsj/wghttp@latest 7 | ``` 8 | 9 | ## Docker 10 | 11 | Docker images are publish at: 12 | 13 | - [Docker Hub](https://hub.docker.com/r/zhusj/wghttp) 14 | 15 | ```bash 16 | docker pull zhusj/wghttp:latest 17 | 18 | ``` 19 | 20 | - [GitHub](https://github.com/zhsj/wghttp/pkgs/container/wghttp) 21 | 22 | ```bash 23 | docker pull ghcr.io/zhsj/wghttp:latest 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/systemd/wghttp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=wghttp 3 | 4 | [Service] 5 | Type=simple 6 | Environment=PRIVATE_KEY=FIXME 7 | Environment=PEER_KEY=FIXME 8 | Environment=PEER_ENDPOINT=FIXME 9 | Environment=CLIENT_IP=FIXME 10 | ExecStart=%h/go/bin/wghttp --listen 127.0.0.1:1080 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=default.target 15 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Running as systemd service 4 | 5 | Since wghttp doesn't need any privilege, it's preferred to run as systemd user service. 6 | 7 | Copy [wghttp.service](./systemd/wghttp.service) to `~/.config/systemd/user/wghttp.service`. 8 | After setting the environment options in `wghttp.service`, run: 9 | 10 | ```bash 11 | systemctl --user daemon-reload 12 | systemctl --user enable --now wghttp 13 | ``` 14 | 15 | ## Options compared to WireGuard configuration file 16 | 17 | For connecting as a client to a VPN gateway, you might have: 18 | 19 | ```ini 20 | [Interface] 21 | Address = 10.200.100.8/24 22 | DNS = 10.200.100.1 23 | PrivateKey = oK56DE9Ue9zK76rAc8pBl6opph+1v36lm7cXXsQKrQM= 24 | 25 | [Peer] 26 | PublicKey = GtL7fZc/bLnqZldpVofMCD6hDjrK28SsdLxevJ+qtKU= 27 | AllowedIPs = 0.0.0.0/0 28 | Endpoint = demo.wireguard.com:51820 29 | PresharedKey = /UwcSPg38hW/D9Y3tcS1FOV0K1wuURMbS0sesJEP5ak= 30 | ``` 31 | 32 | The above configuration is equal to: 33 | 34 | ```bash 35 | wghttp \ 36 | --client-ip=10.200.100.8 \ 37 | --dns=10.200.100.1 \ 38 | --private-key=oK56DE9Ue9zK76rAc8pBl6opph+1v36lm7cXXsQKrQM= \ 39 | --peer-key=GtL7fZc/bLnqZldpVofMCD6hDjrK28SsdLxevJ+qtKU= \ 40 | --peer-endpoint=demo.wireguard.com:51820 \ 41 | --preshared-key=/UwcSPg38hW/D9Y3tcS1FOV0K1wuURMbS0sesJEP5ak= \ 42 | --exit-mode=remote 43 | ``` 44 | 45 | ## Dynamic DNS 46 | 47 | When your server IP is not persistent, you can set a domain with 48 | DDNS for it. `wghttp` will resolve the domain periodically. 49 | 50 | - `--resolve-dns=` 51 | 52 | By default, the server domain is resolved by system resolver. 53 | This option can be set to use a different DNS server. 54 | 55 | - `--resolve-interval=` 56 | 57 | This option controls the interval for resolving server domain. 58 | 59 | Set `--resolve-interval=` to `0` to disable this behaviour. 60 | 61 | ## DNS server format 62 | 63 | Both `--dns=` and `--resolve-dns=` options support following format: 64 | 65 | - Plain DNS 66 | 67 | `8.8.8.8`, `udp://8.8.8.8`, `tcp://8.8.8.8`, 68 | `8.8.8.8:53`, `udp://8.8.8.8:53`, `tcp://8.8.8.8:53`, 69 | 70 | - DNS over TLS 71 | 72 | `tls://8.8.8.8`, `tls://8.8.8.8:853` 73 | 74 | - DNS over HTTPS 75 | 76 | `https://8.8.8.8` 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zhsj/wghttp 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/jessevdk/go-flags v1.5.0 7 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 8 | ) 9 | 10 | require ( 11 | github.com/google/btree v1.0.1 // indirect 12 | golang.org/x/crypto v0.13.0 // indirect 13 | golang.org/x/net v0.15.0 // indirect 14 | golang.org/x/sys v0.12.0 // indirect 15 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 16 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 17 | gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 2 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 3 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 4 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 5 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 6 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 7 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 8 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 9 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 11 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 12 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= 13 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 14 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 15 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 16 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= 17 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= 18 | gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= 19 | gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= 20 | -------------------------------------------------------------------------------- /internal/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/zhsj/wghttp/internal/resolver" 10 | "github.com/zhsj/wghttp/internal/third_party/tailscale/httpproxy" 11 | "github.com/zhsj/wghttp/internal/third_party/tailscale/proxymux" 12 | "github.com/zhsj/wghttp/internal/third_party/tailscale/socks5" 13 | ) 14 | 15 | type dialer func(ctx context.Context, network, address string) (net.Conn, error) 16 | 17 | type Proxy struct { 18 | Dial dialer 19 | DNS string 20 | Stats func() (any, error) 21 | } 22 | 23 | func statsHandler(next http.Handler, stats func() (any, error)) http.Handler { 24 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 25 | if r.URL.Host != "" || r.URL.Path != "/stats" { 26 | next.ServeHTTP(rw, r) 27 | return 28 | } 29 | s, err := stats() 30 | if err != nil { 31 | rw.WriteHeader(http.StatusInternalServerError) 32 | } else { 33 | resp, _ := json.MarshalIndent(s, "", " ") 34 | rw.Header().Set("Content-Type", "application/json") 35 | _, _ = rw.Write(append(resp, '\n')) 36 | } 37 | }) 38 | } 39 | 40 | func dialWithDNS(dial dialer, dns string) dialer { 41 | resolv := resolver.New(dns, dial) 42 | 43 | return func(ctx context.Context, network, address string) (net.Conn, error) { 44 | host, port, err := net.SplitHostPort(address) 45 | if err != nil { 46 | return nil, err 47 | } 48 | if err == nil { 49 | if ip := net.ParseIP(host); ip != nil { 50 | return dial(ctx, network, address) 51 | } 52 | } 53 | 54 | ips, err := resolv.LookupNetIP(ctx, network, host) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | var ( 60 | lastErr error 61 | conn net.Conn 62 | ) 63 | for _, ip := range ips { 64 | addr := net.JoinHostPort(ip.String(), port) 65 | conn, lastErr = dial(ctx, network, addr) 66 | if lastErr == nil { 67 | return conn, nil 68 | } 69 | } 70 | return nil, lastErr 71 | } 72 | } 73 | 74 | func (p Proxy) Serve(ln net.Listener) { 75 | d := dialWithDNS(p.Dial, p.DNS) 76 | 77 | socksListener, httpListener := proxymux.SplitSOCKSAndHTTP(ln) 78 | 79 | httpProxy := &http.Server{Handler: statsHandler(httpproxy.Handler(d), p.Stats)} 80 | socksProxy := &socks5.Server{Dialer: d} 81 | 82 | errc := make(chan error, 2) 83 | go func() { 84 | if err := httpProxy.Serve(httpListener); err != nil { 85 | errc <- err 86 | } 87 | }() 88 | go func() { 89 | if err := socksProxy.Serve(socksListener); err != nil { 90 | errc <- err 91 | } 92 | }() 93 | <-errc 94 | } 95 | -------------------------------------------------------------------------------- /internal/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "testing" 8 | ) 9 | 10 | func TestDialWithDNS(t *testing.T) { 11 | if testing.Short() { 12 | t.Skip() 13 | } 14 | 15 | stdDiar := net.Dialer{ 16 | Resolver: &net.Resolver{ 17 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 18 | return nil, fmt.Errorf("dial to %s", address) 19 | }, 20 | }, 21 | } 22 | 23 | // d := dialWithDNS(stdDiar.DialContext, "https://223.5.5.5") 24 | d := dialWithDNS(func(ctx context.Context, network, address string) (net.Conn, error) { 25 | t.Logf("dial to %s:%s", network, address) 26 | return stdDiar.DialContext(ctx, network, address) 27 | }, "tls://223.5.5.5") 28 | 29 | for _, addr := range []string{ 30 | "example.com:80", 31 | "223.6.6.6:80", 32 | "localhost:22", 33 | "127.0.0.1:22", 34 | } { 35 | t.Run(addr, func(t *testing.T) { 36 | conn, err := d(context.Background(), "tcp", addr) 37 | if err != nil { 38 | t.Error(err) 39 | } else { 40 | t.Logf("remote %s", conn.RemoteAddr()) 41 | conn.Close() 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/resolver/doh.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "time" 12 | ) 13 | 14 | var _ net.Conn = &dohConn{} 15 | 16 | type dohConn struct { 17 | query, resp *bytes.Buffer 18 | 19 | do func() error 20 | } 21 | 22 | func newDoHConn(ctx context.Context, client *http.Client, addr string) (*dohConn, error) { 23 | c := &dohConn{ 24 | query: &bytes.Buffer{}, 25 | resp: &bytes.Buffer{}, 26 | } 27 | 28 | url, err := url.Parse(addr) 29 | if err != nil { 30 | return nil, err 31 | } 32 | // RFC 8484 33 | url.Path = "/dns-query" 34 | 35 | c.do = func() error { 36 | if c.query.Len() <= 2 || c.resp.Len() > 0 { 37 | return nil 38 | } 39 | 40 | // Skip length header 41 | c.query.Next(2) 42 | 43 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), c.query) 44 | if err != nil { 45 | return err 46 | } 47 | req.Header.Set("content-type", "application/dns-message") 48 | req.Header.Set("accept", "application/dns-message") 49 | 50 | resp, err := client.Do(req) 51 | if err != nil { 52 | return err 53 | } 54 | defer resp.Body.Close() 55 | 56 | respBody, err := io.ReadAll(resp.Body) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if resp.StatusCode != http.StatusOK { 62 | return fmt.Errorf("server return %d: %s", resp.StatusCode, respBody) 63 | } 64 | 65 | // Add length header 66 | l := uint16(len(respBody)) 67 | _, err = c.resp.Write([]byte{uint8(l >> 8), uint8(l & ((1 << 8) - 1))}) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | _, err = c.resp.Write(respBody) 73 | return err 74 | } 75 | 76 | return c, nil 77 | } 78 | 79 | func (c *dohConn) Close() error { return nil } 80 | func (c *dohConn) LocalAddr() net.Addr { return nil } 81 | func (c *dohConn) RemoteAddr() net.Addr { return nil } 82 | func (c *dohConn) SetDeadline(time.Time) error { return nil } 83 | func (c *dohConn) SetReadDeadline(time.Time) error { return nil } 84 | func (c *dohConn) SetWriteDeadline(time.Time) error { return nil } 85 | 86 | func (c *dohConn) Write(b []byte) (int, error) { return c.query.Write(b) } 87 | 88 | func (c *dohConn) Read(b []byte) (int, error) { 89 | if err := c.do(); err != nil { 90 | return 0, err 91 | } 92 | 93 | return c.resp.Read(b) 94 | } 95 | -------------------------------------------------------------------------------- /internal/resolver/resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "net" 8 | "net/http" 9 | "net/netip" 10 | "strings" 11 | ) 12 | 13 | var errNotRetry = errors.New("not retry") 14 | 15 | type Resolver struct { 16 | sysAddr, addr string 17 | network string 18 | tlsConfig *tls.Config 19 | httpClient *http.Client 20 | 21 | r *net.Resolver 22 | } 23 | 24 | func (r *Resolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) { 25 | ipNetwork := network 26 | switch network { 27 | case "tcp", "udp": 28 | ipNetwork = "ip" 29 | case "tcp4", "udp4": 30 | ipNetwork = "ip4" 31 | case "tcp6", "udp6": 32 | ipNetwork = "ip6" 33 | } 34 | 35 | return r.r.LookupNetIP(ctx, ipNetwork, host) 36 | } 37 | 38 | func New(dns string, dial func(ctx context.Context, network, address string) (net.Conn, error)) *Resolver { 39 | r := &Resolver{} 40 | switch { 41 | case strings.HasPrefix(dns, "tls://"): 42 | r.addr = withDefaultPort(dns[len("tls://"):], "853") 43 | host, _, _ := net.SplitHostPort(r.addr) 44 | r.tlsConfig = &tls.Config{ 45 | ServerName: host, 46 | } 47 | r.r = &net.Resolver{ 48 | PreferGo: true, 49 | Dial: func(ctx context.Context, _, address string) (net.Conn, error) { 50 | if r.sysAddr == "" { 51 | r.sysAddr = address 52 | } 53 | if r.sysAddr != address { 54 | return nil, errNotRetry 55 | } 56 | conn, err := dial(ctx, "tcp", r.addr) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return tls.Client(conn, r.tlsConfig), nil 61 | }, 62 | } 63 | case strings.HasPrefix(dns, "https://"): 64 | r.httpClient = &http.Client{ 65 | Transport: &http.Transport{ 66 | DialContext: dial, 67 | }, 68 | } 69 | r.r = &net.Resolver{ 70 | PreferGo: true, 71 | Dial: func(ctx context.Context, _, address string) (net.Conn, error) { 72 | if r.sysAddr == "" { 73 | r.sysAddr = address 74 | } 75 | if r.sysAddr != address { 76 | return nil, errNotRetry 77 | } 78 | 79 | return newDoHConn(ctx, r.httpClient, dns) 80 | }, 81 | } 82 | case dns != "": 83 | r.addr = dns 84 | r.network = "udp" 85 | 86 | if strings.HasPrefix(dns, "tcp://") || strings.HasPrefix(dns, "udp://") { 87 | r.addr = dns[len("tcp://"):] 88 | r.network = dns[:len("tcp")] 89 | } 90 | r.addr = withDefaultPort(r.addr, "53") 91 | 92 | r.r = &net.Resolver{ 93 | PreferGo: true, 94 | Dial: func(ctx context.Context, _, address string) (net.Conn, error) { 95 | if r.sysAddr == "" { 96 | r.sysAddr = address 97 | } 98 | if r.sysAddr != address { 99 | return nil, errNotRetry 100 | } 101 | 102 | return dial(ctx, r.network, r.addr) 103 | }, 104 | } 105 | default: 106 | r.r = &net.Resolver{} 107 | } 108 | return r 109 | } 110 | 111 | func withDefaultPort(addr, port string) string { 112 | if _, _, err := net.SplitHostPort(addr); err == nil { 113 | return addr 114 | } 115 | return net.JoinHostPort(addr, port) 116 | } 117 | -------------------------------------------------------------------------------- /internal/resolver/resolver_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | ) 8 | 9 | func TestResolve(t *testing.T) { 10 | if testing.Short() { 11 | t.Skip() 12 | } 13 | 14 | for _, server := range []string{ 15 | "", 16 | "223.5.5.5", 17 | "223.5.5.5:53", 18 | "tcp://223.5.5.5", 19 | "tcp://223.5.5.5:53", 20 | "udp://223.5.5.5", 21 | "udp://223.5.5.5:53", 22 | "tls://223.5.5.5", 23 | "tls://223.5.5.5:853", 24 | "https://223.5.5.5", 25 | "https://223.5.5.5:443", 26 | "https://223.5.5.5:443/dns-query", 27 | } { 28 | t.Run(server, func(t *testing.T) { 29 | r := New(server, (&net.Dialer{}).DialContext) 30 | ips, err := r.LookupNetIP(context.TODO(), "ip4", "www.example.com") 31 | 32 | if err != nil { 33 | t.Error(err) 34 | } else { 35 | t.Logf("got %s", ips) 36 | } 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/third_party/tailscale/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020 Tailscale & AUTHORS. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /internal/third_party/tailscale/httpproxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // HTTP proxy code 6 | 7 | package httpproxy 8 | 9 | import ( 10 | "context" 11 | "io" 12 | "net" 13 | "net/http" 14 | "net/http/httputil" 15 | "strings" 16 | ) 17 | 18 | // Handler returns an HTTP proxy http.Handler using the 19 | // provided backend dialer. 20 | func Handler(dialer func(ctx context.Context, netw, addr string) (net.Conn, error)) http.Handler { 21 | rp := &httputil.ReverseProxy{ 22 | Director: func(r *http.Request) {}, // no change 23 | Transport: &http.Transport{ 24 | DialContext: dialer, 25 | }, 26 | } 27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | if r.Method != "CONNECT" { 29 | backURL := r.RequestURI 30 | if strings.HasPrefix(backURL, "/") || backURL == "*" { 31 | http.Error(w, "bogus RequestURI; must be absolute URL or CONNECT", 400) 32 | return 33 | } 34 | rp.ServeHTTP(w, r) 35 | return 36 | } 37 | 38 | // CONNECT support: 39 | 40 | dst := r.RequestURI 41 | c, err := dialer(r.Context(), "tcp", dst) 42 | if err != nil { 43 | w.Header().Set("Connect-Error", err.Error()) 44 | http.Error(w, err.Error(), 500) 45 | return 46 | } 47 | defer c.Close() 48 | 49 | cc, ccbuf, err := w.(http.Hijacker).Hijack() 50 | if err != nil { 51 | http.Error(w, err.Error(), 500) 52 | return 53 | } 54 | defer cc.Close() 55 | 56 | io.WriteString(cc, "HTTP/1.1 200 OK\r\n\r\n") 57 | 58 | var clientSrc io.Reader = ccbuf 59 | if ccbuf.Reader.Buffered() == 0 { 60 | // In the common case (with no 61 | // buffered data), read directly from 62 | // the underlying client connection to 63 | // save some memory, letting the 64 | // bufio.Reader/Writer get GC'ed. 65 | clientSrc = cc 66 | } 67 | 68 | errc := make(chan error, 1) 69 | go func() { 70 | _, err := io.Copy(cc, c) 71 | errc <- err 72 | }() 73 | go func() { 74 | _, err := io.Copy(c, clientSrc) 75 | errc <- err 76 | }() 77 | <-errc 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /internal/third_party/tailscale/proxymux/mux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package proxymux splits a net.Listener in two, routing SOCKS5 6 | // connections to one and HTTP requests to the other. 7 | // 8 | // It allows for hosting both a SOCKS5 proxy and an HTTP proxy on the 9 | // same listener. 10 | package proxymux 11 | 12 | import ( 13 | "io" 14 | "net" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | // SplitSOCKSAndHTTP accepts connections on ln and passes connections 20 | // through to either socksListener or httpListener, depending the 21 | // first byte sent by the client. 22 | func SplitSOCKSAndHTTP(ln net.Listener) (socksListener, httpListener net.Listener) { 23 | sl := &listener{ 24 | addr: ln.Addr(), 25 | c: make(chan net.Conn), 26 | closed: make(chan struct{}), 27 | } 28 | hl := &listener{ 29 | addr: ln.Addr(), 30 | c: make(chan net.Conn), 31 | closed: make(chan struct{}), 32 | } 33 | 34 | go splitSOCKSAndHTTPListener(ln, sl, hl) 35 | 36 | return sl, hl 37 | } 38 | 39 | func splitSOCKSAndHTTPListener(ln net.Listener, sl, hl *listener) { 40 | for { 41 | conn, err := ln.Accept() 42 | if err != nil { 43 | sl.Close() 44 | hl.Close() 45 | return 46 | } 47 | go routeConn(conn, sl, hl) 48 | } 49 | } 50 | 51 | func routeConn(c net.Conn, socksListener, httpListener *listener) { 52 | if err := c.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil { 53 | c.Close() 54 | return 55 | } 56 | 57 | var b [1]byte 58 | if _, err := io.ReadFull(c, b[:]); err != nil { 59 | c.Close() 60 | return 61 | } 62 | 63 | if err := c.SetReadDeadline(time.Time{}); err != nil { 64 | c.Close() 65 | return 66 | } 67 | 68 | conn := &connWithOneByte{ 69 | Conn: c, 70 | b: b[0], 71 | } 72 | 73 | // First byte of a SOCKS5 session is a version byte set to 5. 74 | var ln *listener 75 | if b[0] == 5 { 76 | ln = socksListener 77 | } else { 78 | ln = httpListener 79 | } 80 | select { 81 | case ln.c <- conn: 82 | case <-ln.closed: 83 | c.Close() 84 | } 85 | } 86 | 87 | type listener struct { 88 | addr net.Addr 89 | c chan net.Conn 90 | mu sync.Mutex // serializes close() on closed. It's okay to receive on closed without locking. 91 | closed chan struct{} 92 | } 93 | 94 | func (ln *listener) Accept() (net.Conn, error) { 95 | // Once closed, reliably stay closed, don't race with attempts at 96 | // further connections. 97 | select { 98 | case <-ln.closed: 99 | return nil, net.ErrClosed 100 | default: 101 | } 102 | select { 103 | case ret := <-ln.c: 104 | return ret, nil 105 | case <-ln.closed: 106 | return nil, net.ErrClosed 107 | } 108 | } 109 | 110 | func (ln *listener) Close() error { 111 | ln.mu.Lock() 112 | defer ln.mu.Unlock() 113 | select { 114 | case <-ln.closed: 115 | // Already closed 116 | default: 117 | close(ln.closed) 118 | } 119 | return nil 120 | } 121 | 122 | func (ln *listener) Addr() net.Addr { 123 | return ln.addr 124 | } 125 | 126 | // connWithOneByte is a net.Conn that returns b for the first read 127 | // request, then forwards everything else to Conn. 128 | type connWithOneByte struct { 129 | net.Conn 130 | 131 | b byte 132 | bRead bool 133 | } 134 | 135 | func (c *connWithOneByte) Read(bs []byte) (int, error) { 136 | if c.bRead { 137 | return c.Conn.Read(bs) 138 | } 139 | if len(bs) == 0 { 140 | return 0, nil 141 | } 142 | c.bRead = true 143 | bs[0] = c.b 144 | return 1, nil 145 | } 146 | -------------------------------------------------------------------------------- /internal/third_party/tailscale/socks5/socks5.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package socks5 is a SOCKS5 server implementation. 6 | // 7 | // This is used for userspace networking in Tailscale. Specifically, 8 | // this is used for dialing out of the machine to other nodes, without 9 | // the host kernel's involvement, so it doesn't proper routing tables, 10 | // TUN, IPv6, etc. This package is meant to only handle the SOCKS5 protocol 11 | // details and not any integration with Tailscale internals itself. 12 | // 13 | // The glue between this package and Tailscale is in net/socks5/tssocks. 14 | package socks5 15 | 16 | import ( 17 | "context" 18 | "encoding/binary" 19 | "fmt" 20 | "io" 21 | "log" 22 | "net" 23 | "strconv" 24 | "time" 25 | ) 26 | 27 | const ( 28 | noAuthRequired byte = 0 29 | noAcceptableAuth byte = 255 30 | 31 | // socks5Version is the byte that represents the SOCKS version 32 | // in requests. 33 | socks5Version byte = 5 34 | ) 35 | 36 | // commandType are the bytes sent in SOCKS5 packets 37 | // that represent the kind of connection the client needs. 38 | type commandType byte 39 | 40 | // The set of valid SOCKS5 commands as described in RFC 1928. 41 | const ( 42 | connect commandType = 1 43 | bind commandType = 2 44 | udpAssociate commandType = 3 45 | ) 46 | 47 | // addrType are the bytes sent in SOCKS5 packets 48 | // that represent particular address types. 49 | type addrType byte 50 | 51 | // The set of valid SOCKS5 address types as defined in RFC 1928. 52 | const ( 53 | ipv4 addrType = 1 54 | domainName addrType = 3 55 | ipv6 addrType = 4 56 | ) 57 | 58 | // replyCode are the bytes sent in SOCKS5 packets 59 | // that represent replies from the server to a client 60 | // request. 61 | type replyCode byte 62 | 63 | // The set of valid SOCKS5 reply types as per the RFC 1928. 64 | const ( 65 | success replyCode = 0 66 | generalFailure replyCode = 1 67 | connectionNotAllowed replyCode = 2 68 | networkUnreachable replyCode = 3 69 | hostUnreachable replyCode = 4 70 | connectionRefused replyCode = 5 71 | ttlExpired replyCode = 6 72 | commandNotSupported replyCode = 7 73 | addrTypeNotSupported replyCode = 8 74 | ) 75 | 76 | // Server is a SOCKS5 proxy server. 77 | type Server struct { 78 | // Logf optionally specifies the logger to use. 79 | // If nil, the standard logger is used. 80 | Logf func(format string, args ...any) 81 | 82 | // Dialer optionally specifies the dialer to use for outgoing connections. 83 | // If nil, the net package's standard dialer is used. 84 | Dialer func(ctx context.Context, network, addr string) (net.Conn, error) 85 | } 86 | 87 | func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, error) { 88 | dial := s.Dialer 89 | if dial == nil { 90 | dialer := &net.Dialer{} 91 | dial = dialer.DialContext 92 | } 93 | return dial(ctx, network, addr) 94 | } 95 | 96 | func (s *Server) logf(format string, args ...any) { 97 | logf := s.Logf 98 | if logf == nil { 99 | logf = log.Printf 100 | } 101 | logf(format, args...) 102 | } 103 | 104 | // Serve accepts and handles incoming connections on the given listener. 105 | func (s *Server) Serve(l net.Listener) error { 106 | defer l.Close() 107 | for { 108 | c, err := l.Accept() 109 | if err != nil { 110 | return err 111 | } 112 | go func() { 113 | defer c.Close() 114 | conn := &Conn{clientConn: c, srv: s} 115 | err := conn.Run() 116 | if err != nil { 117 | s.logf("client connection failed: %v", err) 118 | } 119 | }() 120 | } 121 | } 122 | 123 | // Conn is a SOCKS5 connection for client to reach 124 | // server. 125 | type Conn struct { 126 | // The struct is filled by each of the internal 127 | // methods in turn as the transaction progresses. 128 | 129 | srv *Server 130 | clientConn net.Conn 131 | request *request 132 | } 133 | 134 | // Run starts the new connection. 135 | func (c *Conn) Run() error { 136 | err := parseClientGreeting(c.clientConn) 137 | if err != nil { 138 | c.clientConn.Write([]byte{socks5Version, noAcceptableAuth}) 139 | return err 140 | } 141 | c.clientConn.Write([]byte{socks5Version, noAuthRequired}) 142 | return c.handleRequest() 143 | } 144 | 145 | func (c *Conn) handleRequest() error { 146 | req, err := parseClientRequest(c.clientConn) 147 | if err != nil { 148 | res := &response{reply: generalFailure} 149 | buf, _ := res.marshal() 150 | c.clientConn.Write(buf) 151 | return err 152 | } 153 | if req.command != connect { 154 | res := &response{reply: commandNotSupported} 155 | buf, _ := res.marshal() 156 | c.clientConn.Write(buf) 157 | return fmt.Errorf("unsupported command %v", req.command) 158 | } 159 | c.request = req 160 | 161 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 162 | defer cancel() 163 | srv, err := c.srv.dial( 164 | ctx, 165 | "tcp", 166 | net.JoinHostPort(c.request.destination, strconv.Itoa(int(c.request.port))), 167 | ) 168 | if err != nil { 169 | res := &response{reply: generalFailure} 170 | buf, _ := res.marshal() 171 | c.clientConn.Write(buf) 172 | return err 173 | } 174 | defer srv.Close() 175 | serverAddr, serverPortStr, err := net.SplitHostPort(srv.LocalAddr().String()) 176 | if err != nil { 177 | return err 178 | } 179 | serverPort, _ := strconv.Atoi(serverPortStr) 180 | 181 | var bindAddrType addrType 182 | if ip := net.ParseIP(serverAddr); ip != nil { 183 | if ip.To4() != nil { 184 | bindAddrType = ipv4 185 | } else { 186 | bindAddrType = ipv6 187 | } 188 | } else { 189 | bindAddrType = domainName 190 | } 191 | res := &response{ 192 | reply: success, 193 | bindAddrType: bindAddrType, 194 | bindAddr: serverAddr, 195 | bindPort: uint16(serverPort), 196 | } 197 | buf, err := res.marshal() 198 | if err != nil { 199 | res = &response{reply: generalFailure} 200 | buf, _ = res.marshal() 201 | } 202 | c.clientConn.Write(buf) 203 | 204 | errc := make(chan error, 2) 205 | go func() { 206 | _, err := io.Copy(c.clientConn, srv) 207 | if err != nil { 208 | err = fmt.Errorf("from backend to client: %w", err) 209 | } 210 | errc <- err 211 | }() 212 | go func() { 213 | _, err := io.Copy(srv, c.clientConn) 214 | if err != nil { 215 | err = fmt.Errorf("from client to backend: %w", err) 216 | } 217 | errc <- err 218 | }() 219 | return <-errc 220 | } 221 | 222 | // parseClientGreeting parses a request initiation packet 223 | // and returns a slice that contains the acceptable auth methods 224 | // for the client. 225 | func parseClientGreeting(r io.Reader) error { 226 | var hdr [2]byte 227 | _, err := io.ReadFull(r, hdr[:]) 228 | if err != nil { 229 | return fmt.Errorf("could not read packet header") 230 | } 231 | if hdr[0] != socks5Version { 232 | return fmt.Errorf("incompatible SOCKS version") 233 | } 234 | count := int(hdr[1]) 235 | methods := make([]byte, count) 236 | _, err = io.ReadFull(r, methods) 237 | if err != nil { 238 | return fmt.Errorf("could not read methods") 239 | } 240 | for _, m := range methods { 241 | if m == noAuthRequired { 242 | return nil 243 | } 244 | } 245 | return fmt.Errorf("no acceptable auth methods") 246 | } 247 | 248 | // request represents data contained within a SOCKS5 249 | // connection request packet. 250 | type request struct { 251 | command commandType 252 | destination string 253 | port uint16 254 | destAddrType addrType 255 | } 256 | 257 | // parseClientRequest converts raw packet bytes into a 258 | // SOCKS5Request struct. 259 | func parseClientRequest(r io.Reader) (*request, error) { 260 | var hdr [4]byte 261 | _, err := io.ReadFull(r, hdr[:]) 262 | if err != nil { 263 | return nil, fmt.Errorf("could not read packet header") 264 | } 265 | cmd := hdr[1] 266 | destAddrType := addrType(hdr[3]) 267 | 268 | var destination string 269 | var port uint16 270 | 271 | if destAddrType == ipv4 { 272 | var ip [4]byte 273 | _, err = io.ReadFull(r, ip[:]) 274 | if err != nil { 275 | return nil, fmt.Errorf("could not read IPv4 address") 276 | } 277 | destination = net.IP(ip[:]).String() 278 | } else if destAddrType == domainName { 279 | var dstSizeByte [1]byte 280 | _, err = io.ReadFull(r, dstSizeByte[:]) 281 | if err != nil { 282 | return nil, fmt.Errorf("could not read domain name size") 283 | } 284 | dstSize := int(dstSizeByte[0]) 285 | domainName := make([]byte, dstSize) 286 | _, err = io.ReadFull(r, domainName) 287 | if err != nil { 288 | return nil, fmt.Errorf("could not read domain name") 289 | } 290 | destination = string(domainName) 291 | } else if destAddrType == ipv6 { 292 | var ip [16]byte 293 | _, err = io.ReadFull(r, ip[:]) 294 | if err != nil { 295 | return nil, fmt.Errorf("could not read IPv6 address") 296 | } 297 | destination = net.IP(ip[:]).String() 298 | } else { 299 | return nil, fmt.Errorf("unsupported address type") 300 | } 301 | var portBytes [2]byte 302 | _, err = io.ReadFull(r, portBytes[:]) 303 | if err != nil { 304 | return nil, fmt.Errorf("could not read port") 305 | } 306 | port = binary.BigEndian.Uint16(portBytes[:]) 307 | 308 | return &request{ 309 | command: commandType(cmd), 310 | destination: destination, 311 | port: port, 312 | destAddrType: destAddrType, 313 | }, nil 314 | } 315 | 316 | // response contains the contents of 317 | // a response packet sent from the proxy 318 | // to the client. 319 | type response struct { 320 | reply replyCode 321 | bindAddrType addrType 322 | bindAddr string 323 | bindPort uint16 324 | } 325 | 326 | // marshal converts a SOCKS5Response struct into 327 | // a packet. If res.reply == Success, it may throw an error on 328 | // receiving an invalid bind address. Otherwise, it will not throw. 329 | func (res *response) marshal() ([]byte, error) { 330 | pkt := make([]byte, 4) 331 | pkt[0] = socks5Version 332 | pkt[1] = byte(res.reply) 333 | pkt[2] = 0 // null reserved byte 334 | pkt[3] = byte(res.bindAddrType) 335 | 336 | if res.reply != success { 337 | return pkt, nil 338 | } 339 | 340 | var addr []byte 341 | switch res.bindAddrType { 342 | case ipv4: 343 | addr = net.ParseIP(res.bindAddr).To4() 344 | if addr == nil { 345 | return nil, fmt.Errorf("invalid IPv4 address for binding") 346 | } 347 | case domainName: 348 | if len(res.bindAddr) > 255 { 349 | return nil, fmt.Errorf("invalid domain name for binding") 350 | } 351 | addr = make([]byte, 0, len(res.bindAddr)+1) 352 | addr = append(addr, byte(len(res.bindAddr))) 353 | addr = append(addr, []byte(res.bindAddr)...) 354 | case ipv6: 355 | addr = net.ParseIP(res.bindAddr).To16() 356 | if addr == nil { 357 | return nil, fmt.Errorf("invalid IPv6 address for binding") 358 | } 359 | default: 360 | return nil, fmt.Errorf("unsupported address type") 361 | } 362 | 363 | pkt = append(pkt, addr...) 364 | port := make([]byte, 2) 365 | binary.BigEndian.PutUint16(port, uint16(res.bindPort)) 366 | pkt = append(pkt, port...) 367 | 368 | return pkt, nil 369 | } 370 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/netip" 10 | "os" 11 | "strings" 12 | 13 | "github.com/jessevdk/go-flags" 14 | "golang.zx2c4.com/wireguard/device" 15 | "golang.zx2c4.com/wireguard/tun/netstack" 16 | 17 | "github.com/zhsj/wghttp/internal/proxy" 18 | ) 19 | 20 | //go:embed README.md 21 | var readme string 22 | 23 | var ( 24 | logger *device.Logger 25 | opts options 26 | ) 27 | 28 | func main() { 29 | parser := flags.NewParser(&opts, flags.Default) 30 | parser.LongDescription = fmt.Sprintf("wghttp %s\n\n", version()) 31 | parser.LongDescription += strings.Trim(strings.TrimPrefix(readme, "# wghttp"), "\n") 32 | if _, err := parser.Parse(); err != nil { 33 | code := 1 34 | fe := &flags.Error{} 35 | if errors.As(err, &fe) && fe.Type == flags.ErrHelp { 36 | code = 0 37 | } 38 | os.Exit(code) 39 | } 40 | if opts.Verbose { 41 | logger = device.NewLogger(device.LogLevelVerbose, "") 42 | } else { 43 | logger = device.NewLogger(device.LogLevelError, "") 44 | } 45 | logger.Verbosef("Options: %+v", opts) 46 | 47 | dev, tnet, err := setupNet() 48 | if err != nil { 49 | logger.Errorf("Setup netstack: %v", err) 50 | os.Exit(1) 51 | } 52 | 53 | listener, err := proxyListener(tnet) 54 | if err != nil { 55 | logger.Errorf("Create net listener: %v", err) 56 | os.Exit(1) 57 | } 58 | 59 | proxier := proxy.Proxy{ 60 | Dial: proxyDialer(tnet), DNS: opts.DNS, Stats: stats(dev), 61 | } 62 | proxier.Serve(listener) 63 | 64 | os.Exit(1) 65 | } 66 | 67 | func proxyDialer(tnet *netstack.Net) (dialer func(ctx context.Context, network, address string) (net.Conn, error)) { 68 | switch opts.ExitMode { 69 | case "local": 70 | d := net.Dialer{} 71 | dialer = d.DialContext 72 | case "remote": 73 | dialer = tnet.DialContext 74 | } 75 | return 76 | } 77 | 78 | func proxyListener(tnet *netstack.Net) (net.Listener, error) { 79 | var tcpListener net.Listener 80 | 81 | tcpAddr, err := net.ResolveTCPAddr("tcp", opts.Listen) 82 | if err != nil { 83 | return nil, fmt.Errorf("resolve listen addr: %w", err) 84 | } 85 | 86 | switch opts.ExitMode { 87 | case "local": 88 | tcpListener, err = tnet.ListenTCP(tcpAddr) 89 | if err != nil { 90 | return nil, fmt.Errorf("create listener on netstack: %w", err) 91 | } 92 | case "remote": 93 | tcpListener, err = net.ListenTCP("tcp", tcpAddr) 94 | if err != nil { 95 | return nil, fmt.Errorf("create listener on local net: %w", err) 96 | } 97 | } 98 | logger.Verbosef("Listening on %s", tcpListener.Addr()) 99 | return tcpListener, nil 100 | } 101 | 102 | func setupNet() (*device.Device, *netstack.Net, error) { 103 | clientIPs := []netip.Addr{} 104 | for _, ip := range opts.ClientIPs { 105 | clientIPs = append(clientIPs, netip.Addr(ip)) 106 | } 107 | tun, tnet, err := netstack.CreateNetTUN(clientIPs, nil, opts.MTU) 108 | if err != nil { 109 | return nil, nil, fmt.Errorf("create netstack tun: %w", err) 110 | } 111 | dev := device.NewDevice(tun, newConnBind(opts.ClientID), logger) 112 | 113 | if err := ipcSet(dev); err != nil { 114 | return nil, nil, fmt.Errorf("config device: %w", err) 115 | } 116 | 117 | if err := dev.Up(); err != nil { 118 | return nil, nil, fmt.Errorf("bring up device: %w", err) 119 | } 120 | 121 | return dev, tnet, nil 122 | } 123 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "net" 7 | "net/netip" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type ipT netip.Addr 13 | 14 | func (o *ipT) UnmarshalFlag(value string) error { 15 | ip, err := netip.ParseAddr(value) 16 | *o = ipT(ip) 17 | return err 18 | } 19 | 20 | func (o ipT) String() string { 21 | return netip.Addr(o).String() 22 | } 23 | 24 | type hostPortT struct { 25 | host string 26 | port uint16 27 | } 28 | 29 | func (o *hostPortT) UnmarshalFlag(value string) error { 30 | host, port, err := net.SplitHostPort(value) 31 | if err != nil { 32 | return err 33 | } 34 | port16, err := strconv.ParseUint(port, 10, 16) 35 | *o = hostPortT{host, uint16(port16)} 36 | return err 37 | } 38 | 39 | type keyT string 40 | 41 | func (o *keyT) UnmarshalFlag(value string) error { 42 | key, err := base64.StdEncoding.DecodeString(value) 43 | *o = keyT(hex.EncodeToString(key)) 44 | return err 45 | } 46 | 47 | type timeT int64 48 | 49 | func (o *timeT) UnmarshalFlag(value string) error { 50 | i, err := strconv.ParseInt(value, 10, 32) 51 | if err == nil { 52 | *o = timeT(i) 53 | return nil 54 | } 55 | d, err := time.ParseDuration(value) 56 | *o = timeT(d.Seconds()) 57 | return err 58 | } 59 | 60 | type options struct { 61 | ClientIPs []ipT `long:"client-ip" env:"CLIENT_IP" env-delim:"," required:"true" description:"[Interface].Address\tfor WireGuard client (can be set multiple times)"` 62 | ClientPort int `long:"client-port" env:"CLIENT_PORT" description:"[Interface].ListenPort\tfor WireGuard client (optional)"` 63 | PrivateKey keyT `long:"private-key" env:"PRIVATE_KEY" required:"true" description:"[Interface].PrivateKey\tfor WireGuard client (format: base64)"` 64 | DNS string `long:"dns" env:"DNS" description:"[Interface].DNS\tfor WireGuard network (format: protocol://ip:port)\nProtocol includes udp(default), tcp, tls(DNS over TLS) and https(DNS over HTTPS)"` 65 | MTU int `long:"mtu" env:"MTU" default:"1280" description:"[Interface].MTU\tfor WireGuard network"` 66 | 67 | PeerEndpoint hostPortT `long:"peer-endpoint" env:"PEER_ENDPOINT" required:"true" description:"[Peer].Endpoint\tfor WireGuard server (format: host:port)"` 68 | PeerKey keyT `long:"peer-key" env:"PEER_KEY" required:"true" description:"[Peer].PublicKey\tfor WireGuard server (format: base64)"` 69 | PresharedKey keyT `long:"preshared-key" env:"PRESHARED_KEY" description:"[Peer].PresharedKey\tfor WireGuard network (optional, format: base64)"` 70 | KeepaliveInterval timeT `long:"keepalive-interval" env:"KEEPALIVE_INTERVAL" description:"[Peer].PersistentKeepalive\tfor WireGuard network (optional)"` 71 | 72 | ResolveDNS string `long:"resolve-dns" env:"RESOLVE_DNS" description:"DNS for resolving WireGuard server address (optional, format: protocol://ip:port)\nProtocol includes udp(default), tcp, tls(DNS over TLS) and https(DNS over HTTPS)"` 73 | ResolveInterval timeT `long:"resolve-interval" env:"RESOLVE_INTERVAL" default:"1m" description:"Interval for resolving WireGuard server address (set 0 to disable)"` 74 | 75 | Listen string `long:"listen" env:"LISTEN" default:"localhost:8080" description:"HTTP & SOCKS5 server address"` 76 | ExitMode string `long:"exit-mode" env:"EXIT_MODE" choice:"remote" choice:"local" default:"remote" description:"Exit mode"` 77 | Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` 78 | 79 | ClientID string `long:"client-id" env:"CLIENT_ID" hidden:"true"` 80 | } 81 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "runtime" 7 | "runtime/debug" 8 | "strconv" 9 | "strings" 10 | 11 | "golang.zx2c4.com/wireguard/device" 12 | ) 13 | 14 | func stats(dev *device.Device) func() (any, error) { 15 | return func() (any, error) { 16 | var buf bytes.Buffer 17 | if err := dev.IpcGetOperation(&buf); err != nil { 18 | logger.Errorf("Get device config: %v", err) 19 | return nil, err 20 | } 21 | 22 | stats := struct { 23 | Endpoint string 24 | LastHandshakeTimestamp int64 25 | ReceivedBytes int64 26 | SentBytes int64 27 | 28 | NumGoroutine int 29 | Version string 30 | }{ 31 | NumGoroutine: runtime.NumGoroutine(), 32 | Version: version(), 33 | } 34 | 35 | scanner := bufio.NewScanner(&buf) 36 | for scanner.Scan() { 37 | line := scanner.Text() 38 | if prefix := "endpoint="; strings.HasPrefix(line, prefix) { 39 | stats.Endpoint = strings.TrimPrefix(line, prefix) 40 | } 41 | if prefix := "last_handshake_time_sec="; strings.HasPrefix(line, prefix) { 42 | stats.LastHandshakeTimestamp, _ = strconv.ParseInt(strings.TrimPrefix(line, prefix), 10, 64) 43 | } 44 | if prefix := "rx_bytes="; strings.HasPrefix(line, prefix) { 45 | stats.ReceivedBytes, _ = strconv.ParseInt(strings.TrimPrefix(line, prefix), 10, 64) 46 | } 47 | if prefix := "tx_bytes="; strings.HasPrefix(line, prefix) { 48 | stats.SentBytes, _ = strconv.ParseInt(strings.TrimPrefix(line, prefix), 10, 64) 49 | } 50 | } 51 | return stats, nil 52 | } 53 | } 54 | 55 | func version() string { 56 | info, ok := debug.ReadBuildInfo() 57 | if ok { 58 | return info.Main.Version 59 | } 60 | return "(devel)" 61 | } 62 | --------------------------------------------------------------------------------