Skip to content

Commit a1e062b

Browse files
committed
Enhance proxy support with automatic handling and IPv6 compatibility
- Add TCPDialer for automatic proxy detection from environment variables - Support HTTP/SOCKS proxies with IPv6 addresses and authentication - Implement security validations and secret scrubbing in logs - Update netstack to use TCPDialer for upstream connections
1 parent fb121e7 commit a1e062b

File tree

11 files changed

+289
-118
lines changed

11 files changed

+289
-118
lines changed

compose/docker-compose.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ services:
99
#
1010
# FARCASTER_AGENT_TOKEN
1111
# The Agent's token is obtained when creating a Scanning Agent in the Probely app.
12-
# Learn more at https://help.probely.com/en/articles/6503388-how-to-install-a-scanning-agent
12+
# Learn more at https://help.probely.com/en/articles/6503388-how-to-install-a-scanning-agent
1313
#
14-
# FARCASTER_API_URL
15-
# Probely's API URL
14+
# FARCASTER_API_URL
15+
# Probely's API URL
1616
#
1717
# HTTP_PROXY (optional)
1818
# An advanced option that can be used to configure an HTTP proxy for the Agent to connect to Probely.
@@ -21,6 +21,7 @@ services:
2121
- FARCASTER_AGENT_TOKEN
2222
- FARCASTER_API_URL
2323
- HTTP_PROXY
24+
- FARCASTER_FORCE_TCP
2425
tmpfs:
2526
- /run
2627
cap_add:

farcaster-go/dialers/dialers.go

Lines changed: 138 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,59 @@ import (
1616

1717
"github.com/coder/websocket"
1818
"golang.org/x/net/context"
19+
"golang.org/x/net/http/httpproxy"
1920
"golang.org/x/net/proxy"
2021
)
2122

23+
type ProxyFunc func(addr string) (*url.URL, error)
24+
2225
// Dialer is an interface for a connection dialer
2326
type Dialer interface {
2427
Connect() (net.Conn, error)
2528
String() string
2629
}
2730

