● 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 / sweep / udp.go
← Back to directory
194 lines · 5.3 KB
Raw
1package sweep
2
3import (
4 "context"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "net"
10 "strconv"
11 "strings"
12 "time"
13
14 "github.com/shehzade/contraband/pkg/common"
15)
16
17// ProbeUDP performs an egress probe against targetHost:port using stateful UDP probes.
18func ProbeUDP(ctx context.Context, targetHost string, port int, timeout time.Duration, requireReflection bool) common.ProbeResult {
19 res, err := common.NewProbeResult("sweep", "udp", targetHost, port)
20 if err != nil {
21 return common.ProbeResult{
22 Timestamp: time.Now().UTC(),
23 Stage: "sweep",
24 Protocol: "udp",
25 TargetHost: targetHost,
26 TargetPort: port,
27 Verdict: common.VerdictBlockedL4,
28 ErrorMessage: fmt.Sprintf("failed to initialize probe: %v", err),
29 }
30 }
31
32 targetAddr := net.JoinHostPort(targetHost, strconv.Itoa(port))
33 dialer := &net.Dialer{
34 Timeout: timeout,
35 }
36
37 start := time.Now().UTC()
38 res.Timestamps.StartTCP = start
39 res.Timestamps.ConnectedTCP = start
40
41 // Connected UDP socket enables ICMP Port Unreachable detection
42 conn, dialErr := dialer.DialContext(ctx, "udp", targetAddr)
43 if dialErr != nil {
44 teardown := time.Now().UTC()
45 res.Timestamps.TeardownDone = teardown
46 res.Timestamps.ComputeDurations()
47
48 if ctx.Err() != nil {
49 res.Verdict = common.VerdictBlockedL4
50 res.ErrorMessage = fmt.Sprintf("L4_CANCELED: %v", ctx.Err())
51 return res
52 }
53
54 res.Verdict = common.VerdictBlockedL4
55 res.ErrorMessage = fmt.Sprintf("L4_DIAL_ERROR: %v", dialErr)
56 return res
57 }
58
59 res.LocalAddr = conn.LocalAddr().String()
60
61 // Select port-specific probe packet
62 probeBytes := BuildProbeForPort(port)
63 res.BytesSent = int64(len(probeBytes))
64
65 _ = conn.SetDeadline(time.Now().Add(timeout))
66
67 // Ensure context cancellation interrupts any blocking I/O immediately
68 done := make(chan struct{})
69 defer close(done)
70 go func() {
71 select {
72 case <-ctx.Done():
73 _ = conn.SetDeadline(time.Now())
74 case <-done:
75 }
76 }()
77
78 firstByte := time.Now().UTC()
79 res.Timestamps.FirstByteSent = firstByte
80
81 _, writeErr := conn.Write(probeBytes)
82 if writeErr != nil {
83 _ = conn.Close()
84 teardown := time.Now().UTC()
85 res.Timestamps.TeardownDone = teardown
86 res.Timestamps.ComputeDurations()
87
88 if ctx.Err() != nil {
89 res.Verdict = common.VerdictBlockedL4
90 res.ErrorMessage = fmt.Sprintf("L4_CANCELED: %v", ctx.Err())
91 return res
92 }
93
94 res.Verdict = common.VerdictBlockedL4
95 res.ErrorMessage = fmt.Sprintf("L4_WRITE_ERROR: %v", writeErr)
96 return res
97 }
98
99 buf := make([]byte, 4096)
100 n, readErr := conn.Read(buf)
101 ack := time.Now().UTC()
102 res.Timestamps.AckReceived = ack
103
104 _ = conn.Close()
105 teardown := time.Now().UTC()
106 res.Timestamps.TeardownDone = teardown
107 res.Timestamps.ComputeDurations()
108
109 if readErr != nil {
110 if ctx.Err() != nil {
111 res.Verdict = common.VerdictBlockedL4
112 res.ErrorMessage = fmt.Sprintf("L4_CANCELED: %v", ctx.Err())
113 return res
114 }
115
116 var netErr net.Error
117 if errors.As(readErr, &netErr) && netErr.Timeout() {
118 res.Verdict = common.VerdictBlockedL4
119 res.ErrorMessage = fmt.Sprintf("L4_TIMEOUT: no response within %v (filtered or dropped)", timeout)
120 return res
121 }
122
123 errStr := readErr.Error()
124 if strings.Contains(errStr, "connection refused") || strings.Contains(errStr, "ECONNREFUSED") {
125 res.Verdict = common.VerdictBlockedL4
126 res.ErrorMessage = fmt.Sprintf("L4_CLOSED: ICMP port unreachable (%v)", readErr)
127 return res
128 }
129
130 res.Verdict = common.VerdictBlockedL4
131 res.ErrorMessage = fmt.Sprintf("L4_READ_ERROR: %v", readErr)
132 return res
133 }
134
135 res.BytesRecv = int64(n)
136 rawResp := buf[:n]
137
138 // Try parsing JSON reflection banner
139 var banner common.BannerReflection
140 if err := json.Unmarshal(rawResp, &banner); err == nil {
141 if banner.Status == "OK" && banner.Port == port && strings.EqualFold(banner.Proto, "UDP") {
142 res.Verdict = common.VerdictSuccess
143 res.RawEvidence = string(rawResp)
144 if banner.RemoteIP != "" {
145 res.LocalAddr = banner.RemoteIP
146 }
147 return res
148 }
149 if requireReflection {
150 res.Verdict = common.VerdictBlockedL7
151 res.ErrorMessage = fmt.Sprintf("L7_BANNER_INVALID: reflection banner mismatch: status=%s port=%d (expected %d) proto=%s", banner.Status, banner.Port, port, banner.Proto)
152 res.RawEvidence = string(rawResp)
153 return res
154 }
155 }
156
157 // Validate protocol response if reflection JSON was not returned
158 if isProtocolResponseValid(port, rawResp) {
159 res.Verdict = common.VerdictSuccess
160 res.RawEvidence = hex.EncodeToString(rawResp)
161 return res
162 }
163
164 if requireReflection {
165 res.Verdict = common.VerdictBlockedL7
166 res.ErrorMessage = "L7_BANNER_INVALID: UDP response received but not valid Contraband banner"
167 res.RawEvidence = hex.EncodeToString(rawResp)
168 return res
169 }
170
171 res.Verdict = common.VerdictSuccess
172 res.RawEvidence = hex.EncodeToString(rawResp)
173 return res
174}
175
176func isProtocolResponseValid(port int, data []byte) bool {
177 if len(data) == 0 {
178 return false
179 }
180 switch port {
181 case 53:
182 // DNS response must have at least 12 bytes and QR bit (bit 7 of byte 2) set
183 return len(data) >= 12 && (data[2]&0x80) != 0
184 case 123:
185 // NTP response must be 48 bytes and Mode must be 4 (server) or 5 (broadcast)
186 return len(data) >= 48 && ((data[0]&0x07) == 4 || (data[0]&0x07) == 5)
187 case 161:
188 // SNMP response must start with SEQUENCE (0x30)
189 return len(data) >= 10 && data[0] == 0x30
190 default:
191 // Echo response of generic probe
192 return strings.Contains(string(data), "CTRB_UDP_PROBE")
193 }
194}