├── LICENSE.md ├── README.md ├── client ├── config.go ├── connection_wrapper.go ├── connector.go ├── dialer.go ├── forwarder │ └── forwarder.go ├── multiplexed_connection.go ├── socks5-server │ └── server.go └── tun │ ├── netstack_hacks │ └── udp_unsafe.go │ ├── tun_fallback.go │ └── tun_linux.go ├── cmd ├── tcp_over_http │ ├── direct_dial_middleware.go │ ├── main.go │ └── rlimit.go └── tcp_over_http_server │ └── main.go ├── common └── types.go ├── go.mod ├── go.sum ├── protocol ├── netutils.go ├── packet_connection.go └── types.go └── server ├── config.go ├── http_server.go ├── multiplexed_server.go ├── redirector.go └── utils.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tcp-over-http 2 | 3 | This program is just a proxy server which multiplexes TCP connections into an HTTPS. The primary purpose is to make all connections look like legitimate ones for firewalls (including DPI). 4 | 5 | The closest analog is shadowsocks. 6 | 7 | The only reason why I wrote this is that I want to have better control on how the connections are masked/multiplexed. 8 | 9 | #### Current status 10 | 11 | This software is unstable and non-unique. The code is broken in many places and follows the worst coding practices. It is not recommended to use this piece. 12 | 13 | ### Setup 14 | 15 | First, clone the repo and build all binaries (`go install ./...`). You may install it with `go get github.com/neex/tcp-over-http` as well, but the idea that you won't need source modifications is way too optimistic. 16 | 17 | #### Server (do this before your trip) 18 | 19 | 1. Buy a domain name (e.g. `example.com`). 20 | 2. Rent a VPS. Make sure not to use any popular VPS provider (e.g. DigitalOcean/Vultr/Scaleway) as they're banned in some countries almost completely. 21 | 3. Set up DNS A record pointing to the VPS. 22 | 4. Get a LetsEncrypt certificate using certbot (`apt install certbot && certbot -d example.com`). 23 | 5. Get something legitimate-looking to place on your website, like free bootstrap template. 24 | 6. Create a config for the server part in `/etc/tcp-over-http.yaml`, it should look like this: 25 | ```yaml 26 | listen_addr: ':443' 27 | static_dir: /var/www/html/ 28 | dial_timeout: 2m 29 | token: 30 | domain: 31 | redirector_addr: ':80' 32 | cert_path: /etc/letsencrypt/live//fullchain.pem 33 | key_path: /etc/letsencrypt/live//privkey.pem 34 | ``` 35 | 7. Create systemd module in `/etc/systemd/system/tcp-over-http.service`: 36 | ```yaml 37 | [Unit] 38 | Description=TCP over HTTP 39 | 40 | [Service] 41 | Type=simple 42 | Restart=always 43 | ExecStart=/usr/local/bin/tcp_over_http_server /etc/tcp-over-http.yaml 44 | LimitNOFILE=100000 45 | 46 | [Install] 47 | WantedBy=multi-user.target 48 | ``` 49 | 8. Do `systemctl daemon-reload && systemctl start tcp-over-http`. 50 | 9. Debug errors in journalctl, if any. 51 | 52 | #### Client (test this before your trip) 53 | 54 | 1. Create a client config, should like like: 55 | 56 | ```yaml 57 | address: "https:///establish/" 58 | dns_override: :443 59 | remote_timeout: 30s 60 | connect_timeout: 10s 61 | max_connection_multiplex: 1000 62 | keep_alive_timeout: 10s 63 | ``` 64 | 2. Start the client using something like 65 | ```bash 66 | tcp_over_http --config ./client.yaml proxy :12321 --direct-dial '127.0.0.1|localhost' 67 | ``` 68 | 69 | This command starts socks5 server on :12321, which runs through the tunnel. 70 | 71 | 3. Under linux, you can setup an interface that proxies the connections. Do it like this: 72 | ```bash 73 | sudo ip tuntap add user mode tun hui0 74 | sudo ip link set hui0 up 75 | ``` 76 | 77 | After that, run `tcp-over-http` with `--tun hui0` flag. No server reconfiguration is required. 78 | 79 | Note that this is not an actual VPN. The connections are intercepted and proxies as TCP/UDP streams. 80 | 81 | Also, you will need to set up routes correctly. 82 | 83 | ### License 84 | 85 | This software is distributed under the terms of [MIT License](LICENSE.md). 86 | -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | type Config struct { 11 | Address string `yaml:"address"` 12 | DNSOverride string `yaml:"dns_override"` 13 | RemoteTimeout time.Duration `yaml:"remote_timeout"` 14 | ConnectTimeout time.Duration `yaml:"connect_timeout"` 15 | KeepAliveTimeout time.Duration `yaml:"keep_alive_timeout"` 16 | MaxConnectionMultiplex int `yaml:"max_connection_multiplex"` 17 | } 18 | 19 | func NewConfigFromFile(filename string) (*Config, error) { 20 | f, err := os.Open(filename) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer func() { _ = f.Close() }() 25 | dec := yaml.NewDecoder(f) 26 | cfg := &Config{} 27 | if err := dec.Decode(cfg); err != nil { 28 | return nil, err 29 | } 30 | 31 | return cfg, nil 32 | } 33 | -------------------------------------------------------------------------------- /client/connection_wrapper.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "io/ioutil" 7 | "net" 8 | "sync" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/neex/tcp-over-http/protocol" 13 | ) 14 | 15 | type connectionWrapper struct { 16 | responseOnce sync.Once 17 | disconnectOnce sync.Once 18 | onDisconnect func() 19 | logger *log.Entry 20 | 21 | net.Conn 22 | } 23 | 24 | func (cw *connectionWrapper) Read(b []byte) (n int, err error) { 25 | cw.responseOnce.Do(cw.ensureResponse) 26 | return cw.Conn.Read(b) 27 | } 28 | 29 | func (cw *connectionWrapper) Close() (err error) { 30 | err = cw.Conn.Close() 31 | cw.disconnectOnce.Do(func() { 32 | cw.logger.Info("disconnected") 33 | 34 | if cw.onDisconnect != nil { 35 | cw.onDisconnect() 36 | } 37 | 38 | }) 39 | return 40 | } 41 | 42 | func (cw *connectionWrapper) ensureResponse() { 43 | cw.logger.Trace("reading initial response") 44 | resp, err := protocol.ReadResponse(context.TODO(), cw.Conn) 45 | if err != nil || resp.Err != nil { 46 | if err != nil { 47 | cw.logger.WithError(err).Warn("error while dialing") 48 | } else { 49 | cw.logger.WithError(err).Error("error while dialing") 50 | } 51 | 52 | _ = cw.Conn.Close() 53 | _, _ = io.Copy(ioutil.Discard, cw.Conn) 54 | return 55 | } 56 | 57 | cw.logger.Debug("remote end connected") 58 | } 59 | -------------------------------------------------------------------------------- /client/connector.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type Connector struct { 13 | Config *Config 14 | } 15 | 16 | func (c *Connector) Connect(logger *log.Entry) (*MultiplexedConnection, error) { 17 | parsed, err := url.Parse(c.Config.Address) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | host := c.Config.DNSOverride 23 | 24 | if host == "" { 25 | host = parsed.Host 26 | 27 | if _, _, err := net.SplitHostPort(host); err != nil { 28 | host = net.JoinHostPort(host, parsed.Scheme) 29 | } 30 | } 31 | 32 | d := &net.Dialer{ 33 | Timeout: c.Config.ConnectTimeout, 34 | } 35 | logger.Info("connecting to upstream") 36 | conn, err := d.Dial("tcp", host) 37 | if err != nil { 38 | logger.WithError(err).Error("while connecting to upstream") 39 | return nil, err 40 | } 41 | 42 | if parsed.Scheme == "https" { 43 | conn = tls.Client(conn, &tls.Config{ 44 | NextProtos: []string{"http/1.1"}, 45 | ServerName: parsed.Host, 46 | }) 47 | } 48 | 49 | req, err := http.NewRequest("GET", c.Config.Address, nil) 50 | if err != nil { 51 | _ = conn.Close() 52 | return nil, err 53 | } 54 | 55 | req.Header.Set("user-agent", "") 56 | if err := req.Write(conn); err != nil { 57 | _ = conn.Close() 58 | logger.WithError(err).Error("error while writing initial http request") 59 | return nil, err 60 | } 61 | 62 | logger.Debug("lazy upstream connect successful") 63 | 64 | cw := &connectionWrapper{ 65 | Conn: conn, 66 | logger: logger, 67 | } 68 | 69 | connCfg := &MultiplexedConnectionConfig{ 70 | MaxMultiplexedConnections: c.Config.MaxConnectionMultiplex, 71 | RemoteDialTimeout: c.Config.ConnectTimeout, 72 | KeepAliveTimeout: c.Config.KeepAliveTimeout, 73 | Logger: logger, 74 | } 75 | 76 | return NewMultiplexedConnection(cw, connCfg) 77 | } 78 | -------------------------------------------------------------------------------- /client/dialer.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "math/rand" 7 | "net" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/neex/tcp-over-http/protocol" 15 | ) 16 | 17 | func init() { 18 | rand.Seed(time.Now().UnixNano()) 19 | } 20 | 21 | type Dialer struct { 22 | Connector *Connector 23 | PreconnectPoolSize int 24 | 25 | m sync.Mutex 26 | closed bool 27 | connPool []*MultiplexedConnection 28 | 29 | lastID uint64 30 | 31 | preconnectOnce sync.Once 32 | prevPoolSize int 33 | } 34 | 35 | func (d *Dialer) Close() { 36 | d.m.Lock() 37 | defer d.m.Unlock() 38 | for _, mc := range d.connPool { 39 | mc.Close() 40 | } 41 | d.closed = true 42 | } 43 | 44 | func (d *Dialer) Closed() bool { 45 | d.m.Lock() 46 | defer d.m.Unlock() 47 | 48 | return d.closed 49 | } 50 | 51 | func (d *Dialer) EnablePreconnect() { 52 | d.preconnectOnce.Do(func() { 53 | go func() { 54 | if d.PreconnectPoolSize == 0 { 55 | return 56 | } 57 | d.prevPoolSize = d.PreconnectPoolSize 58 | 59 | for { 60 | if d.Closed() { 61 | d.Close() 62 | return 63 | } 64 | 65 | if !d.refillPreconnectPool() { 66 | time.Sleep(time.Second) 67 | } 68 | } 69 | }() 70 | }) 71 | } 72 | 73 | func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 74 | maybeWrap := func(c net.Conn) net.Conn { 75 | if c != nil && (network == "udp" || network == "udp4" || network == "udp6") { 76 | return protocol.NewPacketConnection(c) 77 | } 78 | return c 79 | } 80 | if c := d.takeFromPool(); c != nil { 81 | if conn, err := d.dialVia(ctx, c, network, address); err == nil { 82 | return maybeWrap(conn), err 83 | } 84 | } 85 | 86 | mc, err := d.makeConn() 87 | 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | conn, err := d.dialVia(ctx, mc, network, address) 93 | return maybeWrap(conn), err 94 | } 95 | 96 | func (d *Dialer) Ping() (time.Duration, error) { 97 | if c := d.takeFromPool(); c != nil { 98 | defer func() { 99 | if c.IsDialable() { 100 | d.putToPool(c) 101 | } 102 | }() 103 | return c.Ping() 104 | } 105 | return 0, errors.New("no active connections") 106 | } 107 | 108 | func (d *Dialer) dialVia(ctx context.Context, c *MultiplexedConnection, network, address string) (net.Conn, error) { 109 | conn, err := c.DialContext(ctx, network, address) 110 | if err != nil { 111 | c.Close() 112 | return nil, err 113 | } 114 | 115 | if c.IsDialable() { 116 | d.putToPool(c) 117 | } 118 | 119 | return conn, err 120 | } 121 | 122 | func (d *Dialer) refillPreconnectPool() bool { 123 | d.m.Lock() 124 | cnt := 0 125 | for i := range d.connPool { 126 | if d.connPool[i].IsDialable() { 127 | d.connPool[cnt] = d.connPool[i] 128 | cnt++ 129 | } 130 | } 131 | d.connPool = d.connPool[:cnt] 132 | d.m.Unlock() 133 | 134 | max := cnt 135 | if cnt < d.prevPoolSize { 136 | max = d.prevPoolSize 137 | } 138 | logger := log.WithField("seen_pool_size", max) 139 | 140 | if max >= d.PreconnectPoolSize { 141 | d.prevPoolSize = cnt 142 | logger.Trace("preconnect pool full") 143 | return false 144 | } 145 | 146 | logger.Info("preconnect triggered") 147 | 148 | conn, err := d.makeConn() 149 | if err != nil { 150 | log.WithError(err).Error("filling upstream connection pool") 151 | return false 152 | } 153 | 154 | d.putToPool(conn) 155 | return true 156 | } 157 | 158 | func (d *Dialer) makeConn() (*MultiplexedConnection, error) { 159 | connID := atomic.AddUint64(&d.lastID, 1) 160 | return d.Connector.Connect(log.WithField("upstream_conn", connID)) 161 | } 162 | 163 | func (d *Dialer) takeFromPool() *MultiplexedConnection { 164 | d.m.Lock() 165 | defer d.m.Unlock() 166 | 167 | for { 168 | n := len(d.connPool) 169 | if n == 0 { 170 | return nil 171 | } 172 | 173 | r := rand.Intn(n) 174 | c := d.connPool[r] 175 | d.connPool[r] = d.connPool[n-1] 176 | d.connPool = d.connPool[:n-1] 177 | 178 | if c.IsDialable() { 179 | return c 180 | } 181 | } 182 | } 183 | 184 | func (d *Dialer) putToPool(c *MultiplexedConnection) { 185 | d.m.Lock() 186 | defer d.m.Unlock() 187 | 188 | d.connPool = append(d.connPool, c) 189 | } 190 | -------------------------------------------------------------------------------- /client/forwarder/forwarder.go: -------------------------------------------------------------------------------- 1 | package forwarder 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/neex/tcp-over-http/common" 11 | ) 12 | 13 | type Forwarder struct { 14 | Dial common.DialContextFunc 15 | DialTimeout time.Duration 16 | } 17 | 18 | type ForwardRequest struct { 19 | ClientConn net.Conn 20 | Network, Address string 21 | OnConnected func() 22 | } 23 | 24 | func (f *Forwarder) ForwardConnection(ctx context.Context, r *ForwardRequest) error { 25 | newCtx, cancel := context.WithCancel(ctx) 26 | defer cancel() 27 | go func() { 28 | <-newCtx.Done() 29 | _ = r.ClientConn.Close() 30 | }() 31 | 32 | dialCtx, dialCtxCancel := context.WithTimeout(newCtx, f.DialTimeout) 33 | upstream, err := f.Dial(dialCtx, r.Network, r.Address) 34 | dialCtxCancel() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | go func() { 40 | <-newCtx.Done() 41 | _ = upstream.Close() 42 | }() 43 | 44 | if r.OnConnected != nil { 45 | r.OnConnected() 46 | } 47 | 48 | var wg sync.WaitGroup 49 | wg.Add(2) 50 | go func() { 51 | defer wg.Done() 52 | defer cancel() 53 | packetCopy(upstream, r.ClientConn, 65536) 54 | }() 55 | 56 | go func() { 57 | defer wg.Done() 58 | defer cancel() 59 | packetCopy(r.ClientConn, upstream, 65536) 60 | }() 61 | 62 | wg.Wait() 63 | return nil 64 | } 65 | 66 | // packetCopy copies streams without quirks used by io.Copy. That is 67 | // useful for connection where packet borders are important (e.g. udp). 68 | func packetCopy(dst io.WriteCloser, src io.Reader, bufsize int) { 69 | defer func() { _ = dst.Close() }() 70 | buf := make([]byte, bufsize) 71 | for { 72 | n, _ := src.Read(buf) 73 | if n == 0 { 74 | break 75 | } 76 | if _, err := dst.Write(buf[:n]); err != nil { 77 | break 78 | } 79 | if n == len(buf) { 80 | buf = make([]byte, len(buf)*2) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /client/multiplexed_connection.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/hashicorp/yamux" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/neex/tcp-over-http/protocol" 15 | ) 16 | 17 | var ErrLimitExceeded = errors.New("connection limit exceeded") 18 | 19 | type MultiplexedConnection struct { 20 | config *MultiplexedConnectionConfig 21 | session *yamux.Session 22 | 23 | m sync.Mutex 24 | dialable, closed bool 25 | cntActive, cntUsed int 26 | } 27 | 28 | type MultiplexedConnectionConfig struct { 29 | MaxMultiplexedConnections int 30 | RemoteDialTimeout time.Duration 31 | KeepAliveTimeout time.Duration 32 | Logger *log.Entry 33 | } 34 | 35 | func NewMultiplexedConnection(conn net.Conn, config *MultiplexedConnectionConfig) (*MultiplexedConnection, error) { 36 | yamuxConfig := *yamux.DefaultConfig() 37 | yamuxConfig.LogOutput = config.Logger.WriterLevel(log.ErrorLevel) 38 | if config.KeepAliveTimeout != 0 { 39 | yamuxConfig.KeepAliveInterval = config.KeepAliveTimeout 40 | yamuxConfig.ConnectionWriteTimeout = config.KeepAliveTimeout 41 | } 42 | 43 | session, err := yamux.Client(conn, &yamuxConfig) 44 | if err != nil { 45 | config.Logger.WithError(err).Fatal("yamux.Client() failed") 46 | return nil, err 47 | } 48 | 49 | mc := &MultiplexedConnection{ 50 | config: config, 51 | session: session, 52 | dialable: true, 53 | } 54 | 55 | return mc, nil 56 | } 57 | 58 | func (c *MultiplexedConnection) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 59 | subConnID := c.registerConnect() 60 | if subConnID == 0 { 61 | return nil, ErrLimitExceeded 62 | } 63 | 64 | subConnIDStr := fmt.Sprintf("%v", subConnID) 65 | max := c.config.MaxMultiplexedConnections 66 | if max > 0 { 67 | subConnIDStr = fmt.Sprintf("%s/%v", subConnIDStr, max) 68 | } 69 | 70 | logger := c.config.Logger.WithFields(log.Fields{ 71 | "subconn": subConnIDStr, 72 | "remote": fmt.Sprintf("%s", address), 73 | }) 74 | 75 | logger.Info("connecting") 76 | conn, err := c.session.Open() 77 | if err != nil { 78 | logger.WithError(err).Error("error in session.Open") 79 | c.registerDisconnect() 80 | return nil, err 81 | } 82 | 83 | req := protocol.ConnectionRequest{ 84 | Network: network, 85 | Address: address, 86 | Timeout: c.config.RemoteDialTimeout, 87 | } 88 | 89 | if err := protocol.WritePacket(ctx, conn, &req); err != nil { 90 | logger.WithError(err).Error("error while writing connection request") 91 | c.registerDisconnect() 92 | return nil, err 93 | } 94 | 95 | logger.Debug("lazy connect successful") 96 | 97 | return &connectionWrapper{ 98 | Conn: conn, 99 | onDisconnect: c.registerDisconnect, 100 | logger: logger, 101 | }, nil 102 | } 103 | 104 | func (c *MultiplexedConnection) Close() { 105 | c.m.Lock() 106 | defer c.m.Unlock() 107 | c.dialable = false 108 | c.checkClose() 109 | } 110 | 111 | func (c *MultiplexedConnection) IsDialable() bool { 112 | c.m.Lock() 113 | defer c.m.Unlock() 114 | 115 | if c.session.IsClosed() { 116 | c.dialable = false 117 | c.checkClose() 118 | } 119 | 120 | if !c.dialable { 121 | return false 122 | } 123 | 124 | return true 125 | } 126 | 127 | func (c *MultiplexedConnection) Ping() (time.Duration, error) { 128 | return c.session.Ping() 129 | } 130 | 131 | func (c *MultiplexedConnection) registerConnect() int { 132 | c.m.Lock() 133 | defer c.m.Unlock() 134 | 135 | if !c.dialable { 136 | return 0 137 | } 138 | 139 | max := c.config.MaxMultiplexedConnections 140 | 141 | c.cntUsed++ 142 | c.cntActive++ 143 | if max > 0 && c.cntUsed >= max { 144 | c.dialable = false 145 | } 146 | return c.cntUsed 147 | } 148 | 149 | func (c *MultiplexedConnection) registerDisconnect() { 150 | c.m.Lock() 151 | defer c.m.Unlock() 152 | c.cntActive-- 153 | c.checkClose() 154 | } 155 | 156 | func (c *MultiplexedConnection) checkClose() { 157 | needClose := c.cntActive == 0 && !c.dialable && !c.closed 158 | if needClose { 159 | go func() { 160 | c.config.Logger.Debug("closing session") 161 | _ = c.session.Close() 162 | }() 163 | c.closed = true 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /client/socks5-server/server.go: -------------------------------------------------------------------------------- 1 | package socks5_server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strconv" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/neex/tcp-over-http/client/forwarder" 14 | ) 15 | 16 | type Socks5Server struct { 17 | Forwarder *forwarder.Forwarder 18 | } 19 | 20 | func (p *Socks5Server) ListenAndServe(ctx context.Context, addr string) error { 21 | newCtx, cancel := context.WithCancel(ctx) 22 | defer cancel() 23 | 24 | lc := &net.ListenConfig{} 25 | lsn, err := lc.Listen(newCtx, "tcp", addr) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | go func() { 31 | <-newCtx.Done() 32 | _ = lsn.Close() 33 | }() 34 | 35 | log.Info("socks5 server started") 36 | 37 | for { 38 | conn, err := lsn.Accept() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | go func() { 44 | l := log.WithField("remote_addr", conn.RemoteAddr()) 45 | err := p.handleConn(newCtx, conn) 46 | if err != nil { 47 | l.WithError(err).Warn("socks5 client handle error") 48 | } else { 49 | l.Debug("socks5 client handling finished") 50 | } 51 | }() 52 | } 53 | } 54 | 55 | func (p *Socks5Server) handleConn(ctx context.Context, conn net.Conn) error { 56 | newCtx, cancel := context.WithCancel(ctx) 57 | defer cancel() 58 | go func() { 59 | <-newCtx.Done() 60 | _ = conn.Close() 61 | }() 62 | 63 | buf := make([]byte, 1024) 64 | if n, err := io.ReadFull(conn, buf[:2]); n != 2 || err != nil { 65 | return fmt.Errorf("read short during first msg, %v", err) 66 | } 67 | 68 | if buf[0] != 5 { 69 | return fmt.Errorf("wrong first byte, %v", buf[0]) 70 | } 71 | 72 | cntAuth := int(buf[1]) 73 | if n, err := io.ReadFull(conn, buf[:cntAuth]); n != cntAuth || err != nil { 74 | return fmt.Errorf("read short during reading auth methods, %v", err) 75 | } 76 | 77 | var auth byte = 0xff 78 | 79 | for i := 0; i < cntAuth; i++ { 80 | if buf[i] == 0 { 81 | auth = 0 82 | } 83 | } 84 | 85 | buf[0] = 0x5 86 | buf[1] = auth 87 | 88 | if n, err := conn.Write(buf[:2]); n != 2 || err != nil { 89 | return fmt.Errorf("invalid connection attempt: write short during hello, %v", err) 90 | } 91 | 92 | if auth != 0 { 93 | return errors.New("auth not found") 94 | } 95 | 96 | if n, err := io.ReadFull(conn, buf[:4]); n != 4 || err != nil { 97 | return fmt.Errorf("read short during request, %v", err) 98 | } 99 | 100 | resp := make([]byte, 10) 101 | resp[0] = 5 102 | resp[3] = 1 103 | 104 | if buf[0] != 5 || buf[1] != 1 || buf[2] != 0 { 105 | resp[1] = 7 106 | _, _ = conn.Write(resp) 107 | return fmt.Errorf("invalid request, %v", buf[:4]) 108 | } 109 | 110 | var host string 111 | switch buf[3] { 112 | case 1: 113 | if n, err := io.ReadFull(conn, buf[:4]); n != 4 || err != nil { 114 | return fmt.Errorf("read short during ipv4 read, %v", err) 115 | } 116 | host = net.IP(buf[:4]).String() 117 | 118 | case 3: 119 | if n, err := io.ReadFull(conn, buf[:1]); n != 1 || err != nil { 120 | return fmt.Errorf("read short during hostname len read, %v", err) 121 | } 122 | l := int(buf[0]) 123 | if n, err := io.ReadFull(conn, buf[:l]); n != l || err != nil { 124 | return fmt.Errorf("read short during hostname read, %v", err) 125 | } 126 | host = string(buf[:l]) 127 | 128 | case 4: 129 | if n, err := io.ReadFull(conn, buf[:16]); n != 16 || err != nil { 130 | return fmt.Errorf("read short during ipv6 read, %v", err) 131 | } 132 | host = net.IP(buf[:16]).String() 133 | 134 | default: 135 | resp[1] = 7 136 | _, _ = conn.Write(resp) 137 | return fmt.Errorf("invalid request, %v", buf[:4]) 138 | } 139 | 140 | if n, err := io.ReadFull(conn, buf[:2]); n != 2 || err != nil { 141 | return fmt.Errorf("read short during port read, %v", err) 142 | } 143 | 144 | port := strconv.Itoa(int(buf[0])*256 + int(buf[1])) 145 | address := net.JoinHostPort(host, port) 146 | 147 | err := p.Forwarder.ForwardConnection(ctx, &forwarder.ForwardRequest{ 148 | ClientConn: conn, 149 | Network: "tcp", 150 | Address: address, 151 | OnConnected: func() { 152 | _, _ = conn.Write(resp) 153 | }, 154 | }) 155 | 156 | if err != nil { 157 | resp[1] = 4 158 | _, _ = conn.Write(resp) 159 | return fmt.Errorf("host unreachable, %v", err) 160 | } 161 | 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /client/tun/netstack_hacks/udp_unsafe.go: -------------------------------------------------------------------------------- 1 | package netstack_hacks 2 | 3 | import ( 4 | "reflect" 5 | "unsafe" 6 | 7 | "github.com/google/netstack/tcpip" 8 | "github.com/google/netstack/tcpip/adapters/gonet" 9 | "github.com/google/netstack/tcpip/stack" 10 | "github.com/google/netstack/tcpip/transport/udp" 11 | "github.com/google/netstack/waiter" 12 | ) 13 | 14 | func CreateUDPEndpoint(wq *waiter.Queue, r *udp.ForwarderRequest) (tcpip.Endpoint, *tcpip.Error) { 15 | rVal := reflect.Indirect(reflect.ValueOf(r)) 16 | routePtr := (**stack.Route)(unsafe.Pointer(rVal.FieldByName("route").UnsafeAddr())) 17 | proto := (*routePtr).NetProto 18 | 19 | ep, err := r.CreateEndpoint(wq) 20 | if err != nil { 21 | return ep, err 22 | } 23 | 24 | epVal := reflect.Indirect(reflect.ValueOf(ep)) 25 | type protos = []tcpip.NetworkProtocolNumber 26 | protosPtr := (*protos)(unsafe.Pointer(epVal.FieldByName("effectiveNetProtos").UnsafeAddr())) 27 | *protosPtr = protos{proto} 28 | return ep, err 29 | } 30 | 31 | func CreatePacketConn(s *stack.Stack, ep tcpip.Endpoint, wq *waiter.Queue) *gonet.PacketConn { 32 | c := &gonet.PacketConn{} 33 | cVal := reflect.Indirect(reflect.ValueOf(c)) 34 | 35 | stackPtr := (**stack.Stack)(unsafe.Pointer(cVal.FieldByName("stack").UnsafeAddr())) 36 | *stackPtr = s 37 | 38 | epPtr := (*tcpip.Endpoint)(unsafe.Pointer(cVal.FieldByName("ep").UnsafeAddr())) 39 | *epPtr = ep 40 | 41 | wqPtr := (**waiter.Queue)(unsafe.Pointer(cVal.FieldByName("wq").UnsafeAddr())) 42 | *wqPtr = wq 43 | 44 | timerVal := cVal.FieldByName("deadlineTimer") 45 | readCancelChPtr := (*chan struct{})(unsafe.Pointer(timerVal.FieldByName("readCancelCh").UnsafeAddr())) 46 | *readCancelChPtr = make(chan struct{}) 47 | 48 | writeCancelChPtr := (*chan struct{})(unsafe.Pointer(timerVal.FieldByName("writeCancelCh").UnsafeAddr())) 49 | *writeCancelChPtr = make(chan struct{}) 50 | return c 51 | } 52 | -------------------------------------------------------------------------------- /client/tun/tun_fallback.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package tun 4 | 5 | import ( 6 | "errors" 7 | 8 | "github.com/neex/tcp-over-http/client/forwarder" 9 | ) 10 | 11 | func ForwardTransportFromTUN(tunName string, f *forwarder.Forwarder) error { 12 | return errors.New("not implemented") 13 | } 14 | -------------------------------------------------------------------------------- /client/tun/tun_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package tun 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "math/rand" 9 | "net" 10 | "time" 11 | 12 | "github.com/google/netstack/tcpip" 13 | "github.com/google/netstack/tcpip/adapters/gonet" 14 | "github.com/google/netstack/tcpip/link/fdbased" 15 | "github.com/google/netstack/tcpip/link/rawfile" 16 | "github.com/google/netstack/tcpip/link/tun" 17 | "github.com/google/netstack/tcpip/network/arp" 18 | "github.com/google/netstack/tcpip/network/ipv4" 19 | "github.com/google/netstack/tcpip/network/ipv6" 20 | "github.com/google/netstack/tcpip/stack" 21 | "github.com/google/netstack/tcpip/transport/tcp" 22 | "github.com/google/netstack/tcpip/transport/udp" 23 | "github.com/google/netstack/waiter" 24 | log "github.com/sirupsen/logrus" 25 | 26 | "github.com/neex/tcp-over-http/client/forwarder" 27 | "github.com/neex/tcp-over-http/client/tun/netstack_hacks" 28 | ) 29 | 30 | func ForwardTransportFromTUN(tunName string, f *forwarder.Forwarder) error { 31 | macAddr, err := net.ParseMAC("de:ad:be:ee:ee:ef") 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | s := stack.New( 37 | []string{ipv4.ProtocolName, ipv6.ProtocolName, arp.ProtocolName}, 38 | []string{tcp.ProtocolName, udp.ProtocolName}, 39 | stack.Options{}) 40 | 41 | mtu, err := rawfile.GetMTU(tunName) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | fd, err := tun.Open(tunName) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | linkID, err := fdbased.New(&fdbased.Options{ 52 | FDs: []int{fd}, 53 | MTU: mtu, 54 | EthernetHeader: false, 55 | Address: tcpip.LinkAddress(macAddr), 56 | }) 57 | 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if err := s.CreateNIC(1, linkID); err != nil { 63 | return errors.New(err.String()) 64 | } 65 | 66 | if err := s.AddAddress(1, arp.ProtocolNumber, arp.ProtocolAddress); err != nil { 67 | return errors.New(err.String()) 68 | } 69 | 70 | s.SetPromiscuousMode(1, true) 71 | 72 | tcpForwarder := tcp.NewForwarder(s, 0, 10, func(r *tcp.ForwarderRequest) { 73 | wq := new(waiter.Queue) 74 | ep, err := r.CreateEndpoint(wq) 75 | if err != nil { 76 | log.WithError(errors.New(err.String())).Error("tcp endpoint not created") 77 | return 78 | } 79 | r.Complete(false) 80 | forwardHelper(gonet.NewConn(wq, ep), "tcp", f) 81 | }) 82 | s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) 83 | 84 | udpForwarder := udp.NewForwarder(s, func(r *udp.ForwarderRequest) { 85 | wq := new(waiter.Queue) 86 | ep, err := netstack_hacks.CreateUDPEndpoint(wq, r) 87 | if err != nil { 88 | log.WithError(errors.New(err.String())).Error("udp endpoint not created") 89 | return 90 | } 91 | go forwardHelper(netstack_hacks.CreatePacketConn(s, ep, wq), "udp", f) 92 | }) 93 | s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) 94 | 95 | return nil 96 | } 97 | 98 | func forwardHelper(conn net.Conn, network string, f *forwarder.Forwarder) { 99 | defer func() { _ = conn.Close() }() 100 | logger := makeLogger(conn).WithField("network", network) 101 | logger.Info("forward from tun") 102 | 103 | err := f.ForwardConnection(context.TODO(), &forwarder.ForwardRequest{ 104 | ClientConn: conn, 105 | Network: network, 106 | Address: conn.LocalAddr().String(), 107 | }) 108 | if err != nil { 109 | logger.WithError(err).Error("forwarder returned error") 110 | } 111 | } 112 | 113 | func makeLogger(conn net.Conn) *log.Entry { 114 | return log.WithField("remote", conn.LocalAddr()). 115 | WithField("local", conn.RemoteAddr()). 116 | WithField("network", conn.LocalAddr().Network()) 117 | } 118 | 119 | func init() { 120 | rand.Seed(time.Now().UnixNano()) 121 | } 122 | -------------------------------------------------------------------------------- /cmd/tcp_over_http/direct_dial_middleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "regexp" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/neex/tcp-over-http/common" 12 | ) 13 | 14 | func DirectDialMiddleware(directHosts *regexp.Regexp, timeout time.Duration, next common.DialContextFunc) common.DialContextFunc { 15 | 16 | directDialer := net.Dialer{Timeout: timeout} 17 | 18 | return func(ctx context.Context, network, address string) (conn net.Conn, e error) { 19 | host, _, err := net.SplitHostPort(address) 20 | if err != nil { 21 | host = address 22 | } 23 | 24 | if directHosts.MatchString(host) { 25 | logger := log.WithField("remote", address) 26 | logger.Info("dialing without proxy") 27 | conn, e = directDialer.DialContext(ctx, network, address) 28 | if e != nil { 29 | logger.WithError(e).Error("error while directly dialing") 30 | } 31 | return 32 | } 33 | 34 | return next(ctx, network, address) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cmd/tcp_over_http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "os" 8 | "regexp" 9 | "sync" 10 | "time" 11 | 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | 15 | "github.com/neex/tcp-over-http/client" 16 | "github.com/neex/tcp-over-http/client/forwarder" 17 | socks5server "github.com/neex/tcp-over-http/client/socks5-server" 18 | "github.com/neex/tcp-over-http/client/tun" 19 | ) 20 | 21 | func main() { 22 | var ( 23 | dialer *client.Dialer 24 | logLevel string 25 | remoteNet string 26 | directDialRegexp string 27 | directDialCompiled *regexp.Regexp 28 | poolSize int 29 | tunDevice string 30 | ) 31 | 32 | cmdDial := &cobra.Command{ 33 | Use: "dial [addr to dial]", 34 | Short: "Dial to addr and connect to stdin/stdout", 35 | Args: cobra.ExactArgs(1), 36 | Run: func(cmd *cobra.Command, args []string) { 37 | if logLevel == "" { 38 | log.SetLevel(log.ErrorLevel) 39 | } 40 | 41 | addr := args[0] 42 | 43 | conn, err := dialer.DialContext(context.Background(), remoteNet, addr) 44 | if err != nil { 45 | log.WithError(err).Fatal("dial failed") 46 | } 47 | 48 | forward(conn, os.Stdin, os.Stdout) 49 | }, 50 | } 51 | cmdDial.PersistentFlags().StringVar(&remoteNet, "remote-net", "tcp", "remote network (tcp/udp)") 52 | 53 | cmdForward := &cobra.Command{ 54 | Use: "forward [local addr] [remote addr]", 55 | Short: "Listen on local addr and forward every connection to remote addr", 56 | Args: cobra.ExactArgs(2), 57 | Run: func(cmd *cobra.Command, args []string) { 58 | localAddr := args[0] 59 | remoteAddr := args[1] 60 | 61 | dialer.PreconnectPoolSize = poolSize 62 | dialer.EnablePreconnect() 63 | 64 | lsn, err := net.Listen("tcp", localAddr) 65 | if err != nil { 66 | log.WithError(err).Fatal("listen failed") 67 | } 68 | 69 | log.Info("forward server started") 70 | 71 | for { 72 | c, err := lsn.Accept() 73 | if err != nil { 74 | log.WithError(err).Fatal("accept failed") 75 | } 76 | 77 | go func(c net.Conn) { 78 | conn, err := dialer.DialContext(context.Background(), remoteNet, remoteAddr) 79 | if err != nil { 80 | log.WithError(err).Error("dial failed early") 81 | return 82 | } 83 | 84 | forward(conn, c, c) 85 | }(c) 86 | } 87 | }, 88 | } 89 | cmdForward.PersistentFlags().IntVar(&poolSize, "preconnect-pool", 5, "preconnect pool size") 90 | cmdForward.PersistentFlags().StringVar(&remoteNet, "remote-net", "tcp", "remote network (tcp/udp)") 91 | 92 | cmdProxy := &cobra.Command{ 93 | Use: "proxy [local addr]", 94 | Short: "Run socks5 server on local addr, optionally also forward tun connections", 95 | Args: cobra.ExactArgs(1), 96 | Run: func(cmd *cobra.Command, args []string) { 97 | localAddr := args[0] 98 | 99 | if poolSize > 0 { 100 | dialer.PreconnectPoolSize = poolSize 101 | dialer.EnablePreconnect() 102 | go func() { 103 | for range time.Tick(10 * time.Second) { 104 | t, err := dialer.Ping() 105 | if err != nil { 106 | log.WithError(err).Error("ping error") 107 | continue 108 | } 109 | log.WithField("roundtrip", t).Info("ping") 110 | } 111 | }() 112 | } 113 | 114 | f := &forwarder.Forwarder{Dial: dialer.DialContext, DialTimeout: 10 * time.Second} 115 | if directDialCompiled != nil { 116 | f.Dial = DirectDialMiddleware(directDialCompiled, 20*time.Second, f.Dial) 117 | } 118 | 119 | if tunDevice != "" { 120 | if err := tun.ForwardTransportFromTUN(tunDevice, f); err != nil { 121 | log.WithError(err).Fatal("tun forward failed") 122 | } 123 | } 124 | 125 | server := &socks5server.Socks5Server{ 126 | Forwarder: f, 127 | } 128 | 129 | if err := server.ListenAndServe(context.Background(), localAddr); err != nil { 130 | log.WithError(err).Fatal("socks5 listen failed") 131 | } 132 | }, 133 | } 134 | cmdProxy.PersistentFlags().IntVar(&poolSize, "preconnect-pool", 5, "preconnect pool size") 135 | cmdProxy.PersistentFlags().StringVar(&directDialRegexp, "direct-dial", "", "the regexp for addresses that should be dialed without proxy") 136 | cmdProxy.PersistentFlags().StringVar(&tunDevice, "tun", "", "tun device to listen on") 137 | cmdProxy.PreRunE = func(cmd *cobra.Command, args []string) error { 138 | if directDialRegexp == "" { 139 | return nil 140 | } 141 | var err error 142 | directDialCompiled, err = regexp.Compile(directDialRegexp) 143 | if err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | var configFilename string 150 | rootCmd := &cobra.Command{Use: "tcp_over_http"} 151 | rootCmd.AddCommand(cmdDial, cmdForward, cmdProxy) 152 | rootCmd.PersistentFlags().StringVarP(&configFilename, "config", "c", "./config.yaml", "path to config") 153 | rootCmd.PersistentFlags().StringVar(&logLevel, "loglevel", "", "loglevel") 154 | rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 155 | if logLevel != "" { 156 | level, err := log.ParseLevel(logLevel) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | log.SetLevel(level) 162 | } 163 | config, err := client.NewConfigFromFile(configFilename) 164 | if err != nil { 165 | return err 166 | } 167 | connector := &client.Connector{Config: config} 168 | dialer = &client.Dialer{Connector: connector} 169 | return nil 170 | } 171 | 172 | if err := rootCmd.Execute(); err != nil { 173 | log.Fatal(err) 174 | } 175 | } 176 | 177 | func forward(conn net.Conn, in io.Reader, out io.WriteCloser) { 178 | var wg sync.WaitGroup 179 | wg.Add(2) 180 | go func() { 181 | defer wg.Done() 182 | _, _ = io.Copy(conn, in) 183 | _ = conn.Close() 184 | }() 185 | 186 | go func() { 187 | defer wg.Done() 188 | _, _ = io.Copy(out, conn) 189 | _ = out.Close() 190 | }() 191 | 192 | wg.Wait() 193 | } 194 | -------------------------------------------------------------------------------- /cmd/tcp_over_http/rlimit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "syscall" 6 | ) 7 | 8 | func init() { 9 | increaseRlimitNofile() 10 | } 11 | 12 | func increaseRlimitNofile() { 13 | var lim syscall.Rlimit 14 | 15 | if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil { 16 | log.Printf("unable to get current RLIMIT_NOFILE: %v", err) 17 | return 18 | } 19 | lim.Cur = lim.Max 20 | if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil { 21 | log.Printf("unable to set new RLIMIT_NOFILE: %v", err) 22 | return 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/tcp_over_http_server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/neex/tcp-over-http/server" 9 | ) 10 | 11 | func main() { 12 | if len(os.Args) != 2 { 13 | log.Fatalf("usage: %s ", os.Args[0]) 14 | } 15 | 16 | config, err := server.NewConfigFromFile(os.Args[1]) 17 | if err != nil { 18 | log.WithError(err).Fatal("loading config") 19 | } 20 | 21 | if config.RedirectorAddr != "" { 22 | go func() { 23 | if err := server.RunRedirectorServer(config); err != nil { 24 | log.WithError(err).Fatal("running redirector") 25 | } 26 | }() 27 | } 28 | 29 | if err := server.RunHTTPServer(config); err != nil { 30 | log.WithError(err).Fatal("running server") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /common/types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | type DialContextFunc = func(ctx context.Context, network, address string) (net.Conn, error) 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/neex/tcp-over-http 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/google/btree v1.0.0 // indirect 7 | github.com/google/netstack v0.0.0-20190806180032-4e5848a54239 8 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d 9 | github.com/sirupsen/logrus v1.4.2 10 | github.com/spf13/cobra v0.0.5 11 | gopkg.in/yaml.v2 v2.2.2 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 10 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 11 | github.com/google/netstack v0.0.0-20190806180032-4e5848a54239 h1:mObFOfR0YeeKIAP0sMMLLuR1+WKxd0VXMwdcJMKnBNw= 12 | github.com/google/netstack v0.0.0-20190806180032-4e5848a54239/go.mod h1:r/rILWg3r1Qy9G1IFMhsqWLq2GjwuYoTuPgG7ckMAjk= 13 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 14 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= 15 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 16 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 17 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 18 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 19 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 20 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 21 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 24 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 25 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 26 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 27 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 28 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 29 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 30 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 31 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 32 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 33 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 34 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 36 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 37 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 38 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 39 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 41 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 45 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 46 | -------------------------------------------------------------------------------- /protocol/netutils.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "net" 11 | "sync" 12 | ) 13 | 14 | const protocolMagic = "Elda" 15 | 16 | func ReadRequest(ctx context.Context, from net.Conn) (*ConnectionRequest, error) { 17 | cr := &ConnectionRequest{} 18 | if err := readPacket(ctx, from, cr); err != nil { 19 | return nil, err 20 | } 21 | return cr, nil 22 | } 23 | 24 | func ReadResponse(ctx context.Context, from net.Conn) (*ConnectionResponse, error) { 25 | cr := &ConnectionResponse{} 26 | if err := readPacket(ctx, from, cr); err != nil { 27 | return nil, err 28 | } 29 | return cr, nil 30 | } 31 | 32 | func WritePacket(ctx context.Context, to net.Conn, val interface{}) error { 33 | buf := bytes.NewBufferString(protocolMagic + "\x00\x00\x00\x00") 34 | enc := json.NewEncoder(buf) 35 | if err := enc.Encode(val); err != nil { 36 | return err 37 | } 38 | 39 | l := buf.Len() - 8 40 | data := buf.Bytes() 41 | binary.BigEndian.PutUint32(data[4:8], uint32(l)) 42 | 43 | var wg sync.WaitGroup 44 | newCtx, cancel := context.WithCancel(ctx) 45 | defer func() { cancel(); wg.Wait() }() 46 | 47 | wg.Add(1) 48 | go func() { 49 | defer wg.Done() 50 | <-newCtx.Done() 51 | if ctx.Err() != nil { 52 | _ = to.Close() 53 | } 54 | }() 55 | 56 | _, err := to.Write(data) 57 | if ctx.Err() != nil { 58 | return ctx.Err() 59 | } 60 | 61 | return err 62 | } 63 | 64 | func readPacket(ctx context.Context, from net.Conn, val interface{}) error { 65 | var wg sync.WaitGroup 66 | newCtx, cancel := context.WithCancel(ctx) 67 | defer func() { cancel(); wg.Wait() }() 68 | 69 | wg.Add(1) 70 | go func() { 71 | defer wg.Done() 72 | <-newCtx.Done() 73 | if ctx.Err() != nil { 74 | _ = from.Close() 75 | } 76 | }() 77 | 78 | checkContext := func(err error) error { 79 | if ctx.Err() != nil { 80 | return ctx.Err() 81 | } 82 | return err 83 | } 84 | 85 | var magic [4]byte 86 | if _, err := io.ReadFull(from, magic[:]); err != nil { 87 | return checkContext(err) 88 | } 89 | 90 | if string(magic[:]) != protocolMagic { 91 | return errors.New("magic mismatch") 92 | } 93 | 94 | var l [4]byte 95 | if _, err := io.ReadFull(from, l[:]); err != nil { 96 | return checkContext(err) 97 | } 98 | 99 | structLen := binary.BigEndian.Uint32(l[:]) 100 | data := make([]byte, structLen) 101 | if _, err := io.ReadFull(from, data); err != nil { 102 | return checkContext(err) 103 | } 104 | 105 | decoder := json.NewDecoder(bytes.NewReader(data)) 106 | if err := decoder.Decode(val); err != nil { 107 | return err 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /protocol/packet_connection.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "net" 9 | ) 10 | 11 | type PacketConnection struct { 12 | bb *bufio.ReadWriter 13 | net.Conn 14 | } 15 | 16 | func NewPacketConnection(conn net.Conn) *PacketConnection { 17 | r := bufio.NewReaderSize(conn, 65538) 18 | w := bufio.NewWriterSize(conn, 65538) 19 | return &PacketConnection{bufio.NewReadWriter(r, w), conn} 20 | } 21 | 22 | func (pc *PacketConnection) Read(buf []byte) (int, error) { 23 | if len(buf) < 65536 { 24 | return 0, errors.New("too small buffer for packet connection") 25 | } 26 | var binLen [2]byte 27 | if _, err := io.ReadFull(pc.Conn, binLen[:]); err != nil { 28 | return 0, err 29 | } 30 | 31 | packetLen := int(binary.BigEndian.Uint16(binLen[:])) 32 | return io.ReadFull(pc.Conn, buf[:packetLen]) 33 | } 34 | 35 | func (pc *PacketConnection) Write(buf []byte) (int, error) { 36 | if len(buf) > 65536 { 37 | return 0, errors.New("too big packet for packet connection") 38 | } 39 | 40 | var binLen [2]byte 41 | binary.BigEndian.PutUint16(binLen[:], uint16(len(buf))) 42 | if _, err := pc.bb.Write(binLen[:]); err != nil { 43 | return 0, err 44 | } 45 | 46 | if _, err := pc.bb.Write(buf); err != nil { 47 | return 0, err 48 | } 49 | 50 | if err := pc.bb.Flush(); err != nil { 51 | return 0, err 52 | } 53 | 54 | return len(buf), nil 55 | } 56 | -------------------------------------------------------------------------------- /protocol/types.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "time" 4 | 5 | type ConnectionRequest struct { 6 | Network string 7 | Address string 8 | Timeout time.Duration 9 | } 10 | 11 | type ConnectionResponse struct { 12 | Err *string 13 | Padding string 14 | } 15 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "os" 6 | "time" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | type Config struct { 13 | ListenAddr string `yaml:"listen_addr"` 14 | Token string `yaml:"token"` 15 | StaticDir string `yaml:"static_dir"` 16 | Domain string `yaml:"domain"` 17 | CertPath string `yaml:"cert_path"` 18 | KeyPath string `yaml:"key_path"` 19 | RedirectorAddr string `yaml:"redirector_addr"` 20 | DialTimeout time.Duration `yaml:"dial_timeout"` 21 | 22 | Certificate tls.Certificate `yaml:"-"` 23 | } 24 | 25 | func NewConfigFromFile(filename string) (*Config, error) { 26 | f, err := os.Open(filename) 27 | if err != nil { 28 | return nil, err 29 | } 30 | defer func() { _ = f.Close() }() 31 | dec := yaml.NewDecoder(f) 32 | cfg := &Config{} 33 | if err := dec.Decode(cfg); err != nil { 34 | return nil, err 35 | } 36 | 37 | if !cfg.IsHTTPS() { 38 | log.Warn("serving without https") 39 | } else { 40 | cfg.Certificate, err = tls.LoadX509KeyPair(cfg.CertPath, cfg.KeyPath) 41 | if err != nil { 42 | return nil, err 43 | } 44 | } 45 | 46 | return cfg, nil 47 | } 48 | 49 | func (c *Config) IsHTTPS() bool { 50 | return c.CertPath != "" 51 | } 52 | -------------------------------------------------------------------------------- /server/http_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func RunHTTPServer(config *Config) error { 14 | mux := makeHTTPMux(config) 15 | srv := http.Server{ 16 | Addr: config.ListenAddr, 17 | Handler: mux, 18 | } 19 | 20 | if config.IsHTTPS() { 21 | srv.TLSConfig = &tls.Config{ 22 | Certificates: []tls.Certificate{config.Certificate}, 23 | } 24 | 25 | return srv.ListenAndServeTLS("", "") 26 | } 27 | 28 | return srv.ListenAndServe() 29 | } 30 | 31 | func makeHTTPMux(config *Config) http.Handler { 32 | mux := http.NewServeMux() 33 | static := http.FileServer(http.Dir(config.StaticDir)) 34 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 35 | log.WithFields(log.Fields{ 36 | "remote_addr": r.RemoteAddr, 37 | "remote_uri": r.RequestURI, 38 | }).Info("static request") 39 | static.ServeHTTP(w, r) 40 | }) 41 | 42 | mux.HandleFunc(fmt.Sprintf("/establish/%s", config.Token), func(w http.ResponseWriter, r *http.Request) { 43 | l := log.WithFields(log.Fields{ 44 | "remote_addr": r.RemoteAddr, 45 | }) 46 | 47 | l.Info("proxy request") 48 | hj, ok := w.(http.Hijacker) 49 | if !ok { 50 | l.Error("connection doesn't support http.Hijacker") 51 | w.WriteHeader(500) 52 | return 53 | } 54 | 55 | conn, br, err := hj.Hijack() 56 | if err != nil { 57 | l.WithError(err).Error("error while hijacking connection") 58 | w.WriteHeader(500) 59 | return 60 | } 61 | 62 | d := net.Dialer{ 63 | Timeout: config.DialTimeout, 64 | } 65 | 66 | hc := &hijackedConn{br: br, Conn: conn} 67 | if err := RunMultiplexedServer(r.Context(), hc, d.DialContext); err != nil { 68 | l.WithError(err).Error("connection handling ended with error") 69 | } 70 | 71 | l.Info("proxy request finished") 72 | }) 73 | 74 | return CheckHost(config, mux) 75 | } 76 | 77 | type hijackedConn struct { 78 | br *bufio.ReadWriter 79 | net.Conn 80 | } 81 | 82 | func (hc *hijackedConn) Read(b []byte) (int, error) { 83 | return hc.br.Read(b) 84 | } 85 | -------------------------------------------------------------------------------- /server/multiplexed_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "net" 10 | "sync" 11 | 12 | "github.com/hashicorp/yamux" 13 | log "github.com/sirupsen/logrus" 14 | 15 | "github.com/neex/tcp-over-http/common" 16 | "github.com/neex/tcp-over-http/protocol" 17 | ) 18 | 19 | func RunMultiplexedServer(ctx context.Context, conn net.Conn, dial common.DialContextFunc) error { 20 | newCtx, cancel := context.WithCancel(ctx) 21 | defer cancel() 22 | go func() { 23 | <-newCtx.Done() 24 | _ = conn.Close() 25 | }() 26 | 27 | packet := &protocol.ConnectionResponse{ 28 | Err: nil, 29 | Padding: hex.EncodeToString(make([]byte, rand.Intn(2)*500+500)), 30 | } 31 | 32 | if err := protocol.WritePacket(ctx, conn, packet); err != nil { 33 | return fmt.Errorf("error while writing initial response: %v", err) 34 | } 35 | 36 | conf := *yamux.DefaultConfig() 37 | conf.LogOutput = log.StandardLogger().WriterLevel(log.ErrorLevel) 38 | sess, err := yamux.Server(conn, &conf) 39 | if err != nil { 40 | return fmt.Errorf("error while creating server: %v", err) 41 | } 42 | 43 | for { 44 | client, err := sess.Accept() 45 | if err == io.EOF { 46 | return nil 47 | } 48 | 49 | if err != nil { 50 | return fmt.Errorf("error while accept: %v", err) 51 | } 52 | 53 | go func() { 54 | _ = processClient(newCtx, client, dial) 55 | }() 56 | } 57 | } 58 | 59 | var isPacket = map[string]bool{ 60 | "tcp": false, 61 | "tcp4": false, 62 | "tcp6": false, 63 | "udp": true, 64 | "udp4": true, 65 | "udp6": true, 66 | } 67 | 68 | func processClient(ctx context.Context, conn net.Conn, dial common.DialContextFunc) error { 69 | newCtx, cancel := context.WithCancel(ctx) 70 | defer cancel() 71 | go func() { 72 | <-newCtx.Done() 73 | _ = conn.Close() 74 | }() 75 | 76 | req, err := protocol.ReadRequest(newCtx, conn) 77 | if err != nil { 78 | return err 79 | } 80 | needPacket, ok := isPacket[req.Network] 81 | if !ok { 82 | err := fmt.Sprintf("Network %#v not allowed", req.Network) 83 | return protocol.WritePacket(newCtx, conn, &protocol.ConnectionResponse{Err: &err}) 84 | } 85 | dialCtx, cancelDialCtx := context.WithTimeout(newCtx, req.Timeout) 86 | upstreamConn, err := dial(dialCtx, req.Network, req.Address) 87 | if upstreamConn != nil { 88 | defer func() { _ = upstreamConn.Close() }() 89 | } 90 | cancelDialCtx() 91 | 92 | var errStr *string 93 | if err != nil { 94 | errStr = new(string) 95 | *errStr = err.Error() 96 | } 97 | 98 | writeErr := protocol.WritePacket(newCtx, conn, &protocol.ConnectionResponse{Err: errStr}) 99 | if err != nil { 100 | return err 101 | } 102 | if writeErr != nil { 103 | return writeErr 104 | } 105 | 106 | if needPacket { 107 | conn = protocol.NewPacketConnection(conn) 108 | } 109 | 110 | var wg sync.WaitGroup 111 | wg.Add(2) 112 | go func() { 113 | defer wg.Done() 114 | buf := make([]byte, 65536) 115 | _, _ = io.CopyBuffer(conn, upstreamConn, buf) 116 | _ = conn.Close() 117 | }() 118 | 119 | go func() { 120 | defer wg.Done() 121 | buf := make([]byte, 65536) 122 | _, _ = io.CopyBuffer(upstreamConn, conn, buf) 123 | _ = upstreamConn.Close() 124 | }() 125 | 126 | wg.Wait() 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /server/redirector.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "net/http" 4 | 5 | func RunRedirectorServer(config *Config) error { 6 | return http.ListenAndServe(config.RedirectorAddr, CheckHost(config, http.HandlerFunc( 7 | func(w http.ResponseWriter, r *http.Request) { 8 | url := *r.URL 9 | url.Scheme = "https" 10 | url.Host = r.Host 11 | http.Redirect(w, r, url.String(), 301) 12 | }, 13 | ))) 14 | } 15 | -------------------------------------------------------------------------------- /server/utils.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func CheckHost(config *Config, handler http.Handler) http.Handler { 10 | if config.Domain == "" { 11 | return handler 12 | } 13 | 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | if r.Host != config.Domain { 16 | log.WithFields(log.Fields{ 17 | "host": r.Host, 18 | "remote_addr": r.RemoteAddr, 19 | }).Warn("request with wrong host") 20 | w.WriteHeader(404) 21 | return 22 | } 23 | handler.ServeHTTP(w, r) 24 | }) 25 | } 26 | --------------------------------------------------------------------------------