├── .github └── workflows │ └── release.yaml ├── .gitignore ├── README.md ├── all_test.go ├── client.go ├── cmd └── http2tcp │ ├── client.go │ ├── main.go │ ├── server.go │ └── utils.go ├── go.mod ├── go.sum └── server.go /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-go@v3 14 | with: 15 | go-version: '1.18' 16 | - name: Cross building 17 | run: | 18 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o .build/http2tcp-linux-amd64 ./cmd/http2tcp 19 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o .build/http2tcp-linux-arm64 ./cmd/http2tcp 20 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o .build/http2tcp-darwin-amd64 ./cmd/http2tcp 21 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o .build/http2tcp-darwin-arm64 ./cmd/http2tcp 22 | - name: Generate body 23 | run: | 24 | echo '**Build At**: 25 | 26 | * `'"$(date)"'` 27 | * `'"$(TZ=Asia/Shanghai date)"'` 28 | 29 | **sha256sum**: 30 | 31 | ```- 32 | '"$(cd .build && sha256sum *)"' 33 | ``` 34 | ' > body.md 35 | - name: Create Release 36 | uses: ncipollo/release-action@v1.11.2 37 | with: 38 | name: main 39 | allowUpdates: true 40 | artifactErrorsFailBuild: true 41 | replacesArtifacts: true 42 | artifacts: .build/* 43 | commit: main 44 | tag: release-main-latest 45 | bodyFile: body.md 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /http2tcp 3 | /http2tcp-* 4 | .vscode/ 5 | .build/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http2tcp 2 | 3 | **http2tcp** is a simple client & server program that turns an HTTP connection to a TCP connection. 4 | 5 | This is kind of useful if you want to hide all ports traffic other than the standard well-known ports 80 and 443. 6 | 7 | ## Usage 8 | 9 | ```shell-session 10 | $ ./http2tcp -h 11 | Usage of http2tcp: 12 | -s, --server Run as server. 13 | -c, --client Run as client. 14 | -l, --listen string Listen address (client & server) 15 | -e, --endpoint string Server endpoint. 16 | -d, --destination string The destination address to connect to 17 | -t, --token string The token used between client and server 18 | -h, --help Show this help 19 | ``` 20 | 21 | Some flags are shared between the client and server. 22 | 23 | ### Example: Proxy SSH connections 24 | 25 | On server: 26 | 27 | ```shell-session 28 | $ ./http2tcp -s -t $TOKEN -l $SERVER_IP_OR_DOMAIN:2222 29 | ``` 30 | 31 | On client: 32 | 33 | ```shell-session 34 | $ ./http2tcp -c -d localhost:22 -e $SERVER_IP_OR_DOMAIN:2222 -t $TOKEN 35 | SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.3 36 | ``` 37 | 38 | Now the http2tcp client is connected to the SSH server running on the server side via an HTTP connection. 39 | 40 | In your ssh_config, if you write the following: 41 | 42 | ```ssh_config 43 | Host some-host 44 | ProxyCommand http2tcp -c -d localhost:22 -e $SERVER_IP_OR_DOMAIN:2222 -t $TOKEN 45 | ``` 46 | 47 | You can now SSH into your server via HTTP. 48 | 49 | ### Multiple client connections 50 | 51 | The client does support multiple connections, just make use of the `-l` flag. 52 | 53 | ### Behind NGINX reverse proxy 54 | 55 | This is actually the standard way to use HTTP2TCP. 56 | 57 | ```nginx 58 | server { 59 | server_name example.com; 60 | listen 443 ssl http2; 61 | location = /some-path/ { 62 | proxy_http_version 1.1; 63 | proxy_set_header Host $host; 64 | proxy_set_header Upgrade $http_upgrade; 65 | proxy_set_header Connection "Upgrade"; 66 | proxy_read_timeout 600s; 67 | proxy_pass http://localhost:2222/; 68 | } 69 | } 70 | ``` 71 | 72 | Now the `ProxyCommand` should be: 73 | 74 | ```ssh_config 75 | ProxyCommand http2tcp -c -d localhost:22 -e https://example.com/some-path/ -t $TOKEN 76 | ``` 77 | -------------------------------------------------------------------------------- /all_test.go: -------------------------------------------------------------------------------- 1 | package http2tcp 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestConn(t *testing.T) { 10 | s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | rw, req, err := Accept(w, r, `123`) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | defer rw.Close() 16 | _ = req 17 | })) 18 | 19 | s.Start() 20 | 21 | conn, err := Dial(s.URL, `123`, ``) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | defer conn.Close() 26 | } 27 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package http2tcp 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | func Dial(server string, token string, userAgent string) (io.ReadWriteCloser, error) { 12 | req, err := http.NewRequest(http.MethodGet, server, nil) 13 | if err != nil { 14 | return nil, err 15 | } 16 | req.Header.Add(`Connection`, `upgrade`) 17 | req.Header.Add(`Upgrade`, httpHeaderUpgrade) 18 | req.Header.Add(`Authorization`, fmt.Sprintf(`%s %s`, authHeaderType, token)) 19 | 20 | if userAgent != `` { 21 | req.Header.Add(`User-Agent`, userAgent) 22 | } 23 | 24 | hc := &http.Client{ 25 | Transport: &http.Transport{ 26 | TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{}, 27 | }, 28 | } 29 | 30 | rsp, err := hc.Do(req) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if rsp.StatusCode != http.StatusSwitchingProtocols { 36 | rsp.Body.Close() 37 | buf := bytes.NewBuffer(nil) 38 | rsp.Write(buf) 39 | return nil, fmt.Errorf("statusCode != 101:\n%s", buf.String()) 40 | } 41 | 42 | // TODO 严格判断 Upgrade 协议头和 Connection。 43 | // https://blog.twofei.com/1485/ 44 | 45 | return rsp.Body.(io.ReadWriteCloser), nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/http2tcp/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "net/url" 8 | "sync" 9 | "time" 10 | 11 | "github.com/movsb/http2tcp" 12 | ) 13 | 14 | type Client struct { 15 | // https://host/path/?query 16 | Server string 17 | Token string 18 | UserAgent string 19 | } 20 | 21 | // If non-empty, when connecting to the server, this User-Agent will be used 22 | // instead of the default `Go-http-client/1.1`. 23 | func (c *Client) SetUserAgent(userAgent string) { 24 | c.UserAgent = userAgent 25 | } 26 | 27 | func (c *Client) Std(to string) { 28 | std := NewStdReadWriteCloser() 29 | c.Conn(std, to) 30 | } 31 | 32 | func (c *Client) Conn(conn io.ReadWriteCloser, to string) { 33 | if err := c.proxy(conn, to); err != nil { 34 | log.Println(err) 35 | } 36 | } 37 | 38 | func (c *Client) Serve(listen string, to string) { 39 | lis, err := net.Listen("tcp", listen) 40 | if err != nil { 41 | log.Fatalln(err) 42 | } 43 | defer lis.Close() 44 | 45 | for { 46 | conn, err := lis.Accept() 47 | if err != nil { 48 | log.Println(err) 49 | time.Sleep(time.Second * 5) 50 | continue 51 | } 52 | go c.Conn(conn, to) 53 | } 54 | } 55 | 56 | func (c *Client) proxy(local io.ReadWriteCloser, addr string) error { 57 | onceCloseLocal := &OnceCloser{Closer: local} 58 | defer onceCloseLocal.Close() 59 | 60 | u, err := url.Parse(c.Server) 61 | if err != nil { 62 | log.Println(err) 63 | return err 64 | } 65 | 66 | a := u.Query() 67 | a.Set(`addr`, addr) 68 | u.RawQuery = a.Encode() 69 | 70 | remote, err := http2tcp.Dial(u.String(), c.Token, c.UserAgent) 71 | if err != nil { 72 | log.Println(err) 73 | return err 74 | } 75 | 76 | onceCloseRemote := &OnceCloser{Closer: remote} 77 | defer onceCloseRemote.Close() 78 | 79 | wg := &sync.WaitGroup{} 80 | wg.Add(2) 81 | 82 | go func() { 83 | defer wg.Done() 84 | 85 | defer onceCloseRemote.Close() 86 | _, _ = io.Copy(remote, local) 87 | }() 88 | 89 | go func() { 90 | defer wg.Done() 91 | 92 | defer onceCloseLocal.Close() 93 | _, _ = io.Copy(local, remote) 94 | }() 95 | 96 | wg.Wait() 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /cmd/http2tcp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | 10 | flag "github.com/spf13/pflag" 11 | ) 12 | 13 | func main() { 14 | log.SetFlags(log.LstdFlags | log.Lshortfile) 15 | 16 | runAsServer := flag.BoolP(`server`, `s`, false, `Run as server. [S]`) 17 | runAsClient := flag.BoolP(`client`, `c`, false, `Run as client. [C]`) 18 | 19 | serverEndpoint := flag.StringP(`endpoint`, `e`, ``, `Server endpoint. [C]`) 20 | token := flag.StringP(`token`, `t`, ``, `The token used between client and server [SC]`) 21 | userAgent := flag.String(`user-agent`, ``, `Use this User-Agent instead of the default Go-http-client/1.1 [C]`) 22 | 23 | listenAddr := flag.StringP(`listen`, `l`, ``, `Listen address [SC]`) 24 | destination := flag.StringP(`destination`, `d`, ``, `The destination address to connect to [C]`) 25 | 26 | help := flag.BoolP(`help`, `h`, false, `Show this help`) 27 | 28 | flag.CommandLine.SortFlags = false 29 | flag.Usage = func() { 30 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", filepath.Base(os.Args[0])) 31 | flag.PrintDefaults() 32 | fmt.Fprintln(os.Stderr) 33 | fmt.Fprintln(os.Stderr, "[S]: server side flag.\n[C]: client side flag.") 34 | os.Exit(1) 35 | } 36 | flag.Parse() 37 | 38 | if !*runAsServer && !*runAsClient || *help { 39 | flag.Usage() 40 | return 41 | } 42 | if *runAsServer { 43 | s := &Server{ 44 | Token: *token, 45 | } 46 | if err := http.ListenAndServe(*listenAddr, s); err != nil { 47 | log.Fatalln(err) 48 | } 49 | return 50 | } 51 | if *runAsClient { 52 | c := &Client{ 53 | Server: *serverEndpoint, 54 | Token: *token, 55 | } 56 | c.SetUserAgent(*userAgent) 57 | if *listenAddr != `` { 58 | c.Serve(*listenAddr, *destination) 59 | } else { 60 | c.Std(*destination) 61 | } 62 | return 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cmd/http2tcp/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "net/http" 8 | "sync" 9 | 10 | "github.com/movsb/http2tcp" 11 | ) 12 | 13 | type Server struct { 14 | Token string 15 | } 16 | 17 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | conn, req, err := http2tcp.Accept(w, r, s.Token) 19 | if err != nil { 20 | log.Println(err) 21 | return 22 | } 23 | s.serve(conn, req) 24 | } 25 | 26 | func (s *Server) serve(conn io.ReadWriteCloser, req *http.Request) { 27 | onceCloseLocal := &OnceCloser{Closer: conn} 28 | defer onceCloseLocal.Close() 29 | 30 | // the URL.Path doesn't matter. 31 | addr := req.URL.Query().Get("addr") 32 | remote, err := net.Dial(`tcp`, addr) 33 | if err != nil { 34 | log.Println(err) 35 | return 36 | } 37 | onceCloseRemote := &OnceCloser{Closer: remote} 38 | defer onceCloseRemote.Close() 39 | 40 | wg := &sync.WaitGroup{} 41 | wg.Add(2) 42 | 43 | go func() { 44 | defer wg.Done() 45 | 46 | defer onceCloseRemote.Close() 47 | _, _ = io.Copy(remote, conn) 48 | }() 49 | 50 | go func() { 51 | defer wg.Done() 52 | 53 | defer onceCloseLocal.Close() 54 | _, _ = io.Copy(conn, remote) 55 | }() 56 | 57 | wg.Wait() 58 | } 59 | -------------------------------------------------------------------------------- /cmd/http2tcp/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | type OnceCloser struct { 10 | io.Closer 11 | once sync.Once 12 | } 13 | 14 | func (c *OnceCloser) Close() (err error) { 15 | c.once.Do(func() { 16 | err = c.Closer.Close() 17 | }) 18 | return 19 | } 20 | 21 | type StdReadWriteCloser struct { 22 | io.ReadCloser 23 | io.WriteCloser 24 | } 25 | 26 | func NewStdReadWriteCloser() *StdReadWriteCloser { 27 | return &StdReadWriteCloser{ 28 | ReadCloser: os.Stdin, 29 | WriteCloser: os.Stdout, 30 | } 31 | } 32 | 33 | func (c *StdReadWriteCloser) Close() error { 34 | err1 := c.ReadCloser.Close() 35 | err2 := c.WriteCloser.Close() 36 | 37 | if err1 != nil { 38 | return err1 39 | } 40 | if err2 != nil { 41 | return err2 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/movsb/http2tcp 2 | 3 | go 1.13 4 | 5 | require github.com/spf13/pflag v1.0.5 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 2 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 3 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package http2tcp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | authHeaderType = `HTTP2TCP` 12 | httpHeaderUpgrade = `http2tcp/1.0` 13 | ) 14 | 15 | func auth(r *http.Request, token string) bool { 16 | a := strings.Fields(r.Header.Get("Authorization")) 17 | if len(a) == 2 && a[0] == authHeaderType && a[1] == token { 18 | return true 19 | } 20 | return false 21 | } 22 | 23 | // type BeforeAccept func(r *http.Request) error 24 | 25 | func Accept(w http.ResponseWriter, r *http.Request, token string) (io.ReadWriteCloser, *http.Request, error) { 26 | if !auth(r, token) { 27 | w.WriteHeader(401) 28 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 29 | return nil, r, fmt.Errorf(`accept: unauthorized`) 30 | } 31 | 32 | if upgrade := r.Header.Get(`Upgrade`); upgrade != httpHeaderUpgrade { 33 | http.Error(w, `upgrade error`, http.StatusBadRequest) 34 | return nil, r, fmt.Errorf(`upgrade error`) 35 | } 36 | 37 | w.Header().Add(`Content-Length`, `0`) 38 | w.Header().Add(`Connection`, `Upgrade`) 39 | w.Header().Add(`Upgrade`, httpHeaderUpgrade) 40 | w.WriteHeader(http.StatusSwitchingProtocols) 41 | local, bio, err := w.(http.Hijacker).Hijack() 42 | if err != nil { 43 | http.Error(w, err.Error(), http.StatusInternalServerError) 44 | return nil, r, fmt.Errorf(`error hijacking`) 45 | } 46 | 47 | return &_ReadWriteCloser{ 48 | Reader: bio, 49 | Writer: local, 50 | Closer: local, 51 | }, r, nil 52 | } 53 | 54 | type _ReadWriteCloser struct { 55 | io.Reader 56 | io.Writer 57 | io.Closer 58 | } 59 | --------------------------------------------------------------------------------