Skip to content

Commit 7cc84ee

Browse files
committed
fix(geodns): strict geo-routing and HTTP 418 for all challenges
- Remove hardcoded fallback locations from GeoDNS routing - Remove 'any available agent' fallback when HTTP proxy enabled - Countries without agents now return NXDOMAIN (or use Core-provided fallback) - Change all challenge responses to HTTP 418 (I'm a teapot) - Update JS detection script to only catch status 418 - This ensures consistent challenge detection and strict geo-routing
1 parent 0fafd65 commit 7cc84ee

3 files changed

Lines changed: 25 additions & 75 deletions

File tree

assets/injected/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
activeRequests--;
4646
fetch(item.src, { method: 'HEAD', cache: 'no-store' }).then(res => {
4747
captureAgent(res.headers);
48-
if (res.status === 403 || res.status === 429) {
48+
if (res.status === 418) {
4949
stats.blockedRequests++;
5050
triggerReload(res.status);
5151
} else processQueue();
@@ -77,7 +77,7 @@
7777
try {
7878
const response = await originalFetch.apply(this, args);
7979
captureAgent(response.headers);
80-
if (response.status === 403 || response.status === 429) {
80+
if (response.status === 418) {
8181
triggerReload(response.status);
8282
return new Promise(() => {});
8383
}
@@ -96,7 +96,7 @@
9696
this.addEventListener('load', () => {
9797
const agent = this.getResponseHeader('d-agent-id');
9898
if (agent) stats.agents.add(agent);
99-
if (this.status === 403 || this.status === 429) triggerReload(this.status);
99+
if (this.status === 418) triggerReload(this.status);
100100
});
101101
return originalXHRSend.apply(this, arguments);
102102
};

dns/server.go

Lines changed: 9 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -612,70 +612,16 @@ func findBestAgentIP(geoDNSMap map[string]string, fallbackMap map[string]string,
612612
}
613613
}
614614

615-
// Try hardcoded fallback locations (legacy)
616-
// Note: Political restrictions applied (UA clients won't route to RU)
617-
fallbackLocations := map[string][]string{
618-
"us": {"ca", "mx", "gb", "de"},
619-
"ca": {"us", "mx", "gb", "de"},
620-
"mx": {"us", "ca", "br", "cl"},
621-
"br": {"ar", "cl", "us", "mx"},
622-
"ar": {"br", "cl", "mx", "us"},
623-
"cl": {"ar", "br", "mx", "us"},
624-
"co": {"br", "ar", "mx", "cl"},
625-
"gb": {"de", "fr", "nl", "us"},
626-
"de": {"nl", "fr", "gb", "pl"},
627-
"fr": {"de", "gb", "es", "it"},
628-
"it": {"fr", "de", "es", "tr"},
629-
"es": {"fr", "it", "br", "mx"},
630-
"nl": {"de", "gb", "fr", "pl"},
631-
"pl": {"de", "ua", "cz", "nl"}, // Poland: avoid RU
632-
"ua": {"pl", "de", "tr", "ro"}, // Ukraine: NEVER route to RU
633-
"ru": {"kz", "by", "fi", "tr"}, // Russia: prefer nearby (KZ, BY), avoid UA
634-
"by": {"ru", "pl", "ua", "lt"}, // Belarus
635-
"kz": {"ru", "uz", "cn", "tr"}, // Kazakhstan
636-
"cn": {"jp", "kr", "sg", "in"},
637-
"jp": {"kr", "cn", "sg", "au"},
638-
"kr": {"jp", "cn", "sg", "au"},
639-
"in": {"sg", "th", "id", "ae"},
640-
"id": {"sg", "th", "au", "in"},
641-
"th": {"sg", "id", "in", "cn"},
642-
"sg": {"id", "th", "in", "au"},
643-
"au": {"nz", "sg", "id", "jp"},
644-
"nz": {"au", "sg", "id", "jp"},
645-
"za": {"eg", "ng", "ae", "gb"},
646-
"eg": {"ae", "tr", "za", "ng"},
647-
"ng": {"za", "eg", "br", "fr"},
648-
"ae": {"ir", "tr", "in", "eg"},
649-
"tr": {"ae", "ir", "eg", "it"},
650-
"ir": {"ae", "tr", "kz", "in"},
651-
"cz": {"de", "pl", "at", "sk"}, // Czech Republic -> Germany, Poland, Austria, Slovakia
652-
"at": {"de", "cz", "it", "ch"}, // Austria -> Germany, Czech, Italy, Switzerland
653-
"sk": {"cz", "pl", "hu", "at"}, // Slovakia -> Czech, Poland, Hungary, Austria
654-
}
655-
656-
if fallbacks, ok := fallbackLocations[clientLocation]; ok {
657-
for _, fallback := range fallbacks {
658-
if ip, ok := geoDNSMap[fallback]; ok {
659-
log.Printf("[GeoDNS] No exact match for '%s', using hardcoded fallback '%s' -> %s", clientLocation, fallback, ip)
660-
return ip
661-
}
662-
}
663-
}
664-
665-
// CRITICAL: When HTTP proxy is enabled, NEVER return origin IP
666-
// Skip "default" key which contains origin IP
615+
// NOTE: Hardcoded fallback locations have been removed.
616+
// Users must be routed to agents in their exact location only.
617+
// Core-provided fallback map can be used for controlled routing decisions.
618+
// If no agent exists for the location, return empty (NXDOMAIN) to prevent
619+
// routing users to wrong countries (e.g., RU users being routed to KZ).
620+
621+
// When HTTP proxy is enabled and no exact match found, do NOT fall back to random agents
622+
// This ensures strict geo-routing and prevents routing users to wrong countries
667623
if httpProxyEnabled {
668-
// Return any available agent IP as last resort
669-
for location, ip := range geoDNSMap {
670-
// Skip "default" key - it contains origin IP
671-
if location == "default" {
672-
continue
673-
}
674-
log.Printf("[GeoDNS] HTTP Proxy enabled, using any available agent '%s' -> %s (skipping default/origin)", location, ip)
675-
return ip
676-
}
677-
678-
log.Printf("[GeoDNS] HTTP Proxy enabled but no agent IPs available for location '%s'", clientLocation)
624+
log.Printf("[GeoDNS] No agent available for location '%s' - returning NXDOMAIN to enforce strict geo-routing", clientLocation)
679625
return ""
680626
}
681627

firewall/challenges.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import (
2323
"github.com/defenra/agent/assets"
2424
)
2525

26+
// StatusChallenge is HTTP 418 (I'm a teapot) - used for all challenge pages
27+
// This allows JS detection scripts to consistently identify challenge responses
28+
const StatusChallenge = 418
29+
2630
// ChallengeResponse represents a challenge response from the firewall
2731
type ChallengeResponse struct {
2832
Blocked bool
@@ -142,7 +146,7 @@ func (cm *ChallengeManager) IssueCookieChallenge(w http.ResponseWriter, r *http.
142146
// Testing mode - return simple redirect
143147
return ChallengeResponse{
144148
Blocked: true,
145-
StatusCode: http.StatusFound,
149+
StatusCode: StatusChallenge,
146150
Headers: map[string]string{
147151
"Set-Cookie": cookie.String(),
148152
"Location": r.RequestURI,
@@ -175,7 +179,7 @@ func (cm *ChallengeManager) IssueCookieChallenge(w http.ResponseWriter, r *http.
175179
// Fallback to simple redirect if template fails
176180
return ChallengeResponse{
177181
Blocked: true,
178-
StatusCode: http.StatusFound,
182+
StatusCode: StatusChallenge,
179183
Headers: map[string]string{
180184
"Set-Cookie": cookie.String(),
181185
"Location": r.RequestURI,
@@ -187,7 +191,7 @@ func (cm *ChallengeManager) IssueCookieChallenge(w http.ResponseWriter, r *http.
187191

188192
return ChallengeResponse{
189193
Blocked: true,
190-
StatusCode: http.StatusOK,
194+
StatusCode: StatusChallenge,
191195
Headers: map[string]string{
192196
"Set-Cookie": cookie.String(),
193197
"Content-Type": "text/html; charset=utf-8",
@@ -253,7 +257,7 @@ func (cm *ChallengeManager) IssueJSChallenge(w http.ResponseWriter, r *http.Requ
253257

254258
return ChallengeResponse{
255259
Blocked: true,
256-
StatusCode: http.StatusOK,
260+
StatusCode: StatusChallenge,
257261
Headers: map[string]string{
258262
"Content-Type": "text/html; charset=utf-8",
259263
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
@@ -340,7 +344,7 @@ func (cm *ChallengeManager) IssueCaptchaChallenge(w http.ResponseWriter, r *http
340344

341345
return ChallengeResponse{
342346
Blocked: true,
343-
StatusCode: http.StatusOK,
347+
StatusCode: StatusChallenge,
344348
Headers: map[string]string{
345349
"Content-Type": "text/html; charset=utf-8",
346350
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
@@ -581,7 +585,7 @@ func (cm *ChallengeManager) fallbackJSChallenge(publicSalt, target, clientIP, ra
581585
log.Printf("[Challenge] CRITICAL: Even fallback template failed: %v", err)
582586
return ChallengeResponse{
583587
Blocked: true,
584-
StatusCode: http.StatusServiceUnavailable,
588+
StatusCode: StatusChallenge,
585589
Headers: map[string]string{
586590
"Content-Type": "text/plain; charset=utf-8",
587591
},
@@ -591,7 +595,7 @@ func (cm *ChallengeManager) fallbackJSChallenge(publicSalt, target, clientIP, ra
591595

592596
return ChallengeResponse{
593597
Blocked: true,
594-
StatusCode: http.StatusOK,
598+
StatusCode: StatusChallenge,
595599
Headers: map[string]string{
596600
"Content-Type": "text/html; charset=utf-8",
597601
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
@@ -622,7 +626,7 @@ func (cm *ChallengeManager) fallbackCaptchaChallenge(captchaData *CaptchaData, a
622626
log.Printf("[Challenge] CRITICAL: Even fallback template failed: %v", err)
623627
return ChallengeResponse{
624628
Blocked: true,
625-
StatusCode: http.StatusServiceUnavailable,
629+
StatusCode: StatusChallenge,
626630
Headers: map[string]string{
627631
"Content-Type": "text/plain; charset=utf-8",
628632
},
@@ -632,7 +636,7 @@ func (cm *ChallengeManager) fallbackCaptchaChallenge(captchaData *CaptchaData, a
632636

633637
return ChallengeResponse{
634638
Blocked: true,
635-
StatusCode: http.StatusOK,
639+
StatusCode: StatusChallenge,
636640
Headers: map[string]string{
637641
"Content-Type": "text/html; charset=utf-8",
638642
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",

0 commit comments

Comments
 (0)