From 233eeb0b38e1e549c5b1ef466b689cd25e5fe925 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Thu, 21 Sep 2023 14:52:26 +0800 Subject: [PATCH] feat: inbound support Hysteria2 --- adapter/outbound/hysteria.go | 4 +- adapter/outbound/hysteria2.go | 4 +- adapter/outbound/util.go | 4 +- constant/metadata.go | 5 + listener/config/hysteria2.go | 24 ++++ listener/inbound/hysteria2.go | 93 +++++++++++++++ listener/parse.go | 7 ++ listener/sing_hysteria2/server.go | 180 ++++++++++++++++++++++++++++++ 8 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 listener/config/hysteria2.go create mode 100644 listener/inbound/hysteria2.go create mode 100644 listener/sing_hysteria2/server.go diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index e30565fb..7cd4ea76 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -117,12 +117,12 @@ type HysteriaOption struct { func (c *HysteriaOption) Speed() (uint64, uint64, error) { var up, down uint64 - up = stringToBps(c.Up) + up = StringToBps(c.Up) if up == 0 { return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) } - down = stringToBps(c.Down) + down = StringToBps(c.Down) if down == 0 { return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) } diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 61c8b7c7..8963da66 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -185,8 +185,8 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { Context: context.TODO(), Dialer: singDialer, ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), - SendBPS: stringToBps(option.Up), - ReceiveBPS: stringToBps(option.Down), + SendBPS: StringToBps(option.Up), + ReceiveBPS: StringToBps(option.Down), SalamanderPassword: salamanderPassword, Password: option.Password, TLSConfig: tlsConfig, diff --git a/adapter/outbound/util.go b/adapter/outbound/util.go index 4b59183e..b048cd8b 100644 --- a/adapter/outbound/util.go +++ b/adapter/outbound/util.go @@ -126,14 +126,14 @@ func safeConnClose(c net.Conn, err error) { var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) -func stringToBps(s string) uint64 { +func StringToBps(s string) uint64 { if s == "" { return 0 } // when have not unit, use Mbps if v, err := strconv.Atoi(s); err == nil { - return stringToBps(fmt.Sprintf("%d Mbps", v)) + return StringToBps(fmt.Sprintf("%d Mbps", v)) } m := rateStringRegexp.FindStringSubmatch(s) diff --git a/constant/metadata.go b/constant/metadata.go index dbd31fd8..70478911 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -30,6 +30,7 @@ const ( TUNNEL TUN TUIC + HYSTERIA2 INNER ) @@ -78,6 +79,8 @@ func (t Type) String() string { return "Tun" case TUIC: return "Tuic" + case HYSTERIA2: + return "Hysteria2" case INNER: return "Inner" default: @@ -110,6 +113,8 @@ func ParseType(t string) (*Type, error) { res = TUN case "TUIC": res = TUIC + case "HYSTERIA2": + res = HYSTERIA2 case "INNER": res = INNER default: diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go new file mode 100644 index 00000000..e8e9c09c --- /dev/null +++ b/listener/config/hysteria2.go @@ -0,0 +1,24 @@ +package config + +import "encoding/json" + +type Hysteria2Server struct { + Enable bool `yaml:"enable" json:"enable"` + Listen string `yaml:"listen" json:"listen"` + Users map[string]string `yaml:"users" json:"users,omitempty"` + Obfs string `yaml:"obfs" json:"obfs,omitempty"` + ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` + Certificate string `yaml:"certificate" json:"certificate"` + PrivateKey string `yaml:"private-key" json:"private-key"` + MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` + ALPN []string `yaml:"alpn" json:"alpn,omitempty"` + Up string `yaml:"up" json:"up,omitempty"` + Down string `yaml:"down" json:"down,omitempty"` + IgnoreClientBandwidth bool `yaml:"ignore-client-bandwidth" json:"ignore-client-bandwidth,omitempty"` + Masquerade string `yaml:"masquerade" json:"masquerade,omitempty"` +} + +func (h Hysteria2Server) String() string { + b, _ := json.Marshal(h) + return string(b) +} diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go new file mode 100644 index 00000000..00feadb1 --- /dev/null +++ b/listener/inbound/hysteria2.go @@ -0,0 +1,93 @@ +package inbound + +import ( + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/sing_hysteria2" + "github.com/Dreamacro/clash/log" +) + +type Hysteria2Option struct { + BaseOption + Users map[string]string `inbound:"users,omitempty"` + Obfs string `inbound:"obfs,omitempty"` + ObfsPassword string `inbound:"obfs-password,omitempty"` + Certificate string `inbound:"certificate"` + PrivateKey string `inbound:"private-key"` + MaxIdleTime int `inbound:"max-idle-time,omitempty"` + ALPN []string `inbound:"alpn,omitempty"` + Up string `inbound:"up,omitempty"` + Down string `inbound:"down,omitempty"` + IgnoreClientBandwidth bool `inbound:"ignore-client-bandwidth,omitempty"` + Masquerade string `inbound:"masquerade,omitempty"` +} + +func (o Hysteria2Option) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Hysteria2 struct { + *Base + config *Hysteria2Option + l *sing_hysteria2.Listener + ts LC.Hysteria2Server +} + +func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + return &Hysteria2{ + Base: base, + config: options, + ts: LC.Hysteria2Server{ + Enable: true, + Listen: base.RawAddress(), + Users: options.Users, + Obfs: options.Obfs, + ObfsPassword: options.ObfsPassword, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + MaxIdleTime: options.MaxIdleTime, + ALPN: options.ALPN, + Up: options.Up, + Down: options.Down, + IgnoreClientBandwidth: options.IgnoreClientBandwidth, + Masquerade: options.Masquerade, + }, + }, nil +} + +// Config implements constant.InboundListener +func (t *Hysteria2) Config() C.InboundConfig { + return t.config +} + +// Address implements constant.InboundListener +func (t *Hysteria2) Address() string { + if t.l != nil { + for _, addr := range t.l.AddrList() { + return addr.String() + } + } + return "" +} + +// Listen implements constant.InboundListener +func (t *Hysteria2) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, natTable C.NatTable) error { + var err error + t.l, err = sing_hysteria2.New(t.ts, tcpIn, udpIn, t.Additions()...) + if err != nil { + return err + } + log.Infoln("Hysteria2[%s] proxy listening at: %s", t.Name(), t.Address()) + return nil +} + +// Close implements constant.InboundListener +func (t *Hysteria2) Close() error { + return t.l.Close() +} + +var _ C.InboundListener = (*Hysteria2)(nil) diff --git a/listener/parse.go b/listener/parse.go index c8e1ddf7..b0fac86a 100644 --- a/listener/parse.go +++ b/listener/parse.go @@ -86,6 +86,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) { return nil, err } listener, err = IN.NewVmess(vmessOption) + case "hysteria2": + hysteria2Option := &IN.Hysteria2Option{} + err = decoder.Decode(mapping, hysteria2Option) + if err != nil { + return nil, err + } + listener, err = IN.NewHysteria2(hysteria2Option) case "tuic": tuicOption := &IN.TuicOption{ MaxIdleTime: 15000, diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go new file mode 100644 index 00000000..1d6ab29a --- /dev/null +++ b/listener/sing_hysteria2/server.go @@ -0,0 +1,180 @@ +package sing_hysteria2 + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strings" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/adapter/outbound" + CN "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/sockopt" + C "github.com/Dreamacro/clash/constant" + LC "github.com/Dreamacro/clash/listener/config" + "github.com/Dreamacro/clash/listener/sing" + "github.com/Dreamacro/clash/log" + + "github.com/metacubex/sing-quic/hysteria2" + + E "github.com/sagernet/sing/common/exceptions" +) + +type Listener struct { + closed bool + config LC.Hysteria2Server + udpListeners []net.PacketConn + services []*hysteria2.Service[string] +} + +func New(config LC.Hysteria2Server, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (*Listener, error) { + var sl *Listener + var err error + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-HYSTERIA2"), + inbound.WithSpecialRules(""), + } + } + + h := &sing.ListenerHandler{ + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.HYSTERIA2, + Additions: additions, + } + + sl = &Listener{false, config, nil, nil} + + cert, err := CN.ParseCert(config.Certificate, config.PrivateKey) + if err != nil { + return nil, err + } + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{cert}, + } + if len(config.ALPN) > 0 { + tlsConfig.NextProtos = config.ALPN + } else { + tlsConfig.NextProtos = []string{"h3"} + } + + var salamanderPassword string + if len(config.Obfs) > 0 { + if config.ObfsPassword == "" { + return nil, errors.New("missing obfs password") + } + switch config.Obfs { + case hysteria2.ObfsTypeSalamander: + salamanderPassword = config.ObfsPassword + default: + return nil, fmt.Errorf("unknown obfs type: %s", config.Obfs) + } + } + var masqueradeHandler http.Handler + if config.Masquerade != "" { + masqueradeURL, err := url.Parse(config.Masquerade) + if err != nil { + return nil, E.Cause(err, "parse masquerade URL") + } + switch masqueradeURL.Scheme { + case "file": + masqueradeHandler = http.FileServer(http.Dir(masqueradeURL.Path)) + case "http", "https": + masqueradeHandler = &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(masqueradeURL) + r.Out.Host = r.In.Host + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + w.WriteHeader(http.StatusBadGateway) + }, + } + default: + return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) + } + } + + service, err := hysteria2.NewService[string](hysteria2.ServiceOptions{ + Context: context.Background(), + Logger: log.SingLogger, + SendBPS: outbound.StringToBps(config.Up), + ReceiveBPS: outbound.StringToBps(config.Down), + SalamanderPassword: salamanderPassword, + TLSConfig: tlsConfig, + IgnoreClientBandwidth: config.IgnoreClientBandwidth, + Handler: h, + MasqueradeHandler: masqueradeHandler, + }) + if err != nil { + return nil, err + } + + userNameList := make([]string, 0, len(config.Users)) + userPasswordList := make([]string, 0, len(config.Users)) + for name, password := range config.Users { + userNameList = append(userNameList, name) + userPasswordList = append(userPasswordList, password) + } + service.UpdateUsers(userNameList, userPasswordList) + + for _, addr := range strings.Split(config.Listen, ",") { + addr := addr + _service := *service + service := &_service // make a copy + + ul, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } + + err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + sl.udpListeners = append(sl.udpListeners, ul) + sl.services = append(sl.services, service) + + go func() { + _ = service.Start(ul) + }() + } + + return sl, nil +} + +func (l *Listener) Close() error { + l.closed = true + var retErr error + for _, service := range l.services { + err := service.Close() + if err != nil { + retErr = err + } + } + for _, lis := range l.udpListeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + return retErr +} + +func (l *Listener) Config() string { + return l.config.String() +} + +func (l *Listener) AddrList() (addrList []net.Addr) { + for _, lis := range l.udpListeners { + addrList = append(addrList, lis.LocalAddr()) + } + return +}