From 3aa7d84ad64dea49e75d3b32c8ff3c596ef1eb0e Mon Sep 17 00:00:00 2001 From: Edgar Sipki Date: Sun, 25 Oct 2026 18:04:40 +0300 Subject: [PATCH] fix(remote): dial remote plugins through passthrough grpc-go's default dns resolver queries the nameservers itself, and on a machine whose first one does not answer (a VPN's) it waited ~20s on every connection to a remote plugin service, localhost included. passthrough lets the system resolver answer at dial time; a host without a port gets 443, as the dns resolver assumed. Co-Authored-By: Claude Opus 5.5 --- internal/adapters/plugin/remote.go | 17 +++++++++++- internal/adapters/plugin/remote_test.go | 35 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 internal/adapters/plugin/remote_test.go diff --git a/internal/adapters/plugin/remote.go b/internal/adapters/plugin/remote.go index 48acb577..4ae1f9d3 100644 --- a/internal/adapters/plugin/remote.go +++ b/internal/adapters/plugin/remote.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "fmt" "log/slog" + "net" "net/url" "strings" "time" @@ -66,7 +67,7 @@ func (e *RemotePluginExecutor) Execute(ctx context.Context, plugin Info, request transportCreds = insecure.NewCredentials() } - conn, err := grpc.NewClient(host, grpc.WithTransportCredentials(transportCreds)) + conn, err := grpc.NewClient(dialTarget(host), grpc.WithTransportCredentials(transportCreds)) if err != nil { return nil, fmt.Errorf("failed to connect to gRPC server %s: %w", host, err) } @@ -103,6 +104,20 @@ func (e *RemotePluginExecutor) Execute(ctx context.Context, plugin Info, request return resp.CodeGeneratorResponse, nil } +// dialTarget is the gRPC target for a plugin host: passthrough, so the system +// resolver answers the name when the connection is made. grpc-go's default dns +// resolver queries the nameservers itself, and on a machine whose first one +// does not answer (a VPN's, typically) it waited about twenty seconds on every +// connection, localhost included. A host without a port gets 443, which is +// what that resolver assumed. +func dialTarget(host string) string { + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "443") + } + + return "passthrough:///" + host +} + // - localhost:8080/python:v1.35 // - http://localhost:8080/python:v1.35 // - https://example.com/python:v1.35 diff --git a/internal/adapters/plugin/remote_test.go b/internal/adapters/plugin/remote_test.go new file mode 100644 index 00000000..188b0c16 --- /dev/null +++ b/internal/adapters/plugin/remote_test.go @@ -0,0 +1,35 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRemotePluginTarget(t *testing.T) { + t.Parallel() + + tests := []struct { + source string + target string + name string + version string + }{ + {"localhost:8080/grpc/go:v1.6.2", "passthrough:///localhost:8080", "grpc/go", "v1.6.2"}, + {"http://127.0.0.1:8080/protocolbuffers/go", "passthrough:///127.0.0.1:8080", "protocolbuffers/go", "latest"}, + {"https://plugins.example.com/grpc/go:v1.6.2", "passthrough:///plugins.example.com:443", "grpc/go", "v1.6.2"}, + {"enterprise.plugins.beta.easyp.tech:443/bufbuild/es:v2.12.0", "passthrough:///enterprise.plugins.beta.easyp.tech:443", "bufbuild/es", "v2.12.0"}, + } + + e := &RemotePluginExecutor{} + + for _, tt := range tests { + host, name, version, err := e.parsePluginURL(tt.source) + require.NoError(t, err, tt.source) + + assert.Equal(t, tt.target, dialTarget(host), tt.source) + assert.Equal(t, tt.name, name, tt.source) + assert.Equal(t, tt.version, version, tt.source) + } +}