fix: name the proxy schemes that work, and stop panicking on the rest

"Which proxy does this support?" has a narrower answer than the docs
gave. `golang.org/x/net/proxy.FromURL` resolves `socks5` and `socks5h`
and nothing else, yet the README and `Proxy`'s doc comment both offered
"socks5, socks4 and socks4a". A `socks4://` URI fails every dial with
`proxy: unknown scheme: socks4`, which is what taking the docs at their
word looks like from the caller's side.

Corrected both, and pointed at `proxy.RegisterDialerType` -- the way
x/net/proxy expects a caller to add a scheme it does not carry.

That escape hatch had a second problem waiting. `establishProxyConnection`
asserted the dialer to `proxy.ContextDialer` unconditionally, but a
registered dialer need only implement `Dial`, and the assertion panicked
on those:

    interface conversion: T is not proxy.ContextDialer: missing method
    DialContext

It now falls back to dialing on a goroutine and abandoning the attempt
when the context expires, which is what x/net/proxy itself does for the
same case.

Closes: #95
This commit is contained in:
dualfroz committed 2026-09-13 15:47:32 +02:00
1 parent 74c6cde98a
commit 87744aa7c1
4 files changed
+80 -3

No files matched your search

+39
View File
@@ -5,6 +5,7 @@ import (
"errors"
"net"
"net/textproto"
"net/url"
"sync/atomic"
"syscall"
"testing"
@@ -12,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/proxy"
)
func TestCheckSMTPUnSupportedVendor(t *testing.T) {
@@ -231,6 +233,43 @@ func TestDialSMTP_ProxyBypassesCustomResolver(t *testing.T) {
assert.False(t, called.Load())
}
// Pins the schemes x/net/proxy actually resolves; socks4 and socks4a never worked.
func TestEstablishProxyConnection_Schemes(t *testing.T) {
// Nothing listens here: a resolved scheme reaches a refused connection.
const proxyAddr = "127.0.0.1:1"
for _, scheme := range []string{"socks5", "socks5h"} {
t.Run(scheme+" is supported", func(tt *testing.T) {
_, err := establishProxyConnection("example.com:25", scheme+"://"+proxyAddr, time.Second)
require.Error(tt, err)
assert.NotContains(tt, err.Error(), "unknown scheme")
})
}
for _, scheme := range []string{"socks4", "socks4a", "http", "https"} {
t.Run(scheme+" is not supported", func(tt *testing.T) {
_, err := establishProxyConnection("example.com:25", scheme+"://"+proxyAddr, time.Second)
require.ErrorContains(tt, err, "proxy: unknown scheme: "+scheme)
})
}
}
type dialerWithoutContext struct{ err error }
func (d dialerWithoutContext) Dial(_, _ string) (net.Conn, error) { return nil, d.err }
func TestEstablishProxyConnection_DialerWithoutContext(t *testing.T) {
wantErr := errors.New("registered dialer reached")
proxy.RegisterDialerType("emailverifiertest", func(*url.URL, proxy.Dialer) (proxy.Dialer, error) {
return dialerWithoutContext{err: wantErr}, nil
})
// Used to panic on the proxy.ContextDialer assertion.
conn, err := establishProxyConnection("example.com:25", "emailverifiertest://127.0.0.1:1080", time.Second)
assert.Nil(t, conn)
require.ErrorIs(t, err, wantErr)
}
func TestPreferredDialError(t *testing.T) {
network := errors.New("dial tcp 1.2.3.4:25: connect: connection refused")
dns := &net.DNSError{Err: "no such host", Name: "mx.example.invalid", IsNotFound: true}