From 02fa2cde0ac8f054ac162268492deb13d117c62d Mon Sep 17 00:00:00 2001 From: xmapst Date: Mon, 25 Mar 2024 10:15:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA:=20HTTP/SOCKS5=E6=B7=B7?= =?UTF-8?q?=E5=90=88=E7=AB=AF=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/cmd/client.go | 54 ++++++++++++++ app/internal/proxymux/mux.go | 141 +++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 app/internal/proxymux/mux.go diff --git a/app/cmd/client.go b/app/cmd/client.go index c1d04bd..b818569 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -21,6 +21,7 @@ import ( "github.com/apernet/hysteria/app/internal/forwarding" "github.com/apernet/hysteria/app/internal/http" + "github.com/apernet/hysteria/app/internal/proxymux" "github.com/apernet/hysteria/app/internal/redirect" "github.com/apernet/hysteria/app/internal/socks5" "github.com/apernet/hysteria/app/internal/tproxy" @@ -63,6 +64,7 @@ type clientConfig struct { Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"` FastOpen bool `mapstructure:"fastOpen"` Lazy bool `mapstructure:"lazy"` + Mixed *mixedConfig `mapstructure:"mixed"` SOCKS5 *socks5Config `mapstructure:"socks5"` HTTP *httpConfig `mapstructure:"http"` TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"` @@ -113,6 +115,14 @@ type clientConfigBandwidth struct { Down string `mapstructure:"down"` } +type mixedConfig struct { + Listen string `mapstructure:"listen"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + DisableUDP bool `mapstructure:"disableUDP"` + Realm string `mapstructure:"realm"` +} + type socks5Config struct { Listen string `mapstructure:"listen"` Username string `mapstructure:"username"` @@ -447,6 +457,11 @@ func runClient(cmd *cobra.Command, args []string) { // Register modes var runner clientModeRunner + if config.Mixed != nil { + runner.Add("Mixed server", func() error { + return clientMixed(*config.Mixed, c) + }) + } if config.SOCKS5 != nil { runner.Add("SOCKS5 server", func() error { return clientSOCKS5(*config.SOCKS5, c) @@ -527,6 +542,45 @@ func (r *clientModeRunner) Run() { } } +func clientMixed(config mixedConfig, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + l, err := correctnet.Listen("tcp", config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + var authFunc func(username, password string) bool + username, password := config.Username, config.Password + if username != "" && password != "" { + authFunc = func(u, p string) bool { + return u == username && p == password + } + } + s := socks5.Server{ + HyClient: c, + AuthFunc: authFunc, + DisableUDP: config.DisableUDP, + EventLogger: &socks5Logger{}, + } + logger.Info("SOCKS5 server listening", zap.String("addr", config.Listen)) + + h := http.Server{ + HyClient: c, + AuthFunc: authFunc, + AuthRealm: config.Realm, + EventLogger: &httpLogger{}, + } + logger.Info("HTTP proxy server listening", zap.String("addr", config.Listen)) + socks5Ln, httpLn := proxymux.SplitSOCKSAndHTTP(l) + go func() { + if err = h.Serve(httpLn); err != nil { + logger.Fatal(err.Error()) + } + }() + return s.Serve(socks5Ln) +} + func clientSOCKS5(config socks5Config, c client.Client) error { if config.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} diff --git a/app/internal/proxymux/mux.go b/app/internal/proxymux/mux.go new file mode 100644 index 0000000..d99b122 --- /dev/null +++ b/app/internal/proxymux/mux.go @@ -0,0 +1,141 @@ +// Package proxymux splits a net.Listener in two, routing SOCKS5 +// connections to one and HTTP requests to the other. +// +// It allows for hosting both a SOCKS5 proxy and an HTTP proxy on the +// same listener. +package proxymux + +import ( + "io" + "net" + "sync" + "time" +) + +// SplitSOCKSAndHTTP accepts connections on ln and passes connections +// through to either socksListener or httpListener, depending the +// first byte sent by the client. +func SplitSOCKSAndHTTP(ln net.Listener) (socksListener, httpListener net.Listener) { + sl := &listener{ + addr: ln.Addr(), + c: make(chan net.Conn), + closed: make(chan struct{}), + } + hl := &listener{ + addr: ln.Addr(), + c: make(chan net.Conn), + closed: make(chan struct{}), + } + + go splitSOCKSAndHTTPListener(ln, sl, hl) + + return sl, hl +} + +func splitSOCKSAndHTTPListener(ln net.Listener, sl, hl *listener) { + for { + conn, err := ln.Accept() + if err != nil { + sl.Close() + hl.Close() + return + } + go routeConn(conn, sl, hl) + } +} + +func routeConn(c net.Conn, socksListener, httpListener *listener) { + if err := c.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil { + c.Close() + return + } + + var b [1]byte + if _, err := io.ReadFull(c, b[:]); err != nil { + c.Close() + return + } + + if err := c.SetReadDeadline(time.Time{}); err != nil { + c.Close() + return + } + + conn := &connWithOneByte{ + Conn: c, + b: b[0], + } + + // First byte of a SOCKS5 session is a version byte set to 5. + var ln *listener + if b[0] == 5 { + ln = socksListener + } else { + ln = httpListener + } + select { + case ln.c <- conn: + case <-ln.closed: + c.Close() + } +} + +type listener struct { + addr net.Addr + c chan net.Conn + mu sync.Mutex // serializes close() on closed. It's okay to receive on closed without locking. + closed chan struct{} +} + +func (ln *listener) Accept() (net.Conn, error) { + // Once closed, reliably stay closed, don't race with attempts at + // further connections. + select { + case <-ln.closed: + return nil, net.ErrClosed + default: + } + select { + case ret := <-ln.c: + return ret, nil + case <-ln.closed: + return nil, net.ErrClosed + } +} + +func (ln *listener) Close() error { + ln.mu.Lock() + defer ln.mu.Unlock() + select { + case <-ln.closed: + // Already closed + default: + close(ln.closed) + } + return nil +} + +func (ln *listener) Addr() net.Addr { + return ln.addr +} + +// connWithOneByte is a net.Conn that returns b for the first read +// request, then forwards everything else to Conn. +type connWithOneByte struct { + net.Conn + + b byte + bRead bool +} + +func (c *connWithOneByte) Read(bs []byte) (int, error) { + if c.bRead { + return c.Conn.Read(bs) + } + if len(bs) == 0 { + return 0, nil + } + c.bRead = true + bs[0] = c.b + return 1, nil +}