diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 976bf66..136da50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,13 +125,13 @@ jobs: run: | echo "version: $version" mkdir -p build_assets - go build -v -o build_assets/V2bX -tags "sing xray with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" + go build -v -o build_assets/V2bX -tags "sing xray hysteria2 with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" - name: Build Mips softfloat V2bX if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle' run: | echo "version: $version" - GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" + GOMIPS=softfloat go build -v -o build_assets/V2bX_softfloat -tags "sing xray hysteria2 with_reality_server with_quic with_grpc with_utls with_wireguard with_acme" -trimpath -ldflags "-X 'github.com/InazumaV/V2bX/cmd.version=$version' -s -w -buildid=" - name: Rename Windows V2bX if: matrix.goos == 'windows' run: | diff --git a/conf/core.go b/conf/core.go index 547aa54..f948c74 100644 --- a/conf/core.go +++ b/conf/core.go @@ -5,10 +5,11 @@ import ( ) type CoreConfig struct { - Type string `json:"Type"` - Name string `json:"Name"` - XrayConfig *XrayConfig `json:"-"` - SingConfig *SingConfig `json:"-"` + Type string `json:"Type"` + Name string `json:"Name"` + XrayConfig *XrayConfig `json:"-"` + SingConfig *SingConfig `json:"-"` + Hysteria2Config *Hysteria2Config `json:"-"` } type _CoreConfig CoreConfig @@ -25,6 +26,9 @@ func (c *CoreConfig) UnmarshalJSON(b []byte) error { case "sing": c.SingConfig = NewSingConfig() return json.Unmarshal(b, c.SingConfig) + case "hysteria2": + c.Hysteria2Config = NewHysteria2Config() + return json.Unmarshal(b, c.Hysteria2Config) } return nil } diff --git a/conf/hy.go b/conf/hy.go new file mode 100644 index 0000000..3355b5c --- /dev/null +++ b/conf/hy.go @@ -0,0 +1,100 @@ +package conf + +import "time" + +type Hysteria2Config struct { + LogConfig Hysteria2LogConfig `json:"Log"` +} + +type Hysteria2LogConfig struct { + Level string `json:"Level"` +} + +func NewHysteria2Config() *Hysteria2Config { + return &Hysteria2Config{ + LogConfig: Hysteria2LogConfig{ + Level: "error", + }, + } +} + +type Hysteria2Options struct { + QUICConfig QUICConfig `json:"QUICConfig"` + Outbounds []Outbounds `json:"Outbounds"` + IgnoreClientBandwidth bool `json:"IgnoreClientBandwidth"` + DisableUDP bool `json:"DisableUDP"` + UDPIdleTimeout time.Duration `json:"UDPIdleTimeout"` + Masquerade serverConfigMasquerade `json:"masquerade"` +} + +type QUICConfig struct { + InitialStreamReceiveWindow uint64 + MaxStreamReceiveWindow uint64 + InitialConnectionReceiveWindow uint64 + MaxConnectionReceiveWindow uint64 + MaxIdleTimeout time.Duration + MaxIncomingStreams int64 + DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. +} + +type ServerConfigOutboundDirect struct { + Mode string `json:"mode"` + BindIPv4 string `json:"bindIPv4"` + BindIPv6 string `json:"bindIPv6"` + BindDevice string `json:"bindDevice"` +} + +type ServerConfigOutboundSOCKS5 struct { + Addr string `json:"addr"` + Username string `json:"username"` + Password string `json:"password"` +} + +type ServerConfigOutboundHTTP struct { + URL string `json:"url"` + Insecure bool `json:"insecure"` +} + +type Outbounds struct { + Name string `json:"name"` + Type string `json:"type"` + Direct ServerConfigOutboundDirect `json:"direct"` + SOCKS5 ServerConfigOutboundSOCKS5 `json:"socks5"` + HTTP ServerConfigOutboundHTTP `json:"http"` +} + +type serverConfigMasqueradeFile struct { + Dir string `json:"dir"` +} + +type serverConfigMasqueradeProxy struct { + URL string `json:"url"` + RewriteHost bool `json:"rewriteHost"` +} + +type serverConfigMasqueradeString struct { + Content string `json:"content"` + Headers map[string]string `json:"headers"` + StatusCode int `json:"statusCode"` +} + +type serverConfigMasquerade struct { + Type string `json:"type"` + File serverConfigMasqueradeFile `json:"file"` + Proxy serverConfigMasqueradeProxy `json:"proxy"` + String serverConfigMasqueradeString `json:"string"` + ListenHTTP string `json:"listenHTTP"` + ListenHTTPS string `json:"listenHTTPS"` + ForceHTTPS bool `json:"forceHTTPS"` +} + +func NewHysteria2Options() *Hysteria2Options { + return &Hysteria2Options{ + QUICConfig: QUICConfig{}, + Outbounds: []Outbounds{}, + IgnoreClientBandwidth: false, + DisableUDP: false, + UDPIdleTimeout: time.Duration(time.Duration.Seconds(30)), + Masquerade: serverConfigMasquerade{}, + } +} diff --git a/conf/node.go b/conf/node.go index 6e443bc..5eb5ac9 100644 --- a/conf/node.go +++ b/conf/node.go @@ -103,17 +103,18 @@ func (n *NodeConfig) UnmarshalJSON(data []byte) (err error) { } type Options struct { - Name string `json:"Name"` - Core string `json:"Core"` - CoreName string `json:"CoreName"` - ListenIP string `json:"ListenIP"` - SendIP string `json:"SendIP"` - DeviceOnlineMinTraffic int64 `json:"DeviceOnlineMinTraffic"` - LimitConfig LimitConfig `json:"LimitConfig"` - RawOptions json.RawMessage `json:"RawOptions"` - XrayOptions *XrayOptions `json:"XrayOptions"` - SingOptions *SingOptions `json:"SingOptions"` - CertConfig *CertConfig `json:"CertConfig"` + Name string `json:"Name"` + Core string `json:"Core"` + CoreName string `json:"CoreName"` + ListenIP string `json:"ListenIP"` + SendIP string `json:"SendIP"` + DeviceOnlineMinTraffic int64 `json:"DeviceOnlineMinTraffic"` + LimitConfig LimitConfig `json:"LimitConfig"` + RawOptions json.RawMessage `json:"RawOptions"` + XrayOptions *XrayOptions `json:"XrayOptions"` + SingOptions *SingOptions `json:"SingOptions"` + Hysteria2Options *Hysteria2Options `json:"Hysteria2Options"` + CertConfig *CertConfig `json:"CertConfig"` } func (o *Options) UnmarshalJSON(data []byte) error { @@ -129,6 +130,9 @@ func (o *Options) UnmarshalJSON(data []byte) error { case "sing": o.SingOptions = NewSingOptions() return json.Unmarshal(data, o.SingOptions) + case "hysteria2": + o.Hysteria2Options = NewHysteria2Options() + return json.Unmarshal(data, o.Hysteria2Options) default: o.Core = "" o.RawOptions = data diff --git a/core/hy2/config.go b/core/hy2/config.go new file mode 100644 index 0000000..f35e1f2 --- /dev/null +++ b/core/hy2/config.go @@ -0,0 +1,316 @@ +package hy2 + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "strings" + "time" + + "github.com/InazumaV/V2bX/api/panel" + "github.com/InazumaV/V2bX/conf" + "github.com/apernet/hysteria/core/server" + "github.com/apernet/hysteria/extras/correctnet" + "github.com/apernet/hysteria/extras/masq" + "github.com/apernet/hysteria/extras/obfs" + "github.com/apernet/hysteria/extras/outbounds" + "go.uber.org/zap" +) + +type masqHandlerLogWrapper struct { + H http.Handler + QUIC bool + Logger *zap.Logger +} + +func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { + m.Logger.Debug("masquerade request", + zap.String("addr", r.RemoteAddr), + zap.String("method", r.Method), + zap.String("host", r.Host), + zap.String("url", r.URL.String()), + zap.Bool("quic", m.QUIC)) + m.H.ServeHTTP(w, r) +} + +const ( + Byte = 1 + Kilobyte = Byte * 1000 + Megabyte = Kilobyte * 1000 + Gigabyte = Megabyte * 1000 + Terabyte = Gigabyte * 1000 +) + +const ( + defaultStreamReceiveWindow = 8388608 // 8MB + defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB + defaultMaxIdleTimeout = 30 * time.Second + defaultMaxIncomingStreams = 1024 + defaultUDPIdleTimeout = 60 * time.Second +) + +func (n *Hysteria2node) getTLSConfig(config *conf.Options) (*server.TLSConfig, error) { + if config.CertConfig == nil { + return nil, fmt.Errorf("the CertConfig is not vail") + } + switch config.CertConfig.CertMode { + case "none", "": + return nil, fmt.Errorf("the CertMode cannot be none") + default: + var certs []tls.Certificate + cert, err := tls.LoadX509KeyPair(config.CertConfig.CertFile, config.CertConfig.KeyFile) + if err != nil { + return nil, err + } + certs = append(certs, cert) + return &server.TLSConfig{ + Certificates: certs, + GetCertificate: func(tlsinfo *tls.ClientHelloInfo) (*tls.Certificate, error) { + cert, err := tls.LoadX509KeyPair(config.CertConfig.CertFile, config.CertConfig.KeyFile) + return &cert, err + }, + }, nil + } +} + +func (n *Hysteria2node) getQUICConfig(config *conf.Options) (*server.QUICConfig, error) { + quic := &server.QUICConfig{} + if config.Hysteria2Options.QUICConfig.InitialStreamReceiveWindow == 0 { + quic.InitialStreamReceiveWindow = defaultStreamReceiveWindow + } else if config.Hysteria2Options.QUICConfig.InitialStreamReceiveWindow < 16384 { + return nil, fmt.Errorf("QUICConfig.InitialStreamReceiveWindowf must be at least 16384") + } + if config.Hysteria2Options.QUICConfig.MaxStreamReceiveWindow == 0 { + quic.MaxStreamReceiveWindow = defaultStreamReceiveWindow + } else if config.Hysteria2Options.QUICConfig.MaxStreamReceiveWindow < 16384 { + return nil, fmt.Errorf("QUICConfig.MaxStreamReceiveWindowf must be at least 16384") + } + if config.Hysteria2Options.QUICConfig.InitialConnectionReceiveWindow == 0 { + quic.InitialConnectionReceiveWindow = defaultConnReceiveWindow + } else if config.Hysteria2Options.QUICConfig.InitialConnectionReceiveWindow < 16384 { + return nil, fmt.Errorf("QUICConfig.InitialConnectionReceiveWindowf must be at least 16384") + } + if config.Hysteria2Options.QUICConfig.MaxConnectionReceiveWindow == 0 { + quic.MaxConnectionReceiveWindow = defaultConnReceiveWindow + } else if config.Hysteria2Options.QUICConfig.MaxConnectionReceiveWindow < 16384 { + return nil, fmt.Errorf("QUICConfig.MaxConnectionReceiveWindowf must be at least 16384") + } + if config.Hysteria2Options.QUICConfig.MaxIdleTimeout == 0 { + quic.MaxIdleTimeout = defaultMaxIdleTimeout + } else if config.Hysteria2Options.QUICConfig.MaxIdleTimeout < 4*time.Second || config.Hysteria2Options.QUICConfig.MaxIdleTimeout > 120*time.Second { + return nil, fmt.Errorf("QUICConfig.MaxIdleTimeoutf must be between 4s and 120s") + } + if config.Hysteria2Options.QUICConfig.MaxIncomingStreams == 0 { + quic.MaxIncomingStreams = defaultMaxIncomingStreams + } else if config.Hysteria2Options.QUICConfig.MaxIncomingStreams < 8 { + return nil, fmt.Errorf("QUICConfig.MaxIncomingStreamsf must be at least 8") + } + // todo fix !linux && !windows && !darwin + quic.DisablePathMTUDiscovery = false + + return quic, nil +} +func (n *Hysteria2node) getConn(info *panel.NodeInfo, config *conf.Options) (net.PacketConn, error) { + uAddr, err := net.ResolveUDPAddr("udp", formatAddress(config.ListenIP, info.Common.ServerPort)) + if err != nil { + return nil, err + } + conn, err := correctnet.ListenUDP("udp", uAddr) + if err != nil { + return nil, err + } + switch strings.ToLower(info.Hysteria2.ObfsType) { + case "", "plain": + return conn, nil + case "salamander": + ob, err := obfs.NewSalamanderObfuscator([]byte(info.Hysteria2.ObfsPassword)) + if err != nil { + return nil, err + } + return obfs.WrapPacketConn(conn, ob), nil + default: + return nil, fmt.Errorf("unsupported obfuscation type") + } +} + +func (n *Hysteria2node) getBandwidthConfig(info *panel.NodeInfo) *server.BandwidthConfig { + band := &server.BandwidthConfig{} + if info.Hysteria2.UpMbps != 0 { + band.MaxTx = (uint64)(info.Hysteria2.UpMbps * Megabyte / 8) + } + if info.Hysteria2.DownMbps != 0 { + band.MaxRx = (uint64)(info.Hysteria2.DownMbps * Megabyte / 8) + + } + return band +} + +func (n *Hysteria2node) getOutboundConfig(config *conf.Options) (server.Outbound, error) { + var obs []outbounds.OutboundEntry + if len(config.Hysteria2Options.Outbounds) == 0 { + // Guarantee we have at least one outbound + obs = []outbounds.OutboundEntry{{ + Name: "default", + Outbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto), + }} + } else { + obs = make([]outbounds.OutboundEntry, len(config.Hysteria2Options.Outbounds)) + for i, entry := range config.Hysteria2Options.Outbounds { + if entry.Name == "" { + return nil, fmt.Errorf("outbounds.name empty outbound name") + } + var ob outbounds.PluggableOutbound + var err error + switch strings.ToLower(entry.Type) { + case "direct": + ob, err = serverConfigOutboundDirectToOutbound(entry.Direct) + case "socks5": + ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5) + case "http": + ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP) + default: + err = fmt.Errorf("outbounds.type unsupported outbound type") + } + if err != nil { + return nil, err + } + obs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob} + } + } + var uOb outbounds.PluggableOutbound // "unified" outbound + + hasACL := false + if hasACL { + // todo fix ACL + } else { + // No ACL, use the first outbound + uOb = obs[0].Outbound + } + Outbound := &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb} + + return Outbound, nil +} + +func (n *Hysteria2node) getMasqHandler(tlsconfig *server.TLSConfig, conn net.PacketConn, info *panel.NodeInfo, config *conf.Options) (http.Handler, error) { + var handler http.Handler + switch strings.ToLower(config.Hysteria2Options.Masquerade.Type) { + case "", "404": + handler = http.NotFoundHandler() + case "file": + if config.Hysteria2Options.Masquerade.File.Dir == "" { + return nil, fmt.Errorf("masquerade.file.dir empty file directory") + } + handler = http.FileServer(http.Dir(config.Hysteria2Options.Masquerade.File.Dir)) + case "proxy": + if config.Hysteria2Options.Masquerade.Proxy.URL == "" { + return nil, fmt.Errorf("masquerade.proxy.url empty proxy url") + } + u, err := url.Parse(config.Hysteria2Options.Masquerade.Proxy.URL) + if err != nil { + return nil, fmt.Errorf(fmt.Sprintf("masquerade.proxy.url %s", err)) + } + handler = &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(u) + // SetURL rewrites the Host header, + // but we don't want that if rewriteHost is false + if !config.Hysteria2Options.Masquerade.Proxy.RewriteHost { + r.Out.Host = r.In.Host + } + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + n.Logger.Error("HTTP reverse proxy error", zap.Error(err)) + w.WriteHeader(http.StatusBadGateway) + }, + } + case "string": + if config.Hysteria2Options.Masquerade.String.Content == "" { + return nil, fmt.Errorf("masquerade.string.content empty string content") + } + if config.Hysteria2Options.Masquerade.String.StatusCode != 0 && + (config.Hysteria2Options.Masquerade.String.StatusCode < 200 || + config.Hysteria2Options.Masquerade.String.StatusCode > 599 || + config.Hysteria2Options.Masquerade.String.StatusCode == 233) { + // 233 is reserved for Hysteria authentication + return nil, fmt.Errorf("masquerade.string.statusCode invalid status code (must be 200-599, except 233)") + } + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for k, v := range config.Hysteria2Options.Masquerade.String.Headers { + w.Header().Set(k, v) + } + if config.Hysteria2Options.Masquerade.String.StatusCode != 0 { + w.WriteHeader(config.Hysteria2Options.Masquerade.String.StatusCode) + } else { + w.WriteHeader(http.StatusOK) // Use 200 OK by default + } + _, _ = w.Write([]byte(config.Hysteria2Options.Masquerade.String.Content)) + }) + default: + return nil, fmt.Errorf("masquerade.type unsupported masquerade type") + } + MasqHandler := &masqHandlerLogWrapper{H: handler, QUIC: true, Logger: n.Logger} + + if config.Hysteria2Options.Masquerade.ListenHTTP != "" || config.Hysteria2Options.Masquerade.ListenHTTPS != "" { + if config.Hysteria2Options.Masquerade.ListenHTTP != "" && config.Hysteria2Options.Masquerade.ListenHTTPS == "" { + return nil, fmt.Errorf("masquerade.listenHTTPS having only HTTP server without HTTPS is not supported") + } + s := masq.MasqTCPServer{ + QUICPort: extractPortFromAddr(conn.LocalAddr().String()), + HTTPSPort: extractPortFromAddr(config.Hysteria2Options.Masquerade.ListenHTTPS), + Handler: &masqHandlerLogWrapper{H: handler, QUIC: false}, + TLSConfig: &tls.Config{ + Certificates: tlsconfig.Certificates, + GetCertificate: tlsconfig.GetCertificate, + }, + ForceHTTPS: config.Hysteria2Options.Masquerade.ForceHTTPS, + } + go runMasqTCPServer(&s, config.Hysteria2Options.Masquerade.ListenHTTP, config.Hysteria2Options.Masquerade.ListenHTTPS, n.Logger) + } + + return MasqHandler, nil +} + +func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string, logger *zap.Logger) { + errChan := make(chan error, 2) + if httpAddr != "" { + go func() { + logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr)) + errChan <- s.ListenAndServeHTTP(httpAddr) + }() + } + if httpsAddr != "" { + go func() { + logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr)) + errChan <- s.ListenAndServeHTTPS(httpsAddr) + }() + } + err := <-errChan + if err != nil { + logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err)) + } +} + +func extractPortFromAddr(addr string) int { + _, portStr, err := net.SplitHostPort(addr) + if err != nil { + return 0 + } + port, err := strconv.Atoi(portStr) + if err != nil { + return 0 + } + return port +} + +func formatAddress(ip string, port int) string { + // 检查 IP 地址是否为 IPv6 + if strings.Contains(ip, ":") { + return fmt.Sprintf("[%s]:%d", ip, port) + } + // 对于 IPv4 地址,直接返回 IP:Port 格式 + return fmt.Sprintf("%s:%d", ip, port) +} diff --git a/core/hy2/hook.go b/core/hy2/hook.go new file mode 100644 index 0000000..ec0d96b --- /dev/null +++ b/core/hy2/hook.go @@ -0,0 +1,26 @@ +package hy2 + +import ( + "sync" + + "github.com/InazumaV/V2bX/common/counter" +) + +type HookServer struct { + Tag string + Counter sync.Map +} + +func (h *HookServer) Log(id string, tx, rx uint64) (ok bool) { + if c, ok := h.Counter.Load(h.Tag); ok { + c.(*counter.TrafficCounter).Rx(id, int(rx)) + c.(*counter.TrafficCounter).Tx(id, int(rx)) + return true + } else { + c := counter.NewTrafficCounter() + h.Counter.Store(h.Tag, c) + c.Rx(id, int(rx)) + c.Tx(id, int(rx)) + return true + } +} diff --git a/core/hy2/hy2.go b/core/hy2/hy2.go new file mode 100644 index 0000000..1b96003 --- /dev/null +++ b/core/hy2/hy2.go @@ -0,0 +1,61 @@ +package hy2 + +import ( + "github.com/InazumaV/V2bX/conf" + vCore "github.com/InazumaV/V2bX/core" + "go.uber.org/zap" +) + +var _ vCore.Core = (*Hysteria2)(nil) + +type Hysteria2 struct { + Hy2nodes map[string]Hysteria2node + Auth *V2bX + Logger *zap.Logger +} + +func init() { + vCore.RegisterCore("hysteria2", New) +} + +func New(c *conf.CoreConfig) (vCore.Core, error) { + loglever := "error" + if c.Hysteria2Config.LogConfig.Level != "" { + loglever = c.Hysteria2Config.LogConfig.Level + } + log, err := initLogger(loglever, "console") + if err != nil { + return nil, err + } + return &Hysteria2{ + Hy2nodes: make(map[string]Hysteria2node), + Auth: &V2bX{ + usersMap: make(map[string]int), + }, + Logger: log, + }, nil +} + +func (h *Hysteria2) Protocols() []string { + return []string{ + "hysteria2", + } +} + +func (h *Hysteria2) Start() error { + return nil +} + +func (h *Hysteria2) Close() error { + for _, n := range h.Hy2nodes { + err := n.Hy2server.Close() + if err != nil { + return err + } + } + return nil +} + +func (h *Hysteria2) Type() string { + return "hysteria2" +} diff --git a/core/hy2/logger.go b/core/hy2/logger.go new file mode 100644 index 0000000..1165feb --- /dev/null +++ b/core/hy2/logger.go @@ -0,0 +1,137 @@ +package hy2 + +import ( + "fmt" + "net" + "strings" + + "github.com/InazumaV/V2bX/limiter" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type serverLogger struct { + Tag string + logger *zap.Logger +} + +var logLevelMap = map[string]zapcore.Level{ + "debug": zapcore.DebugLevel, + "info": zapcore.InfoLevel, + "warn": zapcore.WarnLevel, + "error": zapcore.ErrorLevel, +} + +var logFormatMap = map[string]zapcore.EncoderConfig{ + "console": { + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + MessageKey: "msg", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalColorLevelEncoder, + EncodeTime: zapcore.RFC3339TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + }, + "json": { + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + MessageKey: "msg", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.EpochMillisTimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + }, +} + +func (l *serverLogger) Connect(addr net.Addr, uuid string, tx uint64) { + limiter, err := limiter.GetLimiter(l.Tag) + if err != nil { + l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err)) + } + if _, r := limiter.CheckLimit(uuid, extractIPFromAddr(addr), addr.Network() == "tcp"); r { + l.logger.Warn("Need Reject", zap.String("addr", addr.String()), zap.String("uuid", uuid)) + } + l.logger.Info("client connected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint64("tx", tx)) +} + +func (l *serverLogger) Disconnect(addr net.Addr, uuid string, err error) { + l.logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Error(err)) +} + +func (l *serverLogger) TCPRequest(addr net.Addr, uuid, reqAddr string) { + limiter, err := limiter.GetLimiter(l.Tag) + if err != nil { + l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err)) + } + if _, r := limiter.CheckLimit(uuid, extractIPFromAddr(addr), addr.Network() == "tcp"); r { + l.logger.Warn("Need Reject", zap.String("addr", addr.String()), zap.String("uuid", uuid)) + } + l.logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr)) +} + +func (l *serverLogger) TCPError(addr net.Addr, uuid, reqAddr string, err error) { + if err == nil { + l.logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr)) + } else { + l.logger.Debug("TCP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.String("reqAddr", reqAddr), zap.Error(err)) + } +} + +func (l *serverLogger) UDPRequest(addr net.Addr, uuid string, sessionId uint32, reqAddr string) { + limiter, err := limiter.GetLimiter(l.Tag) + if err != nil { + l.logger.Panic("Get limiter error", zap.String("tag", l.Tag), zap.Error(err)) + } + if _, r := limiter.CheckLimit(uuid, extractIPFromAddr(addr), addr.Network() == "tcp"); r { + l.logger.Warn("Need Reject", zap.String("addr", addr.String()), zap.String("uuid", uuid)) + } + l.logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.String("reqAddr", reqAddr)) +} + +func (l *serverLogger) UDPError(addr net.Addr, uuid string, sessionId uint32, err error) { + if err == nil { + l.logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId)) + } else { + l.logger.Debug("UDP error", zap.String("addr", addr.String()), zap.String("uuid", uuid), zap.Uint32("sessionId", sessionId), zap.Error(err)) + } +} + +func initLogger(logLevel string, logFormat string) (*zap.Logger, error) { + level, ok := logLevelMap[strings.ToLower(logLevel)] + if !ok { + return nil, fmt.Errorf(fmt.Sprintf("unsupported log level: %s\n", logLevel)) + } + enc, ok := logFormatMap[strings.ToLower(logFormat)] + if !ok { + return nil, fmt.Errorf(fmt.Sprintf("unsupported log format: %s\n", logFormat)) + } + c := zap.Config{ + Level: zap.NewAtomicLevelAt(level), + DisableCaller: true, + DisableStacktrace: true, + Encoding: strings.ToLower(logFormat), + EncoderConfig: enc, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + logger, err := c.Build() + if err != nil { + return nil, fmt.Errorf(fmt.Sprintf("failed to initialize logger: %s\n", err)) + } + return logger, nil +} + +func extractIPFromAddr(addr net.Addr) string { + switch v := addr.(type) { + case *net.TCPAddr: + return v.IP.String() + case *net.UDPAddr: + return v.IP.String() + case *net.IPAddr: + return v.IP.String() + default: + return "" + } +} diff --git a/core/hy2/node.go b/core/hy2/node.go new file mode 100644 index 0000000..a4ab3fe --- /dev/null +++ b/core/hy2/node.go @@ -0,0 +1,92 @@ +package hy2 + +import ( + "github.com/InazumaV/V2bX/api/panel" + "github.com/InazumaV/V2bX/conf" + "github.com/apernet/hysteria/core/server" + "go.uber.org/zap" +) + +type Hysteria2node struct { + Hy2server server.Server + Tag string + Logger *zap.Logger + EventLogger server.EventLogger + TrafficLogger server.TrafficLogger +} + +func (n *Hysteria2node) getHyConfig(tag string, info *panel.NodeInfo, config *conf.Options) (*server.Config, error) { + tls, err := n.getTLSConfig(config) + if err != nil { + return nil, err + } + quic, err := n.getQUICConfig(config) + if err != nil { + return nil, err + } + conn, err := n.getConn(info, config) + if err != nil { + return nil, err + } + Outbound, err := n.getOutboundConfig(config) + if err != nil { + return nil, err + } + Masq, err := n.getMasqHandler(tls, conn, info, config) + if err != nil { + return nil, err + } + return &server.Config{ + TLSConfig: *tls, + QUICConfig: *quic, + Conn: conn, + Outbound: Outbound, + BandwidthConfig: *n.getBandwidthConfig(info), + IgnoreClientBandwidth: config.Hysteria2Options.IgnoreClientBandwidth, + DisableUDP: config.Hysteria2Options.DisableUDP, + UDPIdleTimeout: config.Hysteria2Options.UDPIdleTimeout, + EventLogger: n.EventLogger, + TrafficLogger: n.TrafficLogger, + MasqHandler: Masq, + }, nil +} + +func (h *Hysteria2) AddNode(tag string, info *panel.NodeInfo, config *conf.Options) error { + n := Hysteria2node{ + Tag: tag, + Logger: h.Logger, + EventLogger: &serverLogger{ + Tag: tag, + logger: h.Logger, + }, + TrafficLogger: &HookServer{ + Tag: tag, + }, + } + hyconfig, err := n.getHyConfig(tag, info, config) + if err != nil { + return err + } + hyconfig.Authenticator = h.Auth + s, err := server.NewServer(hyconfig) + if err != nil { + return err + } + n.Hy2server = s + h.Hy2nodes[tag] = n + go func() { + if err := s.Serve(); err != nil { + h.Logger.Error("Server Error", zap.Error(err)) + } + }() + return nil +} + +func (h *Hysteria2) DelNode(tag string) error { + err := h.Hy2nodes[tag].Hy2server.Close() + if err != nil { + return err + } + delete(h.Hy2nodes, tag) + return nil +} diff --git a/core/hy2/outbound.go b/core/hy2/outbound.go new file mode 100644 index 0000000..35b45b1 --- /dev/null +++ b/core/hy2/outbound.go @@ -0,0 +1,61 @@ +package hy2 + +import ( + "fmt" + "net" + "strings" + + "github.com/InazumaV/V2bX/conf" + "github.com/apernet/hysteria/extras/outbounds" +) + +func serverConfigOutboundDirectToOutbound(c conf.ServerConfigOutboundDirect) (outbounds.PluggableOutbound, error) { + var mode outbounds.DirectOutboundMode + switch strings.ToLower(c.Mode) { + case "", "auto": + mode = outbounds.DirectOutboundModeAuto + case "64": + mode = outbounds.DirectOutboundMode64 + case "46": + mode = outbounds.DirectOutboundMode46 + case "6": + mode = outbounds.DirectOutboundMode6 + case "4": + mode = outbounds.DirectOutboundMode4 + default: + return nil, fmt.Errorf("outbounds.direct.mode unsupported mode") + } + bindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0 + bindDevice := len(c.BindDevice) > 0 + if bindIP && bindDevice { + return nil, fmt.Errorf("outbounds.direct cannot bind both IP and device") + } + if bindIP { + ip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6) + if len(c.BindIPv4) > 0 && ip4 == nil { + return nil, fmt.Errorf("outbounds.direct.bindIPv4 invalid IPv4 address") + } + if len(c.BindIPv6) > 0 && ip6 == nil { + return nil, fmt.Errorf("outbounds.direct.bindIPv6 invalid IPv6 address") + } + return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6) + } + if bindDevice { + return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice) + } + return outbounds.NewDirectOutboundSimple(mode), nil +} + +func serverConfigOutboundSOCKS5ToOutbound(c conf.ServerConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) { + if c.Addr == "" { + return nil, fmt.Errorf("outbounds.socks5.addr empty socks5 address") + } + return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil +} + +func serverConfigOutboundHTTPToOutbound(c conf.ServerConfigOutboundHTTP) (outbounds.PluggableOutbound, error) { + if c.URL == "" { + return nil, fmt.Errorf("outbounds.http.url empty http address") + } + return outbounds.NewHTTPOutbound(c.URL, c.Insecure) +} diff --git a/core/hy2/user.go b/core/hy2/user.go new file mode 100644 index 0000000..3dd5808 --- /dev/null +++ b/core/hy2/user.go @@ -0,0 +1,70 @@ +package hy2 + +import ( + "net" + "sync" + + "github.com/InazumaV/V2bX/api/panel" + "github.com/InazumaV/V2bX/common/counter" + vCore "github.com/InazumaV/V2bX/core" + "github.com/apernet/hysteria/core/server" +) + +var _ server.Authenticator = &V2bX{} + +type V2bX struct { + usersMap map[string]int + mutex sync.Mutex +} + +func (v *V2bX) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + v.mutex.Lock() + defer v.mutex.Unlock() + if _, exists := v.usersMap[auth]; exists { + return true, auth + } + return false, "" +} + +func (h *Hysteria2) AddUsers(p *vCore.AddUsersParams) (added int, err error) { + var wg sync.WaitGroup + for _, user := range p.Users { + wg.Add(1) + go func(u panel.UserInfo) { + defer wg.Done() + h.Auth.mutex.Lock() + h.Auth.usersMap[u.Uuid] = u.Id + h.Auth.mutex.Unlock() + }(user) + } + wg.Wait() + return len(p.Users), nil +} + +func (h *Hysteria2) DelUsers(users []panel.UserInfo, tag string) error { + var wg sync.WaitGroup + for _, user := range users { + wg.Add(1) + go func(u panel.UserInfo) { + defer wg.Done() + h.Auth.mutex.Lock() + delete(h.Auth.usersMap, u.Uuid) + h.Auth.mutex.Unlock() + }(user) + } + wg.Wait() + return nil +} + +func (h *Hysteria2) GetUserTraffic(tag string, uuid string, reset bool) (up int64, down int64) { + if v, ok := h.Hy2nodes[tag].TrafficLogger.(*HookServer).Counter.Load(tag); ok { + c := v.(*counter.TrafficCounter) + up = c.GetUpCount(uuid) + down = c.GetDownCount(uuid) + if reset { + c.Reset(uuid) + } + return up, down + } + return 0, 0 +} diff --git a/core/imports/hy2.go b/core/imports/hy2.go new file mode 100644 index 0000000..6970f99 --- /dev/null +++ b/core/imports/hy2.go @@ -0,0 +1,5 @@ +//go:build hysteria2 + +package imports + +import _ "github.com/InazumaV/V2bX/core/hy2" diff --git a/go.mod b/go.mod index 8563cf6..906e02e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/InazumaV/V2bX go 1.21.4 require ( + github.com/apernet/hysteria/core v1.3.5-0.20240201034858-bb99579bb92c + github.com/apernet/hysteria/extras v0.0.0-20240201034858-bb99579bb92c github.com/beevik/ntp v1.2.0 github.com/fsnotify/fsnotify v1.7.0 github.com/go-acme/lego/v4 v4.13.2 @@ -15,6 +17,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/xtls/xray-core v1.8.8-0.20240125151013-25c531c6c358 + go.uber.org/zap v1.26.0 golang.org/x/crypto v0.18.0 golang.org/x/sys v0.16.0 google.golang.org/protobuf v1.32.0 @@ -47,7 +50,9 @@ require ( github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/andybalholm/brotli v1.0.6 // indirect + github.com/apernet/quic-go v0.41.1-0.20240122005439-5bf4609c416f // indirect github.com/aws/aws-sdk-go v1.39.0 // indirect + github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/caddyserver/certmagic v0.20.0 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect @@ -90,6 +95,7 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.4 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -182,6 +188,8 @@ require ( github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect github.com/transip/gotransip/v6 v6.20.0 // indirect + github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect + github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 // indirect github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c // indirect github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect @@ -196,7 +204,6 @@ require ( go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect - go.uber.org/zap v1.26.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect golang.org/x/mod v0.14.0 // indirect @@ -221,4 +228,5 @@ require ( lukechampine.com/blake3 v1.2.1 // indirect ) +//github.com/apernet/hysteria/core v1.3.5-0.20240201034858-bb99579bb92c => /root/hysteria/core replace github.com/sagernet/sing-box v1.8.2 => github.com/wyx2685/sing-box_mod v0.0.0 diff --git a/go.sum b/go.sum index 8ddadfd..1786ac5 100644 --- a/go.sum +++ b/go.sum @@ -81,11 +81,19 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apernet/hysteria/core v1.3.5-0.20240201034858-bb99579bb92c h1:rtlUELUe5VAh+4M8W3aX0Ia5Gez9/suH1m5mVpnZibc= +github.com/apernet/hysteria/core v1.3.5-0.20240201034858-bb99579bb92c/go.mod h1:AFHu72Vc6ctm7KwHd28EgIeK4v/FWNVzRJaeqs2dqvQ= +github.com/apernet/hysteria/extras v0.0.0-20240201034858-bb99579bb92c h1:w54no07c0oyuD0qNV2ZPs7CwQk/ATRAqRulR51LlPV0= +github.com/apernet/hysteria/extras v0.0.0-20240201034858-bb99579bb92c/go.mod h1:oH9DY1/YNQYYZARtCnSLJcJBkAv6e80WhkhyqXXBgd8= +github.com/apernet/quic-go v0.41.1-0.20240122005439-5bf4609c416f h1:4jBGc3SlgQT8YFqHhfnK7EVFVY292CxagfNqfPiQZhY= +github.com/apernet/quic-go v0.41.1-0.20240122005439-5bf4609c416f/go.mod h1:4GInxO6ypy63J2NaO5rQx1wRp6K8YHI6zqLG+VswU6I= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo= github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= +github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= github.com/beevik/ntp v1.2.0 h1:n1teVGbd4YM36FlGvWYfccBIdGzeaakHrTlo6RSL8mw= github.com/beevik/ntp v1.2.0/go.mod h1:vD6h1um4kzXpqmLTuu0cCLcC+NfvC0IC+ltmEDA8E78= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -356,6 +364,8 @@ github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -484,6 +494,7 @@ github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= @@ -764,6 +775,10 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip/v6 v6.20.0 h1:AuvwyOZ51f2brzMbTqlRy/wmaM3kF7Vx5Wds8xcDflY= github.com/transip/gotransip/v6 v6.20.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= +github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= @@ -819,8 +834,8 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -884,6 +899,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -920,6 +936,7 @@ golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= @@ -999,6 +1016,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1010,6 +1028,7 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= @@ -1024,6 +1043,7 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -1063,6 +1083,7 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= diff --git a/limiter/limiter.go b/limiter/limiter.go index 804363f..d6bc2ff 100644 --- a/limiter/limiter.go +++ b/limiter/limiter.go @@ -120,15 +120,15 @@ func (l *Limiter) UpdateDynamicSpeedLimit(tag, uuid string, limit int, expire ti return nil } -func (l *Limiter) CheckLimit(email string, ip string, isTcp bool) (Bucket *ratelimit.Bucket, Reject bool) { +func (l *Limiter) CheckLimit(uuid string, ip string, isTcp bool) (Bucket *ratelimit.Bucket, Reject bool) { // ip and conn limiter - if l.ConnLimiter.AddConnCount(email, ip, isTcp) { + if l.ConnLimiter.AddConnCount(uuid, ip, isTcp) { return nil, true } // check and gen speed limit Bucket nodeLimit := l.SpeedLimit userLimit := 0 - if v, ok := l.UserLimitInfo.Load(email); ok { + if v, ok := l.UserLimitInfo.Load(uuid); ok { u := v.(*UserLimitInfo) if u.ExpireTime < time.Now().Unix() && u.ExpireTime != 0 { if u.SpeedLimit != 0 { @@ -136,7 +136,7 @@ func (l *Limiter) CheckLimit(email string, ip string, isTcp bool) (Bucket *ratel u.DynamicSpeedLimit = 0 u.ExpireTime = 0 } else { - l.UserLimitInfo.Delete(email) + l.UserLimitInfo.Delete(uuid) } } else { userLimit = determineSpeedLimit(u.SpeedLimit, u.DynamicSpeedLimit) @@ -145,10 +145,10 @@ func (l *Limiter) CheckLimit(email string, ip string, isTcp bool) (Bucket *ratel // Store online user for device limit ipMap := new(sync.Map) - uid := l.UUIDtoUID[email] + uid := l.UUIDtoUID[uuid] ipMap.Store(ip, uid) // If any device is online - if v, ok := l.UserOnlineIP.LoadOrStore(email, ipMap); ok { + if v, ok := l.UserOnlineIP.LoadOrStore(uuid, ipMap); ok { ipMap := v.(*sync.Map) // If this is a new ip if _, ok := ipMap.LoadOrStore(ip, uid); !ok { @@ -163,10 +163,10 @@ func (l *Limiter) CheckLimit(email string, ip string, isTcp bool) (Bucket *ratel limit := int64(determineSpeedLimit(nodeLimit, userLimit)) * 1000000 / 8 // If you need the Speed limit if limit > 0 { Bucket = ratelimit.NewBucketWithQuantum(time.Second, limit, limit) // Byte/s - if v, ok := l.SpeedLimiter.LoadOrStore(email, Bucket); ok { + if v, ok := l.SpeedLimiter.LoadOrStore(uuid, Bucket); ok { return v.(*ratelimit.Bucket), false } else { - l.SpeedLimiter.Store(email, Bucket) + l.SpeedLimiter.Store(uuid, Bucket) return Bucket, false } } else {