mirror of
https://github.com/denuitt1/mhr-cfw.git
synced 2026-05-17 21:24:36 +03:00
Merge pull request #160 from onlymaj/fix/forwarder-hosts-allowlist
feat(forwarder): scope upstream forwarder via forwarder_hosts config
This commit is contained in:
@@ -179,6 +179,19 @@ Browse `https://httpbin.org/ip` through the proxy — you should see the **VPS's
|
||||
|
||||
> The forwarder must require auth. Without `AUTH_KEY` it refuses to start. Anyone with the URL and key can use it as a relay, so keep both secret.
|
||||
|
||||
### 4. Scope the forwarder to specific hosts (optional)
|
||||
|
||||
By default every request the Worker handles routes through the forwarder, so unrelated traffic also burns VPS bandwidth. To send only the sites that need a stable exit IP through the VPS, list them in `forwarder_hosts` in `config.json` — same syntax as `bypass_hosts` (exact hostname or `.suffix`). Anything not matched falls back to direct `fetch()` on the Worker.
|
||||
|
||||
```json
|
||||
"forwarder_hosts": [
|
||||
"example.com",
|
||||
".cf-protected-suffix"
|
||||
]
|
||||
```
|
||||
|
||||
Leave the list empty (or remove the key) to keep the historical "forward everything" behavior.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
@@ -519,6 +519,19 @@ curl -X POST https://forwarder.example.com/fwd \
|
||||
|
||||
> forwarder بدون `AUTH_KEY` راهاندازی نمیشود. هر کسی که آدرس و کلید را داشته باشد میتواند از آن بهعنوان رله استفاده کند، بنابراین هر دو را محرمانه نگه دارید.
|
||||
|
||||
### ۴. محدود کردن forwarder به میزبانهای خاص (اختیاری)
|
||||
|
||||
بهصورت پیشفرض همهٔ درخواستهایی که Worker پردازش میکند از طریق forwarder عبور میکنند، در نتیجه ترافیک غیرمرتبط هم پهنای باند VPS را مصرف میکند. اگر فقط میخواهید سایتهایی که به IP خروجی پایدار نیاز دارند از مسیر VPS رد شوند، آنها را در `forwarder_hosts` در `config.json` فهرست کنید — همان نحو `bypass_hosts` (نام دقیق دامنه یا الگوی `.suffix`). هر چه با این لیست تطبیق نخورد، روی Worker با `fetch()` مستقیم ارسال میشود.
|
||||
|
||||
```json
|
||||
"forwarder_hosts": [
|
||||
"example.com",
|
||||
".cf-protected-suffix"
|
||||
]
|
||||
```
|
||||
|
||||
اگر این لیست خالی باشد (یا کلید را حذف کنید)، رفتار قبلی یعنی «forward همه» حفظ میشود.
|
||||
|
||||
---
|
||||
|
||||
## تنظیمات پیشرفته config.json
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
".lan",
|
||||
".home.arpa"
|
||||
],
|
||||
"forwarder_hosts": [],
|
||||
"direct_google_exclude": [
|
||||
"gemini.google.com",
|
||||
"aistudio.google.com",
|
||||
|
||||
@@ -38,7 +38,9 @@ export default {
|
||||
}
|
||||
|
||||
const upstreamUrl = (env && env.UPSTREAM_FORWARDER_URL) || "";
|
||||
if (upstreamUrl) {
|
||||
// f === 1: forward; f === 0: skip; missing: legacy client → forward (compat).
|
||||
const wantForward = (req.f === 1) || (req.f === undefined);
|
||||
if (upstreamUrl && wantForward) {
|
||||
const upstreamResp = await forwardViaUpstream(req, env, upstreamUrl);
|
||||
if (upstreamResp) return upstreamResp;
|
||||
// fall through to direct fetch only when fail-mode is open
|
||||
|
||||
+3
-1
@@ -105,7 +105,7 @@ function _buildWorkerPayload(req) {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
var out = {
|
||||
u: req.u,
|
||||
m: (req.m || "GET").toUpperCase(),
|
||||
h: headers,
|
||||
@@ -113,6 +113,8 @@ function _buildWorkerPayload(req) {
|
||||
ct: req.ct || null,
|
||||
r: req.r !== false
|
||||
};
|
||||
if (typeof req.f === "number") out.f = req.f;
|
||||
return out;
|
||||
}
|
||||
|
||||
function doGet(e) {
|
||||
|
||||
@@ -150,6 +150,10 @@ class DomainFronter:
|
||||
minimum=1024,
|
||||
)
|
||||
|
||||
self._forwarder_hosts = self._load_host_rules(
|
||||
config.get("forwarder_hosts", [])
|
||||
)
|
||||
|
||||
# Connection pool — TTL-based, pre-warmed, with concurrency control
|
||||
self._pool: list[tuple[asyncio.StreamReader, asyncio.StreamWriter, float]] = []
|
||||
self._pool_lock = asyncio.Lock()
|
||||
@@ -224,6 +228,33 @@ class DomainFronter:
|
||||
value = default
|
||||
return max(minimum, value)
|
||||
|
||||
@staticmethod
|
||||
def _load_host_rules(raw) -> tuple[set[str], tuple[str, ...]]:
|
||||
"""Parse host strings into (exact_set, suffix_tuple). Mirrors ProxyServer._load_host_rules."""
|
||||
exact: set[str] = set()
|
||||
suffixes: list[str] = []
|
||||
for item in raw or []:
|
||||
h = str(item).strip().lower().rstrip(".")
|
||||
if not h:
|
||||
continue
|
||||
if h.startswith("."):
|
||||
suffixes.append(h)
|
||||
else:
|
||||
exact.add(h)
|
||||
return exact, tuple(suffixes)
|
||||
|
||||
@staticmethod
|
||||
def _host_matches_rules(host: str,
|
||||
rules: tuple[set[str], tuple[str, ...]]) -> bool:
|
||||
exact, suffixes = rules
|
||||
h = host.lower().rstrip(".")
|
||||
if h in exact:
|
||||
return True
|
||||
for s in suffixes:
|
||||
if h.endswith(s):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _ssl_ctx(self) -> ssl.SSLContext:
|
||||
ctx = ssl.create_default_context()
|
||||
if certifi is not None:
|
||||
@@ -1515,6 +1546,13 @@ class DomainFronter:
|
||||
ct = headers.get("Content-Type") or headers.get("content-type")
|
||||
if ct:
|
||||
payload["ct"] = ct
|
||||
# Only emit 'f' when scoped; Worker treats missing 'f' as forward (legacy compat).
|
||||
exact, suffixes = self._forwarder_hosts
|
||||
if exact or suffixes:
|
||||
host = urlparse(url).hostname or ""
|
||||
payload["f"] = 1 if self._host_matches_rules(
|
||||
host, self._forwarder_hosts
|
||||
) else 0
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user