├── .circleci └── config.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.adoc ├── cmd └── h2s │ └── main.go ├── duplex.go ├── go.mod ├── h2s.go ├── socks5.go └── upstream.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | lint: 4 | docker: 5 | - image: circleci/golang:latest 6 | steps: 7 | - checkout 8 | - run: 9 | name: gofmt 10 | command: diff -u <(echo -n) <(gofmt -d .) 11 | - run: 12 | name: govet 13 | command: go vet ./... 14 | release: 15 | docker: 16 | - image: circleci/golang:latest 17 | steps: 18 | - checkout 19 | - run: 20 | name: release 21 | command: curl -sL https://git.io/goreleaser | bash 22 | workflows: 23 | version: 2 24 | release: 25 | jobs: 26 | - lint 27 | - release: 28 | filters: 29 | branches: 30 | ignore: /.*/ 31 | tags: 32 | only: /v[0-9]+(\.[0-9]+)*(-.*)*/ 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | build: 2 | main: cmd/h2s/main.go 3 | binary: h2s 4 | ldflags: -s -w 5 | goos: 6 | - darwin 7 | - linux 8 | - windows 9 | - freebsd 10 | - netbsd 11 | - openbsd 12 | - solaris 13 | goarch: 14 | - amd64 15 | - 386 16 | - arm 17 | - arm64 18 | - mips 19 | - mips64 20 | - mipsle 21 | - mips64le 22 | goarm: 23 | - 5 24 | - 6 25 | - 7 26 | ignore: 27 | - goos: darwin 28 | goarch: 386 29 | - goos: openbsd 30 | goarch: arm 31 | - goos: freebsd 32 | goarch: arm 33 | - goos: netbsd 34 | goarch: arm 35 | - goos: solaris 36 | goarch: arm 37 | 38 | archive: 39 | name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 40 | format: tar.xz 41 | format_overrides: 42 | - goos: windows 43 | format: zip 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Equim 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.adoc: -------------------------------------------------------------------------------- 1 | = h2s 2 | Equim 3 | 4 | image:http://img.shields.io/badge/godoc-reference-5272B4.svg[GoDoc, link=https://godoc.org/ekyu.moe/h2s] 5 | image:https://img.shields.io/github/release/Equim-chan/h2s.svg[Release, link=https://github.com/Equim-chan/h2s/releases/latest] 6 | image:https://img.shields.io/circleci/project/github/Equim-chan/h2s.svg[CircleCI, link=https://circleci.com/gh/Equim-chan/h2s] 7 | image:https://goreportcard.com/badge/github.com/Equim-chan/h2s[Go Report Card, link=https://goreportcard.com/report/github.com/Equim-chan/h2s] 8 | image:https://img.shields.io/github/license/Equim-chan/h2s.svg[License, link=https://github.com/Equim-chan/h2s/blob/master/LICENSE] 9 | 10 | h2s is a tiny CLI tool that wraps one or multiple HTTPS proxies into a SOCKS5 proxy. It does something like https://www.irif.fr/~jch/software/polipo/[polipo] and http://www.privoxy.org/[privoxy] do, but in a reversed way. 11 | 12 | There are already some SOCKS to HTTPS tools out there, but I can hardly find a reversed one (HTTPS to SOCKS), so I decided to make one on my own. 13 | 14 | == Install 15 | You can view the https://github.com/Equim-chan/h2s/releases[release] page for handy prebuilt binaries. 16 | 17 | == Build from Source 18 | h2s relies on stadard libs only. 19 | 20 | [source,bash] 21 | ---- 22 | $ go get -u ekyu.moe/h2s/cmd/h2s 23 | ---- 24 | 25 | == Configure 26 | An example config file: 27 | 28 | [source,js] 29 | ---- 30 | { 31 | // bind is the address h2s will listen to. 32 | // Note that since HTTPS proxy support only TCP, the h2s wrapped SOCKS5 33 | // proxy consequently support only TCP as well. 34 | "bind": "127.0.0.1:1080", 35 | 36 | // upstreams are HTTPS proxy upstreams. 37 | // h2s will do a simple round-robin load balance. 38 | "upstreams": [{ 39 | // If no port is specified, 80 is assumed by default. 40 | "address": "proxy1.example.com", 41 | }, { 42 | "address": "proxy2.example.com:3128", 43 | 44 | // username and password are optional for proxy authentication. 45 | "username": "Alice", 46 | "password": "secret here" 47 | }, { 48 | // An HTTPS proxy over TLS upstream. 49 | // You have to specify port explicitly (usually 443), and set the tls field. 50 | "address": "secure.proxy.example.com:443", 51 | "username": "Secure", 52 | "password": "Yeah!", 53 | 54 | // h2s only provides some basic TLS settings. If you are an advanced user and 55 | // looking for other settings, you may use stunnel(1) to handle TLS instead, 56 | // and simply leave a naive TCP interface to h2s. 57 | "tlsConfig": { 58 | // If empty, serverName is set to the hostname from address. 59 | // Most users could just leave it empty. 60 | "serverName": "secure.proxy.example.com", 61 | 62 | // If you prefer to set a fingerprint instead of providing certs, you can 63 | // set this to true. 64 | // Do not set to true unless you know what you are doing. 65 | "insecureSkipVerify": false, 66 | // Server's SHA256 fingerprint, used to verify as an alternative to providing 67 | // the whole server certs, should be used with insecureSkipVerify to true. 68 | // If both rootCA and sha256Fingerprint are provided, they will both be 69 | // verified. 70 | // 71 | // An example to get fingerprint of a given cert: 72 | // openssl x509 -fingerprint -sha256 -noout -in cert.cer | cut -f2 -d'=' | sed s/://g 73 | // or of a server with TLS enabled: 74 | // openssl s_client -showcerts -connect example.com:443 < /dev/null | \ 75 | // openssl x509 -fingerprint -sha256 -noout | cut -f2 -d'=' | sed s/://g 76 | "sha256fingerprint": "22B975A1409850EF7F4522183E9C5A8955758FC899D70FE257112DA2FC430CCC", 77 | 78 | // rootCA is useful for self-signed certs. Be careful with it. 79 | // If the server has a trusted cert, you don't have to set it. 80 | "rootCA": "/path/to/the/ca/cert", 81 | 82 | // certFile and keyFile are advanced options for client authentication. 83 | // Most users could just leave it empty. 84 | "certFile": "/path/to/the/client/cert", 85 | "keyFile": "/path/to/the/client/key" 86 | } 87 | }], 88 | 89 | // accounts is an optional array of accounts for SOCKS5 authentication 90 | // with no accounts, authentication is disabled 91 | "accounts": [{ 92 | "username": "test server", 93 | "password": "test" 94 | }], 95 | 96 | // timeout optionally sets timeout value when dialing to a upstream 97 | // default "20s" 98 | "timeout": "20s", 99 | // retries optionally specifies the max retries count of dialing to upstreams 100 | // default 3. 101 | "retries": 3 102 | } 103 | ---- 104 | 105 | == Usage 106 | [source,bash] 107 | ---- 108 | $ h2s [-config h2s.json] 109 | ---- 110 | 111 | == References 112 | * https://tools.ietf.org/html/rfc1928[RFC 1928 - SOCKS Protocol Version 5] 113 | * https://tools.ietf.org/html/rfc1929[RFC 1929 - Username/Password Authentication for SOCKS V5] 114 | * https://tools.ietf.org/html/rfc7231[RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content] 115 | 116 | == License 117 | https://github.com/Equim-chan/h2s/blob/master/LICENSE[MIT] 118 | -------------------------------------------------------------------------------- /cmd/h2s/main.go: -------------------------------------------------------------------------------- 1 | package main // import "ekyu.moe/h2s/cmd/h2s" 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "log" 7 | "net" 8 | "os" 9 | 10 | "ekyu.moe/h2s" 11 | ) 12 | 13 | var ( 14 | stdout = log.New(os.Stdout, "", log.Ldate|log.Lmicroseconds) 15 | stderr = log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds) 16 | ) 17 | 18 | type config struct { 19 | *h2s.Config 20 | 21 | Bind string `json:"bind"` 22 | } 23 | 24 | func main() { 25 | n, conf := configure() 26 | if n != 0 { 27 | os.Exit(n) 28 | } 29 | 30 | s, err := h2s.NewServer(conf.Config) 31 | if err != nil { 32 | stderr.Println("h2s: create server:", err) 33 | os.Exit(2) 34 | } 35 | defer s.Close() 36 | 37 | l, err := net.Listen("tcp", conf.Bind) 38 | if err != nil { 39 | stderr.Println("h2s: bind:", err) 40 | os.Exit(2) 41 | } 42 | stdout.Println("Listening on", l.Addr()) 43 | 44 | for { 45 | conn, err := l.Accept() 46 | if err != nil { 47 | stderr.Println("h2s: accept:", err) 48 | continue 49 | } 50 | 51 | go func() { 52 | if err := s.Serve(conn); err != nil { 53 | stderr.Println("h2s: serve:", err) 54 | } 55 | }() 56 | } 57 | } 58 | 59 | func configure() (int, *config) { 60 | configFilename := "" 61 | flag.StringVar(&configFilename, "config", "h2s.json", "config file (json).") 62 | flag.Parse() 63 | 64 | f, err := os.Open(configFilename) 65 | if err != nil { 66 | stderr.Println("open config:", err) 67 | return 1, nil 68 | } 69 | defer f.Close() 70 | 71 | conf := &config{} 72 | if err := json.NewDecoder(f).Decode(conf); err != nil { 73 | stderr.Println("parse config:", err) 74 | return 1, nil 75 | } 76 | f.Close() 77 | 78 | if conf.Bind == "" { 79 | conf.Bind = "127.0.0.1:0" 80 | } 81 | 82 | return 0, conf 83 | } 84 | -------------------------------------------------------------------------------- /duplex.go: -------------------------------------------------------------------------------- 1 | package h2s 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | type closeWriter interface { 9 | CloseWrite() error 10 | } 11 | 12 | type closeReader interface { 13 | CloseRead() error 14 | } 15 | 16 | var pipeBufPool = sync.Pool{ 17 | New: func() interface{} { 18 | return make([]byte, 128*1024) 19 | }, 20 | } 21 | 22 | func duplexPipe(x, y io.ReadWriter) { 23 | var wg sync.WaitGroup 24 | wg.Add(2) 25 | 26 | go pipe(x, y, &wg) 27 | go pipe(y, x, &wg) 28 | 29 | wg.Wait() 30 | } 31 | 32 | func pipe(x, y io.ReadWriter, wg *sync.WaitGroup) { 33 | buf := pipeBufPool.Get().([]byte) 34 | io.CopyBuffer(x, y, buf) 35 | pipeBufPool.Put(buf) 36 | 37 | if cw, ok := x.(closeWriter); ok { 38 | cw.CloseWrite() 39 | } 40 | if cr, ok := y.(closeReader); ok { 41 | cr.CloseRead() 42 | } 43 | 44 | wg.Done() 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ekyu.moe/h2s 2 | -------------------------------------------------------------------------------- /h2s.go: -------------------------------------------------------------------------------- 1 | // Package h2s provides a tool that wraps one or multiple HTTP or HTTPS proxies 2 | // into a SOCKS5 proxy. It does something like polipo and privoxy do, 3 | // but in a reversed way. 4 | package h2s // import "ekyu.moe/h2s" 5 | 6 | import ( 7 | "crypto/hmac" 8 | "crypto/sha256" 9 | "crypto/tls" 10 | "crypto/x509" 11 | "encoding/base64" 12 | "encoding/hex" 13 | "errors" 14 | "io/ioutil" 15 | "net" 16 | "net/http" 17 | "net/url" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | // Upstream is a HTTPS proxy upstream that must support CONNECT method as defined 23 | // in RFC 7231 section 4.3.6. 24 | type Upstream struct { 25 | Address string `json:"address"` 26 | Username string `json:"username,omitempty"` 27 | Password string `json:"password,omitempty"` 28 | 29 | // TLSConfig can be null. 30 | TLSConfig *TLSConfig `json:"tlsConfig"` 31 | } 32 | 33 | // TLSConfig is a simplified version of tls.Config 34 | type TLSConfig struct { 35 | // If empty, ServerName is set to the hostname from Address. 36 | // This is useful in some cases, for example a server behind Cloudflare, 37 | // since Cloudflare would simply reject CONNECT method. 38 | ServerName string `json:"serverName"` 39 | 40 | // If you prefer to set a fingerprint instead of providing certs, you can 41 | // set this to true. 42 | // 43 | // Do not set to true unless you know what you are doing. 44 | InsecureSkipVerify bool `json:"insecureSkipVerify"` 45 | // Server's SHA256 fingerprint, used to verify as an alternative to providing 46 | // the whole server certs. 47 | SHA256Fingerprint string `json:"sha256Fingerprint"` 48 | 49 | // For self-signed certs. Be careful. 50 | RootCA string `json:"rootCA"` 51 | 52 | // For client auth. 53 | CertFile string `json:"certFile"` 54 | KeyFile string `json:"keyFile"` 55 | } 56 | 57 | // Account is used for SOCKS5 authentication. 58 | type Account struct { 59 | Username string `json:"username"` 60 | Password string `json:"password"` 61 | } 62 | 63 | // Config is used to configure an h2s Server. 64 | type Config struct { 65 | // HTTP proxy upstreams. 66 | Upstreams []*Upstream `json:"upstreams"` 67 | 68 | // With no Accounts, authentication is disabled. 69 | Accounts []*Account `json:"accounts,omitempty"` 70 | 71 | // Timeout value when dialing to a upstream. Default "20s". 72 | Timeout string `json:"timeout"` 73 | 74 | // The max retries count of dialing to upstreams. Default 3. 75 | Retries *int `json:"retries"` 76 | } 77 | 78 | type internalUpstream struct { 79 | address string 80 | header http.Header 81 | tlsConfig *tls.Config 82 | } 83 | 84 | type internalAccount struct { 85 | username []byte 86 | password []byte 87 | } 88 | 89 | // Server is a SOCKS5 server that forward all incoming requests via Upstreams 90 | // by HTTP/1.1 CONNECT. 91 | type Server struct { 92 | next chan *internalUpstream 93 | stop chan struct{} 94 | requireAuth bool 95 | account []*internalAccount 96 | retries int 97 | dialer *net.Dialer 98 | 99 | isClosed bool 100 | mu sync.Mutex 101 | } 102 | 103 | // I know, I know 104 | func basicauth(username, password string) http.Header { 105 | h := http.Header{} 106 | h.Set("User-Agent", "") 107 | if username != "" && password != "" { 108 | combined := username + ":" + password 109 | encoded := base64.StdEncoding.EncodeToString([]byte(combined)) 110 | h.Set("Proxy-Authorization", "Basic "+encoded) 111 | } 112 | 113 | return h 114 | } 115 | 116 | // NewServer creates an h2s server instance. 117 | func NewServer(c *Config) (*Server, error) { 118 | s := &Server{} 119 | 120 | if c.Timeout != "" { 121 | timeout, err := time.ParseDuration(c.Timeout) 122 | if err != nil { 123 | return nil, errors.New("parse timeout: " + err.Error()) 124 | } 125 | s.dialer = &net.Dialer{Timeout: timeout} 126 | } else { 127 | s.dialer = &net.Dialer{Timeout: 20 * time.Second} 128 | } 129 | 130 | if c.Retries != nil { 131 | s.retries = *c.Retries 132 | } else { 133 | s.retries = 3 134 | } 135 | 136 | s.requireAuth = len(c.Accounts) > 0 137 | if s.requireAuth { 138 | s.account = make([]*internalAccount, len(c.Accounts)) 139 | for i, v := range c.Accounts { 140 | s.account[i] = &internalAccount{ 141 | username: []byte(v.Username), 142 | password: []byte(v.Password), 143 | } 144 | } 145 | } 146 | 147 | if len(c.Upstreams) == 0 { 148 | return nil, errors.New("no upstreams") 149 | } 150 | 151 | upstreams := make([]*internalUpstream, len(c.Upstreams)) 152 | for i, v := range c.Upstreams { 153 | addr := v.Address 154 | host, port, err := net.SplitHostPort(addr) 155 | if err != nil { 156 | addr += ":80" 157 | host, port, err = net.SplitHostPort(addr) 158 | if err != nil { 159 | return nil, errors.New("invalid address " + v.Address) 160 | } 161 | } 162 | addr = net.JoinHostPort(host, port) 163 | 164 | tlsConfig := (*tls.Config)(nil) 165 | if t := v.TLSConfig; t != nil { 166 | tlsConfig = &tls.Config{ 167 | NextProtos: []string{"http/1.1"}, 168 | } 169 | 170 | if t.ServerName != "" { 171 | tlsConfig.ServerName = t.ServerName 172 | } else { 173 | u, err := url.Parse(v.Address) 174 | if err != nil { 175 | return nil, errors.New("tls: parse server name: " + err.Error()) 176 | } 177 | tlsConfig.ServerName = u.Hostname() 178 | } 179 | 180 | tlsConfig.InsecureSkipVerify = t.InsecureSkipVerify 181 | if t.SHA256Fingerprint != "" { 182 | fin, err := hex.DecodeString(t.SHA256Fingerprint) 183 | if err != nil { 184 | return nil, errors.New("tls: failed to parse fingerprint") 185 | } 186 | if len(fin) != 32 { 187 | return nil, errors.New("tls: fingerprint: wrong length, not like a sha256 digest") 188 | } 189 | 190 | tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 191 | if len(rawCerts) < 1 { 192 | return errors.New("server provides no cert") 193 | } 194 | 195 | sum := sha256.Sum256(rawCerts[0]) 196 | if !hmac.Equal(sum[:], fin) { 197 | return errors.New("wrong fingerprint") 198 | } 199 | 200 | return nil 201 | } 202 | } 203 | 204 | if t.RootCA != "" { 205 | certPool := x509.NewCertPool() 206 | pem, err := ioutil.ReadFile(t.RootCA) 207 | if err != nil { 208 | return nil, errors.New("tls: read rootCAs: " + err.Error()) 209 | } 210 | if !certPool.AppendCertsFromPEM(pem) { 211 | return nil, errors.New("tls: failed to load rootCAs") 212 | } 213 | tlsConfig.RootCAs = certPool 214 | } 215 | 216 | if t.CertFile != "" && t.KeyFile != "" { 217 | cert, err := tls.LoadX509KeyPair(t.CertFile, t.KeyFile) 218 | if err != nil { 219 | return nil, errors.New("tls: load key pair: " + err.Error()) 220 | } 221 | tlsConfig.Certificates = []tls.Certificate{cert} 222 | } 223 | } 224 | 225 | upstreams[i] = &internalUpstream{ 226 | address: addr, 227 | header: basicauth(v.Username, v.Password), 228 | tlsConfig: tlsConfig, 229 | } 230 | } 231 | 232 | s.next = make(chan *internalUpstream) 233 | s.stop = make(chan struct{}) 234 | go func() { 235 | // simple round-robin 236 | for { 237 | for _, v := range upstreams { 238 | select { 239 | case s.next <- v: 240 | case <-s.stop: 241 | close(s.next) 242 | return 243 | } 244 | } 245 | } 246 | }() 247 | 248 | return s, nil 249 | } 250 | 251 | // Close closes s. Already established connections will not be affected. 252 | func (s *Server) Close() error { 253 | s.mu.Lock() 254 | if s.isClosed { 255 | return errors.New("server is already closed") 256 | } 257 | s.stop <- struct{}{} 258 | s.isClosed = true 259 | s.mu.Unlock() 260 | 261 | return nil 262 | } 263 | 264 | // Serve handles a net.Conn, reads request from it with SOCKS5 format, and dispatch 265 | // the request via HTTP CONNECT. Serve closes conn whether an error occurs or 266 | // connection is done. The caller must not use conn again. 267 | func (s *Server) Serve(conn net.Conn) error { 268 | defer conn.Close() 269 | 270 | // this is bad 271 | s.mu.Lock() 272 | isClosed := s.isClosed 273 | s.mu.Unlock() 274 | if isClosed { 275 | return errors.New("server is closed") 276 | } 277 | 278 | if err := s.handshake(conn); err != nil { 279 | return errors.New("handshake: " + err.Error()) 280 | } 281 | 282 | target, err := s.readRequest(conn) 283 | if err != nil { 284 | return errors.New("read request: " + err.Error()) 285 | } 286 | 287 | out, u, err := s.dialUpstream() 288 | if err != nil { 289 | return errors.New("dial upstream: " + err.Error()) 290 | } 291 | defer out.Close() 292 | 293 | if err := s.handshakeUpstream(out, u, target); err != nil { 294 | return errors.New("handshake upstream: " + err.Error()) 295 | } 296 | 297 | // sync 298 | duplexPipe(out, conn) 299 | 300 | return nil 301 | } 302 | -------------------------------------------------------------------------------- /socks5.go: -------------------------------------------------------------------------------- 1 | package h2s 2 | 3 | import ( 4 | "crypto/hmac" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "strconv" 11 | "sync" 12 | ) 13 | 14 | var ( 15 | handshakeBufPool = sync.Pool{ 16 | New: func() interface{} { 17 | return make([]byte, 1+1+255) 18 | }, 19 | } 20 | authBufPool = sync.Pool{ 21 | New: func() interface{} { 22 | return make([]byte, 1+1+255+1+255) 23 | }, 24 | } 25 | reqBufPool = sync.Pool{ 26 | New: func() interface{} { 27 | return make([]byte, 1+1+1+1+255+2) 28 | }, 29 | } 30 | ) 31 | 32 | const ( 33 | socksVersion byte = 0x05 34 | socksNoAuth byte = 0x00 35 | socksAuthUserpass byte = 0x02 36 | socksNoMethod byte = 0xff 37 | ) 38 | 39 | // As per RFC 1928 40 | // 41 | // +----+----------+----------+ 42 | // |VER | NMETHODS | METHODS | 43 | // +----+----------+----------+ 44 | // | 1 | 1 | 1 to 255 | 45 | // +----+----------+----------+ 46 | func (s *Server) handshake(conn io.ReadWriter) (err error) { 47 | buf := handshakeBufPool.Get().([]byte) 48 | defer handshakeBufPool.Put(buf) 49 | 50 | // VER + NMETHODS 51 | _, err = io.ReadFull(conn, buf[:2]) 52 | if err != nil { 53 | return 54 | } 55 | 56 | // VER 57 | if buf[0] != socksVersion { 58 | return fmt.Errorf("unexpected SOCKS version 0x%02x", buf[0]) 59 | } 60 | 61 | // NMETHODS 62 | nmethods := int(buf[1]) 63 | 64 | // METHODS 65 | methods := buf[2 : 2+nmethods] 66 | _, err = io.ReadFull(conn, methods) 67 | if err != nil { 68 | return 69 | } 70 | 71 | targetMethod := socksAuthUserpass 72 | if !s.requireAuth { 73 | targetMethod = socksNoAuth 74 | } 75 | 76 | found := false 77 | for _, v := range methods { 78 | if v == targetMethod { 79 | found = true 80 | break 81 | } 82 | } 83 | 84 | if !found { 85 | conn.Write([]byte{socksVersion, socksNoMethod}) 86 | return errors.New("no acceptable auth method") 87 | } 88 | _, err = conn.Write([]byte{socksVersion, targetMethod}) 89 | if err != nil { 90 | return 91 | } 92 | 93 | // authentication 94 | if s.requireAuth { 95 | return s.auth(conn) 96 | } 97 | 98 | return 99 | } 100 | 101 | const ( 102 | socksAuthVersion byte = 0x01 103 | socksAuthSuccess byte = 0x00 104 | socksAuthFail byte = 0x01 105 | ) 106 | 107 | // As per RFC 1929 108 | // 109 | // +----+------+----------+------+----------+ 110 | // |VER | ULEN | UNAME | PLEN | PASSWD | 111 | // +----+------+----------+------+----------+ 112 | // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | 113 | // +----+------+----------+------+----------+ 114 | func (s *Server) auth(conn io.ReadWriter) (err error) { 115 | buf := authBufPool.Get().([]byte) 116 | defer authBufPool.Put(buf) 117 | 118 | // VER + ULEN 119 | _, err = io.ReadFull(conn, buf[:2]) 120 | if err != nil { 121 | return 122 | } 123 | 124 | if buf[0] != socksAuthVersion { 125 | return fmt.Errorf("unexpected SOCKS authentication VER 0x%02x", buf[0]) 126 | } 127 | 128 | // UNAME + PLEN 129 | usernameLen := int(buf[1]) 130 | _, err = io.ReadFull(conn, buf[2:2+usernameLen+1]) 131 | if err != nil { 132 | return 133 | } 134 | passwordLen := int(buf[2+usernameLen]) 135 | 136 | // PASSWD 137 | _, err = io.ReadFull(conn, buf[2+usernameLen+1:2+usernameLen+1+passwordLen]) 138 | if err != nil { 139 | return 140 | } 141 | 142 | // assume big endian 143 | username := buf[2 : 2+usernameLen] 144 | password := buf[2+usernameLen+1 : 2+usernameLen+1+passwordLen] 145 | 146 | found := false 147 | for _, v := range s.account { 148 | if hmac.Equal(v.username, username) { 149 | if hmac.Equal(v.password, password) { 150 | found = true 151 | break 152 | } 153 | } 154 | } 155 | 156 | if !found { 157 | conn.Write([]byte{socksAuthVersion, socksAuthFail}) 158 | return errors.New("authentication failed") 159 | } 160 | _, err = conn.Write([]byte{socksAuthVersion, socksAuthSuccess}) 161 | 162 | return 163 | } 164 | 165 | const ( 166 | socksConnect byte = 0x01 167 | socksUnsupportedCmd byte = 0x07 168 | socksAddrIpv4 byte = 0x01 169 | socksAddrDomain byte = 0x03 170 | socksAddrIpv6 byte = 0x04 171 | ) 172 | 173 | // As per RFC 1928 174 | // 175 | // +----+-----+-------+------+----------+----------+ 176 | // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 177 | // +----+-----+-------+------+----------+----------+ 178 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 179 | // +----+-----+-------+------+----------+----------+ 180 | func (s *Server) readRequest(conn io.ReadWriter) (target string, err error) { 181 | buf := reqBufPool.Get().([]byte) 182 | defer reqBufPool.Put(buf) 183 | 184 | // VER + CMD + RSV + ATYP 185 | _, err = io.ReadFull(conn, buf[:4]) 186 | if err != nil { 187 | return 188 | } 189 | 190 | if buf[0] != socksVersion { 191 | err = fmt.Errorf("unexpected SOCKS version 0x%02x", buf[0]) 192 | return 193 | } 194 | 195 | if buf[1] != socksConnect { 196 | // RFC didn't define this 197 | _, err = conn.Write([]byte{socksVersion, socksUnsupportedCmd}) 198 | if err != nil { 199 | return 200 | } 201 | 202 | err = fmt.Errorf("unexpected command 0x%02x", buf[1]) 203 | return 204 | } 205 | 206 | // DST.ADDR + DST.PORT 207 | host := "" 208 | validLen := 0 209 | switch buf[3] { 210 | case socksAddrIpv4: 211 | validLen = 4 + net.IPv4len + 2 212 | _, err = io.ReadFull(conn, buf[4:validLen]) 213 | if err != nil { 214 | return 215 | } 216 | host = net.IPv4(buf[4+0], buf[4+1], buf[4+2], buf[4+3]).String() 217 | 218 | case socksAddrDomain: 219 | _, err = io.ReadFull(conn, buf[4:5]) 220 | if err != nil { 221 | return 222 | } 223 | domainLen := int(buf[4]) 224 | validLen = 5 + domainLen + 2 225 | _, err = io.ReadFull(conn, buf[5:validLen]) 226 | if err != nil { 227 | return 228 | } 229 | host = string(buf[5 : 5+domainLen]) 230 | 231 | case socksAddrIpv6: 232 | validLen = 4 + net.IPv6len + 2 233 | _, err = io.ReadFull(conn, buf[4:validLen]) 234 | if err != nil { 235 | return 236 | } 237 | host = net.IP(buf[4 : 4+net.IPv6len]).String() 238 | 239 | default: 240 | _, err = conn.Write([]byte{socksVersion, socksUnsupportedCmd}) 241 | if err != nil { 242 | return 243 | } 244 | 245 | err = fmt.Errorf("unexpected ATYP 0x%02x", buf[3]) 246 | return 247 | } 248 | port := int(binary.BigEndian.Uint16(buf[validLen-2 : validLen])) 249 | 250 | // (ノ=Д=)ノ┻━┻ 251 | _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43}) 252 | if err != nil { 253 | return 254 | } 255 | 256 | addr := net.JoinHostPort(host, strconv.Itoa(port)) 257 | return addr, nil 258 | } 259 | -------------------------------------------------------------------------------- /upstream.go: -------------------------------------------------------------------------------- 1 | package h2s 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | func (s *Server) dialUpstream() (conn net.Conn, u *internalUpstream, err error) { 13 | ok := false 14 | for tried := 0; tried <= s.retries; tried++ { 15 | u, ok = <-s.next 16 | if !ok { 17 | err = errors.New("h2s is already closed") 18 | return 19 | } 20 | 21 | if u.tlsConfig != nil { 22 | conn, err = tls.DialWithDialer(s.dialer, "tcp", u.address, u.tlsConfig) 23 | } else { 24 | conn, err = s.dialer.Dial("tcp", u.address) 25 | } 26 | 27 | if err == nil { 28 | return 29 | } 30 | } 31 | err = errors.New("max retry exceeded: " + err.Error()) 32 | 33 | return 34 | } 35 | 36 | func (s *Server) handshakeUpstream(conn net.Conn, u *internalUpstream, target string) error { 37 | req := &http.Request{ 38 | Method: "CONNECT", 39 | URL: &url.URL{Opaque: target}, 40 | Host: target, 41 | Header: u.header, 42 | } 43 | 44 | if err := req.Write(conn); err != nil { 45 | return err 46 | } 47 | 48 | res, err := http.ReadResponse(bufio.NewReader(conn), req) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if res.StatusCode != 200 { 54 | return errors.New(res.Status) 55 | } 56 | 57 | return nil 58 | } 59 | --------------------------------------------------------------------------------