diff --git a/README.md b/README.md index 5268be6..88017e3 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,15 @@ hysteria_traffic_uplink_bytes_total{auth="aGFja2VyISE="} 37452 "cert": "/home/ubuntu/my_cert.crt", // Cert file (HTTPS proxy) "key": "/home/ubuntu/my_key.crt" // Key file (HTTPS proxy) }, + "tun": { + "name": "tun-hy", // TUN interface name + "timeout": 300, // Timeout in seconds + "address": "192.0.2.2", // TUN interface address, not applicable for Linux + "gateway": "192.0.2.1", // TUN interface gateway, not applicable for Linux + "mask": "255.255.255.252", // TUN interface mask, not applicable for Linux + "dns": [ "8.8.8.8", "8.8.4.4" ], // TUN interface DNS, only applicable for Windows + "persist": false // Persist TUN interface after exit, only applicable for Linux + }, "relay_tcp": { "listen": "127.0.0.1:2222", // TCP relay listen address "remote": "123.123.123.123:22", // TCP relay remote address diff --git a/README.zh.md b/README.zh.md index 67432c3..0ff71a2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -232,6 +232,15 @@ hysteria_traffic_uplink_bytes_total{auth="aGFja2VyISE="} 37452 "cert": "/home/ubuntu/my_cert.crt", // 证书 (变为 HTTPS 代理) "key": "/home/ubuntu/my_key.crt" // 证书密钥 (变为 HTTPS 代理) }, + "tun": { + "name": "tun-hy", // TUN 接口名称 + "timeout": 300, // 超时秒数 + "address": "192.0.2.2", // TUN 接口地址(不适用于 Linux) + "gateway": "192.0.2.1", // TUN 接口网关(不适用于 Linux) + "mask": "255.255.255.252", // TUN 接口子网掩码(不适用于 Linux) + "dns": [ "8.8.8.8", "8.8.4.4" ], // TUN 接口 DNS 服务器(仅适用于 Windows) + "persist": false // 在程序退出之后保留接口(仅适用于 Linux) + }, "relay_tcp": { "listen": "127.0.0.1:2222", // TCP 转发监听地址 "remote": "123.123.123.123:22", // TCP 转发目标地址 diff --git a/cmd/client.go b/cmd/client.go index a322b23..4a635fb 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -15,10 +15,12 @@ import ( "github.com/tobyxdd/hysteria/pkg/socks5" "github.com/tobyxdd/hysteria/pkg/tproxy" "github.com/tobyxdd/hysteria/pkg/transport" + "github.com/tobyxdd/hysteria/pkg/tun" "io" "io/ioutil" "net" "net/http" + "strings" "time" ) @@ -189,6 +191,56 @@ func client(config *clientConfig) { }() } + if len(config.TUN.Name) != 0 { + go func() { + timeout := time.Duration(config.TUN.Timeout) * time.Second + if timeout == 0 { + timeout = 300 * time.Second + } + tunServer, err := tun.NewServer(client, transport.DefaultTransport, + time.Duration(config.TUN.Timeout)*time.Second, + config.TUN.Name, config.TUN.Address, config.TUN.Gateway, config.TUN.Mask, config.TUN.DNS, config.TUN.Persist) + if err != nil { + logrus.WithField("error", err).Fatal("Failed to initialize TUN server") + } + tunServer.ACLEngine = aclEngine + tunServer.RequestFunc = func(addr net.Addr, reqAddr string, action acl.Action, arg string) { + logrus.WithFields(logrus.Fields{ + "action": actionToString(action, arg), + "src": addr.String(), + "dst": reqAddr, + }).Debugf("TUN %s request", strings.ToUpper(addr.Network())) + } + tunServer.ErrorFunc = func(addr net.Addr, reqAddr string, err error) { + if err != nil { + if err == io.EOF { + logrus.WithFields(logrus.Fields{ + "src": addr.String(), + "dst": reqAddr, + }).Debugf("TUN %s EOF", strings.ToUpper(addr.Network())) + } else if err == core.ErrClosed && strings.HasPrefix(addr.Network(), "udp") { + logrus.WithFields(logrus.Fields{ + "src": addr.String(), + "dst": reqAddr, + }).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network())) + } else if err.Error() == "deadline exceeded" && strings.HasPrefix(addr.Network(), "tcp") { + logrus.WithFields(logrus.Fields{ + "src": addr.String(), + "dst": reqAddr, + }).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network())) + } else { + logrus.WithFields(logrus.Fields{ + "error": err, + "src": addr.String(), + "dst": reqAddr, + }).Infof("TUN %s error", strings.ToUpper(addr.Network())) + } + } + } + errChan <- tunServer.ListenAndServe() + }() + } + if len(config.TCPRelay.Listen) > 0 { go func() { rl, err := relay.NewTCPRelay(client, transport.DefaultTransport, diff --git a/cmd/config.go b/cmd/config.go index c371ea1..1723c94 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -88,6 +88,15 @@ type clientConfig struct { Cert string `json:"cert"` Key string `json:"key"` } `json:"http"` + TUN struct { + Name string `json:"name"` + Timeout int `json:"timeout"` + Address string `json:"address"` + Gateway string `json:"gateway"` + Mask string `json:"mask"` + DNS []string `json:"dns"` + Persist bool `json:"persist"` + } `json:"tun"` TCPRelay struct { Listen string `json:"listen"` Remote string `json:"remote"` @@ -118,10 +127,10 @@ type clientConfig struct { } func (c *clientConfig) Check() error { - if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && + if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.TUN.Name) == 0 && len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 && len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 { - return errors.New("no SOCKS5, HTTP, relay or TProxy listen address") + return errors.New("please enable at least one mode") } if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 { return errors.New("no TCP relay remote address") @@ -135,6 +144,9 @@ func (c *clientConfig) Check() error { if c.HTTP.Timeout != 0 && c.HTTP.Timeout <= 4 { return errors.New("invalid HTTP timeout") } + if c.TUN.Timeout != 0 && c.TUN.Timeout < 4 { + return errors.New("invalid TUN timeout") + } if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout <= 4 { return errors.New("invalid TCP relay timeout") } diff --git a/go.mod b/go.mod index c5c9985..75d67e9 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/elazarl/goproxy v0.0.0-20210110162100-a92cc753f88e github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e + github.com/eycorsican/go-tun2socks v1.16.11 github.com/hashicorp/golang-lru v0.5.4 github.com/lucas-clemente/quic-go v0.20.1 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 diff --git a/go.sum b/go.sum index 22f2641..f53aba5 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,8 @@ github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e/go.mod h1:gNh8 github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8= +github.com/eycorsican/go-tun2socks v1.16.11/go.mod h1:wgB2BFT8ZaPKyKOQ/5dljMG/YIow+AIXyq4KBwJ5sGQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= @@ -363,6 +365,8 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b h1:+y4hCMc/WKsDbAPsOQZgBSaSZ26uh2afyaWeVg/3s/c= +github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= @@ -462,6 +466,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= diff --git a/pkg/tun/server.go b/pkg/tun/server.go new file mode 100644 index 0000000..b66cc8b --- /dev/null +++ b/pkg/tun/server.go @@ -0,0 +1,70 @@ +package tun + +import ( + tun2socks "github.com/eycorsican/go-tun2socks/core" + "github.com/eycorsican/go-tun2socks/tun" + "github.com/tobyxdd/hysteria/pkg/acl" + "github.com/tobyxdd/hysteria/pkg/core" + "github.com/tobyxdd/hysteria/pkg/transport" + "io" + "net" + "sync" + "time" +) + +type Server struct { + HyClient *core.Client + Timeout time.Duration + TunDev io.ReadWriteCloser + Transport transport.Transport + ACLEngine *acl.Engine + + RequestFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string) + ErrorFunc func(addr net.Addr, reqAddr string, err error) + + udpConnMap map[tun2socks.UDPConn]*udpConnInfo + udpConnMapLock sync.RWMutex +} + +const ( + MTU = 1500 +) + +func NewServerWithTunDev(hyClient *core.Client, transport transport.Transport, + timeout time.Duration, + tunDev io.ReadWriteCloser) (*Server, error) { + s := &Server{ + HyClient: hyClient, + Transport: transport, + Timeout: timeout, + TunDev: tunDev, + udpConnMap: make(map[tun2socks.UDPConn]*udpConnInfo), + } + return s, nil +} + +func NewServer(hyClient *core.Client, transport transport.Transport, + timeout time.Duration, + name, address, gateway, mask string, dnsServers []string, persist bool) (*Server, error) { + tunDev, err := tun.OpenTunDevice(name, address, gateway, mask, dnsServers, persist) + if err != nil { + return nil, err + } + return NewServerWithTunDev(hyClient, transport, timeout, tunDev) +} + +func (s *Server) ListenAndServe() error { + lwipWriter := tun2socks.NewLWIPStack().(io.Writer) + + tun2socks.RegisterTCPConnHandler(s) + tun2socks.RegisterUDPConnHandler(s) + tun2socks.RegisterOutputFn(func(data []byte) (int, error) { + return s.TunDev.Write(data) + }) + + _, err := io.CopyBuffer(lwipWriter, s.TunDev, make([]byte, MTU)) + if err != nil { + return err + } + return nil +} diff --git a/pkg/tun/tcp.go b/pkg/tun/tcp.go new file mode 100644 index 0000000..8a9ef9e --- /dev/null +++ b/pkg/tun/tcp.go @@ -0,0 +1,80 @@ +package tun + +import ( + "errors" + "fmt" + tun2socks "github.com/eycorsican/go-tun2socks/core" + "github.com/tobyxdd/hysteria/pkg/acl" + "github.com/tobyxdd/hysteria/pkg/utils" + "net" + "strconv" +) + +func (s *Server) Handle(conn net.Conn, target *net.TCPAddr) error { + action, arg := acl.ActionProxy, "" + var resErr error + if s.ACLEngine != nil { + action, arg, _, resErr = s.ACLEngine.ResolveAndMatch(target.IP.String()) + } + if s.RequestFunc != nil { + s.RequestFunc(conn.LocalAddr(), target.String(), action, arg) + } + var closeErr error + defer func() { + if s.ErrorFunc != nil && closeErr != nil { + s.ErrorFunc(conn.LocalAddr(), target.String(), closeErr) + } + }() + switch action { + case acl.ActionDirect: + if resErr != nil { + closeErr = resErr + return resErr + } + rc, err := s.Transport.LocalDialTCP(nil, target) + if err != nil { + closeErr = err + return err + } + go s.relayTCP(conn, rc) + return nil + case acl.ActionProxy: + rc, err := s.HyClient.DialTCP(target.String()) + if err != nil { + closeErr = err + return err + } + go s.relayTCP(conn, rc) + return nil + case acl.ActionBlock: + closeErr = errors.New("blocked in ACL") + // caller will abort the connection when err != nil + return closeErr + case acl.ActionHijack: + rc, err := s.Transport.LocalDial("tcp", net.JoinHostPort(arg, strconv.Itoa(target.Port))) + if err != nil { + closeErr = err + return err + } + go s.relayTCP(conn, rc) + return nil + default: + closeErr = fmt.Errorf("unknown action %d", action) + // caller will abort the connection when err != nil + return closeErr + } +} + +func (s *Server) relayTCP(clientConn, relayConn net.Conn) { + closeErr := utils.PipePairWithTimeout(relayConn, clientConn, s.Timeout) + if s.ErrorFunc != nil { + s.ErrorFunc(clientConn.LocalAddr(), relayConn.RemoteAddr().String(), closeErr) + } + relayConn.Close() + clientConn.Close() + if closeErr != nil && closeErr.Error() == "deadline exceeded" { + if clientConn, ok := clientConn.(tun2socks.TCPConn); ok { + clientConn.Abort() + } + } +} diff --git a/pkg/tun/udp.go b/pkg/tun/udp.go new file mode 100644 index 0000000..9c74e27 --- /dev/null +++ b/pkg/tun/udp.go @@ -0,0 +1,180 @@ +package tun + +import ( + "bytes" + "errors" + "fmt" + tun2socks "github.com/eycorsican/go-tun2socks/core" + "github.com/tobyxdd/hysteria/pkg/acl" + "github.com/tobyxdd/hysteria/pkg/core" + "io" + "net" + "strconv" + "sync/atomic" + "time" +) + +const udpBufferSize = 65535 + +type udpConnInfo struct { + hyConn core.UDPConn + target string + expire atomic.Value +} + +func (s *Server) fetchUDPInput(conn tun2socks.UDPConn, ci *udpConnInfo) { + defer func() { + s.closeUDPConn(conn) + }() + + if s.Timeout > 0 { + go func() { + for { + life := ci.expire.Load().(time.Time).Sub(time.Now()) + if life < 0 { + s.closeUDPConn(conn) + break + } else { + time.Sleep(life) + } + } + }() + } + + var err error + + for { + var bs []byte + var from string + bs, from, err = ci.hyConn.ReadFrom() + if err != nil { + break + } + ci.expire.Store(time.Now().Add(s.Timeout)) + udpAddr, _ := net.ResolveUDPAddr("udp", from) + _, err = conn.WriteFrom(bs, udpAddr) + if err != nil { + break + } + } + + if s.ErrorFunc != nil { + s.ErrorFunc(conn.LocalAddr(), ci.target, err) + } +} + +func (s *Server) Connect(conn tun2socks.UDPConn, target *net.UDPAddr) error { + action, arg := acl.ActionProxy, "" + var resErr error + if s.ACLEngine != nil { + action, arg, _, resErr = s.ACLEngine.ResolveAndMatch(target.IP.String()) + } + if s.RequestFunc != nil { + s.RequestFunc(conn.LocalAddr(), target.String(), action, arg) + } + var hyConn core.UDPConn + var closeErr error + defer func() { + if s.ErrorFunc != nil && closeErr != nil { + s.ErrorFunc(conn.LocalAddr(), target.String(), closeErr) + } + }() + switch action { + case acl.ActionDirect: + if resErr != nil { + closeErr = resErr + return resErr + } + var relayConn net.Conn + relayConn, closeErr = s.Transport.LocalDial("udp", target.String()) + if closeErr != nil { + return closeErr + } + hyConn = &delegatedUDPConn{ + underlayConn: relayConn, + delegatedRemoteAddr: target.String(), + } + case acl.ActionProxy: + hyConn, closeErr = s.HyClient.DialUDP() + if closeErr != nil { + return closeErr + } + case acl.ActionBlock: + closeErr = errors.New("blocked in ACL") + return closeErr + case acl.ActionHijack: + hijackAddr := net.JoinHostPort(arg, strconv.Itoa(target.Port)) + var relayConn net.Conn + relayConn, closeErr = s.Transport.LocalDial("udp", hijackAddr) + if closeErr != nil { + return closeErr + } + hyConn = &delegatedUDPConn{ + underlayConn: relayConn, + delegatedRemoteAddr: target.String(), + } + default: + closeErr = fmt.Errorf("unknown action %d", action) + return nil + } + ci := udpConnInfo{ + hyConn: hyConn, + target: net.JoinHostPort(target.IP.String(), strconv.Itoa(target.Port)), + } + ci.expire.Store(time.Now().Add(s.Timeout)) + s.udpConnMapLock.Lock() + s.udpConnMap[conn] = &ci + s.udpConnMapLock.Unlock() + go s.fetchUDPInput(conn, &ci) + return nil +} + +func (s *Server) ReceiveTo(conn tun2socks.UDPConn, data []byte, addr *net.UDPAddr) error { + s.udpConnMapLock.RLock() + ci, ok := s.udpConnMap[conn] + s.udpConnMapLock.RUnlock() + if !ok { + err := errors.New("previous connection closed for timeout") + s.ErrorFunc(conn.LocalAddr(), addr.String(), err) + return err + } + ci.expire.Store(time.Now().Add(s.Timeout)) + _ = ci.hyConn.WriteTo(data, addr.String()) + return nil +} + +func (s *Server) closeUDPConn(conn tun2socks.UDPConn) { + conn.Close() + s.udpConnMapLock.Lock() + defer s.udpConnMapLock.Unlock() + if c, ok := s.udpConnMap[conn]; ok { + c.hyConn.Close() + delete(s.udpConnMap, conn) + } +} + +type delegatedUDPConn struct { + underlayConn net.Conn + delegatedRemoteAddr string +} + +func (c *delegatedUDPConn) ReadFrom() (bs []byte, addr string, err error) { + buf := make([]byte, udpBufferSize) + n, err := c.underlayConn.Read(buf) + if n > 0 { + bs = append(bs, buf[0:n]...) + } + if err != nil || err == io.EOF { + addr = c.delegatedRemoteAddr + } + return +} + +func (c *delegatedUDPConn) WriteTo(bs []byte, addr string) error { + _, err := io.Copy(c.underlayConn, bytes.NewReader(bs)) + return err +} + +func (c *delegatedUDPConn) Close() error { + return c.underlayConn.Close() +}