diff --git a/cmd/dmsghttp/commands/dmsghttp.go b/cmd/dmsghttp/commands/dmsghttp.go index 01d0c47a3..b93ab4331 100644 --- a/cmd/dmsghttp/commands/dmsghttp.go +++ b/cmd/dmsghttp/commands/dmsghttp.go @@ -123,7 +123,7 @@ func server() { httpClient = &http.Client{ Transport: transport, } - ctx = context.WithValue(context.Background(), "socks5_proxy", proxyAddr) //nolint + ctx = context.WithValue(ctx, "socks5_proxy", proxyAddr) //nolint } var dmsgC *dmsg.Client diff --git a/cmd/dmsgpty-cli/commands/root.go b/cmd/dmsgpty-cli/commands/root.go index 7d47b6866..4e5070d45 100644 --- a/cmd/dmsgpty-cli/commands/root.go +++ b/cmd/dmsgpty-cli/commands/root.go @@ -59,8 +59,6 @@ var RootCmd = &cobra.Command{ // case 2 : config file is old (already contains "wl" key) // - load config file into memory to manipulate whitelists // - writes changes back to config file - println(confPath) - if _, err := os.Stat(confPath); err != nil { cli.Log.Fatalf("Config file %s not found.", confPath) } diff --git a/go.mod b/go.mod index b2a6086be..e0a71614f 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,8 @@ require ( github.com/pires/go-proxyproto v0.11.0 github.com/sirupsen/logrus v1.9.4 github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 - github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68 - github.com/skycoin/skywire v1.3.37 + github.com/skycoin/skycoin v0.28.6-0.20260328152706-a360adb0a4d3 + github.com/skycoin/skywire v1.3.40-0.20260328171146-a5facdc74e72 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 golang.org/x/net v0.52.0 @@ -99,6 +99,7 @@ require ( golang.org/x/arch v0.25.0 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect diff --git a/go.sum b/go.sum index 88256239f..b8b28e230 100644 --- a/go.sum +++ b/go.sum @@ -170,10 +170,10 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 h1:1Nc5EBY6pjfw1kwW0duwyG+7WliWz5u9kgk1h5MnLuA= github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:UXghlricA7J3aRD/k7p/zBObQfmBawwCxIVPVjz2Q3o= -github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68 h1:M+VmlCByq6jRbxio6GOn4h+5jP+nmumiu1xMZZg3MZI= -github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68/go.mod h1:tgVxjBBV4/OxVBDrcpsVK0q/awGxqBjwTUPDBMh9ZcA= -github.com/skycoin/skywire v1.3.37 h1:LIgOrj6PqdH6RAOWsD8TSI/vTyp7kUYE2Ale6pkvjJw= -github.com/skycoin/skywire v1.3.37/go.mod h1:k3TA1edIXR96Jtec5XYVy7EGHQlZL524pCPYRTzBBok= +github.com/skycoin/skycoin v0.28.6-0.20260328152706-a360adb0a4d3 h1:yDTe5ISvCGJ/wsbaXcXKO/MgyB8/pam3GHsvwbVlPk8= +github.com/skycoin/skycoin v0.28.6-0.20260328152706-a360adb0a4d3/go.mod h1:tgVxjBBV4/OxVBDrcpsVK0q/awGxqBjwTUPDBMh9ZcA= +github.com/skycoin/skywire v1.3.40-0.20260328171146-a5facdc74e72 h1:KXG0RGXVDh2KNn35kGC5+mWXU6hlYYjowU9TXiGSwMU= +github.com/skycoin/skywire v1.3.40-0.20260328171146-a5facdc74e72/go.mod h1:GmBT7f+kIxR2ym3iBbtBNE75/W8XfG4JbkJODzD2azA= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= @@ -251,8 +251,8 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= -google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s= -google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= diff --git a/pkg/dmsg/client.go b/pkg/dmsg/client.go index dbadadffa..ec4e53471 100644 --- a/pkg/dmsg/client.go +++ b/pkg/dmsg/client.go @@ -112,7 +112,10 @@ func NewClient(pk cipher.PubKey, sk cipher.SecKey, dc disc.APIClient, conf *Conf // Init callback: on set session. c.EntityCommon.setSessionCallback = func(ctx context.Context) error { - if err := c.EntityCommon.updateClientEntry(ctx, c.done, c.conf.ClientType); err != nil { + c.sessionsMx.Lock() + err := c.EntityCommon.updateClientEntry(ctx, c.done, c.conf.ClientType) + c.sessionsMx.Unlock() + if err != nil { return err } // Client is 'ready' once we have successfully updated the discovery entry @@ -123,7 +126,9 @@ func NewClient(pk cipher.PubKey, sk cipher.SecKey, dc disc.APIClient, conf *Conf // Init callback: on delete session. c.EntityCommon.delSessionCallback = func(ctx context.Context) error { + c.sessionsMx.Lock() err := c.EntityCommon.updateClientEntry(ctx, c.done, c.conf.ClientType) + c.sessionsMx.Unlock() return err } @@ -275,6 +280,7 @@ func (ce *Client) Serve(ctx context.Context) { select { case ce.errCh <- err: default: + ce.log.WithError(err).Warn("Error channel full, dropping error.") } ce.sesMx.Unlock() } @@ -376,12 +382,11 @@ func (ce *Client) serveWait() { t := time.NewTimer(bo) defer t.Stop() - if newBO := time.Duration(float64(bo) * ce.factor); ce.maxBO == 0 || newBO <= ce.maxBO { - ce.bo = newBO - if newBO > ce.maxBO { - ce.bo = ce.maxBO - } + newBO := time.Duration(float64(bo) * ce.factor) + if ce.maxBO > 0 && newBO > ce.maxBO { + newBO = ce.maxBO } + ce.bo = newBO <-t.C } diff --git a/pkg/dmsg/entity_common.go b/pkg/dmsg/entity_common.go index 348a251ea..1b700d3e2 100644 --- a/pkg/dmsg/entity_common.go +++ b/pkg/dmsg/entity_common.go @@ -110,15 +110,16 @@ func (c *EntityCommon) SessionCount() int { func (c *EntityCommon) setSession(ctx context.Context, dSes *SessionCommon) bool { c.sessionsMx.Lock() - defer c.sessionsMx.Unlock() - if _, ok := c.sessions[dSes.RemotePK()]; ok { + c.sessionsMx.Unlock() return false } c.sessions[dSes.RemotePK()] = dSes + cb := c.setSessionCallback + c.sessionsMx.Unlock() - if c.setSessionCallback != nil { - if err := c.setSessionCallback(ctx); err != nil { + if cb != nil { + if err := cb(ctx); err != nil { c.log. WithField("func", "EntityCommon.setSession"). WithError(err). @@ -131,15 +132,17 @@ func (c *EntityCommon) setSession(ctx context.Context, dSes *SessionCommon) bool func (c *EntityCommon) delSession(ctx context.Context, pk cipher.PubKey) { c.sessionsMx.Lock() delete(c.sessions, pk) - if c.delSessionCallback != nil { - if err := c.delSessionCallback(ctx); err != nil { + cb := c.delSessionCallback + c.sessionsMx.Unlock() + + if cb != nil { + if err := cb(ctx); err != nil { c.log. WithField("func", "EntityCommon.delSession"). WithError(err). Warn("Callback returned non-nil error.\n") } } - c.sessionsMx.Unlock() } // updateServerEntry updates the dmsg server's entry within dmsg discovery. @@ -147,7 +150,7 @@ func (c *EntityCommon) delSession(ctx context.Context, pk cipher.PubKey) { // Caller must hold c.sessionsMx. func (c *EntityCommon) updateServerEntry(ctx context.Context, addr string, maxSessions int, authPassphrase string) (err error) { if addr == "" { - panic("updateServerEntry cannot accept empty 'addr' input") // this should never happen + return errors.New("updateServerEntry cannot accept empty 'addr' input") } // Record last update on success. @@ -158,6 +161,9 @@ func (c *EntityCommon) updateServerEntry(ctx context.Context, addr string, maxSe }() availableSessions := maxSessions - len(c.sessions) + if availableSessions < 0 { + availableSessions = 0 + } entry, err := c.dc.Entry(ctx, c.pk) if err != nil { diff --git a/pkg/dmsg/listener.go b/pkg/dmsg/listener.go index 520183a00..cc9f13342 100644 --- a/pkg/dmsg/listener.go +++ b/pkg/dmsg/listener.go @@ -7,9 +7,12 @@ import ( "sync" "sync/atomic" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil" ) +var listenerLog = logging.MustGetLogger("dmsg_listener") + // Listener listens for remote-initiated streams. type Listener struct { porter *netutil.Porter @@ -59,6 +62,7 @@ func (l *Listener) introduceStream(tp *Stream) error { return ErrEntityClosed default: + listenerLog.WithField("addr", l.addr).Warn("Accept buffer full, dropping stream.") _ = tp.Close() //nolint:errcheck return ErrAcceptChanMaxed } diff --git a/pkg/dmsg/server.go b/pkg/dmsg/server.go index 8cb22513f..8ed37950d 100644 --- a/pkg/dmsg/server.go +++ b/pkg/dmsg/server.go @@ -73,9 +73,13 @@ func NewServer(pk cipher.PubKey, sk cipher.SecKey, dc disc.APIClient, conf *Serv s.addrDone = make(chan struct{}) s.maxSessions = conf.MaxSessions s.setSessionCallback = func(ctx context.Context) error { + s.sessionsMx.Lock() + defer s.sessionsMx.Unlock() return s.updateServerEntry(ctx, s.AdvertisedAddr(), s.maxSessions, conf.AuthPassphrase) } s.delSessionCallback = func(ctx context.Context) error { + s.sessionsMx.Lock() + defer s.sessionsMx.Unlock() return s.updateServerEntry(ctx, s.AdvertisedAddr(), s.maxSessions, conf.AuthPassphrase) } s.authPassphrase = conf.AuthPassphrase diff --git a/pkg/dmsg/session_common.go b/pkg/dmsg/session_common.go index a71930c0a..fd2c59ade 100644 --- a/pkg/dmsg/session_common.go +++ b/pkg/dmsg/session_common.go @@ -186,8 +186,7 @@ func (sc *SessionCommon) Close() error { sc.sm.mutx.Lock() if sc.sm.smux != nil { err = sc.sm.smux.Close() - } - if sc.sm.yamux != nil { + } else if sc.sm.yamux != nil { err = sc.sm.yamux.Close() } sc.sm.mutx.Unlock() diff --git a/pkg/dmsgcurl/dmsgcurl.go b/pkg/dmsgcurl/dmsgcurl.go index 7faab3682..52a37d4db 100644 --- a/pkg/dmsgcurl/dmsgcurl.go +++ b/pkg/dmsgcurl/dmsgcurl.go @@ -182,7 +182,12 @@ func parseOutputFile(name string, urlPath string) (*os.File, error) { } if stat.IsDir() { - f, err := os.Create(filepath.Join(name, urlPath)) //nolint + // Sanitize the URL path to prevent directory traversal. + cleanPath := filepath.Base(urlPath) + if cleanPath == "." || cleanPath == "/" || cleanPath == "" { + cleanPath = "index.html" + } + f, err := os.Create(filepath.Join(name, cleanPath)) //nolint if err != nil { return nil, err } diff --git a/pkg/dmsghttp/http.go b/pkg/dmsghttp/http.go index d457b6a3b..e11fbeb13 100644 --- a/pkg/dmsghttp/http.go +++ b/pkg/dmsghttp/http.go @@ -30,6 +30,7 @@ func ListenAndServe(ctx context.Context, _ cipher.SecKey, a http.Handler, _ disc WriteTimeout: 3 * time.Second, IdleTimeout: 30 * time.Second, ReadHeaderTimeout: 3 * time.Second, + MaxHeaderBytes: 1 << 14, // 16KB Handler: a, } diff --git a/pkg/dmsgpty/host.go b/pkg/dmsgpty/host.go index 68040b659..229caaead 100644 --- a/pkg/dmsgpty/host.go +++ b/pkg/dmsgpty/host.go @@ -11,6 +11,7 @@ import ( "net/url" "strings" "sync/atomic" + "time" "github.com/sirupsen/logrus" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" @@ -64,6 +65,7 @@ func (h *Host) ServeCLI(ctx context.Context, lis net.Listener) error { // This is the main comment for reference https://github.com/golang/go/issues/45729#issuecomment-1104607098 if err, ok := err.(net.Error); ok && err.Temporary() { //nolint log.Warn("Failed to accept CLI connection with temporary error, continuing...") + time.Sleep(50 * time.Millisecond) continue } if err == io.ErrClosedPipe || strings.Contains(err.Error(), "use of closed network connection") { @@ -118,6 +120,7 @@ func (h *Host) ListenAndServe(ctx context.Context, port uint16) error { // This is the main comment for reference https://github.com/golang/go/issues/45729#issuecomment-1104607098 if err, ok := err.(net.Error); ok && err.Temporary() { //nolint log.Warn("Failed to accept dmsg.Stream with temporary error, continuing...") + time.Sleep(50 * time.Millisecond) continue } if err == io.ErrClosedPipe || err == dmsg.ErrEntityClosed || @@ -197,15 +200,15 @@ func (h *Host) log() logrus.FieldLogger { // cliEndpoints returns the endpoints served for CLI connections. func cliEndpoints(h *Host) (mux hostMux) { - mux.Handle(WhitelistURI, handleWhitelist(h)) - mux.Handle(PtyURI, handlePty(h)) - mux.Handle(PtyProxyURI, handleProxy(h)) + mux.Handle(WhitelistURI, handleWhitelist(h)) //nolint:errcheck,gosec + mux.Handle(PtyURI, handlePty(h)) //nolint:errcheck,gosec + mux.Handle(PtyProxyURI, handleProxy(h)) //nolint:errcheck,gosec return mux } // dmsgEndpoints returns the endpoints served for remote dmsg connections. func dmsgEndpoints(h *Host) (mux hostMux) { - mux.Handle(PtyURI, handlePty(h)) + mux.Handle(PtyURI, handlePty(h)) //nolint:errcheck,gosec return mux } diff --git a/pkg/dmsgpty/host_mux.go b/pkg/dmsgpty/host_mux.go index acce0571f..6de3cb64f 100644 --- a/pkg/dmsgpty/host_mux.go +++ b/pkg/dmsgpty/host_mux.go @@ -4,6 +4,7 @@ package dmsgpty import ( "context" "errors" + "fmt" "net" "net/rpc" "net/url" @@ -22,15 +23,16 @@ type hostMux struct { type handleFunc func(ctx context.Context, uri *url.URL, rpcS *rpc.Server) error -func (h *hostMux) Handle(pattern string, fn handleFunc) { +func (h *hostMux) Handle(pattern string, fn handleFunc) error { pattern = strings.TrimPrefix(pattern, "/") if _, err := path.Match(pattern, ""); err != nil { - panic(err) + return fmt.Errorf("invalid mux pattern %q: %w", pattern, err) } h.entries = append(h.entries, muxEntry{ pat: pattern, fn: fn, }) + return nil } func (h *hostMux) ServeConn(ctx context.Context, conn net.Conn) error { @@ -49,7 +51,7 @@ func (h *hostMux) ServeConn(ctx context.Context, conn net.Conn) error { for _, entry := range h.entries { ok, err := path.Match(entry.pat, uri.EscapedPath()) if err != nil { - panic(err) + return fmt.Errorf("path match error for pattern %q: %w", entry.pat, err) } if !ok { continue diff --git a/pkg/dmsgpty/pty_gateway.go b/pkg/dmsgpty/pty_gateway.go index dab7ea9c2..c387c8527 100644 --- a/pkg/dmsgpty/pty_gateway.go +++ b/pkg/dmsgpty/pty_gateway.go @@ -1,6 +1,8 @@ // Package dmsgpty pkg/dmsgpty/pty_gateway.go package dmsgpty +import "fmt" + // WinSize wraps around pty.Winsize and *windows.Coord type WinSize struct { X uint16 @@ -41,11 +43,21 @@ func (g *LocalPtyGateway) Stop(_, _ *struct{}) error { return g.ses.Stop() } +// maxPtyReadSize is the maximum bytes that can be requested in a single PTY read. +const maxPtyReadSize = 64 * 1024 // 64KB + // Read reads from the local pty. func (g *LocalPtyGateway) Read(reqN *int, respB *[]byte) error { - b := make([]byte, *reqN) - n, err := g.ses.Read(b) - *respB = b[:n] + n := *reqN + if n <= 0 { + return fmt.Errorf("invalid read size: %d", n) + } + if n > maxPtyReadSize { + n = maxPtyReadSize + } + b := make([]byte, n) + nr, err := g.ses.Read(b) + *respB = b[:nr] return err } @@ -93,9 +105,16 @@ func (g *ProxiedPtyGateway) Stop(_, _ *struct{}) error { // Read reads from the remote pty. func (g *ProxiedPtyGateway) Read(reqN *int, respB *[]byte) error { - b := make([]byte, *reqN) - n, err := g.ptyC.Read(b) - *respB = b[:n] + n := *reqN + if n <= 0 { + return fmt.Errorf("invalid read size: %d", n) + } + if n > maxPtyReadSize { + n = maxPtyReadSize + } + b := make([]byte, n) + nr, err := g.ptyC.Read(b) + *respB = b[:nr] return err } diff --git a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil/porter.go b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil/porter.go index a1be3514d..522999a2a 100644 --- a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil/porter.go +++ b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil/porter.go @@ -3,6 +3,7 @@ package netutil import ( "context" + "errors" "io" "os" "sync" @@ -76,28 +77,39 @@ func (p *Porter) ReserveChild(port, subPort uint16, v interface{}) (bool, func() return true, p.makeChildFreer(port, subPort) } +// ErrEphemeralPortSpace is returned when no ephemeral ports are available. +var ErrEphemeralPortSpace = errors.New("ephemeral port space exhausted") + // ReserveEphemeral reserves a new ephemeral port. // It returns the reserved ephemeral port, a function to clear the reservation and an error (if any). +// Returns ErrEphemeralPortSpace if all ephemeral ports (minEph through 65535) are occupied. func (p *Porter) ReserveEphemeral(ctx context.Context, v interface{}) (uint16, func(), error) { p.Lock() defer p.Unlock() - for { + // Scan at most the full ephemeral range (minEph..65535). + // The old code had no bound, spinning forever when all ports were taken + // while holding the mutex — burning 100% CPU on map lookups. + maxRange := uint32(65535-p.minEph) + 1 + for i := uint32(0); i < maxRange; i++ { + select { + case <-ctx.Done(): + return 0, nil, ctx.Err() + default: + } + p.eph++ if p.eph < p.minEph { p.eph = p.minEph } if _, ok := p.ports[p.eph]; ok { - select { - case <-ctx.Done(): - return 0, nil, ctx.Err() - default: - continue - } + continue } p.ports[p.eph] = PorterValue{Value: v} return p.eph, p.makePortFreer(p.eph), nil } + + return 0, nil, ErrEphemeralPortSpace } // PortValue returns the value stored under a given port. diff --git a/vendor/modules.txt b/vendor/modules.txt index ed5cda215..99577ad97 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -320,7 +320,7 @@ github.com/sirupsen/logrus/hooks/syslog # github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 ## explicit github.com/skycoin/noise -# github.com/skycoin/skycoin v0.28.6-0.20260325014814-f48988877c68 +# github.com/skycoin/skycoin v0.28.6-0.20260328152706-a360adb0a4d3 ## explicit; go 1.26.1 github.com/skycoin/skycoin/src/cipher github.com/skycoin/skycoin/src/cipher/base58 @@ -329,7 +329,7 @@ github.com/skycoin/skycoin/src/cipher/ripemd160 github.com/skycoin/skycoin/src/cipher/secp256k1-go github.com/skycoin/skycoin/src/cipher/secp256k1-go/secp256k1-go2 github.com/skycoin/skycoin/src/util/logging -# github.com/skycoin/skywire v1.3.37 +# github.com/skycoin/skywire v1.3.40-0.20260328171146-a5facdc74e72 ## explicit; go 1.26.1 github.com/skycoin/skywire/deployment github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo @@ -484,6 +484,8 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm +# google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 +## explicit; go 1.25.0 # google.golang.org/protobuf v1.36.11 ## explicit; go 1.23 google.golang.org/protobuf/encoding/protowire