31+
// TCPProxyFromEnvironment returns the proxy URL for the given address based on
32+
// HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables.
33+
// If the address should be connected to directly, nil is returned.
34+
func TCPProxyFromEnvironment(addr string) (*url.URL, error) {
35+
// Create a fake URL with the address to check proxy rules
36+
// We use http scheme as default for TCP connections
37+
u := &url.URL{
38+
Scheme: "http",
39+
Host: addr,
40+
}
41+
return httpproxy.FromEnvironment().ProxyFunc()(u)
42+
}
43+
44+
// normalizeProxyURL adds the appropriate scheme to a proxy URL if missing
45+
func normalizeProxyURL(proxyStr string, isSocks bool) string {
46+
if proxyStr == "" || strings.Contains(proxyStr, "://") {
47+
return proxyStr
48+
}
49+
50+
if isSocks {
51+
return "socks5://" + proxyStr
52+
}
53+
return "http://" + proxyStr
54+
}
55+
56+
// parseProxyURL tries to parse proxy URL from environment variables
57+
func parseProxyURL(vars []string) *url.URL {
58+
for _, v := range vars {
59+
if val := os.Getenv(v); val != "" {
60+
// Add default scheme if missing
61+
isSocks := strings.Contains(strings.ToUpper(v), "SOCKS")
62+
val = normalizeProxyURL(val, isSocks)
63+
64+
if u, err := url.Parse(val); err == nil {
65+
return u
66+
}
67+
}
68+
}
69+
return nil
70+
}
71+
2872
// DirectDialer connects directly to the target
2973
type DirectDialer struct {
3074
addr string
@@ -150,13 +194,7 @@ func NewSOCKS5Dialer(proxyURL *url.URL, addr string, timeout time.Duration) *SOC
150194

151195
func (s *SOCKS5Dialer) Connect() (net.Conn, error) {
152196
dialer := &net.Dialer{Timeout: s.timeout}
153-
auth := &proxy.Auth{}
154-
if s.proxyURL.User != nil {
155-
auth.User = s.proxyURL.User.Username()
156-
if pass, ok := s.proxyURL.User.Password(); ok {
157-
auth.Password = pass
158-
}
159-
}
197+
auth := extractSOCKS5Auth(s.proxyURL)
160198

161199
socks5, err := proxy.SOCKS5("tcp", s.proxyURL.Host, auth, dialer)
162200
if err != nil {
@@ -203,19 +241,14 @@ func (s *WebSocketDialer) Connect() (net.Conn, error) {
203241
}
204242

205243
// Configure proxies
206-
if s.proxy.Scheme == "http" || s.proxy.Scheme == "https" {
244+
switch s.proxy.Scheme {
245+
case "http", "https":
207246
transport.Proxy = func(req *http.Request) (*url.URL, error) {
208247
return s.proxy, nil
209248
}
210249

211-
} else if s.proxy.Scheme == "socks5" {
212-
auth := &proxy.Auth{}
213-
if s.proxy.User != nil {
214-
auth.User = s.proxy.User.Username()
215-
if pass, ok := s.proxy.User.Password(); ok {
216-
auth.Password = pass
217-
}
218-
}
250+
case "socks5":
251+
auth := extractSOCKS5Auth(s.proxy)
219252
dialer, err := proxy.SOCKS5("tcp", s.proxy.Host, auth, &net.Dialer{
220253
Timeout: s.timeout,
221254
})
@@ -227,7 +260,7 @@ func (s *WebSocketDialer) Connect() (net.Conn, error) {
227260
return dialer.(proxy.ContextDialer).DialContext(ctx, network, addr)
228261
}
229262

230-
} else {
263+
default:
231264
return nil, fmt.Errorf("unsupported proxy scheme: %s", s.proxy.Scheme)
232265
}
233266

@@ -291,6 +324,21 @@ func generateProxyAuth(user *url.Userinfo) string {
291324
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
292325
}
293326

327+
// extractSOCKS5Auth extracts authentication from URL for SOCKS5 proxy
328+
func extractSOCKS5Auth(u *url.URL) *proxy.Auth {
329+
if u == nil || u.User == nil {
330+
return &proxy.Auth{}
331+
}
332+
333+
auth := &proxy.Auth{
334+
User: u.User.Username(),
335+
}
336+
if pass, ok := u.User.Password(); ok {
337+
auth.Password = pass
338+
}
339+
return auth
340+
}
341+
294342
// DialConfig is a configuration for creating dialers
295343
type DialConfig struct {
296344
// Configuration options
@@ -306,8 +354,8 @@ func NewDialConfig() *DialConfig {
306354
enableTLS: os.Getenv("ENABLE_TLS") == "true",
307355
enableWS: os.Getenv("ENABLE_WS") == "true",
308356
enableWSS: os.Getenv("ENABLE_WSS") == "true",
309-
httpProxy: parseHTTPProxy(),
310-
socksProxy: parseSOCKSProxy(),
357+
httpProxy: ParseHTTPProxy(),
358+
socksProxy: ParseSOCKSProxy(),
311359
}
312360
}
313361

@@ -348,13 +396,9 @@ func (dc *DialConfig) WithHTTPProxyString(proxyURLStr string) *DialConfig {
348396
return dc
349397
}
350398

351-
if !strings.HasPrefix(proxyURLStr, "http://") && !strings.HasPrefix(proxyURLStr, "https://") {
352-
proxyURLStr = "http://" + proxyURLStr
353-
}
354-
355-
proxyURL, err := url.Parse(proxyURLStr)
356-
if err == nil {
357-
dc.httpProxy = proxyURL
399+
proxyURLStr = normalizeProxyURL(proxyURLStr, false)
400+
if u, err := url.Parse(proxyURLStr); err == nil {
401+
dc.httpProxy = u
358402
}
359403
return dc
360404
}
@@ -366,13 +410,9 @@ func (dc *DialConfig) WithSOCKSProxyString(proxyURLStr string) *DialConfig {
366410
return dc
367411
}
368412

369-
if !strings.HasPrefix(proxyURLStr, "socks5://") {
370-
proxyURLStr = "socks5://" + proxyURLStr
371-
}
372-
373-
proxyURL, err := url.Parse(proxyURLStr)
374-
if err == nil {
375-
dc.socksProxy = proxyURL
413+
proxyURLStr = normalizeProxyURL(proxyURLStr, true)
414+
if u, err := url.Parse(proxyURLStr); err == nil {
415+
dc.socksProxy = u
376416
}
377417
return dc
378418
}
@@ -381,25 +421,27 @@ func (dc *DialConfig) WithSOCKSProxyString(proxyURLStr string) *DialConfig {
381421
func (dc *DialConfig) Dialers(addr string, timeout time.Duration) []Dialer {
382422
var dialers []Dialer
383423

384-
// If HTTP proxy is configured, add proxy strategies first
424+
// Helper to create WebSocket URLs
425+
makeWSURL := func(secure bool) *url.URL {
426+
scheme := "ws"
427+
if secure {
428+
scheme = "wss"
429+
}
430+
return &url.URL{Scheme: scheme, Host: addr, Path: "/"}
431+
}
432+
433+
// If HTTP proxy is configured, add proxy strategies
385434
if dc.httpProxy != nil {
386435
dialers = append(dialers,
387436
NewHTTPProxyDialer(dc.httpProxy, addr, timeout))
388437

389-
if dc.enableTLS {
390-
dialers = append(dialers,
391-
NewHTTPProxyDialer(dc.httpProxy, addr, timeout))
392-
}
393-
394438
if dc.enableWS {
395-
wsURL := &url.URL{Scheme: "ws", Host: addr, Path: "/"}
396439
dialers = append(dialers,
397-
NewWebSocketDialer(wsURL, dc.httpProxy, false, timeout))
440+
NewWebSocketDialer(makeWSURL(false), dc.httpProxy, false, timeout))
398441

399442
if dc.enableWSS {
400-
wssURL := &url.URL{Scheme: "wss", Host: addr, Path: "/"}
401443
dialers = append(dialers,
402-
NewWebSocketDialer(wssURL, dc.httpProxy, false, timeout))
444+
NewWebSocketDialer(makeWSURL(true), dc.httpProxy, false, timeout))
403445
}
404446
}
405447
} else if dc.socksProxy != nil {
@@ -419,53 +461,76 @@ func (dc *DialConfig) Dialers(addr string, timeout time.Duration) []Dialer {
419461

420462
// Add WebSocket direct if enabled
421463
if dc.enableWS {
422-
wsURL := &url.URL{Scheme: "ws", Host: addr, Path: "/"}
423464
dialers = append(dialers,
424-
NewWebSocketDialer(wsURL, nil, false, timeout))
465+
NewWebSocketDialer(makeWSURL(false), nil, false, timeout))
425466

426467
if dc.enableWSS {
427-
wssURL := &url.URL{Scheme: "wss", Host: addr, Path: "/"}
428468
dialers = append(dialers,
429-
NewWebSocketDialer(wssURL, nil, false, timeout))
469+
NewWebSocketDialer(makeWSURL(true), nil, false, timeout))
430470
}
431471
}
432472

433473
return dialers
434474
}
435475

436-
func parseHTTPProxy() *url.URL {
437-
proxyURLStr := os.Getenv("HTTP_PROXY")
438-
if proxyURLStr == "" {
439-
proxyURLStr = os.Getenv("HTTPS_PROXY")
440-
}
441-
if proxyURLStr == "" {
442-
return nil
443-
}
476+
func ParseHTTPProxy() *url.URL {
477+
// Try each proxy variable in order
478+
vars := []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"}
479+
return parseProxyURL(vars)
480+
}
444481

445-
if !strings.HasPrefix(proxyURLStr, "http://") && !strings.HasPrefix(proxyURLStr, "https://") {
446-
proxyURLStr = "http://" + proxyURLStr
447-
}
482+
func ParseSOCKSProxy() *url.URL {
483+
return parseProxyURL([]string{"SOCKS5_PROXY"})
484+
}
448485

449-
proxyURL, err := url.Parse(proxyURLStr)
450-
if err != nil {
451-
return nil
452-
}
453-
return proxyURL
486+
// TCPDialer is a smart dialer that handles proxy configuration automatically
487+
type TCPDialer struct {
488+
proxyFunc ProxyFunc
489+
timeout time.Duration
454490
}
455491

456-
func parseSOCKSProxy() *url.URL {
457-
proxyURLStr := os.Getenv("SOCKS5_PROXY")
458-
if proxyURLStr == "" {
459-
return nil
492+
// NewTCPDialer creates a new TCP dialer with the given proxy function and timeout
493+
func NewTCPDialer(proxyFunc ProxyFunc, timeout time.Duration) *TCPDialer {
494+
if proxyFunc == nil {
495+
// Default to environment-based proxy configuration.
496+
proxyFunc = TCPProxyFromEnvironment
460497
}
461-
462-
if !strings.HasPrefix(proxyURLStr, "socks5://") {
463-
proxyURLStr = "socks5://" + proxyURLStr
498+
return &TCPDialer{
499+
proxyFunc: proxyFunc,
500+
timeout: timeout,
464501
}
502+
}
465503

466-
proxyURL, err := url.Parse(proxyURLStr)
504+
// DialContext connects to the given address, using a proxy if configured
505+
func (d *TCPDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
506+
// Determine if we should use a proxy
507+
proxyURL, err := d.proxyFunc(addr)
467508
if err != nil {
468-
return nil
509+
return nil, fmt.Errorf("proxy resolution failed: %w", err)
510+
}
511+
512+
if proxyURL != nil {
513+
// Use proxy connection
514+
switch proxyURL.Scheme {
515+
case "http", "https":
516+
proxyDialer := NewHTTPProxyDialer(proxyURL, addr, d.timeout)
517+
return proxyDialer.Connect()
518+
case "socks5":
519+
socksDialer := NewSOCKS5Dialer(proxyURL, addr, d.timeout)
520+
return socksDialer.Connect()
521+
default:
522+
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
523+
}
469524
}
470-
return proxyURL
525+
526+
// Direct connection
527+
dialer := &net.Dialer{
528+
Timeout: d.timeout,
529+
}
530+
return dialer.DialContext(ctx, network, addr)
531+
}
532+
533+
// Dial connects to the given address (non-context version)
534+
func (d *TCPDialer) Dial(network, addr string) (net.Conn, error) {
535+
return d.DialContext(context.Background(), network, addr)
471536
}

farcaster-go/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ require (
3030
go.uber.org/multierr v1.11.0 // indirect
3131
golang.org/x/mod v0.26.0 // indirect
3232
golang.org/x/sync v0.16.0 // indirect
33+
golang.org/x/text v0.27.0 // indirect
3334
golang.org/x/time v0.12.0 // indirect
3435
golang.org/x/tools v0.35.0 // indirect
3536
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect

farcaster-go/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6666
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
6767
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
6868
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
69+
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
70+
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
6971
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
7072
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
7173
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=

farcaster-go/wireguard/netstack/netstack.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const (
3232
keepaliveInterval = 120 * time.Second
3333
keepaliveCount = 4
3434

35+
defaultConnectTimeout = 10 * time.Second
36+
defaultReadTimeout = 10 * time.Second
37+
3538
maxInFlightTCP = 1024
3639
)
3740

@@ -260,11 +263,12 @@ func (ns *netstack) forwardTCP(fwd *netstackTCPFwd) {
260263
defer fwd.Cleanup()
261264

262265
// Try to connect to the server (upstream) first.
263-
upstream, err := fwd.ConnectUpstream(keepaliveInterval, keepaliveCount)
266+
upstream, err := fwd.ConnectUpstream()
264267
if err != nil {
265268
ns.logConnectionError(err, "Upstream connection failed")
266269
return
267270
}
271+
268272
downstream, err := fwd.ConnectDownstream(keepaliveInterval, keepaliveCount)
269273
if err != nil {
270274
ns.logConnectionError(err, "Downstream connection failed")

0 commit comments

Comments
 (0)