● 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 / fallback.go
← Back to directory
291 lines · 7.3 KB
Raw
1package proxy
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "os"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/shehzade/contraband/pkg/common"
13)
14
15// ProxyCandidate represents a potential proxy hop in the fallback chain.
16type ProxyCandidate struct {
17 Name string
18 Dialer ProxyDialer
19 Source string
20}
21
22// FallbackEngine implements an intelligent 5-tier proxy fallback cascade:
23// 1. Explicit Flag -> 2. System Proxy -> 3. WPAD -> 4. Environment Variables -> 5. Direct Egress
24type FallbackEngine struct {
25 opts Options
26 logger common.Logger
27 mu sync.RWMutex
28 lastHop string
29 lastMode string
30 directDialer *DirectDialer
31 customCandidates []ProxyCandidate
32}
33
34// NewFallbackEngine initializes a new FallbackEngine.
35func NewFallbackEngine(opts Options) *FallbackEngine {
36 timeout := opts.Timeout
37 if timeout <= 0 {
38 timeout = 10 * time.Second
39 }
40 return &FallbackEngine{
41 opts: opts,
42 logger: opts.Logger,
43 directDialer: NewDirectDialer(timeout),
44 lastMode: "auto",
45 }
46}
47
48// SetCustomCandidates overrides candidate discovery with custom candidates (useful for testing).
49func (fe *FallbackEngine) SetCustomCandidates(candidates []ProxyCandidate) {
50 fe.mu.Lock()
51 defer fe.mu.Unlock()
52 fe.customCandidates = candidates
53}
54
55func (fe *FallbackEngine) ProxyHop() string {
56 fe.mu.RLock()
57 defer fe.mu.RUnlock()
58 return fe.lastHop
59}
60
61func (fe *FallbackEngine) Mode() string {
62 fe.mu.RLock()
63 defer fe.mu.RUnlock()
64 return fe.lastMode
65}
66
67// DialContext traverses the candidate chain until a successful connection is established.
68func (fe *FallbackEngine) DialContext(ctx context.Context, network, targetAddr string) (net.Conn, error) {
69 candidates := fe.buildCandidates(ctx, targetAddr)
70
71 var lastErr error
72 var explicitProxyErr error
73 for _, c := range candidates {
74 fe.logger.Debugf("Attempting egress via %s [%s]", c.Name, c.Source)
75
76 conn, err := c.Dialer.DialContext(ctx, network, targetAddr)
77 if err == nil {
78 fe.mu.Lock()
79 fe.lastHop = c.Dialer.ProxyHop()
80 fe.lastMode = c.Dialer.Mode()
81 fe.mu.Unlock()
82
83 if lastErr != nil {
84 fe.logger.Warnf("Primary proxy candidate failed (%v); fell back to %s (Hop: %s, Mode: %s)", lastErr, c.Name, c.Dialer.ProxyHop(), c.Dialer.Mode())
85 } else {
86 fe.logger.Infof("Egress established via %s (Hop: %s, Mode: %s)", c.Name, c.Dialer.ProxyHop(), c.Dialer.Mode())
87 }
88 return conn, nil
89 }
90
91 fe.logger.Warnf("Egress candidate %s [%s] failed: %v", c.Name, c.Source, err)
92 if c.Name == "ExplicitProxy" {
93 explicitProxyErr = err
94 }
95 lastErr = err
96 }
97
98 if explicitProxyErr != nil {
99 return nil, fmt.Errorf("explicit proxy traversal failed: %w", explicitProxyErr)
100 }
101 return nil, fmt.Errorf("all proxy traversal candidates exhausted, last error: %w", lastErr)
102}
103
104func (fe *FallbackEngine) buildCandidates(ctx context.Context, targetAddr string) []ProxyCandidate {
105 fe.mu.RLock()
106 if len(fe.customCandidates) > 0 {
107 c := make([]ProxyCandidate, len(fe.customCandidates))
108 copy(c, fe.customCandidates)
109 fe.mu.RUnlock()
110 return c
111 }
112 fe.mu.RUnlock()
113
114 var candidates []ProxyCandidate
115
116 // 1. Explicit Flag (if specified and not auto/direct)
117 mode := strings.ToLower(fe.opts.Mode)
118 if mode != "" && mode != "auto" && mode != "direct" {
119 d, err := NewDialerFromOptions(fe.opts)
120 if err != nil {
121 fe.logger.Warnf("Failed to initialize explicit proxy dialer %q: %v", fe.opts.Mode, err)
122 } else {
123 candidates = append(candidates, ProxyCandidate{
124 Name: "ExplicitProxy",
125 Dialer: d,
126 Source: "CLI / Configuration Flag",
127 })
128 // If explicitly given, do not try other proxies before Direct
129 candidates = append(candidates, ProxyCandidate{
130 Name: "DirectFallback",
131 Dialer: fe.directDialer,
132 Source: "Direct Egress (Fallback)",
133 })
134 return candidates
135 }
136 }
137
138 // 2. System Proxy (Windows WinHTTP / Registry)
139 if winCfg, _ := GetWindowsProxyConfig(); winCfg != nil {
140 if winCfg.Proxy != "" && !shouldBypass(targetAddr, winCfg.ProxyBypass) {
141 sysOpts := fe.opts
142 sysOpts.Mode = winCfg.Proxy
143 if d, err := NewDialerFromOptions(sysOpts); err == nil {
144 candidates = append(candidates, ProxyCandidate{
145 Name: "WindowsSystemProxy",
146 Dialer: d,
147 Source: "Windows Registry Internet Settings",
148 })
149 }
150 }
151 if winCfg.AutoConfigURL != "" && fe.opts.PACURL == "" {
152 fe.opts.PACURL = winCfg.AutoConfigURL
153 }
154 }
155
156 // 3. WPAD Auto-Discovery
157 wpadCtx, cancel := context.WithTimeout(ctx, fe.opts.WPADTimeout)
158 wpadRes, err := DiscoverWPAD(wpadCtx, fe.opts)
159 cancel()
160 if err == nil && wpadRes != nil && wpadRes.Script != "" {
161 host, _, _ := net.SplitHostPort(targetAddr)
162 if host == "" {
163 host = targetAddr
164 }
165 directives, err := EvaluatePACScript(ctx, wpadRes.Script, "http://"+targetAddr, host)
166 if err == nil {
167 for i, d := range directives {
168 if d.Type == DirectiveDirect {
169 continue
170 }
171 pacOpts := fe.opts
172 pacOpts.Mode = d.ToURL()
173 if pd, err := NewDialerFromOptions(pacOpts); err == nil {
174 candidates = append(candidates, ProxyCandidate{
175 Name: fmt.Sprintf("WPADDirective_%d", i+1),
176 Dialer: pd,
177 Source: fmt.Sprintf("WPAD PAC (%s)", wpadRes.PACURL),
178 })
179 }
180 }
181 }
182 }
183
184 // 4. Environment Variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, with NO_PROXY)
185 if !isNoProxy(targetAddr) {
186 envProxy := getEnvProxy(targetAddr)
187 if envProxy != "" {
188 envOpts := fe.opts
189 envOpts.Mode = envProxy
190 if ed, err := NewDialerFromOptions(envOpts); err == nil {
191 candidates = append(candidates, ProxyCandidate{
192 Name: "EnvProxy",
193 Dialer: ed,
194 Source: "Environment Variables",
195 })
196 }
197 }
198 }
199
200 // 5. Direct Egress
201 candidates = append(candidates, ProxyCandidate{
202 Name: "DirectEgress",
203 Dialer: fe.directDialer,
204 Source: "Direct TCP Socket",
205 })
206
207 return candidates
208}
209
210func getEnvProxy(targetAddr string) string {
211 if strings.HasSuffix(targetAddr, ":443") {
212 if p := os.Getenv("HTTPS_PROXY"); p != "" {
213 return p
214 }
215 if p := os.Getenv("https_proxy"); p != "" {
216 return p
217 }
218 }
219 if p := os.Getenv("HTTP_PROXY"); p != "" {
220 return p
221 }
222 if p := os.Getenv("http_proxy"); p != "" {
223 return p
224 }
225 if p := os.Getenv("ALL_PROXY"); p != "" {
226 return p
227 }
228 if p := os.Getenv("all_proxy"); p != "" {
229 return p
230 }
231 return ""
232}
233
234func isNoProxy(targetAddr string) bool {
235 noProxy := os.Getenv("NO_PROXY")
236 if noProxy == "" {
237 noProxy = os.Getenv("no_proxy")
238 }
239 if noProxy == "" {
240 return false
241 }
242 return shouldBypass(targetAddr, noProxy)
243}
244
245func shouldBypass(targetAddr, bypassRules string) bool {
246 if bypassRules == "" {
247 return false
248 }
249 host, _, err := net.SplitHostPort(targetAddr)
250 if err != nil {
251 host = targetAddr
252 }
253 host = strings.ToLower(strings.TrimSpace(host))
254
255 rules := strings.FieldsFunc(bypassRules, func(r rune) bool {
256 return r == ',' || r == ';' || r == ' '
257 })
258
259 targetIP := net.ParseIP(host)
260
261 for _, rule := range rules {
262 rule = strings.ToLower(strings.TrimSpace(rule))
263 if rule == "" {
264 continue
265 }
266 if rule == "*" {
267 return true
268 }
269 if rule == "<local>" && !strings.Contains(host, ".") {
270 return true
271 }
272 if rule == host {
273 return true
274 }
275 if strings.HasPrefix(rule, ".") && strings.HasSuffix(host, rule) {
276 return true
277 }
278 if !strings.HasPrefix(rule, ".") && strings.HasSuffix(host, "."+rule) {
279 return true
280 }
281 // CIDR check
282 if targetIP != nil {
283 if _, cidr, err := net.ParseCIDR(rule); err == nil {
284 if cidr.Contains(targetIP) {
285 return true
286 }
287 }
288 }
289 }
290 return false
291}