● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / client / proxy / proxy.go
← Back to directory
177 lines · 5.4 KB
Raw
1package proxy
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net"
8 "net/url"
9 "strings"
10 "time"
11
12 "github.com/shehzade/contraband/pkg/common"
13)
14
15var (
16 ErrProxyForbidden = errors.New("proxy access forbidden (403)")
17 ErrProxyGateway = errors.New("proxy gateway error (502/504)")
18 ErrProxyAuthFailed = errors.New("proxy authentication failed (407)")
19 ErrWPADNotFound = errors.New("wpad pac script not discovered")
20 ErrNoProxyMatch = errors.New("no proxy found matching target")
21)
22
23// Dialer provides context-aware connection establishment to target destinations.
24type Dialer interface {
25 DialContext(ctx context.Context, network, addr string) (net.Conn, error)
26}
27
28// ProxyDialer extends Dialer to expose upstream proxy metadata for telemetry and forensic reporting.
29type ProxyDialer interface {
30 Dialer
31 // ProxyHop returns the upstream proxy address (e.g. "10.0.0.1:8080" or "proxy.corp.local:3128"),
32 // or an empty string if direct connection was established.
33 ProxyHop() string
34 // Mode returns the operational mode: "direct", "http_connect", "socks5".
35 Mode() string
36}
37
38// Resolver resolves the optimal ProxyDialer for a given target address.
39type Resolver interface {
40 GetDialer(ctx context.Context, targetAddr string) (ProxyDialer, error)
41}
42
43type AuthType string
44
45const (
46 AuthNone AuthType = "none"
47 AuthBasic AuthType = "basic"
48 AuthDigest AuthType = "digest"
49 AuthNTLM AuthType = "ntlm"
50 AuthNegotiate AuthType = "negotiate"
51 AuthAuto AuthType = "auto" // Interrogate 407 Proxy-Authenticate headers
52)
53
54type Options struct {
55 // Mode: "auto", "direct", or an explicit proxy URL (e.g. "http://10.0.0.1:8080", "socks5://127.0.0.1:1080")
56 Mode string
57 AuthType AuthType
58 Username string
59 Password string
60 Domain string // Windows domain for NTLMv2 / Kerberos
61 Workstation string // Workstation name for NTLMv2
62 PACURL string // Explicit PAC URL (skips WPAD discovery if provided)
63 Timeout time.Duration // Proxy connection & handshake timeout (default: 10s)
64 WPADTimeout time.Duration // WPAD discovery timeout (default: 3s)
65 UserAgent string // User-Agent string for HTTP CONNECT / PAC fetch
66 InsecureSkipVerify bool // Skip TLS cert validation for HTTPS proxies
67 Logger common.Logger
68}
69
70// DefaultOptions returns production-grade defaults.
71func DefaultOptions() Options {
72 return Options{
73 Mode: "auto",
74 AuthType: AuthAuto,
75 Timeout: 10 * time.Second,
76 WPADTimeout: common.DefaultWPADTimeout, // 3s
77 UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
78 Logger: common.GetLogger(),
79 }
80}
81
82// sanitizeProxyURL converts unencoded backslashes in userinfo (e.g. CORP\corpuser) to %5C.
83func sanitizeProxyURL(rawURL string) string {
84 atIdx := strings.LastIndex(rawURL, "@")
85 if atIdx != -1 {
86 schemeIdx := strings.Index(rawURL, "://")
87 start := 0
88 if schemeIdx != -1 {
89 start = schemeIdx + 3
90 }
91 if start < atIdx {
92 userinfo := rawURL[start:atIdx]
93 if strings.Contains(userinfo, "\\") {
94 escapedUserinfo := strings.ReplaceAll(userinfo, "\\", "%5C")
95 return rawURL[:start] + escapedUserinfo + rawURL[atIdx:]
96 }
97 }
98 }
99 return rawURL
100}
101
102// FromCommonConfig maps common.ProxyConfig into proxy.Options.
103func FromCommonConfig(cfg common.ProxyConfig) Options {
104 opts := DefaultOptions()
105 if cfg.Mode != "" {
106 opts.Mode = sanitizeProxyURL(cfg.Mode)
107 }
108 if cfg.AuthType != "" {
109 opts.AuthType = AuthType(cfg.AuthType)
110 }
111 // Parse credentials if embedded in URL: scheme://user:pass@host:port
112 if strings.Contains(opts.Mode, "://") {
113 if u, err := url.Parse(opts.Mode); err == nil && u.User != nil {
114 opts.Username = u.User.Username()
115 opts.Password, _ = u.User.Password()
116 }
117 }
118 return opts
119}
120
121// DirectDialer dials targets directly without any proxy hop.
122type DirectDialer struct {
123 dialer *net.Dialer
124}
125
126// NewDirectDialer creates a new DirectDialer with the configured timeout.
127func NewDirectDialer(timeout time.Duration) *DirectDialer {
128 if timeout <= 0 {
129 timeout = 10 * time.Second
130 }
131 return &DirectDialer{
132 dialer: &net.Dialer{
133 Timeout: timeout,
134 KeepAlive: 30 * time.Second,
135 },
136 }
137}
138
139func (d *DirectDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
140 return d.dialer.DialContext(ctx, network, addr)
141}
142
143func (d *DirectDialer) ProxyHop() string { return "" }
144func (d *DirectDialer) Mode() string { return "direct" }
145
146// NewDialer creates a ProxyDialer from common.ProxyConfig, routing through the fallback engine if "auto".
147func NewDialer(cfg common.ProxyConfig) (ProxyDialer, error) {
148 opts := FromCommonConfig(cfg)
149 return NewDialerFromOptions(opts)
150}
151
152// NewDialerFromOptions creates a ProxyDialer based on the provided Options.
153func NewDialerFromOptions(opts Options) (ProxyDialer, error) {
154 opts.Mode = sanitizeProxyURL(opts.Mode)
155 mode := strings.ToLower(opts.Mode)
156 if mode == "" || mode == "auto" {
157 return NewFallbackEngine(opts), nil
158 }
159 if mode == "direct" {
160 return NewDirectDialer(opts.Timeout), nil
161 }
162
163 // Explicit proxy URL
164 u, err := url.Parse(opts.Mode)
165 if err != nil {
166 return nil, fmt.Errorf("invalid proxy URL %q: %w", opts.Mode, err)
167 }
168
169 switch strings.ToLower(u.Scheme) {
170 case "http", "https":
171 return NewHTTPConnectDialer(opts)
172 case "socks5", "socks5h":
173 return NewSOCKS5Dialer(opts)
174 default:
175 return nil, fmt.Errorf("unsupported proxy protocol scheme %q", u.Scheme)
176 }
177}