diff --git a/api/config.go b/api/config.go index e5261a7..3cb09ea 100644 --- a/api/config.go +++ b/api/config.go @@ -54,7 +54,7 @@ func getConfig(ctx *gin.Context) { if ok && ss != nil { status := ss.Status() svc.Status = &config.ServiceStatus{ - CreateTime: status.CreateTime().UnixNano(), + CreateTime: status.CreateTime().Unix(), State: string(status.State()), } if st := status.Stats(); st != nil { @@ -69,7 +69,7 @@ func getConfig(ctx *gin.Context) { for _, ev := range status.Events() { if !ev.Time.IsZero() { svc.Status.Events = append(svc.Status.Events, config.ServiceEvent{ - Time: ev.Time.UnixNano(), + Time: ev.Time.Unix(), Msg: ev.Message, }) } diff --git a/config/config.go b/config/config.go index c511738..bb05bda 100644 --- a/config/config.go +++ b/config/config.go @@ -359,13 +359,15 @@ type ForwardNodeConfig struct { Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"` HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"` TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"` - Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"` - Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` + // DEPRECATED by HTTP.Auth + Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"` + Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"` } type HTTPNodeConfig struct { Host string `yaml:",omitempty" json:"host,omitempty"` Header map[string]string `yaml:",omitempty" json:"header,omitempty"` + Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"` } type TLSNodeConfig struct { diff --git a/config/parsing/node/parse.go b/config/parsing/node/parse.go index d1cc6ef..7f51e99 100644 --- a/config/parsing/node/parse.go +++ b/config/parsing/node/parse.go @@ -168,10 +168,28 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No chain.NetworkNodeOption(cfg.Network), } if cfg.HTTP != nil { - opts = append(opts, chain.HTTPNodeOption(&chain.HTTPNodeSettings{ + settings := &chain.HTTPNodeSettings{ Host: cfg.HTTP.Host, Header: cfg.HTTP.Header, - })) + } + + auth := cfg.HTTP.Auth + if auth == nil { + auth = cfg.Auth + } + if auth != nil { + settings.Auther = xauth.NewAuthenticator( + xauth.AuthsOption(map[string]string{auth.Username: auth.Password}), + xauth.LoggerOption(log.WithFields(map[string]any{ + "kind": "node", + "node": cfg.Name, + "addr": cfg.Addr, + "host": cfg.Host, + "protocol": cfg.Protocol, + })), + ) + } + opts = append(opts, chain.HTTPNodeOption(settings)) } if cfg.TLS != nil { tlsCfg := &chain.TLSNodeSettings{ @@ -185,18 +203,5 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No } opts = append(opts, chain.TLSNodeOption(tlsCfg)) } - if cfg.Auth != nil { - opts = append(opts, chain.AutherNodeOption( - xauth.NewAuthenticator( - xauth.AuthsOption(map[string]string{cfg.Auth.Username: cfg.Auth.Password}), - xauth.LoggerOption(logger.Default().WithFields(map[string]any{ - "kind": "node", - "node": cfg.Name, - "addr": cfg.Addr, - "host": cfg.Host, - "protocol": cfg.Protocol, - })), - ))) - } return chain.NewNode(cfg.Name, cfg.Addr, opts...), nil } diff --git a/connector/tunnel/bind.go b/connector/tunnel/bind.go index 9370184..b9f5164 100644 --- a/connector/tunnel/bind.go +++ b/connector/tunnel/bind.go @@ -27,7 +27,8 @@ func (c *tunnelConnector) Bind(ctx context.Context, conn net.Conn, network, addr "endpoint": endpoint, "tunnel": c.md.tunnelID.String(), }) - log.Infof("create tunnel on %s/%s OK, tunnel=%s, connector=%s", addr, network, c.md.tunnelID.String(), cid) + log.Infof("create tunnel on %s/%s OK, tunnel=%s, connector=%s, weight=%d", + addr, network, c.md.tunnelID.String(), cid, cid.Weight()) session, err := mux.ServerSession(conn, c.md.muxCfg) if err != nil { @@ -72,7 +73,7 @@ func (c *tunnelConnector) initTunnel(conn net.Conn, network, address string) (ad req.Features = append(req.Features, af) // dst address req.Features = append(req.Features, &relay.TunnelFeature{ - ID: c.md.tunnelID.ID(), + ID: c.md.tunnelID, }) if _, err = req.WriteTo(conn); err != nil { return @@ -100,7 +101,7 @@ func (c *tunnelConnector) initTunnel(conn net.Conn, network, address string) (ad } case relay.FeatureTunnel: if feature, _ := f.(*relay.TunnelFeature); feature != nil { - cid = relay.NewConnectorID(feature.ID[:]) + cid = feature.ID } } } diff --git a/connector/tunnel/connector.go b/connector/tunnel/connector.go index 8a1c6dc..2b4eb95 100644 --- a/connector/tunnel/connector.go +++ b/connector/tunnel/connector.go @@ -86,7 +86,7 @@ func (c *tunnelConnector) Connect(ctx context.Context, conn net.Conn, network, a req.Features = append(req.Features, af) // dst address req.Features = append(req.Features, &relay.TunnelFeature{ - ID: c.md.tunnelID.ID(), + ID: c.md.tunnelID, }) if _, err := req.WriteTo(conn); err != nil { diff --git a/connector/tunnel/metadata.go b/connector/tunnel/metadata.go index d678b62..5163293 100644 --- a/connector/tunnel/metadata.go +++ b/connector/tunnel/metadata.go @@ -40,6 +40,10 @@ func (c *tunnelConnector) parseMetadata(md mdata.Metadata) (err error) { c.md.tunnelID = relay.NewTunnelID(uuid[:]) } + if weight := mdutil.GetInt(md, "tunnel.weight"); weight > 0 { + c.md.tunnelID = c.md.tunnelID.SetWeight(uint8(weight)) + } + c.md.muxCfg = &mux.Config{ Version: mdutil.GetInt(md, "mux.version"), KeepAliveInterval: mdutil.GetDuration(md, "mux.keepaliveInterval"), diff --git a/go.mod b/go.mod index cb0e2a0..fb19f2e 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/gin-contrib/cors v1.5.0 github.com/gin-gonic/gin v1.9.1 - github.com/go-gost/core v0.0.0-20240103125300-5a427b4eaf99 + github.com/go-gost/core v0.0.0-20240127130604-04314fa08476 github.com/go-gost/gosocks4 v0.0.1 github.com/go-gost/gosocks5 v0.4.0 github.com/go-gost/plugin v0.0.0-20240103125338-9c84e29cb81a - github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 + github.com/go-gost/relay v0.4.1-0.20240128081525-e36d5f4a8322 github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 github.com/go-redis/redis/v8 v8.11.5 github.com/gobwas/glob v0.2.3 @@ -23,7 +23,7 @@ require ( github.com/pion/dtls/v2 v2.2.6 github.com/pires/go-proxyproto v0.7.0 github.com/prometheus/client_golang v1.17.0 - github.com/quic-go/quic-go v0.40.0 + github.com/quic-go/quic-go v0.40.1 github.com/quic-go/webtransport-go v0.6.0 github.com/refraction-networking/utls v1.5.4 github.com/rs/xid v1.3.0 diff --git a/go.sum b/go.sum index 365c584..ef09d24 100644 --- a/go.sum +++ b/go.sum @@ -49,16 +49,16 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-gost/core v0.0.0-20240103125300-5a427b4eaf99 h1:/0hmilnQBEDlOaRcO+TlwaHH8a5ig6nc2aAsU4FGZcw= -github.com/go-gost/core v0.0.0-20240103125300-5a427b4eaf99/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= +github.com/go-gost/core v0.0.0-20240127130604-04314fa08476 h1:4TA4ErfFw2CsVv5K5oqqYpUn68aZFrV+ONb4aGPJ1QQ= +github.com/go-gost/core v0.0.0-20240127130604-04314fa08476/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= github.com/go-gost/gosocks4 v0.0.1 h1:+k1sec8HlELuQV7rWftIkmy8UijzUt2I6t+iMPlGB2s= github.com/go-gost/gosocks4 v0.0.1/go.mod h1:3B6L47HbU/qugDg4JnoFPHgJXE43Inz8Bah1QaN9qCc= github.com/go-gost/gosocks5 v0.4.0 h1:EIrOEkpJez4gwHrMa33frA+hHXJyevjp47thpMQsJzI= github.com/go-gost/gosocks5 v0.4.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= github.com/go-gost/plugin v0.0.0-20240103125338-9c84e29cb81a h1:ME7P1Brcg4C640DSPqlvQr7JuvvQfJ8QpmS3yCFlK3A= github.com/go-gost/plugin v0.0.0-20240103125338-9c84e29cb81a/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw= -github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 h1:qAG1OyjvdA5h221CfFSS3J359V3d2E7dJWyP29QoDSI= -github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8= +github.com/go-gost/relay v0.4.1-0.20240128081525-e36d5f4a8322 h1:R2a+Lx6XVvWdskGUUjteJ62WYBAskDHySgqNC6y8dI8= +github.com/go-gost/relay v0.4.1-0.20240128081525-e36d5f4a8322/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8= github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 h1:xj8gUZGYO3nb5+6Bjw9+tsFkA9sYynrOvDvvC4uDV2I= github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= @@ -182,8 +182,8 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw= -github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= +github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q= +github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY= github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= github.com/refraction-networking/utls v1.5.4/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw= diff --git a/handler/forward/local/handler.go b/handler/forward/local/handler.go index e3b743a..cc9e3f0 100644 --- a/handler/forward/local/handler.go +++ b/handler/forward/local/handler.go @@ -247,18 +247,19 @@ func (h *forwardHandler) handleHTTP(ctx context.Context, rw io.ReadWriter, remot }) log.Debugf("find node for host %s -> %s(%s)", req.Host, target.Name, target.Addr) - if auther := target.Options().Auther; auther != nil { - username, password, _ := req.BasicAuth() - id, ok := auther.Authenticate(ctx, username, password) - if !ok { - resp.StatusCode = http.StatusUnauthorized - resp.Header.Set("WWW-Authenticate", "Basic") - log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) - return resp.Write(rw) - } - ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) - } if httpSettings := target.Options().HTTP; httpSettings != nil { + if auther := httpSettings.Auther; auther != nil { + username, password, _ := req.BasicAuth() + id, ok := auther.Authenticate(ctx, username, password) + if !ok { + resp.StatusCode = http.StatusUnauthorized + resp.Header.Set("WWW-Authenticate", "Basic") + log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) + return resp.Write(rw) + } + ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) + } + if httpSettings.Host != "" { req.Host = httpSettings.Host } diff --git a/handler/forward/remote/handler.go b/handler/forward/remote/handler.go index ea92caa..6766805 100644 --- a/handler/forward/remote/handler.go +++ b/handler/forward/remote/handler.go @@ -248,18 +248,18 @@ func (h *forwardHandler) handleHTTP(ctx context.Context, rw io.ReadWriter, remot }) log.Debugf("find node for host %s -> %s(%s)", req.Host, target.Name, target.Addr) - if auther := target.Options().Auther; auther != nil { - username, password, _ := req.BasicAuth() - id, ok := auther.Authenticate(ctx, username, password) - if !ok { - resp.StatusCode = http.StatusUnauthorized - resp.Header.Set("WWW-Authenticate", "Basic") - log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) - return resp.Write(rw) - } - ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) - } if httpSettings := target.Options().HTTP; httpSettings != nil { + if auther := httpSettings.Auther; auther != nil { + username, password, _ := req.BasicAuth() + id, ok := auther.Authenticate(ctx, username, password) + if !ok { + resp.StatusCode = http.StatusUnauthorized + resp.Header.Set("WWW-Authenticate", "Basic") + log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) + return resp.Write(rw) + } + ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) + } if httpSettings.Host != "" { req.Host = httpSettings.Host } diff --git a/handler/tunnel/bind.go b/handler/tunnel/bind.go index 188691b..aa76849 100644 --- a/handler/tunnel/bind.go +++ b/handler/tunnel/bind.go @@ -30,6 +30,8 @@ func (h *tunnelHandler) handleBind(ctx context.Context, conn net.Conn, network, if network == "udp" { connectorID = relay.NewUDPConnectorID(uuid[:]) } + // copy weight from tunnelID + connectorID = connectorID.SetWeight(tunnelID.Weight()) v := md5.Sum([]byte(tunnelID.String())) endpoint := hex.EncodeToString(v[:8]) @@ -47,7 +49,7 @@ func (h *tunnelHandler) handleBind(ctx context.Context, conn net.Conn, network, } resp.Features = append(resp.Features, af, &relay.TunnelFeature{ - ID: connectorID.ID(), + ID: connectorID, }, ) resp.WriteTo(conn) @@ -84,7 +86,7 @@ func (h *tunnelHandler) handleBind(ctx context.Context, conn net.Conn, network, } } - log.Debugf("%s/%s: tunnel=%s, connector=%s established", addr, network, tunnelID, connectorID) + log.Debugf("%s/%s: tunnel=%s, connector=%s, weight=%d established", addr, network, tunnelID, connectorID, connectorID.Weight()) return } diff --git a/handler/tunnel/handler.go b/handler/tunnel/handler.go index af385e4..0d40624 100644 --- a/handler/tunnel/handler.go +++ b/handler/tunnel/handler.go @@ -216,7 +216,7 @@ func (h *tunnelHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl } case relay.FeatureTunnel: if feature, _ := f.(*relay.TunnelFeature); feature != nil { - tunnelID = relay.NewTunnelID(feature.ID[:]) + tunnelID = feature.ID } case relay.FeatureNetwork: if feature, _ := f.(*relay.NetworkFeature); feature != nil { diff --git a/handler/tunnel/tunnel.go b/handler/tunnel/tunnel.go index b71f6eb..7d3d432 100644 --- a/handler/tunnel/tunnel.go +++ b/handler/tunnel/tunnel.go @@ -3,16 +3,20 @@ package tunnel import ( "context" "sync" - "sync/atomic" "time" "github.com/go-gost/core/logger" "github.com/go-gost/core/sd" "github.com/go-gost/relay" "github.com/go-gost/x/internal/util/mux" + "github.com/go-gost/x/selector" "github.com/google/uuid" ) +const ( + MaxWeight uint8 = 0xff +) + type Connector struct { id relay.ConnectorID tid relay.TunnelID @@ -67,11 +71,11 @@ type Tunnel struct { id relay.TunnelID connectors []*Connector t time.Time - n uint64 close chan struct{} mu sync.RWMutex sd sd.SD ttl time.Duration + rw *selector.RandomWeighted[*Connector] } func NewTunnel(node string, tid relay.TunnelID, ttl time.Duration) *Tunnel { @@ -81,6 +85,7 @@ func NewTunnel(node string, tid relay.TunnelID, ttl time.Duration) *Tunnel { t: time.Now(), close: make(chan struct{}), ttl: ttl, + rw: selector.NewRandomWeighted[*Connector](), } if t.ttl <= 0 { t.ttl = defaultTTL @@ -112,21 +117,34 @@ func (t *Tunnel) GetConnector(network string) *Connector { t.mu.RLock() defer t.mu.RUnlock() - var connectors []*Connector + rw := t.rw + rw.Reset() + + found := false for _, c := range t.connectors { if c.Session().IsClosed() { continue } + + weight := c.ID().Weight() + if weight == 0 { + weight = 1 + } + if network == "udp" && c.id.IsUDP() || network != "udp" && !c.id.IsUDP() { - connectors = append(connectors, c) + if weight == MaxWeight && !found { + rw.Reset() + found = true + } + + if weight == MaxWeight || !found { + rw.Add(c, int(weight)) + } } } - if len(connectors) == 0 { - return nil - } - n := atomic.AddUint64(&t.n, 1) - 1 - return connectors[n%uint64(len(connectors))] + + return rw.Next() } func (t *Tunnel) CloseOnIdle() bool { diff --git a/hop/plugin/http.go b/hop/plugin/http.go index 0025e01..9e77da8 100644 --- a/hop/plugin/http.go +++ b/hop/plugin/http.go @@ -24,10 +24,6 @@ type httpPluginRequest struct { Src string `json:"src"` } -type httpPluginResponse struct { - Node string `json:"node"` -} - type httpPlugin struct { name string url string @@ -101,18 +97,8 @@ func (p *httpPlugin) Select(ctx context.Context, opts ...hop.SelectOption) *chai return nil } - res := httpPluginResponse{} - if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { - p.log.Error(resp.Status) - return nil - } - - if res.Node == "" { - return nil - } - var cfg config.NodeConfig - if err := json.NewDecoder(bytes.NewReader([]byte(res.Node))).Decode(&cfg); err != nil { + if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil { p.log.Error(err) return nil } diff --git a/listener/udp/metadata.go b/listener/udp/metadata.go index 2c7eeec..6cb7b30 100644 --- a/listener/udp/metadata.go +++ b/listener/udp/metadata.go @@ -27,7 +27,7 @@ func (l *udpListener) parseMetadata(md mdata.Metadata) (err error) { readBufferSize = "readBufferSize" readQueueSize = "readQueueSize" backlog = "backlog" - keepAlive = "keepAlive" + keepalive = "keepalive" ttl = "ttl" ) @@ -49,7 +49,7 @@ func (l *udpListener) parseMetadata(md mdata.Metadata) (err error) { if l.md.backlog <= 0 { l.md.backlog = defaultBacklog } - l.md.keepalive = mdutil.GetBool(md, keepAlive) + l.md.keepalive = mdutil.GetBool(md, keepalive) return } diff --git a/selector/strategy.go b/selector/strategy.go index 4a93448..fe4d886 100644 --- a/selector/strategy.go +++ b/selector/strategy.go @@ -35,7 +35,7 @@ func (s *roundRobinStrategy[T]) Apply(ctx context.Context, vs ...T) (v T) { } type randomStrategy[T any] struct { - rw *randomWeighted[T] + rw *RandomWeighted[T] mu sync.Mutex } @@ -43,7 +43,7 @@ type randomStrategy[T any] struct { // The node will be selected randomly. func RandomStrategy[T any]() selector.Strategy[T] { return &randomStrategy[T]{ - rw: newRandomWeighted[T](), + rw: NewRandomWeighted[T](), } } diff --git a/selector/weighted.go b/selector/weighted.go index 956342e..bb50f2a 100644 --- a/selector/weighted.go +++ b/selector/weighted.go @@ -10,25 +10,25 @@ type randomWeightedItem[T any] struct { weight int } -type randomWeighted[T any] struct { +type RandomWeighted[T any] struct { items []*randomWeightedItem[T] sum int r *rand.Rand } -func newRandomWeighted[T any]() *randomWeighted[T] { - return &randomWeighted[T]{ +func NewRandomWeighted[T any]() *RandomWeighted[T] { + return &RandomWeighted[T]{ r: rand.New(rand.NewSource(time.Now().UnixNano())), } } -func (rw *randomWeighted[T]) Add(item T, weight int) { +func (rw *RandomWeighted[T]) Add(item T, weight int) { ri := &randomWeightedItem[T]{item: item, weight: weight} rw.items = append(rw.items, ri) rw.sum += weight } -func (rw *randomWeighted[T]) Next() (v T) { +func (rw *RandomWeighted[T]) Next() (v T) { if len(rw.items) == 0 { return } @@ -46,7 +46,7 @@ func (rw *randomWeighted[T]) Next() (v T) { return rw.items[len(rw.items)-1].item } -func (rw *randomWeighted[T]) Reset() { +func (rw *RandomWeighted[T]) Reset() { rw.items = nil rw.sum = 0 }